Skip to content

Add get_current_pdf to the pdf-server example (app tool + interact action) - #820

Open
ochafik wants to merge 2 commits into
mainfrom
pdf-server-get-current-pdf
Open

ochafik wants to merge 2 commits into
mainfrom
pdf-server-get-current-pdf

Conversation

@ochafik

@ochafik ochafik commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Apps that produce or edit documents (editors, annotators, generators) need a way to hand their current output file back to the model. ui/download-file saves to the user's disk without the model ever seeing the bytes, and waiting for a tool call to finish doesn't fit a long-running editor whose output materializes when the user is done editing.

A simple pattern covers this: the app exposes a "get output file" tool that returns the current document as an embedded resource in the tool result. The model learns of the file through a call it made itself, and gets the bytes in the same result. This PR adds that pattern to the pdf-server example — on both tool surfaces, so it works with hosts that call view-registered tools directly and with hosts that only drive server tools (e.g. Claude Desktop over stdio).

Changes

  • get_current_pdf app tool (view-registered via app.registerTool): returns the currently displayed PDF — with all in-viewer annotations and form fills baked in via the existing getAnnotatedPdfBytes() path — as an embedded application/pdf resource, plus a text block with the file name, size, and whether it includes unsaved edits.
  • get_current_pdf interact action (server-mediated): same result shape through the existing request-response bridge — interact enqueues a get_current_pdf command, the viewer builds the bytes and posts them back through the new app-only submit_current_pdf tool (base64 + file name + edit status), and the blocking interact call returns the text summary + embedded resource. In a commands batch, the embedded resource is kept as the command's 1:1 content slot; a single-action call keeps both parts.
  • HTTP body limit raised (jsonLimit: "64mb"): express's default 100 kB JSON limit rejected the viewer's multi-MB submits. This was latent for submit_save_data and screenshot-bearing submit_page_data as well.
  • Accurate "edited" detection: the Download button named a pristine document <title> - edited.pdf whenever the PDF had native annotations or form values, because annotationMap/formFieldValues are seeded with baseline state. The new hasInViewerEdits() mirrors the diff getAnnotatedPdfBytes actually applies and is shared by the Download path and both tool surfaces.
  • README: documents the viewer's app tools (previously only discoverable in code) and the dual-surface export.
  • Tests:
    • Unit: full interact round-trip (enqueue → poll → submit → embedded resource), viewer-error propagation, unknown-requestId guard.
    • E2E (app tool): first direct coverage of a view-registered tool — the test plays the host, posting tools/list/tools/call JSON-RPC into the app iframe from the sandbox frame; asserts a pristine %PDF export (not marked edited), then adds an annotation and asserts the re-export differs and is marked edited.
    • E2E (interact): drives the server-mediated path through the basic-host UI and asserts the same embedded-resource result.

Testing

  • bun test src examples — 443 tests, 0 fail
  • npx tsc -p examples/pdf-server/tsconfig.json --noEmit and tsconfig.server.json — clean
  • npx playwright test tests/e2e/pdf-get-current-pdf.spec.ts — 2 passed (app-tool path exports the ~1.9 MB arXiv PDF and round-trips an annotation; interact path returns the same resource through the bridge)
  • npx playwright test tests/e2e/pdf-annotations.spec.ts tests/e2e/pdf-viewer-zoom.spec.ts tests/e2e/pdf-incremental-load.spec.ts — all pass

Notes

  • Both surfaces embed the whole document as base64; the tool/action descriptions tell the model to prefer a single call once editing is done. No size cap is enforced (the download path has none either) — happy to add one if preferred.
  • The resource uri is file:///<URI-encoded file name> for consistency with the Download path's downloadFile payload.

Generated by Claude Code

Apps that produce or edit documents need a way to hand their current
output file back to the model. This adds the pattern to the pdf-server
example: the viewer registers a get_current_pdf view tool that returns
the current document — with annotations and form fills applied — as an
embedded PDF resource in the tool result, so the file can be retrieved
without a save to disk.

Also replaces the edited-document heuristic shared with the Download
button: annotationMap/formFieldValues are seeded with baseline state
from the PDF, so a pristine document with native annotations or form
values was named "<title> - edited.pdf". The check now mirrors the
diff getAnnotatedPdfBytes actually bakes in.

The e2e test drives the tool over the view's PostMessageTransport from
the sandbox frame — the first direct coverage of a view-registered tool.
@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@820

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@820

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@820

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@820

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@820

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@820

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@820

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@820

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@820

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@820

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@820

@modelcontextprotocol/server-lazy-auth

npm i https://pkg.pr.new/@modelcontextprotocol/server-lazy-auth@820

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@820

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@820

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@820

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@820

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@820

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@820

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@820

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@820

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@820

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@820

commit: 820417f

Hosts that only drive server tools (e.g. Claude Desktop over stdio)
could not reach the view-registered get_current_pdf. This adds it as an
interact action using the same request-response bridge as get_pages and
save_as: the server enqueues a get_current_pdf command, the viewer
builds the bytes and posts them back through the app-only
submit_current_pdf tool (base64 + file name + edit status), and the
blocking interact call returns the same text summary + embedded PDF
resource as the app tool.

In batches the embedded resource is kept as the command's content slot;
a single-action call keeps both the summary text and the resource.

Also raises the HTTP transport's JSON body limit (express default:
100kb), which rejected the viewer's multi-MB submits — a latent issue
for submit_save_data and screenshot-bearing submit_page_data too.
@ochafik ochafik changed the title Add get_current_pdf app tool to the pdf-server example Add get_current_pdf to the pdf-server example (app tool + interact action) Oct 22, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants