{
  "markdown": "# omitly-mcp\n\nA [Model Context Protocol](https://modelcontextprotocol.io) server that exposes\nOmitly's **local, verifiable PDF redaction** to AI agents (Claude Code, Claude\nDesktop, and any other MCP client).\n\n> **Repository scope and licence — please read before opening a PR.**\n>\n> This repository is **source-available, not open source**. See [`LICENSE`](LICENSE):\n> the code is published so you can read exactly what runs on your machine before\n> you let it touch a confidential document. It is not licensed for reuse in other\n> projects.\n>\n> It contains the MCP server and the compiled wasm detection bundle. The Omitly\n> redaction engine, the tamper-evidence seal and the licensing implementation are\n> **not** in this repository and are developed privately; this code calls the\n> engine, it does not contain it.\n>\n> Development happens in a private repository and is mirrored here on release, so\n> **pull requests cannot be merged**. Issues and security reports are very welcome\n> — see [`SECURITY.md`](SECURITY.md).\n\nThe point of difference: an agent can redact a document **without uploading it\nanywhere**. Redaction runs on-device through the Omitly engine and returns a\nsigned audit log proving the data was removed — the opposite of pasting a\nconfidential file into a chat model.\n\n**Five of the eleven tools (`find_sensitive_regions`, `locate_text`,\n`check_redaction`, `verify_redaction`, `extract_pdf_text`) work out of the\nbox — `npm install`, no Rust toolchain, no native binary, no desktop app.**\nThey run on a wasm-bindgen build of the same detector that powers the web\nleak-checker at omitly.app, bundled directly in this package. `create_pdf`,\nthe two write tools (`redact_pdf`, `redact_by_entity`), and the two\nseal-verification tools (`verify_seal`, `verify_document`) still need a\nconfigured native engine — see \"Build & run\" below. Neither seal tool has a\nwasm fallback yet: there is no wasm seal-verification path (tracked in\nissue #113), so both always require the native engine, even though checking\na seal needs no licence.\n\n## Tools\n\n| Tool | What it does |\n|------|--------------|\n| `find_sensitive_regions` | Scans a PDF on-device and returns PII candidates — email/SSN/phone/card plus Australian identifiers (TFN, ABN, ACN, Medicare, Centrelink CRN, IHI, BSB; check-digit validated where a published algorithm exists) — with page + exact coordinates, so the agent selects by entity and never guesses geometry. Best-effort pattern matching, not a compliance assessment. Optional `regions` (`generic`/`us`/`au`) narrows the listed kinds. |\n| `locate_text` | Resolves literal strings the model supplies (names, addresses — anything regex can't catch) to their page + coordinates. The model does the recognition; the engine does the geometry. |\n| `check_redaction` | Audits an ALREADY-redacted PDF and reports whether sensitive text still survives underneath the redaction marks, in prior incremental-update revisions, metadata, AcroForm fields, or attachments — the \"did my black boxes actually remove the data?\" check, with a coverage report scoping what was inspected. Free tier (wasm) is EVALUATION-marked and capped to a monthly number of free checks; a configured licensed engine is not capped. |\n| `extract_pdf_text` | Extracts a PDF's full text, page by page, PII-MASKED BY DEFAULT so raw sensitive values never flood the model's context window. Each page's `spans` report the CHAR offset (not byte offset) and kind of every masked value, so an agent can still reason about position without seeing the raw value. `masked: false` is a documented, explicit opt-in to raw text. Free, no licence, works out of the box on the bundled wasm engine — a native engine is preferred when available (also enables the `regions` filter; wasm ignores it and scans every pattern). Never renders pages to images. |\n| `redact_by_entity` | One-shot: find + filter by kind (`email`/`ssn`/`phone`/`card`/`tfn`/`abn`/`acn`/`medicare`/`crn`/`ihi`/`bsb`) and/or `regions` + redact + verify. The \"just scrub the obvious PII\" shortcut. |\n| `redact_pdf` | Removes the underlying data from given regions of a PDF, verifies nothing survives, writes the redacted file, and returns the audit log. |\n| `verify_redaction` | Re-scans an already-redacted PDF and returns the verification verdict — the redaction-completeness check. |\n| `verify_seal` | Cryptographically checks a PDF's embedded Omitly audit report and trailing Ed25519 tamper-evidence seal — the tamper-evidence check, distinct from `verify_redaction`. **Integrity, not identity:** the signing key is per-install and rides inside the file, so a valid seal means \"unchanged since sealed by the holder of this key\", never \"produced by Omitly\" — compare `sealFingerprint` out-of-band for origin. Requires a native engine; no wasm fallback exists. |\n| `verify_document` | Recipient trust-verification (omitly#113): the same seal/report check as `verify_seal` — not a survivor re-scan — aimed at someone who *received* a PDF from someone else and wants to confirm it's authentic and unaltered, without paying or licensing anything. Free, no licence. Currently requires a native engine like `verify_seal` (no wasm seal-verification path yet). |\n| `create_pdf` | Generates a clean PDF from Markdown/HTML on-device, rendered through a real browser engine so it looks printed — instead of writing a throwaway reportlab/LaTeX script. |\n| `check_license` | Reports the current licence or trial state — tier, trial days left, the vendor-signed licensee name, which resolution step supplied the licence, and whether it is bound to this machine. Free, takes no arguments, reads no document, and is re-resolved on every call so buy → save licence → call again works without a restart. **Never returns the device fingerprint or the licence file's contents** — device binding is a yes/no. Requires a native engine: the wasm free tier has no licence concept. |\n\n## PDF generation (`create_pdf`)\n\n`create_pdf` is served by a **separate** binary, `omitly-pdf` (in\n`crates/omitly-pdf`), kept apart from the redaction engine because generation is\na different trust model from verifiable redaction. It renders Markdown (or raw\nHTML) through a headless Chromium-family browser (Chrome/Chromium/Edge/Brave;\noverride with `OMITLY_BROWSER_BIN`) — the same engine family the Omitly app's\nwebview uses, so output looks printed rather than script-generated. `omitly-pdf`\nships with the Omitly desktop application; its source is not in this repository.\nPoint `OMITLY_PDF_BIN` at the binary to enable this tool.\n\n```jsonc\n// stdin\n{ \"command\": \"create\", \"outputPath\": \"/abs/out.pdf\",\n  \"source\": \"# Hello\\n\\nBody **markdown**\", \"format\": \"markdown\", \"title\": \"Hello\" }\n// stdout\n{ \"ok\": true, \"output\": \"/abs/out.pdf\" }\n```\n\nTypical agent flows:\n- Quick: **`redact_by_entity`** (find + redact + verify in one call).\n- Careful: **`find_sensitive_regions` / `locate_text` → review → `redact_pdf` → `verify_redaction`**.\n  Coordinates from `find`/`locate` drop straight into `redact` as its `regions` argument.\n\nSee [DEMO.md](./DEMO.md) for a full Claude Code walkthrough.\n\n## Status\n\nThe MCP surface (eleven tools, schemas, transport), the native engine binary\n(`crates/omitly-cli`, built as `omitly-redact`), and the bundled wasm engine\n(`crates/leakcheck-wasm`, covering the four free tools without a native\nbinary) are all implemented and pass end-to-end tests. `find_sensitive_regions`\nis a first-pass detector (ASCII patterns, per-show-operator matching): treat\nits hits as *candidates for review*, not a completeness guarantee. An LLM can\nalways supply additional regions directly.\n\n**Privacy of findings.** Detection results are returned with a **masked**\npreview (e.g. `•••-••-6789`), never the raw value. The file isn't uploaded *and*\nthe secret detected inside it isn't sent back through the model — redaction is\ndriven entirely by page + coordinates, so the plaintext stays on the machine.\n\n### Engine contract (implemented in `crates/omitly-cli`)\n\nThe server spawns `OMITLY_REDACT_BIN`, writes a JSON request to stdin, and reads\na JSON response from stdout. Any failure returns `{ \"ok\": false, \"error\": \"...\" }`\n(the process still exits 0, so the caller reads `ok` rather than the exit code).\n\n```jsonc\n// stdin\n{ \"command\": \"find\", \"pdfPath\": \"...\" }\n// stdout\n{ \"ok\": true, \"count\": 2, \"regions\": [\n  { \"page\": 0, \"x\": 250.4, \"y\": 610.4, \"width\": 79.2, \"height\": 14.4, \"kind\": \"ssn\", \"preview\": \"•••-••-6789\" } ] }\n// `preview` is masked — the raw value never leaves the process; redaction is driven by coordinates.\n```\n\n```jsonc\n// stdin — \"masked\" omitted ⇒ true (the default); pass \"masked\": false for the\n// documented raw-text opt-in. \"regions\" narrows detected kinds (generic\n// kinds like email/card always apply).\n{ \"command\": \"extract_text\", \"pdfPath\": \"...\" }\n// stdout — \"spans\" offsets are CHAR (not byte) offsets into \"text\", valid\n// against either the masked or the raw text of the same page (masking never\n// changes a page's character count). A page that could not be decoded\n// reports \"contentDecoded\": false with empty text/spans rather than being\n// silently skipped.\n{ \"ok\": true, \"masked\": true, \"pages\": [\n  { \"page\": 0, \"contentDecoded\": true,\n    \"text\": \"Sensitive sample line: SSN •••-••-6789\",\n    \"spans\": [ { \"kind\": \"ssn\", \"start\": 24, \"end\": 35 } ] } ] }\n```\n\n```jsonc\n// stdin\n{ \"command\": \"redact\", \"pdfPath\": \"...\", \"outputPath\": \"...\",\n  \"regions\": [{ \"page\": 0, \"x\": 72, \"y\": 700, \"width\": 200, \"height\": 14, \"reason\": \"PII.SSN\" }] }\n// stdout — also writes \"<outputPath>.audit.json\" beside the file\n{ \"ok\": true, \"output\": \"...\", \"audit\": { \"verdict\": \"pass\", \"regions\": [ ... ], \"warnings\": [], \"metadataScrubbed\": true } }\n```\n\n```jsonc\n// stdin — recovers the redacted regions from \"<pdfPath>.audit.json\"\n{ \"command\": \"verify\", \"pdfPath\": \"...\" }\n// stdout — hiddenContent re-checks thumbnails / document actions / embedded\n// files on the delivered bytes (omitly#171); any fail flips the verdict\n{ \"ok\": true, \"verdict\": \"pass\", \"regions\": [ ... ], \"metadataScrubbed\": true,\n  \"hiddenContent\": [ { \"class\": \"thumbnails\", \"verification\": { \"result\": \"pass\" } }, ... ] }\n```\n\n```jsonc\n// stdin — checks the embedded audit report + trailing Ed25519 seal, not\n// redaction completeness (that's \"verify\" above)\n{ \"command\": \"verify_seal\", \"pdfPath\": \"...\" }\n// stdout — verdict is one of: no_report | seal_invalid |\n// seal_unsupported_version | incomplete | verified. seal_unsupported_version\n// means this verifier is too old to check the seal at all — sealValid is\n// `null` (checked nothing), never true or false; carriesAuditReport flags\n// whether the file also carries an Omitly audit report (escalation signal).\n{ \"ok\": true, \"verdict\": \"verified\", \"sealValid\": true, \"sealFingerprint\": \"...\",\n  \"allPassed\": true, \"metadataScrubbed\": true, \"regionCount\": 2, \"pageCount\": 4,\n  \"warnings\": [], \"licenseProvenance\": null,\n  \"inputSha256\": \"...\", \"outputSha256\": \"...\",\n  \"sourceFilename\": \"...\", \"outputFilename\": \"...\" }\n```\n\nThe MCP tool `verify_document` (omitly#113) shells out to the exact same\n`verify_seal` engine command above — it is the recipient-facing name/wording\nfor the same seal/report integrity check, not a separate engine command.\n\n## Build & run\n\n**Free tools only (find_sensitive_regions, locate_text, check_redaction,\nverify_redaction, extract_pdf_text) — no native engine needed:**\n\n```bash\ncd omitly-mcp\nnpm install    # published releases ship the wasm build already bundled\nnpm run build  # plain tsc; the wasm bundle ships prebuilt in wasm/\nnode dist/index.js\n```\n\n`npm install omitly-mcp` from the registry gets a package with `wasm/`\nalready built — a published install never needs Rust.\n\nThis repository also ships the compiled wasm bundle in `wasm/`, alongside\n`wasm/leakcheck_wasm_bg.wasm.sha256` so you can verify the byte-for-byte\nartifact you received. It is the same bundle published in the npm package.\nThat means `npm install && npm run build && npm test` works here with no Rust\ntoolchain: `npm run build` is plain `tsc`. The wasm is compiled from the\nOmitly detection engine, whose Rust source is not part of this repository\n(see \"Repository scope\" below).\n\n**Everything, including `create_pdf`, `verify_seal`, `verify_document`, and\nthe two write tools (`redact_pdf`, `redact_by_entity`):** neither seal tool\nhas a wasm fallback — unlike the five free tools above, they always need the\nnative engine configured, even though checking a seal carries no licence\nrequirement (see \"Licensing\" below).\n\n```bash\n# Build and start the MCP server (no Rust toolchain needed)\nnpm install\nnpm run build\nnode dist/index.js\n\n# To enable the native-engine tools as well, point at a directory containing\n# the Omitly engine binaries (omitly-redact, omitly-pdf). These ship with the\n# Omitly desktop application; their source is not in this repository.\nOMITLY_ENGINE_DIR=/abs/path/to/engine node dist/index.js\n```\n\nOne env var covers both binaries: `OMITLY_ENGINE_DIR` is the directory holding\n`omitly-redact` and `omitly-pdf`. Per-binary overrides (`OMITLY_REDACT_BIN`,\n`OMITLY_PDF_BIN`) win over the directory when set. When `OMITLY_ENGINE_DIR`\n(or `OMITLY_REDACT_BIN`) isn't set, `find_sensitive_regions`, `locate_text`,\n`check_redaction`, and `extract_pdf_text` transparently use the bundled wasm\nengine instead — same detector, no native binary (`extract_pdf_text`'s\noptional `regions` filter is native-only; wasm scans every pattern and notes\nthat the filter was ignored). `verify_redaction` does too, but with a\nnarrower check: without a native engine there's no `<path>.audit.json`\nsidecar to verify specific regions against, so it falls back to a general\nre-scan of the whole file (still useful — a non-empty result still means the\nfile isn't clean — just not the same rigor as the sidecar-based check).\n\n`find`/`redact` need `qpdf` for the redaction pipeline (`QPDF_BIN` overrides the\nPATH lookup). `find` alone (native or wasm) is read-only and works without it.\n\n## Access control\n\nEvery path in a tool call comes from the model, so the server confines all\nreads and writes to one allowed directory:\n\n- **`OMITLY_ALLOWED_DIR`** — set it in the MCP config (recommended). Without\n  it, the directory the server was started in is used.\n- Symlinks are resolved before the check, so a link inside the root pointing\n  outside it is refused.\n- Outputs **never overwrite an existing file** (or its `.audit.json` sidecar);\n  the agent is asked to pick a fresh name instead.\n- **`OMITLY_ENGINE_TIMEOUT_MS`** (default 120000) — a wedged engine process is\n  killed at the deadline instead of hanging the agent's tool call.\n\nThese are guardrails against confused-deputy mistakes, not a sandbox against a\nhostile local user — see `docs/THREAT-MODEL.md`.\n\n## Licensing\n\n`redact_pdf`/`redact_by_entity` are the write surface, enforced **inside the\nengine binary** (not in this server, and not bypassable by the bundled wasm\nfallback — wasm never touches these two tools): a Pro or Personal licence\n(`OMITLY_LICENSE_FILE`, or the Omitly desktop app's activated licence on the\nsame machine) runs unmarked; otherwise the shared 14-day trial applies and\nthe audit output is permanently marked as evaluation output. The redaction\nitself is never degraded, and licence checks never touch the network.\n`find_sensitive_regions`, `locate_text`, `check_redaction`, `verify_redaction`,\n`verify_seal`, `verify_document`, and `extract_pdf_text` are free — and\n`verify_redaction`/`verify_seal`/`verify_document`/`extract_pdf_text` are free\nforever with **no cap and no marking** (recipient-side verification and\non-device extraction are the point, not a metered funnel). The two free\n*detection* tools (`find_sensitive_regions`, `check_redaction`) on the wasm\ntier — i.e. with no native engine configured — are metered (omitly#226):\nresults carry an `evaluation: true` flag plus an EVALUATION banner, and after\na monthly number of free checks (default 10, `OMITLY_FREE_CAP` to tune) the\ntool returns a structured `{ blocked: true, reason: \"free-cap\" }` refusal\nuntil the month rolls over. The count lives in `~/.omitly/usage.json`\n(override the directory with `OMITLY_STATE_DIR`; written 0600) and is\n**local-only — nothing ever phones home**; deleting the file resets the free\ncount, which is accepted (the no-network doctrine makes it unavoidable), and\nthe counter is deliberately never consulted by any paid write path. Calls\nserved by a configured native engine are not metered here — that user is in\nthe engine funnel, where the licence rules above apply. `verify_seal` and\n`verify_document` (both native-only) stay free by the same design in\n`crates/omitly-cli` (no Pro/Personal licence check on that command path\neither — both tools shell out to the identical `verify_seal` engine command).\n\n## Register with Claude Code\n\n```bash\nclaude mcp add omitly -- env \\\n  OMITLY_ENGINE_DIR=/path/to/engine-dir \\\n  OMITLY_ALLOWED_DIR=/path/agents/may/touch \\\n  node /abs/path/to/omitly-mcp/dist/index.js\n```\n\nOr in Claude Desktop's `claude_desktop_config.json`:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"omitly\": {\n      \"command\": \"node\",\n      \"args\": [\"/abs/path/to/omitly-mcp/dist/index.js\"],\n      \"env\": {\n        \"OMITLY_ENGINE_DIR\": \"/path/to/engine-dir\",\n        \"OMITLY_ALLOWED_DIR\": \"/path/agents/may/touch\"\n      }\n    }\n  }\n}\n```\n\n## One-click install for Claude Desktop (MCPB, free tier only)\n\n`mcpb/` packages the four free/diagnosis tools (`check_redaction`,\n`find_sensitive_regions`, `locate_text`, `verify_redaction`) — never the write\ntools — as a self-contained [MCPB](https://github.com/anthropics/mcpb) `.mcpb`\nextension: no Node/npm/Rust toolchain on the end user's machine, just\n\"Install Extension…\" in Claude Desktop. This is a deliberately smaller,\nseparate server (`mcpb/server/index.js`) from `dist/index.js` above, so the\nbundle can never expose `redact_pdf`/`redact_by_entity`/`create_pdf` even by\naccident.\n\n**Download:** [releases.omitly.app/mcp/omitly-leak-check.mcpb](https://releases.omitly.app/mcp/omitly-leak-check.mcpb)\n— always the current version (published by `publish-npm.yml` on every real\n`omitly-mcp` release; a versioned copy + checksum also live at\n[mcp/latest.json](https://releases.omitly.app/mcp/latest.json)). Drag the\ndownloaded file into Claude Desktop, or use \"Install Extension…\".\n\nOr build it yourself from source:\n\n```bash\nnpm run mcpb:pack   # copy wasm/ into mcpb/wasm + npm install + mcpb pack\n                     # → dist-mcpb/omitly-leak-check.mcpb\n```\n\n`mcpb:pack` needs no Rust toolchain — it reuses the prebuilt `wasm/` in this repository\n(builds the shared wasm detector once, then copies it into `mcpb/` — see\n`mcpb/scripts/copy-wasm.mjs`). The packed `.mcpb` itself needs nothing but\nNode, already bundled inside Claude Desktop.\n\n**Not signed** (`mcpb sign` needs a code-signing cert we don't have yet — same\ngate as desktop app signing). `mcpb info` on the packed file confirms\n`WARNING: Not signed`. Whether Claude Desktop's \"Install Extension…\" flow\nblocks or just warns on an unsigned `.mcpb` has NOT been confirmed against the\nreal Desktop app in this change (no Desktop GUI in this environment) — that\ncheck is still open, tracked in omitly#225.\n",
  "bytes": 19207,
  "sha": "537bb034da420a42df8b9b1db75dd365a7d47505466605953b1311b174758f91",
  "repo_slug": "omitly/omitly-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_omitly_omitly_mcp_53b60506/readme"
}