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.
Files changed (3) hide show
  1. package/README.md +46 -13
  2. package/dist/index.js +60 -4
  3. 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": "pnpm",
28
- "args": ["dlx", "differino-mcp"],
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 diff summary plus the visual document-review manifest when available. This is the only tool that consumes a comparison (your free comparison or 1 credit).
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 a summary with added/removed/modified block counts plus a link to the full comparison. Successful responses include `creditsRemaining` and `freeComparisonRemaining`. 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`.
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
- Responses include visual artifact metadata when available:
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 one returns `429` with `code: "RATE_LIMITED"`, a `retryAfter` field (seconds), and a `Retry-After` header.
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 | | Invalid request: missing files, unsupported file type, file too large, or invalid parameters |
298
- | 400 | `INVALID_FILE` | File content does not match its extension (magic-byte check), or upload verification failed |
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
- | 401 | | Missing or invalid API key |
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 | `VERSION_NOT_FOUND` | Comparison, version, or export job not found in your workspace |
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
- When present, the `code` field is machine-readable; the MCP server prefixes it to error messages (for example `[NO_CREDITS] No credits remaining...`) and appends `retryAfter` / `buyUrl` details when the API provides them.
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 JSON.stringify(result, null, 2);
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 JSON.stringify(result, null, 2);
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: '0.5.0' }, { capabilities: { tools: {} } });
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 text diff summary. 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 manifest with page images, red/green change regions, stable groupId values, and salience metadata; use comparison_mode="text" for faster text-only comparison. ' +
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.1",
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.0.0"
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
  }