Skip to content

feat: enableWebMCP registers the editor operations as WebMCP tools on the host page - #47

Open
bendersej wants to merge 8 commits into
mainfrom
enable-webmcp
Open

feat: enableWebMCP registers the editor operations as WebMCP tools on the host page#47
bendersej wants to merge 8 commits into
mainfrom
enable-webmcp

Conversation

@bendersej

@bendersej bendersej commented Sep 2, 2026

Copy link
Copy Markdown
Member

Background

SimplePDF's entry for the OpenAI WebMCP Challenge (Aug 25 – Sep 3, 2026). This PR is the SDK half of the entry; the demo app builds on it.

An in-browser agent (ChatGPT's browser, Chrome with WebMCP) discovers tools on the page it is looking at, not inside iframes. The SimplePDF editor already registers its operations as WebMCP tools, but inside its own document, so every site that embeds the editor exposes nothing to the agent. enableWebMCP lifts the editor's operations to the host page's document.modelContext and forwards each call to the editor over the existing bridge: the PDF bytes stay in the tab and reach no SimplePDF server (what the agent reads, field values and extracted text, goes to the agent runtime the person attached), and the editor validates every call like any other request (editing, allowlisted origin, plan).

Changes

  • createEmbed({ enableWebMCP }) and <EmbedPDF enableWebMCP />: true registers every agentic operation, { exclude: [...] } withholds some (for example submit, so a person keeps the decision), off by default; a malformed value or a misspelled name throws EmbedConfigError at construction
  • Tool names and camelCase inputs match @simplepdf/embed/tools; the two readers carry the specification's readOnlyHint + untrustedContentHint (their output is document-derived), every writer an explicit MCP destructiveHint; each call returns the MCP tool-result envelope carrying the editor's Result (isError on failure), the shape the editor's in-page tools already return
  • The generator emits each agentic operation's tool input_schema (camelCase keys) into its own generated module read only by the lazy WebMCP module, and the agentic tool names into a second tiny module the root validates against, so the root bundle stays within budget
  • Registration waits for the editor to leave booting (an agent enumerating tools at page load cannot post into a listener-less iframe), probes for a model context on later transitions while none is usable, and lazy-loads the module only then; dispose() aborts the registration signal and unregisters every tool
  • Tool names are page-level: the first embed owns a name, a second one is warned and registers only what is free, and only the owning signal ever frees a name
  • Tests for the bridge path (registration set, exclude shape + names, wire round trip, error flagging, readiness gate with a control embed, dispose before the module resolves, duplicates and ownership, no-context and invalid-context diagnostics with later pickup, navigator fallback, best-effort registration) and the React prop (registers, unregisters on unmount, no remount on equal values, remount on change); READMEs, changeset (minor for both packages), API reports, a budget row and a load guard for the lazy chunk in both module formats

Notes

  • Codex was rate-limited for the review loop (quota resets Sep 7); a second, adversarially framed Claude reviewer stood in, plus BADobe on the postMessage surface.
  • Deliberately not done here, with reasons: AGENTIC_TOOL_NAMES is not exported from the root (no consumer); the manifest descriptions that mention get_fields are owned by the editor's contract; registering only the operations the tenant configuration allows needs an EDITOR_READY payload change on the editor side (follow-up); no client-side cap on in-flight tool calls (a same-page script has the same power today).

Review outcomes

# What changed Before After Location
1 Annotations follow the WebMCP spec; readers flagged untrusted (required fix) No host-page tools; the editor's in-page map uses MCP hints only readOnlyHint + untrustedContentHint on the two readers, destructiveHint on the writers embed/src/webmcp.ts#L48-L64
2 exclude validated for shape and names at construction (required fix) No option; an untyped caller could not fail Malformed value or unknown name throws EmbedConfigError before anything mounts embed/src/mount.ts#L209-L240
3 Registration waits for editor readiness and re-probes (required fix) No registration Starts on the first non-booting transition; latch released when no usable context or the chunk failed to load embed/src/bridge.ts#L167-L199
4 Page-level tool names owned per signal (required fix) No registration First embed owns a name, second is warned; only the owner frees it embed/src/webmcp.ts#L80-L88
5 Input schemas and tool names generated into dedicated modules (required fix) Operations table carried no schemas tool-input-schemas.ts (lazy) + agentic-tool-names.ts (root); /protocol unchanged in size embed/scripts/generate.mjs#L356-L400
6 Readiness gate pinned by a test that fails when registration happens early (required fix) No test Control embed on the read host, names freed before the gated embed readies; regression injected and caught embed/test/webmcp.test.ts#L172-L186
7 One decoder for the option, shared by bridge and React No option normalizeWebMCPOptions (@internal) and modelContextCandidates in one shared root module embed/src/webmcp-shared.ts#L9-L38
8 Lazy chunk budgeted and load-checked in both formats Budget walker followed static imports only Lazy imports walked, one LAZY_CHUNKS table drives budget + load guard embed/scripts/lazy-chunks.mjs#L4-L6
9 React key normalized so equal values never remount No prop Sorted exclude, off-states collapse, remount on real change, tests for both react/src/embed-pdf.tsx#L211-L214
10 Docs state the real data path and the one-embed-per-page constraint No section README section, option row, changeset embed/README.md#L74-L90

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.

1 participant