Skip to content

Commit 884bf93

Browse files
tomassurinclaude
andauthored
Add OAuth browser flow with DCR for stdio transport (#22)
## Summary - Introduces `DwsApiClient` abstraction with pluggable token provider (API key or OAuth) - Adds OAuth browser flow module (`src/auth/nutrient-oauth.ts`) with PKCE, token caching (`~/.nutrient/credentials.json`), refresh, and Dynamic Client Registration - When no `NUTRIENT_DWS_API_KEY` is set, the server opens a browser for Nutrient OAuth consent on first tool call (similar to `gh auth login`) - DCR registers a client automatically at `{AUTH_SERVER_URL}/oauth/register` — no pre-registered client ID needed - Adds Winston logger with file transport for stdio mode (avoids interfering with MCP protocol on stdout/stderr) - Adds Github actions setup to run lint and basic tests ## Auth flow 1. If `NUTRIENT_DWS_API_KEY` is set, use the API key directly 2. Otherwise, check for a cached token at `~/.nutrient/credentials.json` 3. If the cached token is valid, return it 4. If expired but a refresh token exists, refresh it 5. If no token or refresh fails, register a client via DCR and start a browser OAuth flow, then cache the token ## Environment variables | Variable | Default | Description | |----------|---------|-------------| | `NUTRIENT_DWS_API_KEY` | — | API key (skips OAuth if set) | | `AUTH_SERVER_URL` | `https://api.nutrient.io` | OAuth server base URL | | `CLIENT_ID` | — | Override DCR with a specific client ID | | `DWS_API_BASE_URL` | `https://api.nutrient.io` | DWS API base URL | Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 28ef4e4 commit 884bf93

24 files changed

Lines changed: 2122 additions & 315 deletions

.github/workflows/tests.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
pull_request:
8+
9+
concurrency:
10+
group: tests-${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
test:
15+
name: Lint, Build, Test
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 20
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup pnpm
24+
uses: pnpm/action-setup@v4
25+
with:
26+
version: 10
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: 20
32+
cache: pnpm
33+
34+
- name: Install dependencies
35+
run: pnpm install --frozen-lockfile
36+
37+
- name: Lint
38+
run: pnpm exec eslint src tests
39+
40+
- name: Build
41+
run: pnpm run build
42+
43+
- name: Run tests
44+
run: pnpm run test:ci

README.md

Lines changed: 102 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@ A Model Context Protocol (MCP) server that connects AI assistants to the [Nutrie
1616

1717
Once configured, you (or your AI agent) can process documents through natural language:
1818

19-
**You:** *"Merge report-q1.pdf and report-q2.pdf into a single document"*
20-
**AI:** *"Done! I've merged both reports into combined-report.pdf (24 pages total)."*
19+
**You:** _"Merge report-q1.pdf and report-q2.pdf into a single document"_
20+
**AI:** _"Done! I've merged both reports into combined-report.pdf (24 pages total)."_
2121

22-
**You:** *"Redact all social security numbers and email addresses from application.pdf"*
23-
**AI:** *"I found and redacted 5 SSNs and 3 email addresses. The redacted version is saved as application-redacted.pdf."*
22+
**You:** _"Redact all social security numbers and email addresses from application.pdf"_
23+
**AI:** _"I found and redacted 5 SSNs and 3 email addresses. The redacted version is saved as application-redacted.pdf."_
2424

25-
**You:** *"Digitally sign this contract with a visible signature on page 3"*
26-
**AI:** *"I've applied a PAdES-compliant digital signature to contract.pdf. The signed document is saved as contract-signed.pdf."*
25+
**You:** _"Digitally sign this contract with a visible signature on page 3"_
26+
**AI:** _"I've applied a PAdES-compliant digital signature to contract.pdf. The signed document is saved as contract-signed.pdf."_
2727

28-
**You:** *"Convert this PDF to markdown"*
29-
**AI:** *"Here's the markdown content extracted from your document..."*
28+
**You:** _"Convert this PDF to markdown"_
29+
**AI:** _"Here's the markdown content extracted from your document..."_
3030

31-
**You:** *"OCR this scanned document in German and extract the text"*
32-
**AI:** *"I've processed the scan with German OCR. Here's the extracted text..."*
31+
**You:** _"OCR this scanned document in German and extract the text"_
32+
**AI:** _"I've processed the scan with German OCR. Here's the extracted text..."_
3333

3434
## Quick Start
3535

@@ -57,11 +57,11 @@ Open Settings → Developer → Edit Config, then add:
5757
"args": ["-y", "@nutrient-sdk/dws-mcp-server"],
5858
"env": {
5959
"NUTRIENT_DWS_API_KEY": "YOUR_API_KEY_HERE",
60-
"SANDBOX_PATH": "/your/sandbox/directory"
60+
"SANDBOX_PATH": "/your/sandbox/directory",
6161
// "C:\\your\\sandbox\\directory" for Windows
62-
}
63-
}
64-
}
62+
},
63+
},
64+
},
6565
}
6666
```
6767

@@ -80,13 +80,14 @@ Create `.cursor/mcp.json` in your project root:
8080
"args": ["-y", "@nutrient-sdk/dws-mcp-server"],
8181
"env": {
8282
"NUTRIENT_DWS_API_KEY": "YOUR_API_KEY_HERE",
83-
"SANDBOX_PATH": "/your/project/documents"
83+
"SANDBOX_PATH": "/your/project/documents",
8484
// "C:\\your\\project\\documents" for Windows
85-
}
86-
}
87-
}
85+
},
86+
},
87+
},
8888
}
8989
```
90+
9091
</details>
9192

9293
<details>
@@ -102,13 +103,14 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
102103
"args": ["-y", "@nutrient-sdk/dws-mcp-server"],
103104
"env": {
104105
"NUTRIENT_DWS_API_KEY": "YOUR_API_KEY_HERE",
105-
"SANDBOX_PATH": "/your/sandbox/directory"
106+
"SANDBOX_PATH": "/your/sandbox/directory",
106107
// "C:\\your\\sandbox\\directory" for Windows
107-
}
108-
}
109-
}
108+
},
109+
},
110+
},
110111
}
111112
```
113+
112114
</details>
113115

114116
<details>
@@ -132,6 +134,7 @@ Add to `.vscode/settings.json` in your project:
132134
}
133135
}
134136
```
137+
135138
</details>
136139

137140
<details>
@@ -142,6 +145,7 @@ Any MCP-compatible client can connect using stdio transport:
142145
```bash
143146
NUTRIENT_DWS_API_KEY=your_key SANDBOX_PATH=/your/path npx @nutrient-sdk/dws-mcp-server
144147
```
148+
145149
</details>
146150

147151
### 3. Restart Your AI Client
@@ -154,26 +158,26 @@ Drop documents into your sandbox directory and start giving instructions!
154158

155159
## Available Tools
156160

157-
| Tool | Description |
158-
|------|-------------|
161+
| Tool | Description |
162+
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159163
| **document_processor** | All-in-one document processing: merge PDFs, convert formats, apply OCR, watermark, rotate, redact, flatten annotations, extract text/tables/key-value pairs, and more |
160-
| **document_signer** | Digitally sign PDFs with PAdES-compliant CMS or CAdES signatures, with customizable visible/invisible signature appearances |
161-
| **sandbox_file_tree** | Browse files in the sandbox directory (when sandbox mode is enabled) |
162-
| **directory_tree** | Browse directory contents (when sandbox mode is disabled) |
164+
| **document_signer** | Digitally sign PDFs with PAdES-compliant CMS or CAdES signatures, with customizable visible/invisible signature appearances |
165+
| **sandbox_file_tree** | Browse files in the sandbox directory (when sandbox mode is enabled) |
166+
| **directory_tree** | Browse directory contents (when sandbox mode is disabled) |
163167

164168
### Document Processor Capabilities
165169

166-
| Feature | Description |
167-
|---------|-------------|
168-
| Document Creation | Merge PDFs, Office docs (DOCX, XLSX, PPTX), and images into a single document |
169-
| Format Conversion | PDF ↔ DOCX, images (PNG, JPEG, WebP), PDF/A, PDF/UA, HTML, Markdown |
170-
| Editing | Watermark (text/image), rotate pages, flatten annotations |
171-
| Security | Redact sensitive data (SSNs, credit cards, emails, etc.), password protection, permission control |
172-
| Data Extraction | Extract text, tables, or key-value pairs as structured JSON |
173-
| OCR | Multi-language optical character recognition for scanned documents |
174-
| Optimization | Compress and linearize PDFs without quality loss |
175-
| Annotations | Import XFDF annotations, flatten annotations |
176-
| Digital Signing | PAdES-compliant CMS and CAdES digital signatures (via document_signer tool) |
170+
| Feature | Description |
171+
| ----------------- | ------------------------------------------------------------------------------------------------- |
172+
| Document Creation | Merge PDFs, Office docs (DOCX, XLSX, PPTX), and images into a single document |
173+
| Format Conversion | PDF ↔ DOCX, images (PNG, JPEG, WebP), PDF/A, PDF/UA, HTML, Markdown |
174+
| Editing | Watermark (text/image), rotate pages, flatten annotations |
175+
| Security | Redact sensitive data (SSNs, credit cards, emails, etc.), password protection, permission control |
176+
| Data Extraction | Extract text, tables, or key-value pairs as structured JSON |
177+
| OCR | Multi-language optical character recognition for scanned documents |
178+
| Optimization | Compress and linearize PDFs without quality loss |
179+
| Annotations | Import XFDF annotations, flatten annotations |
180+
| Digital Signing | PAdES-compliant CMS and CAdES digital signatures (via document_signer tool) |
177181

178182
## Use with AI Agent Frameworks
179183

@@ -212,6 +216,7 @@ npx @nutrient-sdk/dws-mcp-server
212216
```
213217

214218
When sandbox mode is enabled:
219+
215220
- Relative paths resolve relative to the sandbox directory
216221
- All input file paths are validated to ensure they reside in the sandbox
217222
- Processed files are saved within the sandbox
@@ -222,25 +227,80 @@ When sandbox mode is enabled:
222227

223228
Processed files are saved to a location determined by the AI. To guide output placement, use natural language (e.g., "save the result to `output/result.pdf`") or create an `output` directory in your sandbox.
224229

230+
### Authentication
231+
232+
The server authenticates to the Nutrient DWS API (`https://api.nutrient.io`) using one of:
233+
234+
| Method | When | Config |
235+
|--------|------|--------|
236+
| **API key** | `NUTRIENT_DWS_API_KEY` is set | Static key passed as Bearer token to DWS API |
237+
| **OAuth browser flow** | No API key set | Opens browser for Nutrient OAuth consent, caches token locally |
238+
239+
When no API key is configured, the server opens a browser-based OAuth flow on the first tool call (similar to `gh auth login`). Tokens are cached at `~/.nutrient/credentials.json` and refreshed automatically.
240+
225241
### Environment Variables
226242

227-
| Variable | Required | Description |
228-
|----------|----------|-------------|
229-
| `NUTRIENT_DWS_API_KEY` | Yes | Your Nutrient DWS API key ([get one free](https://dashboard.nutrient.io/sign_up/)) |
230-
| `SANDBOX_PATH` | Recommended | Directory to restrict file operations to |
243+
| Variable | Required | Description |
244+
| ---------------------- | ----------- | -------------------------------------------------------------------------------------------- |
245+
| `NUTRIENT_DWS_API_KEY` | No* | Nutrient DWS API key ([get one free](https://dashboard.nutrient.io/sign_up/)) |
246+
| `SANDBOX_PATH` | Recommended | Directory to restrict file operations to |
247+
| `CLIENT_ID` | No | OAuth client ID. Skips DCR and enables token refresh when set |
248+
| `DWS_API_BASE_URL` | No | DWS API base URL (default: `https://api.nutrient.io`) |
249+
| `LOG_LEVEL` | No | Winston logger level (`info` default). Logs are written to `MCP_LOG_FILE` in stdio mode |
250+
| `MCP_LOG_FILE` | No | Override log file path (default: system temp directory) |
251+
252+
\* If omitted, the server uses an OAuth browser flow to authenticate with the Nutrient API.
253+
254+
### Security Note: Token Storage
255+
256+
When using the OAuth browser flow, access tokens and refresh tokens are cached in plaintext at `~/.nutrient/credentials.json` (permissions `0600`). This file contains credentials equivalent to your API key. Do not commit it to version control or include it in shared backups.
231257

232258
## Troubleshooting
233259

260+
### Reset authentication to a clean state
261+
262+
If OAuth authentication stops working, delete the cached token file to start fresh:
263+
264+
```bash
265+
rm ~/.nutrient/credentials.json
266+
```
267+
268+
The server will automatically register a new client and open the browser for consent on the next tool call.
269+
270+
### FAQ
271+
234272
**Server not appearing in Claude Desktop?**
273+
235274
- Ensure Node.js 18+ is installed (`node --version`)
236275
- Check the config file path is correct for your OS
237276
- Restart Claude Desktop completely (check Task Manager/Activity Monitor)
238277

278+
**Browser doesn't open for OAuth login?**
279+
280+
- This happens in headless or remote environments (SSH, Docker, CI). Set `NUTRIENT_DWS_API_KEY` instead — the server skips the browser flow when an API key is configured.
281+
- On macOS, ensure a default browser is set in System Settings → Desktop & Dock → Default web browser.
282+
283+
**"Token exchange failed" or "OAuth authorization failed"?**
284+
285+
- Delete `~/.nutrient/credentials.json` and try again.
286+
- If using a custom `AUTH_SERVER_URL`, verify the server is reachable and its `/oauth/token` endpoint is working.
287+
288+
**"Dynamic client registration failed"?**
289+
290+
- If using a custom `AUTH_SERVER_URL`, verify it is reachable.
291+
- Ensure the custom auth server supports RFC 7591 Dynamic Client Registration at its `/oauth/register` endpoint.
292+
239293
**"API key invalid" errors?**
294+
240295
- Verify your API key at [dashboard.nutrient.io](https://dashboard.nutrient.io)
241296
- Ensure the key is set correctly in the `env` section (no extra spaces)
242297

298+
**Token expired but refresh fails?**
299+
300+
- The server automatically refreshes expired tokens using the cached refresh token. If refresh fails (e.g., the refresh token was revoked), delete `~/.nutrient/credentials.json` — the server will re-authenticate via the browser on the next call.
301+
243302
**Files not found?**
303+
244304
- Check that `SANDBOX_PATH` points to an existing directory
245305
- Ensure your documents are inside the sandbox directory
246306
- Use the `sandbox_file_tree` tool to verify visible files

docs/testing.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# DWS MCP Server — Local Testing
2+
3+
This guide covers local testing against both production DWS (`api.nutrient.io`) and local DWS debug builds. Docker/deployment steps are intentionally omitted.
4+
5+
## Prerequisites
6+
7+
- Node.js 18+
8+
- pnpm
9+
- Project dependencies installed:
10+
11+
```bash
12+
pnpm install
13+
```
14+
15+
## Run Commands
16+
17+
```bash
18+
pnpm run build && pnpm start
19+
```
20+
21+
---
22+
23+
## stdio Transport
24+
25+
### With API key
26+
27+
```bash
28+
export NUTRIENT_DWS_API_KEY=your_dws_api_key
29+
pnpm run build && pnpm start
30+
```
31+
32+
### With OAuth browser flow
33+
34+
When no API key is set, the server opens a browser for Nutrient OAuth consent on the first tool call. Tokens are cached at `~/.nutrient/credentials.json`.
35+
36+
```bash
37+
pnpm run build && pnpm start
38+
```
39+
40+
To test against a local DWS auth server instead of production:
41+
42+
```bash
43+
export AUTH_SERVER_URL=http://localhost:4000
44+
export DWS_API_BASE_URL=http://localhost:4000
45+
pnpm run build && pnpm start
46+
```
47+
48+
The OAuth flow will use `{AUTH_SERVER_URL}/oauth/authorize` and `{AUTH_SERVER_URL}/oauth/token`. The `CLIENT_ID` env var can override the default client ID (`nutrient-dws-mcp-server`).
49+
50+
---
51+
52+
## Environment Variable Reference
53+
54+
| Variable | Default | Description |
55+
|------------------------|----------------------------|--------------------------------------------------|
56+
| `DWS_API_BASE_URL` | `https://api.nutrient.io` | DWS API base URL |
57+
| `NUTRIENT_DWS_API_KEY`|| DWS API key (optional in OAuth mode) |
58+
| `AUTH_SERVER_URL` | `https://api.nutrient.io` | Authorization server base URL (for OAuth) |
59+
| `CLIENT_ID` || OAuth client ID (stdio OAuth flow) |
60+
| `SANDBOX_PATH` || Filesystem sandbox root |
61+
| `LOG_LEVEL` | `info` | Winston logger level |
62+
| `MCP_LOG_FILE` | auto (tmpdir) | Override log file path |
63+
64+
---
65+
66+
## MCP Inspector
67+
68+
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a browser-based tool for interactively testing and debugging MCP servers.
69+
70+
For stdio transport, pass the server as a subprocess directly:
71+
72+
```bash
73+
npx @modelcontextprotocol/inspector -- npx @nutrient-sdk/dws-mcp-server
74+
```
75+
76+
Or when developing locally:
77+
78+
```bash
79+
npx @modelcontextprotocol/inspector -- node dist/index.js
80+
```
81+
82+
---
83+
84+
## Common Failures
85+
86+
| Error | Cause | Fix |
87+
|-------|-------|-----|
88+
| Browser doesn't open (stdio OAuth) | Running in headless/CI | Set `NUTRIENT_DWS_API_KEY` instead |
89+
| Token exchange fails | Auth server misconfigured | Check `AUTH_SERVER_URL` and OAuth endpoints |
90+
| `NUTRIENT_DWS_API_KEY` errors | API key invalid or expired | Verify at [dashboard.nutrient.io](https://dashboard.nutrient.io) |

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,24 @@
3838
"LICENSE"
3939
],
4040
"scripts": {
41+
"start": "node dist/index.js",
4142
"build": "tsc && shx chmod +x dist/index.js",
4243
"format": "prettier --write .",
4344
"lint": "eslint .",
4445
"pretest": "tsc --project tsconfig.test.json --noEmit",
4546
"test": "vitest run",
47+
"test:ci": "vitest run --exclude tests/build-api-examples.test.ts --exclude tests/signing-api-examples.test.ts",
48+
"test:integration": "vitest run tests/build-api-examples.test.ts tests/signing-api-examples.test.ts",
4649
"test:watch": "vitest",
47-
"clean": "shx rm -rf dist"
50+
"clean": "shx rm -rf dist",
51+
"prepublishOnly": "pnpm run build"
4852
},
4953
"dependencies": {
5054
"@modelcontextprotocol/sdk": "^1.25.2",
5155
"axios": "^1.13.2",
5256
"form-data": "^4.0.5",
57+
"open": "^11.0.0",
58+
"winston": "^3.19.0",
5359
"zod": "^3.25.76"
5460
},
5561
"devDependencies": {

0 commit comments

Comments
 (0)