differino-mcp 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -13
- package/dist/index.js +60 -4
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -6,7 +6,11 @@ MCP (Model Context Protocol) server for [Differino](https://www.differino.com) -
|
|
|
6
6
|
|
|
7
7
|
### 1. Install
|
|
8
8
|
|
|
9
|
+
No install is needed: MCP clients can run the published package with `npx`. If you prefer a global install:
|
|
10
|
+
|
|
9
11
|
```bash
|
|
12
|
+
npm install -g differino-mcp
|
|
13
|
+
# or
|
|
10
14
|
pnpm add -g differino-mcp
|
|
11
15
|
```
|
|
12
16
|
|
|
@@ -24,8 +28,8 @@ Add to your `claude_desktop_config.json`:
|
|
|
24
28
|
{
|
|
25
29
|
"mcpServers": {
|
|
26
30
|
"differino": {
|
|
27
|
-
"command": "
|
|
28
|
-
"args": ["
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "differino-mcp"],
|
|
29
33
|
"env": {
|
|
30
34
|
"DIFFERINO_API_KEY": "dfn_your_api_key_here"
|
|
31
35
|
}
|
|
@@ -34,6 +38,8 @@ Add to your `claude_desktop_config.json`:
|
|
|
34
38
|
}
|
|
35
39
|
```
|
|
36
40
|
|
|
41
|
+
With pnpm, use `"command": "pnpm", "args": ["dlx", "differino-mcp"]` instead. Cursor and other MCP clients take the same `command` / `args` / `env` shape.
|
|
42
|
+
|
|
37
43
|
### Local development
|
|
38
44
|
|
|
39
45
|
From the repository root:
|
|
@@ -52,18 +58,26 @@ pnpm --dir mcp build
|
|
|
52
58
|
|
|
53
59
|
### compare_documents
|
|
54
60
|
|
|
55
|
-
Compare two local files and get a
|
|
61
|
+
Compare two local files and get a compact result. This is the only tool that consumes a comparison (your free comparison or 1 credit).
|
|
56
62
|
|
|
57
63
|
**Parameters:**
|
|
58
64
|
- `file_a_path` (required) -- Absolute path to the original document
|
|
59
65
|
- `file_b_path` (required) -- Absolute path to the modified document
|
|
60
66
|
- `comparison_mode` (optional) -- `visual` for full document review, or `text` for fast text-only comparison (default: `visual`)
|
|
61
67
|
|
|
62
|
-
The tool uploads both files, waits for extraction and comparison, and returns
|
|
68
|
+
The tool uploads both files, waits for extraction and comparison, and returns:
|
|
69
|
+
|
|
70
|
+
- `id`, `status`, `url` (web view) and `comparisonMode`
|
|
71
|
+
- `summary`: added/removed/modified block counts
|
|
72
|
+
- `visual.status` plus `visual.totalChanges`, `visual.changedPageCount` and `visual.pageCount` when the visual manifest exists
|
|
73
|
+
- `creditsRemaining` and `freeComparisonRemaining`
|
|
74
|
+
- `next`: the tools to call for details
|
|
75
|
+
|
|
76
|
+
It deliberately does NOT include the page-by-page manifest (hundreds of KB of image URLs on long documents). Call `summarize_visual_diff` for a cheap overview of what changed, `get_text_diff` for the exact text, or `get_comparison` for the full contract with signed page image URLs. If the server is still working when the internal wait expires, the response has `status: "processing"`; poll `get_comparison` with the returned `id` until `status` is `ready`.
|
|
63
77
|
|
|
64
78
|
Files up to 45 MB each are supported. Small pairs (up to ~4 MB combined) go through the multipart endpoint; larger files are uploaded automatically via signed upload URLs (`POST /api/v1/uploads` -> direct PUT to storage -> `POST /api/v1/uploads/complete` -> compare by version ids). This is transparent: you always just call `compare_documents`.
|
|
65
79
|
|
|
66
|
-
|
|
80
|
+
`get_comparison` responses include the full visual artifact contract when available:
|
|
67
81
|
|
|
68
82
|
- `comparisonMode`: `visual` or `text`
|
|
69
83
|
- `visual.status`: `ready`, `pending`, `missing`, or `disabled`
|
|
@@ -74,6 +88,7 @@ Responses include visual artifact metadata when available:
|
|
|
74
88
|
- `regions[].salience`: `subtle`, `visible`, `structural`, or `technical`, plus `salienceReason` when available
|
|
75
89
|
- `totalChanges` and `changedPageCount`: summary fields for compact controls and optional review navigation; paired fragments with the same `groupId` count as one change
|
|
76
90
|
- `visual.diffUrl`: signed URL for the visual diff JSON when available
|
|
91
|
+
- `visualManifest` (top level): deprecated duplicate of `visual.manifest`, kept for older integrations and removed in a future version
|
|
77
92
|
|
|
78
93
|
Agents should treat `visual.manifest` as the primary surface for professional document review: render the two documents continuously, draw exact inline highlights when coordinates are present, use `groupId` for selection and synchronized scrolling, and derive center-gutter/minimap markers from the region coordinates. If `visual.status` is not `ready`, agents should use the text diff summary and comparison URL instead of assuming there are no visual changes.
|
|
79
94
|
|
|
@@ -116,7 +131,7 @@ Example:
|
|
|
116
131
|
|
|
117
132
|
### export_comparison_pdf
|
|
118
133
|
|
|
119
|
-
Export a finished comparison as a PDF report and get a temporary download URL (valid for about 5 minutes). With `wait: true` (default) the tool polls the export job every 2 seconds for up to 3 minutes and returns the `downloadUrl`. Free workspaces get a watermarked PDF; workspaces with credits export without watermark. Free (exports do not consume credits).
|
|
134
|
+
Export a finished comparison as a PDF report and get a temporary download URL (valid for about 5 minutes). With `wait: true` (default) the tool polls the export job every 2 seconds for up to 3 minutes and returns the `downloadUrl`. Export jobs move through `pending`, `processing`, `completed` (with `downloadUrl`) or `failed`; with `wait: false` you get the `jobId` right after queueing and poll the REST status endpoint yourself. Free workspaces get a watermarked PDF; workspaces with credits export without watermark. Free (exports do not consume credits).
|
|
120
135
|
|
|
121
136
|
**Parameters:**
|
|
122
137
|
- `comparison_id` (required) -- The comparison UUID (must have `status: "ready"`)
|
|
@@ -189,6 +204,7 @@ Example response:
|
|
|
189
204
|
```json
|
|
190
205
|
{
|
|
191
206
|
"credits": 42,
|
|
207
|
+
"creditsRemaining": 42,
|
|
192
208
|
"freeComparisonRemaining": 0,
|
|
193
209
|
"plan": "pro",
|
|
194
210
|
"workspaceId": "3b2f...",
|
|
@@ -271,9 +287,11 @@ Steps 1-3 are free; only step 4 consumes the free comparison or a credit.
|
|
|
271
287
|
|
|
272
288
|
`POST /api/v1/compare` accepts an optional `Idempotency-Key` header (up to 200 characters). Repeating a key within your workspace returns the existing comparison (`idempotentReplay: true`) without charging again. Use it to make retries safe.
|
|
273
289
|
|
|
290
|
+
The key is bound to the request it was first used with (file contents or version ids, `comparison_mode`, `accuracy_mode`). Sending the same key with different inputs returns `422 IDEMPOTENCY_KEY_REUSED` and the id of the earlier comparison; use a fresh key for a new comparison. Keys do not expire.
|
|
291
|
+
|
|
274
292
|
### Rate limits
|
|
275
293
|
|
|
276
|
-
All limits are per workspace. Exceeding
|
|
294
|
+
All limits are per workspace. Exceeding a rate limit returns `429` with `code: "RATE_LIMITED"`, a `retryAfter` field (seconds), and a `Retry-After` header. `429 EXPORT_QUEUE_FULL` carries `retryAfter: 30` in the body.
|
|
277
295
|
|
|
278
296
|
| Endpoint | Limit |
|
|
279
297
|
|----------|-------|
|
|
@@ -292,20 +310,27 @@ All limits are per workspace. Exceeding one returns `429` with `code: "RATE_LIMI
|
|
|
292
310
|
|
|
293
311
|
### Error codes
|
|
294
312
|
|
|
313
|
+
Every error response carries a machine-readable `code`:
|
|
314
|
+
|
|
295
315
|
| HTTP | `code` | Meaning |
|
|
296
316
|
|------|--------|---------|
|
|
297
|
-
| 400 | |
|
|
298
|
-
| 400 | `INVALID_FILE` |
|
|
317
|
+
| 400 | `INVALID_REQUEST` | Malformed body or parameters: bad JSON, missing files or fields, invalid UUIDs, out-of-range `limit` |
|
|
318
|
+
| 400 | `INVALID_FILE` | Unsupported file type, content does not match its extension (magic-byte check), or upload verification failed |
|
|
299
319
|
| 400 | `FILE_TOO_LARGE` | Declared file size exceeds the 45 MB per-file limit |
|
|
300
320
|
| 400 | `VERSION_FAILED` / `VERSION_NOT_PENDING` | Referenced version cannot be used (processing failed, or already completed) |
|
|
301
|
-
|
|
|
321
|
+
| 400 | `VERSION_NOT_COMPLETED` | Referenced version was created but its upload was never completed; PUT the file and call `/api/v1/uploads/complete` first |
|
|
322
|
+
| 401 | `UNAUTHORIZED` | Missing or invalid API key |
|
|
302
323
|
| 402 | `NO_CREDITS` | No free comparison left and no credits remaining; the response includes `buyUrl` (https://www.differino.com/billing?pack=comparino) |
|
|
303
|
-
| 404 | `
|
|
324
|
+
| 404 | `NOT_FOUND` | Comparison or export job not found in your workspace |
|
|
325
|
+
| 404 | `VERSION_NOT_FOUND` | Version not found in your workspace |
|
|
304
326
|
| 413 | `PAYLOAD_TOO_LARGE` | Multipart body exceeds ~4.5 MB total; use the signed-upload flow |
|
|
327
|
+
| 422 | `IDEMPOTENCY_KEY_REUSED` | Same `Idempotency-Key` with different files, versions, mode or accuracy; the response includes the earlier `comparisonId` |
|
|
305
328
|
| 429 | `RATE_LIMITED` | Rate limit exceeded; retry after `retryAfter` seconds |
|
|
306
|
-
| 429 | `EXPORT_QUEUE_FULL` | More than 5 export jobs pending/processing in your workspace |
|
|
329
|
+
| 429 | `EXPORT_QUEUE_FULL` | More than 5 export jobs pending/processing in your workspace; `retryAfter: 30` |
|
|
330
|
+
| 500 | `INTERNAL_ERROR` / `EXTRACTION_FAILED` / `COMPARISON_FAILED` | Server-side failure; metering consumed for a comparison that did not complete is refunded |
|
|
331
|
+
| 504 | `EXTRACTION_TIMEOUT` | Text extraction did not finish within the request window; metering refunded |
|
|
307
332
|
|
|
308
|
-
|
|
333
|
+
The MCP server prefixes the code to error messages (for example `[NO_CREDITS] No credits remaining...`) and appends `retryAfter` / `buyUrl` details when the API provides them.
|
|
309
334
|
|
|
310
335
|
## Supported Formats
|
|
311
336
|
|
|
@@ -317,3 +342,11 @@ When present, the `code` field is machine-readable; the MCP server prefixes it t
|
|
|
317
342
|
|
|
318
343
|
Every account gets 1 free comparison (lifetime, not monthly). After that, each comparison costs 1 credit. Checking results, listing comparisons, exporting PDFs, and reading diffs are always free.
|
|
319
344
|
Buy credit packs at [differino.com/pricing](https://www.differino.com/pricing).
|
|
345
|
+
|
|
346
|
+
## About this repository
|
|
347
|
+
|
|
348
|
+
This repo contains the source of the `differino-mcp` npm package: a thin MCP client for the [Differino](https://www.differino.com) REST API. The Differino comparison engine, web app, and infrastructure are proprietary and not part of this repository. Using the API consumes Differino credits; see [pricing](https://www.differino.com/pricing).
|
|
349
|
+
|
|
350
|
+
## License
|
|
351
|
+
|
|
352
|
+
MIT (this MCP client only).
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,22 @@ const path = __importStar(require("path"));
|
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
const API_KEY = process.env.DIFFERINO_API_KEY || '';
|
|
46
46
|
const BASE_URL = process.env.DIFFERINO_URL || 'https://www.differino.com';
|
|
47
|
+
// The version announced to MCP clients is the one in package.json, so a
|
|
48
|
+
// release only has to bump one place. The literal is a fallback for unusual
|
|
49
|
+
// installs where the file is not next to dist/.
|
|
50
|
+
const FALLBACK_VERSION = '0.5.3';
|
|
51
|
+
function readPackageVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
54
|
+
if (typeof pkg?.version === 'string' && pkg.version)
|
|
55
|
+
return pkg.version;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// fall through
|
|
59
|
+
}
|
|
60
|
+
return FALLBACK_VERSION;
|
|
61
|
+
}
|
|
62
|
+
const SERVER_VERSION = readPackageVersion();
|
|
47
63
|
const REQUEST_TIMEOUT_MS = 300_000; // 5 minutes: compare can wait on extraction + diff
|
|
48
64
|
const EXPORT_POLL_INTERVAL_MS = 2_000;
|
|
49
65
|
const EXPORT_POLL_TIMEOUT_MS = 180_000; // 3 minutes
|
|
@@ -155,6 +171,46 @@ async function putToSignedUrl(uploadUrl, buffer, mimeType, label) {
|
|
|
155
171
|
throw new Error(`Uploading ${label} to storage failed (HTTP ${res.status}): ${body.slice(0, 200)}`);
|
|
156
172
|
}
|
|
157
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* compare_documents used to return the whole REST response, including the
|
|
176
|
+
* visual manifest with up to four signed URLs per page: hundreds of KB of
|
|
177
|
+
* tokens on long documents, all of it retrievable later with get_comparison.
|
|
178
|
+
* Return the decision-relevant part and point at the cheaper follow-up tools.
|
|
179
|
+
*/
|
|
180
|
+
function summarizeCompareResult(result) {
|
|
181
|
+
const visual = result?.visual ?? {};
|
|
182
|
+
const manifest = visual.manifest;
|
|
183
|
+
const status = result?.status ?? 'unknown';
|
|
184
|
+
const out = {
|
|
185
|
+
id: result?.id ?? null,
|
|
186
|
+
status,
|
|
187
|
+
url: result?.url ?? null,
|
|
188
|
+
comparisonMode: result?.comparisonMode ?? null,
|
|
189
|
+
summary: result?.summary ?? null,
|
|
190
|
+
visual: {
|
|
191
|
+
status: visual.status ?? result?.visualStatus ?? 'unknown',
|
|
192
|
+
totalChanges: manifest?.totalChanges ?? null,
|
|
193
|
+
changedPageCount: manifest?.changedPageCount ?? null,
|
|
194
|
+
pageCount: Array.isArray(manifest?.pages) ? manifest.pages.length : null,
|
|
195
|
+
},
|
|
196
|
+
creditsRemaining: result?.creditsRemaining ?? null,
|
|
197
|
+
freeComparisonRemaining: result?.freeComparisonRemaining ?? null,
|
|
198
|
+
};
|
|
199
|
+
if (result?.idempotentReplay)
|
|
200
|
+
out.idempotentReplay = true;
|
|
201
|
+
if (result?.message)
|
|
202
|
+
out.message = result.message;
|
|
203
|
+
out.next =
|
|
204
|
+
status === 'ready'
|
|
205
|
+
? [
|
|
206
|
+
'summarize_visual_diff: cheapest page-by-page overview of what changed (visual comparisons)',
|
|
207
|
+
'get_text_diff: exact text of the changed blocks',
|
|
208
|
+
'get_comparison: full response contract including page image URLs',
|
|
209
|
+
'export_comparison_pdf: downloadable report',
|
|
210
|
+
]
|
|
211
|
+
: ['get_comparison: poll with this id until status is ready'];
|
|
212
|
+
return JSON.stringify(out, null, 2);
|
|
213
|
+
}
|
|
158
214
|
async function compareDocuments(args) {
|
|
159
215
|
const { file_a_path, file_b_path, comparison_mode = 'visual' } = args;
|
|
160
216
|
// Validate files exist
|
|
@@ -198,7 +254,7 @@ async function compareDocuments(args) {
|
|
|
198
254
|
method: 'POST',
|
|
199
255
|
body: form,
|
|
200
256
|
});
|
|
201
|
-
return
|
|
257
|
+
return summarizeCompareResult(result);
|
|
202
258
|
}
|
|
203
259
|
async function compareViaSignedUploads(args) {
|
|
204
260
|
const { nameA, nameB, extA, extB, fileABuffer, fileBBuffer, comparison_mode } = args;
|
|
@@ -236,7 +292,7 @@ async function compareViaSignedUploads(args) {
|
|
|
236
292
|
comparisonMode: comparison_mode,
|
|
237
293
|
}),
|
|
238
294
|
});
|
|
239
|
-
return
|
|
295
|
+
return summarizeCompareResult(result);
|
|
240
296
|
}
|
|
241
297
|
async function getBalance() {
|
|
242
298
|
const result = await apiCall('/api/v1/me');
|
|
@@ -438,13 +494,13 @@ async function getTextDiff(args) {
|
|
|
438
494
|
// ---------------------------------------------------------------------------
|
|
439
495
|
// MCP Server
|
|
440
496
|
// ---------------------------------------------------------------------------
|
|
441
|
-
const server = new index_js_1.Server({ name: 'differino', version:
|
|
497
|
+
const server = new index_js_1.Server({ name: 'differino', version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
442
498
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
443
499
|
tools: [
|
|
444
500
|
{
|
|
445
501
|
name: 'compare_documents',
|
|
446
502
|
description: 'Compare two documents (PDF, DOCX, or TXT) and see the differences. ' +
|
|
447
|
-
'Reads files from local disk, uploads them to Differino, and returns a
|
|
503
|
+
'Reads files from local disk, uploads them to Differino, and returns a compact result: comparison id and URL, summary counters (added/removed/modified), visual.status with totalChanges and changedPageCount, and the remaining balance. It does NOT include the page-by-page manifest; call summarize_visual_diff for a cheap overview of the changes, get_text_diff for exact text, or get_comparison for the full contract with page image URLs. Files up to 45 MB each are supported: large files are uploaded automatically via signed upload URLs. Use comparison_mode="visual" for the full document-review pipeline (page renders, red/green regions, groupId, salience); use comparison_mode="text" for faster text-only comparison. ' +
|
|
448
504
|
'If the response has status="processing", the comparison is still running: poll get_comparison with the returned id until status="ready". Consumes the free comparison or 1 credit; the response includes creditsRemaining and freeComparisonRemaining.',
|
|
449
505
|
inputSchema: {
|
|
450
506
|
type: 'object',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "differino-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "MCP server for Differino: compare PDF, DOCX, and TXT documents from any AI agent",
|
|
5
5
|
"packageManager": "pnpm@9.0.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"start": "node dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"typescript": "^5.3.0",
|
|
@@ -33,5 +33,9 @@
|
|
|
33
33
|
"pdf",
|
|
34
34
|
"docx",
|
|
35
35
|
"differino"
|
|
36
|
-
]
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/sweetcucumbersalad/differino-mcp.git"
|
|
40
|
+
}
|
|
37
41
|
}
|