omitly-mcp 0.0.0 → 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,582 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Omitly MCP server.
4
+ *
5
+ * Exposes Omitly's local, verifiable PDF redaction to MCP clients (Claude Code,
6
+ * Claude Desktop, etc.) as callable tools. The whole point: an agent can redact a
7
+ * document *without uploading it anywhere* — redaction runs on-device via the
8
+ * Omitly engine and returns a signed audit log proving the data is gone.
9
+ *
10
+ * The natural flow across the three tools:
11
+ * find_sensitive_regions → the engine extracts text WITH coordinates and
12
+ * returns PII candidates, so the model works in entity space ("redact every
13
+ * SSN") and never has to guess PDF point geometry from a rendered image.
14
+ * redact_pdf → remove the chosen regions' bytes, verify, audit.
15
+ * verify_redaction → independently re-confirm the output.
16
+ *
17
+ * Integration point — the engine binary:
18
+ * This server shells out to a local CLI (`omitly-redact`, in
19
+ * crates/omitly-cli) that wraps the `redaction-core` Rust crate. Build it with
20
+ * `cargo build -p omitly-cli` and point OMITLY_REDACT_BIN at the binary. The
21
+ * contract is documented in README.md and in `runEngine()` below: a JSON
22
+ * request on stdin, a JSON response (result + audit log) on stdout.
23
+ */
24
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
25
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
+ import { spawn } from "node:child_process";
27
+ import { existsSync, readFileSync } from "node:fs";
28
+ import { createRequire } from "node:module";
29
+ import * as path from "node:path";
30
+ import { z } from "zod";
31
+ import { allowedRoot, confineInput, confineOutput } from "./paths.js";
32
+ /**
33
+ * WASM fallback for the four detection/verify tools — bundled in the npm
34
+ * package (see package.json "files"/"build:wasm"), so `find_sensitive_regions`,
35
+ * `locate_text`, `check_redaction` and `verify_redaction` work out of the box
36
+ * with NO native engine binary and NO Rust toolchain on the caller's machine.
37
+ * This is the wasm-bindgen twin of `redaction-core`'s detector (same crate
38
+ * already shipped for the web leak-checker at omitly.app) — detection-only,
39
+ * no qpdf/process/filesystem, so it crosses to wasm cleanly.
40
+ *
41
+ * `redact_pdf`/`redact_by_entity`/`create_pdf` still require a configured
42
+ * native engine (ENGINE_BIN/PDF_BIN below) — write access and PDF generation
43
+ * (a real headless browser) can't run in wasm. When a native engine IS
44
+ * configured, it's preferred over wasm for the four tools too (more complete:
45
+ * the native `find` path also covers survivor/off-page detection the exact
46
+ * same way; wasm and native currently agree on output shape for this reason).
47
+ */
48
+ const wasmRequire = createRequire(import.meta.url);
49
+ let wasmEngine;
50
+ try {
51
+ wasmEngine = wasmRequire("../wasm/leakcheck_wasm.js");
52
+ }
53
+ catch {
54
+ // Not built (e.g. local dev before `npm run build`) — tools below fail
55
+ // loudly with a clear message rather than silently no-op.
56
+ wasmEngine = undefined;
57
+ }
58
+ /** Normalises the wasm `ScanResult` to the same field names the native `find`
59
+ * response already uses (`regions` instead of `leaks`), so every downstream
60
+ * formatter in this file works unchanged regardless of which engine ran. */
61
+ function wasmScanToFindShape(raw) {
62
+ const r = JSON.parse(raw);
63
+ if (!r.ok)
64
+ return { ok: false, error: r.error ?? "wasm scan failed" };
65
+ return {
66
+ ok: true,
67
+ clean: r.clean,
68
+ count: r.count,
69
+ total_findings: r.total_findings,
70
+ regions: r.leaks,
71
+ survivors: r.survivors,
72
+ off_page: r.off_page,
73
+ coverage: r.coverage,
74
+ };
75
+ }
76
+ function runWasmScan(bytes) {
77
+ if (!wasmEngine) {
78
+ return { ok: false, error: "bundled wasm engine not built — run `npm run build` (needs wasm-pack + Rust)" };
79
+ }
80
+ return wasmScanToFindShape(wasmEngine.scan(new Uint8Array(bytes)));
81
+ }
82
+ function runWasmLocate(bytes, needles) {
83
+ if (!wasmEngine) {
84
+ return { ok: false, error: "bundled wasm engine not built — run `npm run build` (needs wasm-pack + Rust)" };
85
+ }
86
+ const raw = JSON.parse(wasmEngine.locate(new Uint8Array(bytes), JSON.stringify(needles)));
87
+ if (!raw.ok)
88
+ return { ok: false, error: raw.error ?? "wasm locate failed" };
89
+ return { ok: true, count: raw.count, regions: raw.leaks };
90
+ }
91
+ /** Single source of truth for the server version (keeps the MCP handshake in
92
+ * lockstep with the published package). */
93
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
94
+ /**
95
+ * Engine discovery — one env var for the common case, two for overrides:
96
+ * OMITLY_ENGINE_DIR directory holding both `omitly-redact` and `omitly-pdf`
97
+ * OMITLY_REDACT_BIN / OMITLY_PDF_BIN per-binary overrides (win over the dir)
98
+ * Two binaries by design: document generation is a different trust model from
99
+ * the verifiable redaction engine (see crates/omitly-pdf/Cargo.toml).
100
+ */
101
+ function discover(explicit, name) {
102
+ if (explicit)
103
+ return explicit;
104
+ const dir = process.env.OMITLY_ENGINE_DIR;
105
+ if (!dir)
106
+ return undefined;
107
+ for (const cand of [path.join(dir, name), path.join(dir, `${name}.exe`)]) {
108
+ if (existsSync(cand))
109
+ return cand;
110
+ }
111
+ return undefined;
112
+ }
113
+ const ENGINE_BIN = discover(process.env.OMITLY_REDACT_BIN, "omitly-redact");
114
+ const PDF_BIN = discover(process.env.OMITLY_PDF_BIN, "omitly-pdf");
115
+ /** Kill a wedged engine rather than hang the agent's tool call forever. */
116
+ const ENGINE_TIMEOUT_MS = Number(process.env.OMITLY_ENGINE_TIMEOUT_MS) || 120_000;
117
+ /** Filesystem confinement root (see paths.ts) — resolved once at startup so a
118
+ * misconfigured OMITLY_ALLOWED_DIR fails loudly here, not mid-conversation. */
119
+ const ROOT = allowedRoot();
120
+ const regionSchema = z.object({
121
+ page: z.number().int().min(0).describe("0-based page index"),
122
+ x: z.number().describe("left, in PDF points (origin bottom-left)"),
123
+ y: z.number().describe("bottom, in PDF points"),
124
+ width: z.number().positive(),
125
+ height: z.number().positive(),
126
+ reason: z.string().optional().describe("audit-log reason, e.g. 'PII: SSN'"),
127
+ });
128
+ /**
129
+ * Invoke the local Omitly engine CLI with a JSON request on stdin and parse the
130
+ * JSON response from stdout. Contract (proposed for the redaction-core CLI):
131
+ *
132
+ * stdin: { "command": "redact" | "verify", ...args }
133
+ * stdout: { "ok": true, "output": "<path>", "audit": { ...signed audit log } }
134
+ * { "ok": false, "error": "<message>" }
135
+ */
136
+ function spawnEngine(bin, request) {
137
+ return new Promise((resolve, reject) => {
138
+ const child = spawn(bin, [], { stdio: ["pipe", "pipe", "pipe"] });
139
+ let out = "";
140
+ let err = "";
141
+ const timer = setTimeout(() => {
142
+ child.kill("SIGKILL");
143
+ reject(new Error(`engine timed out after ${ENGINE_TIMEOUT_MS}ms and was killed ` +
144
+ `(override with OMITLY_ENGINE_TIMEOUT_MS)`));
145
+ }, ENGINE_TIMEOUT_MS);
146
+ child.stdout.on("data", (d) => (out += d.toString()));
147
+ child.stderr.on("data", (d) => (err += d.toString()));
148
+ child.stdin.on("error", () => { }); // EPIPE if the engine dies early; close handles it
149
+ child.on("error", (e) => {
150
+ clearTimeout(timer);
151
+ reject(e);
152
+ });
153
+ child.on("close", (code) => {
154
+ clearTimeout(timer);
155
+ if (code !== 0) {
156
+ reject(new Error(`engine exited ${code}: ${err.trim() || "no stderr"}`));
157
+ return;
158
+ }
159
+ try {
160
+ resolve(JSON.parse(out));
161
+ }
162
+ catch {
163
+ reject(new Error(`engine returned non-JSON output: ${out.slice(0, 500)}`));
164
+ }
165
+ });
166
+ child.stdin.write(JSON.stringify(request));
167
+ child.stdin.end();
168
+ });
169
+ }
170
+ /** Drive the redaction engine (`OMITLY_REDACT_BIN`). */
171
+ function runEngine(request) {
172
+ if (!ENGINE_BIN) {
173
+ return Promise.reject(new Error("No redaction engine configured. Set OMITLY_ENGINE_DIR to the directory " +
174
+ "holding the omitly-redact binary (or OMITLY_REDACT_BIN to the binary " +
175
+ "itself). Redaction runs on-device; nothing is uploaded."));
176
+ }
177
+ return spawnEngine(ENGINE_BIN, request);
178
+ }
179
+ /** Drive the document generator (`OMITLY_PDF_BIN`). */
180
+ function runPdfEngine(request) {
181
+ if (!PDF_BIN) {
182
+ return Promise.reject(new Error("No PDF generator configured. Set OMITLY_ENGINE_DIR to the directory " +
183
+ "holding the omitly-pdf binary (or OMITLY_PDF_BIN to the binary itself). " +
184
+ "PDF generation renders on-device; nothing is uploaded."));
185
+ }
186
+ return spawnEngine(PDF_BIN, request);
187
+ }
188
+ const server = new McpServer({ name: "omitly-mcp", version: VERSION });
189
+ const REGIONS = z.enum(["generic", "us", "au"]);
190
+ /** Run "find" via the native engine if configured, else the bundled wasm
191
+ * fallback (see the wasm block near the top of this file). `regions`
192
+ * narrowing (generic/us/au) only applies to the native path — wasm always
193
+ * scans every pattern, a safe default (more results, never fewer) rather
194
+ * than silently dropping the requested narrowing. */
195
+ function findViaEngineOrWasm(confinedPath, regions) {
196
+ if (ENGINE_BIN)
197
+ return runEngine({ command: "find", pdfPath: confinedPath, regions });
198
+ const res = runWasmScan(readFileSync(confinedPath));
199
+ if (res.ok && regions && regions.length) {
200
+ res.note = "Regional narrowing (regions filter) needs a native engine — showing all detected kinds.";
201
+ }
202
+ return res;
203
+ }
204
+ server.tool("find_sensitive_regions", "Scan a PDF on-device and return candidate regions that look like PII — " +
205
+ "emails, US SSNs, phone numbers, card numbers, and Australian identifiers " +
206
+ "(TFN, ABN, ACN, Medicare, Centrelink CRN, IHI, BSB; kinds 'tfn'/'abn'/" +
207
+ "'acn'/'medicare'/'crn'/'ihi'/'bsb') — each with the page and exact " +
208
+ "coordinates (in PDF " +
209
+ "points) the redaction engine needs. Use this FIRST so you select regions " +
210
+ "by entity ('redact every TFN') and pass the returned coordinates straight " +
211
+ "to redact_pdf, instead of guessing geometry from a rendered page. Numeric " +
212
+ "kinds are check-digit validated where a published algorithm exists (CRN " +
213
+ "has none — its matches are format-only). Candidates are best-effort " +
214
+ "pattern matches for review — not a completeness guarantee and not a " +
215
+ "compliance assessment; the file is never uploaded — detection runs " +
216
+ "locally. Each candidate carries a MASKED preview (e.g. '•••-••-6789'), " +
217
+ "never the raw value: the secret stays on the machine. You don't need the " +
218
+ "plaintext to redact — drive it by page + coordinates. (A human reviewer " +
219
+ "has the file open locally for full context.)", {
220
+ pdfPath: z.string().describe("absolute path to the PDF to scan"),
221
+ regions: z
222
+ .array(REGIONS)
223
+ .optional()
224
+ .describe("narrow LISTED pattern kinds to these regional packs (generic kinds " +
225
+ "always listed; confirmed under-mark survivors always report); omit " +
226
+ "to scan everything — the safe default"),
227
+ }, async ({ pdfPath, regions }) => {
228
+ try {
229
+ const res = await findViaEngineOrWasm(confineInput(pdfPath, ROOT), regions);
230
+ if (!res?.ok) {
231
+ return {
232
+ content: [{ type: "text", text: `Scan failed: ${res?.error ?? "unknown error"}` }],
233
+ isError: true,
234
+ };
235
+ }
236
+ const found = res.regions ?? [];
237
+ const summary = `Found ${found.length} candidate region(s).\n` +
238
+ `Pass any subset to redact_pdf as its "regions" argument (page + x/y/width/height carry over).\n\n` +
239
+ (res.note ? `Note: ${res.note}\n\n` : "") +
240
+ JSON.stringify(found, null, 2);
241
+ return { content: [{ type: "text", text: summary }] };
242
+ }
243
+ catch (e) {
244
+ return {
245
+ content: [{ type: "text", text: `Could not scan: ${e.message}` }],
246
+ isError: true,
247
+ };
248
+ }
249
+ });
250
+ server.tool("locate_text", "Locate exact text strings in a PDF and return each occurrence's page and " +
251
+ "coordinates (in PDF points). Use this for what pattern-matching can't catch " +
252
+ "— names, addresses, account references — by doing the entity recognition " +
253
+ "YOURSELF and passing the literal strings here; the engine resolves where " +
254
+ "they sit so you never guess geometry from a rendered page. Feed the returned " +
255
+ "regions straight to redact_pdf. Case-insensitive; a string the PDF splits " +
256
+ "across text operators may not match as one run. Each hit returns a masked " +
257
+ "preview, not the raw text. Nothing is uploaded.", {
258
+ pdfPath: z.string().describe("absolute path to the PDF to search"),
259
+ texts: z.array(z.string()).min(1).describe("literal strings to locate"),
260
+ }, async ({ pdfPath, texts }) => {
261
+ try {
262
+ const confined = confineInput(pdfPath, ROOT);
263
+ const res = ENGINE_BIN
264
+ ? await runEngine({ command: "locate_text", pdfPath: confined, texts })
265
+ : runWasmLocate(readFileSync(confined), texts);
266
+ if (!res?.ok) {
267
+ return {
268
+ content: [{ type: "text", text: `Search failed: ${res?.error ?? "unknown error"}` }],
269
+ isError: true,
270
+ };
271
+ }
272
+ const regions = res.regions ?? [];
273
+ const summary = `Located ${regions.length} occurrence(s). Pass any subset to redact_pdf as "regions".\n\n` +
274
+ JSON.stringify(regions, null, 2);
275
+ return { content: [{ type: "text", text: summary }] };
276
+ }
277
+ catch (e) {
278
+ return {
279
+ content: [{ type: "text", text: `Could not search: ${e.message}` }],
280
+ isError: true,
281
+ };
282
+ }
283
+ });
284
+ server.tool("redact_by_entity", "Find and redact PII in a PDF in ONE on-device step: scan, keep only the " +
285
+ "requested entity kinds (email/ssn/phone/card plus the Australian " +
286
+ "tfn/abn/acn/medicare/crn/ihi/bsb — omit `kinds` to redact every kind " +
287
+ "detected), remove them, verify, and return what was redacted plus the " +
288
+ "audit log. This is the 'just scrub the obvious PII' shortcut; when you need " +
289
+ "to review before removing, call find_sensitive_regions first. Same caveat as " +
290
+ "the detector: matches are best-effort pattern matching, not a completeness " +
291
+ "guarantee and not a compliance assessment. Nothing is uploaded.", {
292
+ pdfPath: z.string().describe("absolute path to the source PDF"),
293
+ outputPath: z.string().describe("absolute path to write the redacted PDF"),
294
+ kinds: z
295
+ .array(z.enum([
296
+ "email",
297
+ "ssn",
298
+ "phone",
299
+ "card",
300
+ "tfn",
301
+ "abn",
302
+ "acn",
303
+ "medicare",
304
+ "crn",
305
+ "ihi",
306
+ "bsb",
307
+ ]))
308
+ .optional()
309
+ .describe("entity kinds to redact; omit to redact all detected"),
310
+ regions: z
311
+ .array(REGIONS)
312
+ .optional()
313
+ .describe("narrow to these regional packs (intersects with `kinds`); omit for all"),
314
+ drawBox: z.boolean().optional().describe("also paint a black bar (default: opaque fill only)"),
315
+ }, async ({ pdfPath, outputPath, kinds, regions, drawBox }) => {
316
+ try {
317
+ const res = await runEngine({
318
+ command: "redact_entities",
319
+ pdfPath: confineInput(pdfPath, ROOT),
320
+ outputPath: confineOutput(outputPath, ROOT),
321
+ kinds,
322
+ regions,
323
+ drawBox,
324
+ });
325
+ if (!res?.ok) {
326
+ return {
327
+ content: [{ type: "text", text: `Redaction failed: ${res?.error ?? "unknown error"}` }],
328
+ isError: true,
329
+ };
330
+ }
331
+ const redacted = res.redacted ?? [];
332
+ if (redacted.length === 0) {
333
+ return {
334
+ content: [{ type: "text", text: `No matching entities found — nothing was written.` }],
335
+ };
336
+ }
337
+ return {
338
+ content: [
339
+ {
340
+ type: "text",
341
+ text: `Redacted ${redacted.length} entit${redacted.length === 1 ? "y" : "ies"} → ${res.output}\n` +
342
+ `Verification: ${res.audit?.verdict ?? "see audit log"}\n\n` +
343
+ `Removed:\n${JSON.stringify(redacted, null, 2)}\n\n` +
344
+ `Audit log:\n${JSON.stringify(res.audit, null, 2)}`,
345
+ },
346
+ ],
347
+ };
348
+ }
349
+ catch (e) {
350
+ return {
351
+ content: [{ type: "text", text: `Could not run redaction: ${e.message}` }],
352
+ isError: true,
353
+ };
354
+ }
355
+ });
356
+ server.tool("redact_pdf", "Permanently redact regions of a PDF on-device using Omitly. Removes the " +
357
+ "underlying text and image data (not a black box over it), verifies nothing " +
358
+ "survives in each region, and returns a signed audit log. The file is never " +
359
+ "uploaded — redaction happens locally.", {
360
+ pdfPath: z.string().describe("absolute path to the source PDF"),
361
+ outputPath: z.string().describe("absolute path to write the redacted PDF"),
362
+ regions: z.array(regionSchema).min(1).describe("regions to remove"),
363
+ }, async ({ pdfPath, outputPath, regions }) => {
364
+ try {
365
+ const res = await runEngine({
366
+ command: "redact",
367
+ pdfPath: confineInput(pdfPath, ROOT),
368
+ outputPath: confineOutput(outputPath, ROOT),
369
+ regions,
370
+ });
371
+ if (!res?.ok) {
372
+ return {
373
+ content: [{ type: "text", text: `Redaction failed: ${res?.error ?? "unknown error"}` }],
374
+ isError: true,
375
+ };
376
+ }
377
+ return {
378
+ content: [
379
+ {
380
+ type: "text",
381
+ text: `Redacted ${regions.length} region(s) → ${res.output}\n` +
382
+ `Verification: ${res.audit?.verdict ?? "see audit log"}\n\n` +
383
+ `Audit log:\n${JSON.stringify(res.audit, null, 2)}`,
384
+ },
385
+ ],
386
+ };
387
+ }
388
+ catch (e) {
389
+ return {
390
+ content: [{ type: "text", text: `Could not run redaction: ${e.message}` }],
391
+ isError: true,
392
+ };
393
+ }
394
+ });
395
+ server.tool("verify_redaction", "Re-scan an already-redacted PDF on-device and confirm nothing recoverable " +
396
+ "remains. With a configured native engine and this file's own " +
397
+ "`<path>.audit.json` sidecar (written by redact_pdf/redact_by_entity), this " +
398
+ "re-checks exactly the regions that were redacted — the strongest form of " +
399
+ "this check. Without a native engine (or without that sidecar — e.g. the " +
400
+ "file wasn't redacted by this tool), it falls back to a general on-device " +
401
+ "re-scan of the whole file and reports whether anything is still " +
402
+ "detectable — a good-faith re-check, not a claim of the same rigor as the " +
403
+ "sidecar-based path.", {
404
+ pdfPath: z.string().describe("absolute path to the redacted PDF to verify"),
405
+ }, async ({ pdfPath }) => {
406
+ try {
407
+ const confined = confineInput(pdfPath, ROOT);
408
+ if (ENGINE_BIN) {
409
+ const hasSidecar = existsSync(`${confined}.audit.json`);
410
+ if (hasSidecar) {
411
+ const res = await runEngine({ command: "verify", pdfPath: confined });
412
+ return {
413
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
414
+ isError: res?.ok === false,
415
+ };
416
+ }
417
+ }
418
+ // Fallback: general re-scan (wasm if no native engine; the native `find`
419
+ // command otherwise) — no sidecar, so nothing to check regions against,
420
+ // but a non-empty result still means recoverable PII survived.
421
+ const res = ENGINE_BIN
422
+ ? await runEngine({ command: "find", pdfPath: confined })
423
+ : runWasmScan(readFileSync(confined));
424
+ if (!res?.ok) {
425
+ return {
426
+ content: [{ type: "text", text: `Could not verify: ${res?.error ?? "unknown error"}` }],
427
+ isError: true,
428
+ };
429
+ }
430
+ const clean = res.clean ?? (res.total_findings ?? 0) === 0;
431
+ const summary = clean
432
+ ? `✅ No recoverable PII found on the surfaces scanned (general re-scan — no redaction sidecar to check specific regions against).`
433
+ : `⚠️ Found ${res.total_findings ?? res.regions?.length ?? 0} recoverable item(s) — this file is not clean.\n\n${JSON.stringify(res, null, 2)}`;
434
+ return { content: [{ type: "text", text: summary }], isError: !clean };
435
+ }
436
+ catch (e) {
437
+ return {
438
+ content: [{ type: "text", text: `Could not verify: ${e.message}` }],
439
+ isError: true,
440
+ };
441
+ }
442
+ });
443
+ server.tool("create_pdf", "Generate a clean PDF from Markdown (or raw HTML) on-device, rendered through " +
444
+ "a real browser engine so it looks printed — not like a script's best guess. " +
445
+ "Give it Markdown inline via `source` (or a file via `sourcePath`) and an " +
446
+ "`outputPath`; it writes the PDF and returns the path. Use this instead of " +
447
+ "writing a one-off reportlab/LaTeX/pandoc script. Nothing is uploaded.", {
448
+ outputPath: z.string().describe("absolute path to write the PDF"),
449
+ source: z.string().optional().describe("inline Markdown/HTML (omit if using sourcePath)"),
450
+ sourcePath: z.string().optional().describe("absolute path to a Markdown/HTML file"),
451
+ format: z.enum(["markdown", "html"]).optional().describe("input format (default: markdown)"),
452
+ title: z.string().optional().describe("document <title> / metadata"),
453
+ css: z.string().optional().describe("extra CSS appended after the default print styles"),
454
+ }, async ({ outputPath, source, sourcePath, format, title, css }) => {
455
+ try {
456
+ const res = await runPdfEngine({
457
+ command: "create",
458
+ outputPath: confineOutput(outputPath, ROOT),
459
+ source,
460
+ sourcePath: sourcePath === undefined ? undefined : confineInput(sourcePath, ROOT),
461
+ format,
462
+ title,
463
+ css,
464
+ });
465
+ if (!res?.ok) {
466
+ return {
467
+ content: [{ type: "text", text: `PDF generation failed: ${res?.error ?? "unknown error"}` }],
468
+ isError: true,
469
+ };
470
+ }
471
+ return { content: [{ type: "text", text: `Created PDF → ${res.output}` }] };
472
+ }
473
+ catch (e) {
474
+ return {
475
+ content: [{ type: "text", text: `Could not generate PDF: ${e.message}` }],
476
+ isError: true,
477
+ };
478
+ }
479
+ });
480
+ server.tool("check_redaction", "Audit an ALREADY-redacted PDF and report whether sensitive text still survives " +
481
+ "underneath the redaction — the 'did my black boxes actually remove the data?' " +
482
+ "check. Most tools redact by drawing a rectangle over text while leaving the " +
483
+ "characters in the file, where they stay selectable and extractable. This " +
484
+ "re-extracts the text on-device and flags any emails, SSNs, phone or card " +
485
+ "numbers that are still present, each with a MASKED preview — the raw value " +
486
+ "never leaves the machine. It checks the page text layer, text surviving UNDER " +
487
+ "redaction marks, incremental-update prior revisions (the classic 'redacted then " +
488
+ "saved, original still in the file' failure), document metadata, AcroForm field " +
489
+ "values and embedded attachments, and returns a coverage report so a clean result " +
490
+ "is scoped to what was inspected. A non-empty result means the redaction leaked. " +
491
+ "Nothing is uploaded. (Pattern-based: names/addresses, image-only text, and the " +
492
+ "surfaces listed as not-inspected aren't covered; absence of hits isn't proof of " +
493
+ "completeness.)", {
494
+ pdfPath: z.string().describe("absolute path to the supposedly-redacted PDF to audit"),
495
+ }, async ({ pdfPath }) => {
496
+ try {
497
+ const res = await findViaEngineOrWasm(confineInput(pdfPath, ROOT), undefined);
498
+ if (!res?.ok) {
499
+ return {
500
+ content: [{ type: "text", text: `Audit failed: ${res?.error ?? "unknown error"}` }],
501
+ isError: true,
502
+ };
503
+ }
504
+ const regions = res.regions ?? [];
505
+ const survivors = res.survivors ?? [];
506
+ const offPage = res.off_page ?? [];
507
+ const cov = res.coverage ?? {};
508
+ const total = res.total_findings ?? regions.length;
509
+ const clean = res.clean ?? total === 0;
510
+ // Honest coverage disclosure — what was inspected, and what was NOT, so a
511
+ // "clean" result is scoped and never reads as "no PII anywhere".
512
+ const scanned = [
513
+ `${cov.pages_scanned ?? "?"}/${cov.pages_total ?? "?"} page text layer`,
514
+ cov.prior_revisions_scanned && "prior (superseded) revisions",
515
+ cov.metadata_scanned && "metadata",
516
+ cov.acroform_scanned && "form fields",
517
+ cov.attachments_scanned && "attachments",
518
+ (cov.form_xobjects_scanned ?? 0) > 0 && `${cov.form_xobjects_scanned} Form XObject(s)`,
519
+ (cov.annotation_appearances_scanned ?? 0) > 0 &&
520
+ `${cov.annotation_appearances_scanned} annotation appearance(s)`,
521
+ ].filter(Boolean);
522
+ const notScanned = [
523
+ ...(cov.not_scanned ?? []),
524
+ ...((cov.pages_failed ?? []).length
525
+ ? [`${cov.pages_failed.length} page(s) that could not be parsed`]
526
+ : []),
527
+ ];
528
+ const scope = `Scanned: ${scanned.join(", ")}.` +
529
+ (notScanned.length ? `\nNOT inspected: ${notScanned.join("; ")}.` : "");
530
+ if (clean) {
531
+ return {
532
+ content: [
533
+ {
534
+ type: "text",
535
+ text: `✅ No recoverable PII found on the surfaces scanned in ${pdfPath}.\n\n` +
536
+ `${scope}\n\n` +
537
+ `This is a scoped result, not a completeness guarantee — the surfaces listed as ` +
538
+ `NOT inspected, plus names/addresses (which need human or model review), are out of ` +
539
+ `scope. For a removed-and-verified result with a signed audit log, use the Omitly app.`,
540
+ },
541
+ ],
542
+ };
543
+ }
544
+ const byKind = {};
545
+ for (const r of [...regions, ...survivors, ...offPage])
546
+ byKind[r.kind] = (byKind[r.kind] ?? 0) + 1;
547
+ const tally = Object.entries(byKind)
548
+ .map(([k, n]) => `${n} ${k}${n > 1 ? "s" : ""}`)
549
+ .join(", ");
550
+ const parts = [];
551
+ if (regions.length)
552
+ parts.push(`Text layer (${regions.length}):\n${JSON.stringify(regions, null, 2)}`);
553
+ if (survivors.length)
554
+ parts.push(`Surviving UNDER a redaction mark (${survivors.length}):\n${JSON.stringify(survivors, null, 2)}`);
555
+ if (offPage.length)
556
+ parts.push(`Off-page — prior revisions / metadata / form fields / attachments (${offPage.length}):\n` +
557
+ `${JSON.stringify(offPage, null, 2)}`);
558
+ return {
559
+ content: [
560
+ {
561
+ type: "text",
562
+ text: `⚠️ LEAK: this "redacted" PDF still contains ${total} sensitive item(s) (${tally}) — ` +
563
+ `the redaction did not actually remove the data.\n\n` +
564
+ `${parts.join("\n\n")}\n\n` +
565
+ `${scope}\n\n` +
566
+ `Previews are masked; the raw values stayed on-device. To actually remove this data ` +
567
+ `(not just cover it) and get an independent verification + signed audit log, redact it ` +
568
+ `with Omitly — https://omitly.app`,
569
+ },
570
+ ],
571
+ };
572
+ }
573
+ catch (e) {
574
+ return {
575
+ content: [{ type: "text", text: `Could not audit: ${e.message}` }],
576
+ isError: true,
577
+ };
578
+ }
579
+ });
580
+ const transport = new StdioServerTransport();
581
+ await server.connect(transport);
582
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}