Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ separate example to locate.
### Tools currently shipped
This is the complete registered set, not a selection.

- `unlock_pdf`, `get_pdf_unlock_status`, `lock_pdf`.
- `display_pdf`, `list_pdfs`, `read_pdf_fields`, `fill_pdf`, `bulk_fill_from_csv`, `save_profile`, `load_profile`, `list_profiles`, `fill_with_profile`, `extract_to_csv`, `validate_pdf`, `read_pdf_content`, `read_pdf_pages`, `read_pdf_layout`, `search_pdf_text`, `convert_pdf_to_markdown`, `verify_table_proposal`, `get_pdf_identity`, `get_pdf_info`, `get_page_analysis`, `inspect_pdf_accessibility`, `compare_pdfs`, `render_pdf_page`, `render_pdf_region`, `fetch_pdf_from_url`, `merge_pdfs`, `split_pdf`, `rotate_pdf_pages`, `reorder_pdf_pages`, `apply_page_plan`, `create_signature`, `list_signatures`, `load_signature`, `detect_signature_zones`, `add_signature_field`, `prepare_signing_packet`, `apply_signature`, `apply_text`, `start_lumin_authorization`, `finish_lumin_authorization`, `prepare_lumin_request`, `send_lumin_request`, `check_lumin_status`, `download_lumin_artifact`, `get_active_document`, `set_active_document`, `get_allowed_directories`, `get_pdf_resource_uri`, `create_extraction_workspace`, `inspect_extraction_state`, `read_extraction_workspace`, `read_extraction_chunk`, `submit_extraction_proposal`, `verify_extraction_proposal`, `verify_extraction_batch`, `review_extraction_workspace`, `delete_extraction_workspace`, `reveal_in_finder`, `read_pdf_bytes` (app-only), `open_pdf_workspace` (app-only), `import_host_pdf` (app-only).

`open_pdf_workspace` opens an empty task-selection screen through optional
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ one-click signup experience. An ordinary user should not need to create a
developer app. If signing is not configured, ask the installation's maintainer;
creating a personal Lumin account alone does not enable the integration.

For password-protected local PDFs, ask PDF Tools to unlock the document. Enter
the password in its private local browser window, never in chat. Access is
limited to the exact file and the current server session, expires after ten
minutes, and can be ended with `lock_pdf`. Unlocking preserves the document's
permissions and encryption; it does not enable otherwise unsupported encrypted
rendering or comparison. See [private password entry](https://github.com/Open-Document-Alliance/PDF-Tools/blob/abaf94096c7e99328d40407506adff5b16584d1d/docs/ENCRYPTED_PDF_CREDENTIAL_PROPOSAL.md).

Once configured, ask PDF Tools to connect Lumin. Sign in and approve access in
the browser, never by sharing passwords or callback URLs in chat. If you need
an account, create it on Lumin's website. If signup does not return to the
Expand Down Expand Up @@ -244,12 +251,15 @@ that same view and reports raw pixels unavailable.

## Core Tools

This list is complete: it names every tool the server registers. One of them,
`read_pdf_bytes`, is available only to the in-app viewer and is not packed into
This list is complete for default discovery. Three tools, `read_pdf_bytes`,
`open_pdf_workspace`, and `import_host_pdf`, are app-only and are not packed into
the `.mcpb` manifest that ordinary model workflows discover.

### Viewer and Reading

- `unlock_pdf` (opens private local password entry, outside chat)
- `get_pdf_unlock_status` (checks the current session's unlock state)
- `lock_pdf` (forgets the current session's credential)
- `display_pdf`
- `fetch_pdf_from_url`
- `list_pdfs`
Expand Down
60 changes: 43 additions & 17 deletions docs/ENCRYPTED_PDF_CREDENTIAL_PROPOSAL.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,58 @@
# Private password entry for encrypted PDFs

Status: design proposal, not implemented or claimed as shipped.
Status: implemented in the private-entry source tranche. Release and desktop-host
qualification are separate from source tests; this document does not claim an
OpenAI allowlisting or public release.

## Current boundary

Several PDF tools currently accept a PDF password in their MCP arguments.
Those arguments are model-visible. The existing viewer has no independent
password-entry prompt, and Lumin's browser OAuth flow is provider-specific.
Neither is an encrypted-PDF credential service.
PDF tools no longer accept top-level `password`, `before_password`, or
`after_password` arguments. Legacy calls, including null or empty values, fail
before filesystem or parser actions without reflecting their values. A PDF form
field or extraction-schema property named `password` is ordinary document data,
not an authentication channel. Low-level isolated worker protocols remain
internal and are not MCP tools.

The download tool no longer accepts custom HTTP headers. Authenticated
downloads should happen through the provider's user-facing interface, followed
by opening the saved local file. Lumin authentication continues to use its
existing browser OAuth flow, not credentials supplied in chat.

## Proposed user experience
## User experience

When an encrypted document needs a password, PDF Tools offers a local unlock
window. The user enters the password there, outside chat. The assistant receives
only a locked/unlocked/expired status for that document. A visible lock action
and short expiry end access. Ordinary non-encrypted workflows remain unchanged.
only an awaiting-password, locked, unlocked, or expired status for that document.
Call `unlock_pdf` with the local path, enter the password in the opened browser
window, then retry the PDF operation. `get_pdf_unlock_status` checks access;
`lock_pdf` and the window's Lock button clear it. Access expires after at most
ten minutes and ends with the server session. Ordinary non-encrypted workflows
remain unchanged. Restarting the host requires unlocking again.

The implementation must provide a direct user-to-local-server channel.
The implementation provides a direct user-to-local-server channel.
Sending the password through an app-only MCP tool is not an acceptable shortcut:
it would still be a tool argument. A viewer-only PDF.js prompt would likewise
not unlock extraction or mutation tools running in the server.

## Proposed implementation boundary
## Implemented boundary

- Use a local, user-facing entry surface connected to the already-running
server through authenticated local IPC or a tightly bounded loopback service.
Select and qualify that transport before shipping. For a browser surface,
require an unpredictable session capability, exact Host/Origin checks,
POST-only submission, expiry, no external assets, and no request-body logging.
The browser connects only to `127.0.0.1` on an ephemeral port, with a random
256-bit capability initially carried in the fragment and removed from browser
history by the page. The URL is never returned through MCP. Requests require
exact Host/Origin, POST, JSON content type, bounded bodies and the capability.
The page has no external assets and a restrictive CSP; request bodies are not
logged. Pending windows last five minutes. A successful window retains its
lock capability for the credential lifetime. At most eight documents and one
password validation are active; attempts and validation time are bounded.
- Keep credentials in process memory, not command-line arguments, URLs, MCP
calls/results, analytics, logs, temporary plaintext PDFs, or saved profiles.
JavaScript memory and operating-system swap do not provide guaranteed erasure.
- Bind each entry to the canonical path, exact encrypted source hash and file
identity. Revalidate before use; changed bytes invalidate it. Scope access to
the server session and document, not all files with the same password.
- Remove public password arguments when this mechanism is implemented. Reject
- Remove public password arguments from discovery. Reject
legacy password arguments without echoing their values, and resolve the
credential internally before invoking existing bounded PDF.js/QPDF workers.
- Expire entries, cap their count and lifetime, clear them on explicit lock,
Expand All @@ -49,6 +62,18 @@ not unlock extraction or mutation tools running in the server.
produce encrypted outputs or fail; the existing QPDF reprotection contract
remains in force.

Each input to a multi-document mutation must be unlocked independently. The
existing worker requires the same password for all encrypted inputs; this
tranche does not add mixed-password merge support. Encrypted rendering,
geometry, comparison, and accessibility inspection remain unsupported where
the underlying operation already refused them. Unlocking does not lift those
limits. Locking does not erase text already read, completed output files, or
existing extraction workspaces.

This is a local trusted-user boundary, not protection against malware, browser
extensions, a hostile same-user process, root, OS swap, or a compromised host.
There is no credential persistence or transfer between server instances.

## Qualification before enabling

Test wrong passwords, cancel/expiry/restart, changed sources, concurrent
Expand All @@ -58,6 +83,7 @@ MCP messages, tool errors, logs, process arguments, persisted state or output
artifacts. Test read and write permission distinctions and encrypted-output
preservation, then exercise the real user entry flow in supported desktop hosts.

This proposal requires review and implementation before a submission may claim
password-free encrypted-PDF authentication. The annotation and download changes
do not implement this separate capability.
An installed-host run and exact packaged-source check are required before
claiming desktop qualification. Synthetic test passwords may appear in test
fixture code; they must not appear in the captured product MCP traffic, logs,
saved profiles, or generated document metadata.
22 changes: 20 additions & 2 deletions docs/MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ only copied field values or the value-free validation report return. The
plaintext buffer is released in a `finally` block, including parsing failures.
Viewer metadata and host imports do not gain a new decryption route.

### Private encrypted-PDF access

`unlock_pdf` opens a local browser window; `get_pdf_unlock_status` and `lock_pdf`
observe or clear document-scoped access. Public MCP password fields are removed.
Credentials enter through the bounded loopback service in
`server/private-pdf-credentials.js`, are retained only in process memory for at
most ten minutes, and bind the exact canonical source, inode/device, bytes and
server session. The dispatch boundary resolves private credentials against the
same source envelope used by isolated workers and before extraction-cache access.
Permissions and encrypted-output preservation remain worker responsibilities.
Unsupported encrypted geometry/comparison tools remain unsupported.

Use the synthetic private-entry harness for integration tests, never a production
environment override. The harness substitutes only the platform browser opener;
password submission still crosses the actual authenticated local HTTP boundary.
See `docs/ENCRYPTED_PDF_CREDENTIAL_PROPOSAL.md` for the user flow, limits and
trusted-local-user qualification.

Host imports have no source file before validation. Their bounded original
bytes are temporarily written to a private local inspection directory, then
checked using the same isolation service and original import validator. That
Expand Down Expand Up @@ -441,7 +459,7 @@ mcp__<display_name, spaces underscored, non [A-Za-z0-9_-] stripped>__<tool_name>
Identifiers over **64 characters** fail in the host. This shipped as a real
defect (issue #44): the original benefit-led directory title
`PDF Tools - Fill, Sign, Merge, Split, Extract` normalizes to 41 characters and
pushes 31 of the current 58 packed tool identifiers past the ceiling.
pushes 32 of the current 61 packed tool identifiers past the ceiling.

The naming strategy is therefore **dual**:

Expand All @@ -457,7 +475,7 @@ Budgets are computed by `scripts/tool-identifier-budget.mjs` and gated in

- `PDF Tools`: longest identifier 43, headroom 21
- `PDF Tools: Fill, Sign & Edit`: longest identifier 59, headroom 5
- Original long title: longest identifier 75, 31 identifiers over the limit
- Original long title: longest identifier 75, 32 identifiers over the limit

**The trap when adding a tool.** The shipped short brand has generous headroom,
so a new long tool name will not break it and every host-facing check stays
Expand Down
8 changes: 4 additions & 4 deletions docs/MCP_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ template discovery is also unsupported and deterministically returns JSON-RPC

### Tools

The runtime returns 61 uniquely named tools. Every tool has an object input
The runtime returns 64 uniquely named tools. Every tool has an object input
schema plus `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, and
`openWorldHint` annotations. Annotations are user-interface hints, never an
authorization boundary; path allowlists and signature-intent checks remain the
Expand All @@ -42,7 +42,7 @@ every tool in both runtime copies. The handler evidence and classification
rules are recorded in
[`TOOL_ANNOTATION_AUDIT_2026-07-21.md`](TOOL_ANNOTATION_AUDIT_2026-07-21.md).

The source manifest lists all 61 tools. The packed MCPB manifest lists the 58
The source manifest lists all 64 tools. The packed MCPB manifest lists the 61
normal model-workflow tools and omits `read_pdf_bytes`, `open_pdf_workspace`
and `import_host_pdf`, whose runtime metadata marks them `ui.visibility: ["app"]`.
`tools_generated: true` explicitly tells MCPB hosts that runtime discovery
Expand Down Expand Up @@ -81,7 +81,7 @@ error branches where required.

The separately configured optional `propose_pdf_ocr`, `start_pdf_ocr_job`,
`resume_pdf_ocr_job`, and `read_pdf_ocr_job` tools add four structured handlers,
bringing an opted-in runtime to 65 tools with 61 output schemas.
bringing an opted-in runtime to 68 tools with 61 output schemas.
They are absent from default discovery and both manifests. No OCR engine is bundled;
see [Optional OCR setup and limitations](LOCAL_OCR.md).

Expand Down Expand Up @@ -123,7 +123,7 @@ exact-output-identity preconditions. New evaluation suites must bind v3
explicitly. The grader selects the allowlisted contract and trust registry
declared by each suite, so historical evidence remains valid under its original
stack and is not silently rescored. The six existing trajectory jobs do not
constitute behavioral trajectory coverage of all 61 tools.
constitute behavioral trajectory coverage of all 64 tools.
`get_pdf_identity` is covered by its contract, handler, filesystem-race, and
agent-workflow tests rather than by those six retained jobs.

Expand Down
8 changes: 4 additions & 4 deletions docs/OUTPUT_SCHEMAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ an `isError` result is never forced through a success schema.
| `verify_extraction_batch` | one new immutable generation containing ordered per-proposal replay results and their separate typed statuses |
| `review_extraction_workspace` | source-bound viewer metadata plus a bounded generation-pinned page of current values, correction history/citations, or page coverage |

The following four tools remain intentionally text-only and therefore do not
The following seven tools remain intentionally text-only and therefore do not
advertise `outputSchema`: `list_pdfs`, `list_profiles`, `load_profile`,
and `save_profile`. Their error results are also
`save_profile`, `unlock_pdf`, `get_pdf_unlock_status`, and `lock_pdf`. Their error results are also
text-only; attaching an undeclared `structuredContent` error would create a
wire contract that discovery does not publish.

Expand Down Expand Up @@ -204,9 +204,9 @@ before loading the target PDF, writing output, or changing active-document
state.

The executable source of truth is `server/output-schemas.js`. The MCP contract
tests assert this complete matrix of 61 structured tools and four text-only
tests assert this complete matrix of 61 structured tools and seven text-only
tools, including the configured-only OCR contracts. Default discovery still
has 57 structured tools and four text-only tools; enabling local OCR adds four
has 57 structured tools and seven text-only tools; enabling local OCR adds four
structured tools. Tests compile every schema through the pinned SDK validator, reject newer
unsupported JSON Schema keywords, exercise live success and error branches, and
require byte-identical source/share runtime files.
3 changes: 3 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
],
"icon": "icon.png",
"tools": [
{ "name": "unlock_pdf", "description": "Open private local PDF password entry outside chat" },
{ "name": "get_pdf_unlock_status", "description": "Check this document's private session access" },
{ "name": "lock_pdf", "description": "Clear this document's private in-memory password" },
{
"name": "import_host_pdf",
"description": "Import app-supplied PDF bytes into the already-permitted private workspace (app-only)"
Expand Down
3 changes: 3 additions & 0 deletions manifest.mcpb.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
],
"icon": "icon.png",
"tools": [
{ "name": "unlock_pdf", "description": "Open private local PDF password entry outside chat" },
{ "name": "get_pdf_unlock_status", "description": "Check this document's private session access" },
{ "name": "lock_pdf", "description": "Clear this document's private in-memory password" },
{
"name": "display_pdf",
"description": "Display an interactive PDF viewer with search, navigation, zoom, and form field detection"
Expand Down
1 change: 1 addition & 0 deletions package-for-friend.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const SHARE_FILES = [
"server/pdfjs-worker.js",
"server/qpdf-decrypt-worker.js",
"server/qpdf-decrypt.js",
"server/private-pdf-credentials.js",
"server/resource-uri.js",
"server/skill-files.js",
"server/skills.js",
Expand Down
2 changes: 1 addition & 1 deletion pdf-toolkit-mcp-share/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Once installed, ask Claude in Cursor:
- *"Rotate page 3 by 90 degrees"*

## Tools Available
This page names a selection, not the whole surface: the server registers 58
This page names a selection, not the whole surface: the server registers 64
tools. The root `README.md` lists every one, and `docs/OUTPUT_SCHEMAS.md`
carries their structured output contracts.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,13 @@ Use these stage semantics:
report `CONTENT_UNAVAILABLE_PASSWORD_REQUIRED`, plan no tools, and list the
failed PDF content-read tool as prohibited under the current decision.
Do not retry it or substitute another password-dependent content read.
Reconsider content reads only in a new planning turn after the password is
supplied.
Offer unlock_pdf so the user can enter the password in its private local
window. Never ask for a password in chat or pass password, before_password,
or after_password as a tool argument. Reconsider content reads after
get_pdf_unlock_status reports unlocked. Use lock_pdf to clear session access;
access also expires after ten minutes or when the source changes. A schema
label such as pdf_password in a planning record means missing private access,
never a request to collect the credential in the record.

Use these record boundaries:

Expand Down
2 changes: 1 addition & 1 deletion pdf-toolkit-mcp-share/server/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const PDF_LIB_ENCRYPTED_MESSAGE =
+ "which has no decryption support, so supplying a password here will not help. "
+ "Decrypt the file first (for example with qpdf) and retry. "
+ "To read an encrypted PDF as it is, use read_pdf_layout, convert_pdf_to_markdown, "
+ "or get_pdf_info, which accept a password and decrypt with PDF.js.";
+ "or get_pdf_info after entering the password in the private window opened by unlock_pdf. Those tools decrypt with PDF.js.";

// Parameter text for tools that decrypt with PDF.js for one part of their work
// but still load page geometry through pdf-lib, which stops on encryption.
Expand Down
Loading
Loading