Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.github
node_modules
dist
coverage
.DS_Store
.env
.env.*
tests
resources
docs
*.log
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
runs-on: ubuntu-24.04

steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 20

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build

- name: Lint
run: pnpm lint

- name: Test
run: pnpm test
103 changes: 103 additions & 0 deletions .github/workflows/container-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Container Publish

on:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read
packages: write
attestations: write
id-token: write

concurrency:
group: container-publish-${{ github.ref }}
cancel-in-progress: true

env:
IMAGE_NAME: ghcr.io/pspdfkit/nutrient-dws-mcp-server

jobs:
verify:
runs-on: ubuntu-24.04

steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 20

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build

- name: Lint
run: pnpm lint

- name: Test
run: pnpm test

publish:
runs-on: ubuntu-24.04
needs: verify

steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Derive image metadata
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=sha-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Attest build provenance
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ COPY --from=builder /app/dist ./dist
RUN chown -R appuser:appgroup /app
USER appuser

# MCP runs over stdio
# Transport is selected at runtime via MCP_TRANSPORT.
ENTRYPOINT ["node", "dist/index.js"]
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,58 @@ NUTRIENT_DWS_API_KEY=your_key SANDBOX_PATH=/your/path npx @nutrient-sdk/dws-mcp-
```
</details>

### Remote HTTP Mode

For hosted or Co-work style deployments, the server also supports Streamable HTTP:

```bash
MCP_TRANSPORT=http \
MCP_HOST=0.0.0.0 \
PORT=5100 \
MCP_ALLOWED_HOSTS=connector.internal.example.com,connector.example.com \
MCP_BEARER_TOKENS_JSON='[{"token":"replace-me","clientId":"claude-cowork-prod","scopes":["mcp"],"allowedTools":["document_processor","document_signer","ai_redactor","check_credits"]}]' \
NUTRIENT_DWS_API_KEY=your_key \
node dist/index.js
```

Remote mode exposes:

- `POST /mcp` for initialize and request traffic
- `GET /mcp` for server-to-client event streams
- `DELETE /mcp` for session teardown
- `GET /health` for load balancer and readiness checks

Security model:

- bearer-token auth is required for every `/mcp` request
- sessions are bound to the authenticated principal
- the advertised MCP tool list is filtered per principal at session initialization
- stdio mode remains unchanged for local desktop clients

### Published Container Image

This repo is the image owner for remote deployments.

- registry: `ghcr.io/pspdfkit/nutrient-dws-mcp-server`
- `latest`: current default-branch image
- `sha-<commit>`: immutable commit image
- `vX.Y.Z`: release tag image

For Hosted-style deployments, use an immutable image tag in `TF_VAR_dws_mcp_docker_image` and keep the bearer-principal policy in environment-injected secrets.

Example:

```bash
docker run --rm -p 5100:5100 \
-e MCP_TRANSPORT=http \
-e MCP_HOST=0.0.0.0 \
-e PORT=5100 \
-e MCP_ALLOWED_HOSTS=mcp.example.com \
-e MCP_BEARER_TOKENS_JSON='[{"token":"replace-me","clientId":"claude-cowork-prod","scopes":["mcp"],"allowedTools":["document_processor","document_signer","check_credits"]}]' \
-e NUTRIENT_DWS_API_KEY=your_key \
ghcr.io/pspdfkit/nutrient-dws-mcp-server:sha-<commit>
```

### 3. Restart Your AI Client

Restart the application to pick up the new MCP server configuration.
Expand Down Expand Up @@ -228,6 +280,25 @@ Processed files are saved to a location determined by the AI. To guide output pl
|----------|----------|-------------|
| `NUTRIENT_DWS_API_KEY` | Yes | Your Nutrient DWS API key ([get one free](https://dashboard.nutrient.io/sign_up/)) |
| `SANDBOX_PATH` | Recommended | Directory to restrict file operations to |
| `MCP_TRANSPORT` | No | `stdio` (default) or `http` |
| `MCP_HOST` | HTTP only | Host to bind the HTTP server to. Defaults to `127.0.0.1` |
| `PORT` | HTTP only | Port for HTTP mode. Defaults to `5100` |
| `MCP_ALLOWED_HOSTS` | Optional | Comma-separated allowed `Host` headers when binding beyond loopback |
| `MCP_BEARER_TOKEN` | HTTP only | Single inbound bearer token for `/mcp` |
| `MCP_BEARER_TOKEN_CLIENT_ID` | Optional | Principal ID paired with `MCP_BEARER_TOKEN` |
| `MCP_BEARER_TOKEN_SCOPES` | Optional | Comma-separated scopes paired with `MCP_BEARER_TOKEN` |
| `MCP_BEARER_TOKEN_ALLOWED_TOOLS` | Optional | Comma-separated tool allowlist paired with `MCP_BEARER_TOKEN` |
| `MCP_BEARER_TOKENS_JSON` | HTTP only | JSON array of principal configs for multi-token remote deployments |

### Secret Management

Do not commit bearer tokens or DWS API keys into repo config.

Recommended pattern:

- local/dev: inject env vars directly, or source them from 1Password CLI / `op://...` references
- hosted environments: inject secrets through the platform secret store into container env vars
- rotate remote bearer tokens by supplying overlapping entries in `MCP_BEARER_TOKENS_JSON`

## Troubleshooting

Expand All @@ -245,6 +316,10 @@ Processed files are saved to a location determined by the AI. To guide output pl
- Ensure your documents are inside the sandbox directory
- Use the `sandbox_file_tree` tool to verify visible files

**Running tests in CI or local dev?**
- `pnpm test` runs the deterministic local suite
- live DWS example suites are opt-in and require `RUN_LIVE_DWS_EXAMPLE_TESTS=1`

## Contributing

Please see the contribution guidelines in [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"node": ">=18.0.0",
"pnpm": ">=9.10.0"
},
"packageManager": "pnpm@10.18.2",
"type": "module",
"files": [
"dist",
Expand All @@ -49,16 +50,20 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"axios": "^1.13.2",
"express": "^5.1.0",
"form-data": "^4.0.5",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@types/express": "^5.0.3",
"@types/node": "^22.19.5",
"@types/supertest": "^6.0.3",
"dotenv": "^16.6.1",
"eslint": "^9.39.2",
"prettier": "^3.7.4",
"shx": "^0.4.0",
"supertest": "^7.1.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.52.0",
"vitest": "^4.0.16"
Expand Down
Loading