npm normalizes the bare https URL to git+https://….git at publish time
and warns about it on every package. Storing the canonical form removes
the warning and matches what npm's trusted-publisher check compares
against the repository.
WSL job copies the checkout onto ext4 before building (9P was the cost: 605 s to 348 s); Playwright installs only the headless shell; every tsconfig lists its @types explicitly, a verified no-op on 5.9 and the prerequisite for TypeScript 7.
The Windows WSL job is the critical path of every CI run (589 to 752 s).
Its checkout sits on the Windows drive, which WSL reaches over 9P, and
writing node_modules there is what makes npm ci take about three minutes
where the Linux job takes 23 s. The step now copies the tree onto the
distro's ext4 filesystem before installing and building, as Microsoft's
WSL guidance recommends for I/O-heavy work.
The e2e job downloaded the full Chromium build (about 165 MB) on every
run, but a headless run with no channel launches chromium-headless-shell
and never opens it. `--only-shell` skips that download.
Every tsconfig now lists the @types packages it uses (node, bun,
vite/client, dom-speech-recognition) instead of relying on TypeScript
auto-including everything under node_modules/@types. On TypeScript 5.9
this is a no-op: identical diagnostics across all 48 projects and
byte-identical emitted .d.ts files. It is a prerequisite for TypeScript 7,
which drops the auto-inclusion; with these lists the Go compiler passes
all 48 projects, so that upgrade becomes a version bump once TypeDoc
supports it.
Two cache-based approaches were evaluated and rejected: an npm cache for
the WSL job (npm ci is slow there because of 9P, not downloads, and a
prefix-restored cache grows without bound) and a Playwright browser cache
(Playwright's docs advise against it; the download is under 10 s).
Guide fact-checked claim by claim against the built package: three wording fixes, seven coverage gaps (including the double-registration guard now covering the base Protocol's progress/cancelled handlers), and the legacy setRequestHandler overload's @deprecated moved onto a documented call signature so editors report it.
Guide: name @modelcontextprotocol/express in the server-author row, say
the two SDKs share no types rather than "do not interoperate" (they speak
the same wire protocol), note zod 4.0/4.1 lack ~standard.jsonSchema, list
the full 1.x-shaped extra, cover tool callbacks in the handler-context
bullet, document that the double-registration guard now covers the base
Protocol's notifications/progress and notifications/cancelled handlers,
mention ResourceNotFoundError, add the Express row to the import map, and
add a "What stays the same" section.
The @deprecated tag on the legacy setRequestHandler / setNotificationHandler
overload sat on the parameter inside an intersection type, which TypeScript
does not report. The overload now comes from LegacyRequestHandlerSetter /
LegacyNotificationHandlerSetter, named types with a documented call
signature, so editors flag the 1.x call and not the 2.x forms.
README links to the quickstart use the lowercase slug typedoc emits.
Runs the published 1.7.5 App and AppBridge against the 2.x build over an in-memory transport in both pairings, via an npm alias devDependency and sdk 1.30. Complements wire-compat.test.ts: this one runs the real 1.x code and is the only test that catches a coordinated 2.x wire rename.
setRequestHandler(Schema, handler) and setNotificationHandler(Schema, handler) work on App and AppBridge as deprecated overloads forwarding to the 2.x method-keyed form; AppRequest, AppNotification and AppResult return as deprecated type aliases. Both removed in 3.0.
Install @modelcontextprotocol/ext-apps@1.7.5 under the npm alias ext-apps-v1
(plus @modelcontextprotocol/sdk@1.30.0, its peer) as devDependencies and run
the 2.x App against the 1.x AppBridge and vice versa over an in-memory
duplex, with each bridge fronting a real Client+McpServer of its own SDK
generation. Covers initialize, notifications, view-to-host requests,
tool/resource proxying, error codes, AbortSignal cancellation, teardown and
close.
They appear in App's public setRequestHandler / setNotificationHandler
signatures, so TypeDoc's link validation needs them reachable from the
entry point.
Consumers on 1.x wrote setRequestHandler(Schema, handler) and
setNotificationHandler(Schema, handler) with a whole-message Zod schema.
SDK 2.0 keys handlers by method name, so App and AppBridge now accept
that form as a third, deprecated overload: a small helper detects the
schema, extracts the method literal, forwards to the 2.x
(method, { params }, handler) form, and hands the legacy handler the
reassembled { method, params } message plus a 1.x-shaped extra
(signal, requestId, sessionId, _meta). A one-time console warning
points at the new form. The double-registration guard applies to both
forms.
The AppRequest, AppNotification and AppResult unions that 1.x fed to
the SDK's Protocol generics come back as deprecated type aliases, since
they leaked through the root entry and cost nothing to keep.
Both go away in 3.0.
Follow-up to #720 and #705: regenerated lockfile, ^2.0.0 peers with client required, types from @modelcontextprotocol/client, restored double-registration guard, deprecated raw-shape registerAppTool overload, migration guide, wire-compat tests, MCPB build from the local package.
- pdf-server build-mcpb.mjs packs the ext-apps build from this checkout
into the staging dir instead of asking the registry for ^2.0.0, which
is not published yet; CI's "Build MCPB bundle" step failed on ETARGET.
- Claim notifications/cancelled and notifications/progress in the
MethodRegistry right after super() so a direct setNotificationHandler
cannot silently replace the base Protocol's own handlers.
- Migration guide and add-app-to-server skill: custom methods use the
three-argument setRequestHandler("m", { params }, handler) form (the
two-argument form is only for spec methods); zod schemas live in
@modelcontextprotocol/core, not client/server; it is
toolInfo.tool.outputSchema that loosened in schema.json, and the
recursive JSON-value def is nested under three definitions; core is a
required peer that client already depends on.
- app-bridge.ts: fix "forthe"/"forresult" typos in @see links, update
three JSDoc examples to the method-string setNotificationHandler form,
move the UntypedHandlerSetter type below the import block (same in
app.ts).
- wire-compat.test.ts: say the fixtures are 1.x-shaped, not captures.
Add explicit .js extensions to the relative imports in app.ts,
app-bridge.ts and their tests so the emitted declarations resolve under
NodeNext (#704). Replace the zod 3 JSON Schema fallback test, whose
code path was removed, with the remaining non-Standard-JSON-Schema
rejection case as its own test.
The base SDK Protocol silently replaces an existing request or
notification handler. v1's ProtocolWithEvents threw instead, so a stray
setRequestHandler / setNotificationHandler could not disconnect on*
handlers or addEventListener listeners. Restore that guard on both App
and AppBridge with a small MethodRegistry, keep replace semantics for the
on* setters via a protected replaceRequestHandler, and release methods
on removeRequestHandler / removeNotificationHandler.
Also:
- give oncreatesamplingmessage a getter, a replace warning and the same
clearable shape as the other request setters
- let App register handlers for custom (non-spec) methods; the default
branch of assertRequestHandlerCapability threw a misleading
"No handler for method X registered" error
- move the RESOURCE_URI_META_KEY / RESOURCE_MIME_TYPE docs to
src/constants.ts where the constants now live
- fix stale "inherited from Server" wording and point @see links at
@modelcontextprotocol/client, which is what hosts install
Replay JSON-RPC messages recorded from ext-apps 1.7.x (sdk 1.x key
order and ids) through the 2.x App and AppBridge over a raw in-memory
transport and assert exactly what 2.x emits: the ui/initialize
handshake, params-less ui/notifications/initialized, tools/call in both
directions, and the -32002 to -32602 re-encoding on the host side. Also
check that AppBridge never sends an MCP initialize toward the View and
that the outer-client proxy works with a default (non-legacy) 2.x
Client/Server pair.
Install instructions now list the split base MCP SDK packages at ^2.0.0
by role (core comes in transitively, server only for server authors),
require Node 20+, and state that the wire protocol is unchanged so 2.x
Views and 1.x hosts interoperate. The agent skills lose the exact
prerelease wording and add-app-to-server gains a v1 to v2 import
mapping. docs/migrate-to-2.md lists the breaking changes, the peer
matrix and the host-side error deltas.
The zod peer range is ^4.2.0, whose schemas implement
~standard.jsonSchema, so the lazy zod/v4 toJSONSchema fallback for
zod 3.25 is dead code. Re-export StandardSchemaWithJSON from the client
package instead of keeping a structurally identical local copy.
Consumers using moduleResolution NodeNext need explicit extensions in
the emitted declaration files. Covers every source file except app.ts,
app-bridge.ts and events.ts (and their tests), which are being reworked
separately.
The app-only consumer now also exercises the ./react entry. The
server-only consumer no longer asserts that client is absent from
node_modules (npm auto-installs required peers) but still checks that
the server entry does not bundle it. Surface npm and esbuild errors
instead of swallowing them.
SDK v2 registerTool takes Standard JSON Schema objects (z.object(...))
and keeps a deprecated overload for the v1 raw-shape form. Mirror that
here so existing servers passing { field: z.string() } keep compiling;
the call is forwarded to the SDK, which auto-wraps the shape. Tests
exercise both forms end to end through a real McpServer and Client.
spec.types.ts previously inferred CallToolResult, ContentBlock and
friends from core's Zod schemas via a local core-types module so that
the type surface stayed free of a client package edge. Now that client
is a required peer, take the types from it directly and delete the
indirection. Generated schemas are unchanged.
The client, core and server packages become ^2.0.0 peers and client is
now a required peer (App and AppBridge extend its Protocol class);
server stays optional for View-only consumers. Examples move to
zod ^4.2.0 to match the library peer range.
The root package is not yet published at 2.0.0, so the lockfile now
links the examples' @modelcontextprotocol/ext-apps dependency to the
repository root instead of a registry copy. This drops the stale 1.x
tarball (and its sdk 1.x peer) from the lockfile and makes
scripts/link-self.mjs a no-op.
npm pack runs the prepare script for directory specs even with
--ignore-scripts and its output can pollute stdout, so locate the packed
tarball on disk instead of parsing --json output. Run npm with a minimal
environment so caller npm config cannot leak into the synthetic
consumers, and require exact versions for the consumers' dependencies.
Add a CI job that runs the check on Node 22.
The emitted .d.ts copy relative specifiers verbatim, and the extensionless form
(e.g. `from "./events"`) doesn't resolve under Node16/NodeNext, hiding inherited
members such as `addEventListener` (TS2339). Appending `.js` matches the existing
SDK-import style and works with both bundler-mode type-checking and Bun.build.
Types-only; runtime is unaffected.
Fixes#704
App and AppBridge extend the public Protocol from @modelcontextprotocol/client; the iframe wire protocol (ui/initialize only) is unchanged. Peers move to the split SDK 2.0 packages with client and server as optional role peers. Fixups (lockfile, peer ranges, restored handler guards, docs, wire-compat tests) follow in a separate PR.
Co-authored-by: Andrew Khadder <andrew@manufact.com>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Closes#702
Non-breaking transitive updates via `npm audit fix`, plus a `sharp` major
bump. Takes `npm audit` from 15 findings (2 critical, 7 high, 4 moderate,
2 low) to 3 moderate.
Critical / high:
- shell-quote 1.8.3 -> 1.9.0 (GHSA-w7jw-789q-3m8p, GHSA-395f-4hp3-45gv)
- @babel/core 7.28.6 -> 7.29.7 (GHSA-4x5r-pxfx-6jf8)
- sharp ^0.34.5 -> ^0.35.3 (GHSA-f88m-g3jw-g9cj; libvips CVE-2026-33327,
CVE-2026-33328, CVE-2026-35590, CVE-2026-35591)
- brace-expansion 1.1.16 / 2.1.2 (GHSA-3jxr-9vmj-r5cp)
- fast-uri 3.1.4 (GHSA-v2hh-gcrm-f6hx, GHSA-4c8g-83qw-93j6)
- hono 4.12.31 (8 advisories, incl. GHSA-88fw-hqm2-52qc, GHSA-hvrm-45r6-mjfj)
- linkify-it 5.0.2 (GHSA-22p9-wv53-3rq4, GHSA-v245-v573-v5vm)
Plus markdown-it 14.3.0, body-parser 2.3.0, type-is 2.1.0, undici 7.28.0
and vite 6.4.3.
sharp is a devDependency used only by tests/e2e/generate-grid-screenshots,
which is testIgnore'd unless GENERATE_SCREENSHOTS=1. The only breaking
change in 0.35 is the Node floor moving to >=20.9.0, and CI runs Node 22;
the API used (png / resize / toFile) is unchanged.
The 3 remaining findings are all the same advisory (GHSA-frvp-7c67-39w9,
path traversal in @hono/node-server's serve-static on Windows), reported
once each for @hono/node-server, @modelcontextprotocol/sdk and
@modelcontextprotocol/ext-apps. The fix is in @hono/node-server 2.0.5 but
sdk 1.29.0 pins ^1.19.9, so the range cannot reach it; that needs an
upstream bump. It is not reachable here either way — the SDK imports only
getRequestListener and serve from that package, never serve-static.
The Protocol-based migration needs the Protocol class exported from the
package roots, which lands in modelcontextprotocol/typescript-sdk#2501.
Pin the preview builds so the branch is installable; swap to the next
published beta once that PR is released.
Replaces the Client/Server subclassing from the v2 migration with direct
Protocol extension, now that the SDK exports Protocol from the client
package root. This restores the v1 wire contract on the iframe channel:
ui/initialize is the first and only handshake, so deployed hosts keep
working unchanged.
Deleted with the double handshake: the inner MCP initialize round-trip
and its pinned protocol version, the contentless inner capabilities and
their retro-registration plumbing, the oninitialized defineProperty
shadow, and the getCapabilities/getHostCapabilities split (v1
getCapabilities restored). Tests pin the wire order and assert no MCP
initialize is sent in either direction.
Note: package.json overrides point the SDK packages at locally built
tarballs carrying the Protocol export; replace with published versions
before any upstream PR.
The consent page shipped without a viewport meta tag, so mobile browsers
laid it out at a desktop width. Its Approve/Deny buttons were 37px tall
and nested a <button> inside an <a>, which is invalid HTML — interactive
content is not permitted inside a link.
- Add a viewport meta tag to the consent page
- Replace the <a><button> nesting with anchors styled as buttons
(role="button" plus a Space-key handler to keep button keyboard
semantics)
- Lay the actions out as a wrapping flex row with equal 56px tap targets
- Add touch-action: manipulation so a tap resolves to a click immediately
rather than waiting on the double-tap-to-zoom gesture
- Suppress long-press text selection and the iOS callout menu on the
buttons, scoped so the card's client/redirect details stay copyable
- Give buttons an :active style since removing the native tap highlight
otherwise leaves no press feedback
- Apply the same 56px minimum and touch handling to the in-app view's
buttons (the fullscreen toggle was ~33x33)
* lazy-auth-server: support mounting under a base path
createApp() previously built every advertised URL (AS metadata
endpoints, PRM resource, WWW-Authenticate resource_metadata, consent
approve link, elicitation callbacks) from the bare origin, so mounting
the app inside another Express app — app.use("/lazy-auth",
createApp()) — advertised URLs that pointed at the host's root and
collided with the host's own OAuth endpoints.
Derive the public base URL from Express's req.baseUrl instead (empty
when standalone, so standalone behavior is unchanged), and treat
PUBLIC_URL as including any mount path. Document the mounting pattern,
including the root well-known rewrite hosts must add for RFC 8414/9728
path-insertion discovery.
* lazy-auth-server: derive DIST_DIR from import.meta.url
import.meta.filename and import.meta.dirname are undefined in some
module-VM contexts (e.g. importing this package from jest), which made
createApp() throw at import time. import.meta.url is defined
everywhere; derive the dist directory from it instead.
* Require PKCE and bind redirect_uri in lazy-auth-server token exchange
Hardens the example's mock authorization server:
- /authorize now requires a PKCE S256 code_challenge (the MCP auth
spec mandates PKCE for clients; previously a code issued without a
challenge skipped verification at the token endpoint)
- /token rejects authorization-code exchanges where a provided
redirect_uri does not match the authorization request (RFC 6749
§4.1.3); OAuth 2.1 clients that omit it still work, relying on the
now-mandatory PKCE binding
* Enforce single-use authorization codes
Track redeemed code IDs (jti) in memory until the code's own 5-minute
expiry; replaying a code at the token endpoint now fails with
invalid_grant (RFC 6749 §4.1.2). Also documents why wildcard CORS is
intentional for this demo (browser-based hosts must read
WWW-Authenticate; no ambient credentials exist to protect).
No SDK API changes since 1.7.2.
Examples:
- Add lazy-auth-server example (#679)
- Publish lazy-auth-server to npm (added to publish workflows)
Also syncs example dependency ranges to ^1.7.0 (output of npm run bump).
Adds an example MCP App server demonstrating lazy (on-demand) OAuth:
the server connects and lists tools without authentication, and only
prompts for auth when a protected tool is called, by responding 401
with a WWW-Authenticate header pointing at protected-resource metadata.
- Public `show_auth_button` app calls the protected `get_secret` tool
via callServerTool; the host runs the OAuth flow on 401 and retries
- Embedded mock authorization server (authorization-code + PKCE,
short-lived HS256 tokens, refresh + session revocation) so the whole
flow runs from a single process
- TTL-scoped endpoint paths (/ttl/<seconds>/mcp) let slow or automated
clients request longer-lived tokens per connection, threaded through
OAuth via RFC 8707 resource indicators
- `elicit_url` / `elicit_by_error` tools demonstrating URL elicitation
No SDK API changes since 1.7.1.
Examples:
- pdf-server: switch pdf-lib to maintained @cantoo/pdf-lib fork (#651)
- bump example transitives to latest patches (#658)
- vite-plugin-solid 2.11.10 -> 2.11.12 (basic-server-solid)
- babel-preset-solid 1.9.10 -> 1.9.12
- babel-plugin-jsx-dom-expressions 0.40.3 -> 0.40.6
- vite-prerender-plugin 0.5.12 -> 0.5.13 (basic-server-preact)
- @modelcontextprotocol/ext-apps 1.0.0 -> 1.7.1 (lockfile had a stale
registry pin of the repo's own published package that was never bumped
alongside package.json's version field)
Tighten the basic-server-solid range to ^2.11.12 to track the patch
series with current Solid HMR fixes.
The original pdf-lib package has been unmaintained since Nov 2021.
@cantoo/pdf-lib is an actively maintained, API-compatible fork with
ongoing releases and accumulated bugfixes.
Also updates test assertions from the deprecated PDFNumber.value() to
PDFNumber.asNumber() per the fork's API guidance.
* perf(pdf-server): lazy form extraction via range transport + incremental viewer scans
Server: display_pdf now opens the document via PDFDataRangeTransport
(disableAutoFetch) and only runs the per-page form/annotation walk when
getFieldObjects() is non-empty, so form-free PDFs are probed with ~10-25%
of bytes instead of a full download. The unused viewFieldInfo Map is
removed.
Viewer: getDocument sets disableAutoFetch/disableStream; baseline
annotation scan and field-name mapping run lazily per rendered page
instead of walking every page after load, so first paint no longer
schedules a full-file pull.
E2E: new range-counting HTTPS fixture (W-9 for forms, generated
text+image PDF for no-forms) with stallAfterBytes control, and four
regression tests asserting form fields are returned, <30% served on
no-forms display_pdf, first page renders while later ranges are stalled,
and overlap stays bounded.
* fix(pdf-server): handle separated field/widget trees in extractFormSchema
pdfjs getFieldObjects() returns the full field-tree array. For PDFs with
a separated structure (pdf-lib, some authoring tools) the typed widget
sits at fields[1+] behind a typeless container at fields[0]; the previous
code only inspected fields[0] and skipped them all. Pick the first entry
with a non-empty type instead.
Makes the e2e forms.pdf fixture fully generated (no checked-in third-party
asset on the hot path); fw9.pdf stays as a unit-test fixture for the
hierarchical/XFA case.
* fix(pdf-server): address range-transport hangs and lazy-scan tombstone loss
PdfCacheRangeTransport:
- abort() is a no-op stub on PDFDataRangeTransport (it's the hook pdfjs
calls *on* the transport, not an upstream error channel). Expose a
.failed promise that rejects on the first fetch error and race every
pdfjs await against it in display_pdf, so transient network errors
surface into the existing catch instead of hanging the tool call.
- pdfjs coalesces adjacent missing chunks into one unbounded
requestDataRange; readPdfRange clamps each call to MAX_CHUNK_BYTES.
Loop and deliver in slices so every requested chunk is marked loaded.
Viewer (mcp-app.ts):
- The lazy per-page baseline scan left pdfBaselineAnnotations incomplete,
so persistAnnotations and getAnnotatedPdfBytes silently dropped
restoredRemovedIds tombstones for unvisited pages. Union those ids into
the computed diff and removedRefs.
Test fixture: release stalled handlers before resetStats/close so a
failing stalled test doesn't hang afterAll; fail fast if started with
NODE_ENV=production. NODE_TLS_REJECT_UNAUTHORIZED scope documented (full
per-process scoping needs a validateUrl localhost allow, tracked
separately).
* test(pdf-server): bridge coverage gaps and switch fixture to gated loopback HTTP
PdfCacheRangeTransport.deliver(): pdf.js's reader is keyed by the
original begin and removed after one delivery, so accumulate slices and
call onDataRange once with the full buffer (the previous multi-call
approach threw inside pdfjs). Covered by a new integration test that
drives getDocument()/getPage(1) on a >1MB PDF through a clamping
in-memory readPdfRange.
validateUrl: allow http://127.0.0.1|localhost|[::1] only when
PDF_SERVER_ALLOW_LOOPBACK_HTTP is set, so a remote deploy can't be made
to probe its own ports. Covered by env-on/off unit tests.
Fixture switched to plain HTTP (no openssl, no
NODE_TLS_REJECT_UNAUTHORIZED). Adds /error.pdf (500s after 50KB) and two
e2e tests: page 2 renders after stall release (>512KB object path), and
display_pdf returns gracefully on mid-load 500. Existing <30% test now
samples stats before the iframe loads.
New unit tests: display_pdf returns (not hangs) on mid-load fetch
failure via in-memory MCP client; computeDiff/serializeDiff contract
tests pinning the restoredRemovedIds tombstone-preservation behaviour.
* test(pdf-server): e2e for restoredRemovedIds tombstone preservation through viewer
Adds /with-native-annot.pdf fixture (2 pages, native /Text annot on
page 2) and a Playwright test that drives delete on page 2 → iframe
reload → interact add_annotations on page 1 (persist with page 2
unscanned) → assert localStorage diff still contains the removed id →
page 2 still shows it gone. Covers the mcp-app.ts glue the unit tests
can't reach.
* refactor(pdf-server): simplify per round-2 review and fix false-pass test
server.ts: extract probeFormFields() so display_pdf calls one helper
instead of inlining 40 lines of transport/orFail plumbing; make
extractFormSchema's fieldObjects param required (the optional branch
only existed for a test helper).
server.test.ts: the >1MB integration test now forces the image XObject
fetch via getOperatorList() and asserts max requestDataRange span >
MAX_CHUNK_BYTES, so it can't go vacuous. Dedupe makeRandomJpeg by
importing from the fixture.
mcp-app.ts: restore per-annotation try/catch isolation in the lazy
baseline scan (a throw was skipping AnnotationLayer.render); only
carry forward restoredRemovedIds while the baseline scan is incomplete
so a stale id can't pin dirty=true once every page is scanned.
tests: bump fixture JPEG to 1.1MB; consolidate 7 e2e tests to 4 (merge
overlap into the byte-budget test, merge stall+page-2, drop the
error-e2e duplicate of the unit integration test); delete
run-fixture.mjs; drop /error.pdf and ERROR_AFTER_BYTES; rename
tombstone test's describe and clarify page-2 covers the viewer
transport.
* fix(pdf-server): preserve partial probeFormFields results when extractFormFieldInfo throws
* test(pdf-server): fix tombstone e2e — deleted native annotations become a cleared card, not removed from DOM
* test(pdf-server): mark tombstone e2e as fixme — needs basic-host iframe-reload replay support
* test(pdf-server): link tombstone fixme to tracking issue #642
* perf(pdf-server): share cache across server instances and dedupe form parsing
In stateless HTTP deployments createServer() is called per request, so
the per-instance pdfCache and the 4 MB viewer HTML were discarded after
every call. Hoist both to module scope.
Also refactor extractFormSchema/extractFormFieldInfo to accept an
already-parsed PDFDocumentProxy so display_pdf downloads and parses the
PDF once instead of twice.
* perf(pdf-server): cap total cache bytes with LRU eviction
The module-level sharedPdfCache could grow unbounded within the 60s
lifetime window under a burst of distinct URLs. Track running total
bytes and evict least-recently-used entries on insert when it would
exceed CACHE_MAX_TOTAL_BYTES (256MB). getCacheEntry now bumps the
accessed entry to the end of insertion order so eviction targets the
LRU entry rather than the oldest insert.
createPdfCache takes an optional maxTotalBytes for testability.
When FastMCP is created with the default host=127.0.0.1, MCP SDK 1.26+
automatically enables DNS rebinding protection that only allows requests
from localhost. This breaks Docker where requests come from
host.docker.internal, causing 421 Invalid Host header errors.
By passing host=HOST (0.0.0.0 for Docker), DNS rebinding protection
is not auto-enabled, allowing requests from any host.
* test: add unit coverage for PostMessageTransport source validation
PostMessageTransport is the iframe↔host communication layer and contains
the primary security boundary (event.source validation). Previously it
had zero unit coverage — only exercised indirectly via E2E tests, where
the cross-app injection test is a no-op due to cross-origin contentDocument
restrictions.
Covers 18 test cases across:
- Source validation: trusted source delivers, untrusted/null sources
dropped silently (no onerror DoS vector), transport survives rejection
- Message format: valid requests/notifications delivered; ambient
non-JSON-RPC traffic (devtools, extensions) silently ignored;
malformed jsonrpc:'2.0' payloads surface via onerror
- send(): posts to configured target with '*' targetOrigin
- Lifecycle: start() registers listener, close() removes it and fires
onclose, post-close messages ignored, multi-transport isolation
Uses a minimal in-memory window stub rather than jsdom/happy-dom —
consistent with the existing bun test setup which has no DOM configured.
Note: the transport validates event.source (window identity), NOT
event.origin. send() posts with '*'. Origin is intentionally not part
of this transport's trust model.
* fix: resolve TS diagnostics in message-transport tests
- TS2352: cast `{ id: string }` through `unknown` before
`MessageEventSource` (lines 366, 370) — no structural overlap
- TS80007: drop ineffective `await` on bun's `.resolves` matcher
(line 355) — it returns void, not Promise<void>
* docs: clarify origin validation lives in sandbox proxy, not transport
* ci: automate release process with workflow-driven version bumps and PR flow
Adds a Release workflow that bumps all package versions, generates release
notes, and opens a PR. Merging the PR auto-tags and creates the GitHub
Release, which triggers npm-publish. Also adds `npm run bump` for local use
and a release.yml config for categorized auto-generated notes.
* ci: simplify release workflow and flatten npm-publish approval chain
- Drop auto-generated RELEASES.md prepending (notes are written manually in PR)
- Flatten npm-publish job dependencies so all publish jobs wait for approval
together instead of sequentially, reducing 3 approval clicks to 1
- Remove Release environment from publish-mcpb (only needs GH_TOKEN)
- Use --generate-notes for GitHub Release creation
* fix: address review findings in release automation
- release.yml: use RELEASE_TOKEN PAT for gh release create so the
release:published event triggers npm-publish (GITHUB_TOKEN events
don't cascade to other workflows)
- release.yml: checkout merge_commit_sha in tag job instead of ref:main
to avoid tagging later commits
- release.yml: checkout ref:main in prepare job so dispatching from a
feature branch doesn't pollute the release PR
- release.yml: constrain preid to type:choice[beta] to match
npm-publish.yml's tag detection
- release.yml: drop -f on push so re-runs fail loudly instead of
clobbering manual RELEASES.md edits; drop pr-edit fallback
- release.yml: ensure release label exists before gh pr create
- release.yml: make tag push idempotent for re-runs after partial
failure
- bump-version.mjs: update workspace dependency ranges so major bumps
don't leave examples pointing at the old major
- bump-version.mjs: run npm install --package-lock-only so npm ci
on the release PR doesn't fail on lockfile mismatch
- npm-publish.yml: add npm-tag detection to publish-examples so beta
example releases don't overwrite latest
- .github/release.yml: use [bot] suffix in author excludes so
dependabot PRs are actually filtered from release notes
- CONTRIBUTING.md: document RELEASE_TOKEN setup; fix npm run -- syntax
* style: auto-fix prettier formatting
* build: add npm run bump; parallel npm-publish approval
- scripts/bump-version.mjs (+ npm run bump): bumps root and all workspace
package versions, updates examples' ^X.Y.0 dependency range on the root
package (matters on major bumps), and refreshes package-lock so npm ci
accepts the release PR.
- npm-publish.yml: all publish jobs now depend directly on [build, test]
instead of chaining, so they enter "waiting for approval" together and a
single Review-deployments click approves the lot. Prerelease versions
(any X.Y.Z-…) publish under --tag beta so they don't take latest.
- CONTRIBUTING.md: release steps now use npm run bump.
Dropped from the original change: the workflow_dispatch release.yml
(required a long-lived PAT to cascade events), label-based
.github/release.yml auto-notes (PRs aren't labeled today), and the
release-X.Y npm-tag for non-main branches (no backport branches yet).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(react): close App on useApp unmount; relax late-handler guard for re-registration
useApp's effect cleanup now closes the App (and its transport) instead
of only flipping a `mounted` flag. Under React StrictMode dev
double-invoke, the abandoned first instance's PostMessageTransport
otherwise keeps a window `message` listener and receives every host
postMessage alongside the second instance.
The late-handler guard from #629 now tracks `_everHadListener` per
event and only flags the *first* registration of a one-shot event
post-handshake. Re-registration (React useEffect cleanup + re-add on
dep change) of an event that had a pre-connect handler is silent,
including under `strict: true`.
* feat(app-bridge): warn on second ui/initialize
If a View double-mounts (e.g. React StrictMode in dev) without closing
the previous App, AppBridge now logs a console.warn when it receives a
second ui/initialize. Behavior is unchanged — it still responds and the
latest appInfo/appCapabilities replace the previous values — this is
purely a diagnostic for host implementers.
_assertInitialized used console.error while _assertHandlerTiming and the
AppBridge override used console.warn. Align all three on console.warn for
the non-strict path; strict still throws.
Tests: hoist the console.warn spy to beforeEach/afterEach in the
pre-handshake describe to avoid bun:test spy stacking now that all three
guards target the same console method.
Mirrors the outbound-side guard from #623: registering a toolinput /
toolinputpartial / toolresult / toolcancelled handler after the
ui/initialize handshake has completed now logs a console.warn (or
throws under { strict: true }), since the host may have already fired
the notification by then. hostcontextchanged is exempt — late
listeners for repeating events are legitimate.
Covers both the on* setters and addEventListener.
Zod v4's object parser compiles a fast path with `new Function()`, which
violates CSP without `unsafe-eval`. Hosts that enforce the spec's default
CSP (e.g. VS Code) cannot load any View that parses an incoming message —
the SDK's generated schemas trip this on the very first ui/initialize
roundtrip.
The App constructor now sets `z.config({ jitless: true })` by default so
Views work out of the box under strict CSP. New `AppOptions.allowUnsafeEval`
(default false) skips this for Views that know their host permits
`unsafe-eval` and want Zod's faster JIT path.
Addresses #374. Also relevant to #199 (threejs-server has a separate
`new Function()` use in its own shader compiler that this does not fix).
New `examples/` are costly to review and maintain across SDK changes.
Add a policy section stating that example PRs from outside contributors
are not accepted, and redirect contributors to open an issue instead.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The runtime console.error/warn strings in App._assertInitialized and
AppBridge.replaceRequestHandler linked to a tracking issue. Issue URLs
in runtime logs are brittle and send developers to a discussion thread
rather than actionable docs; the message itself already says what to do.
JSDoc @see links retained.
* fix(types): forbid csp/permissions on McpUiToolMeta via never
Hosts read _meta.ui.csp and _meta.ui.permissions from the resource
(resources/read content item, with resources/list entry as fallback),
never from the tool. Typing them as `never` on McpUiToolMeta gives
TypeScript users a compile-time error pointing at McpUiResourceMeta
instead of silently accepting and ignoring them.
Companion to anthropics/apps#38705 (host-side console.warn).
Refs anthropics/claude-ai-mcp#40.
* drop misleading #40 link from McpUiToolMeta.csp JSDoc
* feat: Add tool registration and bidirectional tool support
This PR adds comprehensive tool support for MCP Apps, enabling apps
to register their own tools and handle tool calls from the host.
- Add `registerTool()` method for registering tools with input/output schemas
- Add `oncalltool` setter for handling tool call requests from host
- Add `onlisttools` setter for handling tool list requests from host
- Add `sendToolListChanged()` for notifying host of tool updates
- Registered tools support enable/disable/update/remove operations
- Add `sendCallTool()` method for calling tools on the app
- Add `sendListTools()` method for listing available app tools
- Fix: Use correct ListToolsResultSchema (was ListToolsRequestSchema)
- Add comprehensive tests for tool registration lifecycle
- Add tests for input/output schema validation
- Add tests for bidirectional tool call communication
- Add tests for tool list change notifications
- All 27 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* nits
* Update apps.mdx
* feat: Add automatic request handlers for app tool registration
Implement automatic `oncalltool` and `onlisttools` handlers that are
initialized when apps register tools. This removes the need for manual
handler setup and ensures tools work seamlessly out of the box.
- Add automatic `oncalltool` handler that routes calls to registered tools
- Add automatic `onlisttools` handler that returns full Tool objects with JSON schemas
- Convert Zod schemas to MCP-compliant JSON Schema using `zod-to-json-schema`
- Add 27 comprehensive tests covering automatic handlers and tool lifecycle
- Test coverage includes error handling, schema validation, and multi-app isolation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* type updates
* fix: Ensure tools/list returns valid JSON Schema for all tools
- Always return inputSchema as object (never undefined)
- Keep filter for enabled tools only in list
- Update test to match behavior (only enabled tools in list)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* type updates
* rm zod-to-json-schema
* fix: Update RegisteredTool to use 'handler' instead of 'callback' (SDK API change)
* refactor: Rename sendCallTool/sendListTools to callTool/listTools
Avoid double-verb naming pattern for consistency with existing API.
* feat: Add screenshot and click support to SDK
- Add McpUiScreenshotRequest/Result and McpUiClickRequest/Result types
- Add onscreenshot and onclick handlers to App class
- Add screenshot() and click() methods to AppBridge class
- Generate updated Zod schemas
* feat(map-server): Enable registerTool with navigate-to and get-current-view
- Uncomment and fix the navigate-to tool for animated navigation
- Add get-current-view tool to query camera position and bounding box
- Add flyToBoundingBox function for smooth camera animation
- Add setLabel function for displaying location labels
* feat(pdf-server): Add widget interaction tools
- get-document-info: Get title, current page, total pages, zoom level
- go-to-page: Navigate to a specific page
- get-page-text: Extract text from a page
- search-text: Search for text across the document
- set-zoom: Adjust zoom level
* feat(shadertoy-server): Add widget interaction tools
* feat(examples): Add widget interaction tools to budget-allocator, shadertoy, wiki-explorer, and threejs
budget-allocator:
- get-allocations: Get current budget allocations
- set-allocation: Set allocation for a category
- set-total-budget: Adjust total budget
- set-company-stage: Change stage for benchmarks
- get-benchmark-comparison: Compare against benchmarks
shadertoy:
- set-shader-source: Update shader source code
- get-shader-info: Get shader source and compilation status
- Sends errors via updateModelContext
wiki-explorer:
- search-article: Search for Wikipedia articles
- get-current-article: Get current article info
- highlight-node: Highlight a graph node
- get-visible-nodes: List visible nodes
threejs:
- set-scene-source: Update the Three.js scene source code
- get-scene-info: Get current scene state and any errors
- Sends syntax errors to model via updateModelContext
* feat: Add expand-node tool to wiki-explorer and update tool descriptions
Wiki Explorer:
- Add expand-node tool - the critical missing tool for graph exploration
- Claude can now programmatically expand nodes to discover linked articles
Server descriptions updated to mention widget tools:
- map-server: navigate-to, get-current-view
- pdf-server: go-to-page, get-page-text, search-text, set-zoom, get-document-info
- budget-allocator: get-allocations, set-allocation, set-total-budget, etc.
- shadertoy: set-shader-source, get-shader-info
- wiki-explorer: expand-node, search-article, highlight-node, etc.
All descriptions now mention 'Use list_widget_tools to discover available actions.'
* refactor: Simplify tool descriptions - remove client implementation details
The server tool descriptions now just mention that widgets are interactive
and can be controlled, without teaching the model about list_widget_tools
(which is the client's responsibility to teach).
Before: 'The widget exposes tools: X, Y, Z. Use list_widget_tools to discover...'
After: 'The widget is interactive and exposes tools for X and Y.'
* Remove ui/screenshot and ui/click protocol methods
These will be proposed separately from the app tool registration changes.
* fix(app): auto-register tools capability in registerTool
When registerTool is called before connect() on an App created without
explicit tools capability, setRequestHandler's capability assertion
would throw, breaking app initialization at module load.
Auto-register { tools: { listChanged: true } } on first registerTool
call (pre-connect only), mirroring McpServer.registerTool behavior.
Fixes pdf-annotations e2e failures where the PDF canvas never rendered
because registerTool threw at module scope.
* pdf-server: replace app-tools with 1:1 interact command mapping
Converts the 5 placeholder app-registered tools into 12 tools that map
directly to the server's interact commands (navigate, search, find,
search_navigate, zoom, add_annotations, update_annotations,
remove_annotations, highlight_text, fill_form, get_text, get_screenshot)
plus the existing get-document-info.
Implementation stays DRY by dispatching through the existing
processCommands() handler — each tool callback constructs a PdfCommand
and runs it via a small runCommand() wrapper. For get_text and
get_screenshot, the page-data collection is extracted from
handleGetPages into a shared collectPageData() helper so results can
be returned directly instead of round-tripping through the server.
Tool names and zod schemas mirror the interact command parameter shapes
from server.ts so the model sees the same surface whether it goes
through interact or app-tools. The server-side interact tool is
unchanged and remains available for hosts without app-tool support.
* chore: drop unused test vars and stale Request/Result imports
* docs(spec): address review feedback on app tool registration
- Add outputSchema to Tool struct (matches SDK + core MCP)
- Reword "black box" bullets: drop stale screenshots reference, clarify no DOM access
- Clarify push vs pull contrast with explicit setWidgetState + ui/update-model-context refs
- Add Task-reference future-work bullet for tools outliving render lifecycle
* docs(spec): clarify app DOM is not host-introspectable; frame tools as pull complementing update-model-context push
* fix(app): emit title and omit absent outputSchema in tools/list
* feat(app): accept Standard Schema in registerTool/registerAppTool
App.registerTool and registerAppTool now accept any schema implementing
the Standard Schema spec (Zod ≥3.25, ArkType, Valibot, …) instead of
zod-only. Uses ~standard.validate / ~standard.jsonSchema directly, so
app.ts no longer imports zod at runtime — zod is now an optional peer.
Mirrors the StandardSchemaWithJSON type from
modelcontextprotocol/typescript-sdk#1689 so bumping to SDK v2 is a
drop-in import swap. Adds @standard-schema/spec (types-only) and a
hand-rolled non-zod test.
* docs(spec): reference core MCP Tasks for long-running app tools
App tools/call MAY be task-augmented per the core MCP Tasks utility
(spec 2025-11-25). Adds a Long-Running App Tools subsection: hosts proxy
tasks/* while the app is mounted; apps SHOULD delegate long-lived work
to the server so the task survives iframe teardown. Removes the stale
'future extension' bullet — Tasks is shipped.
* docs(spec): note Schema Validation accepts any Standard Schema library
* chore: clean unused-var and no-op-await diagnostics in app.ts and tests
* refactor(app): fold error prefix into validateStandardSchema; clarify registerAppTool runtime caveat
* chore(typedoc): mark RequestHandlerExtra as intentionally not exported
AppToolCallback (introduced with the Standard Schema migration) references
the local RequestHandlerExtra alias in its parameter type. The alias is a
private Parameters<> derivation and isn't part of the public API surface,
so add it to intentionallyNotExported alongside AppOptions/MethodSchema.
* fix(app): restore zod v3.25 compat via lazy z.toJSONSchema fallback
zod v3.25.x implements ~standard.validate but not ~standard.jsonSchema,
so constraining to StandardSchemaWithJSON broke the ^3.25.0 peer range.
Widen registerTool to StandardSchemaV1 and fall back to a lazy zod/v4
import for serialization when ~standard.jsonSchema is absent and
vendor==='zod'. Non-zod schemas without jsonSchema get a clear error.
* docs(spec): slim Long-Running App Tools to a pointer into core MCP Tasks
* chore: add TODO(sdk-v2) marker in standard-schema.ts
* fix(app): align registerTool runtime with declared types
- Handler reads registeredTool.{input,output}Schema so update() takes effect
- Call cb(extra) when no inputSchema, matching AppToolCallback<undefined>
- Throw on duplicate name; guard list_changed pre-connect; notify on register
- Drop misleading zod optional peer (still required by generated/schema.ts)
- examples: fix pdf search/find stale match count; threejs set-scene-source re-render
- spec: Zod → Standard Schema; clarify tools availability via tools/list after init
* feat(app): type structuredContent from outputSchema in registerTool
AppToolCallback gains an Out param: when outputSchema is provided,
structuredContent is required and typed via StandardSchemaV1.InferOutput,
with an `isError: true` escape hatch. Without outputSchema, return type
is unchanged (CallToolResult). Uses intersection rather than Omit since
CallToolResult's index signature swallows Omit's known keys.
Also drops the `as any` casts on z.object(...) in tests — zod 4 satisfies
StandardSchemaV1 directly.
* fix(app): registerTool edge cases + example tool correctness
App.registerTool:
- Skip outputSchema validation when result.isError (matches AppToolResult type)
- Pin callback arity to original config.inputSchema; only the validation schema is mutable via update()
- Default missing tools/call arguments to {}
- remove() is a no-op if the handle is stale (re-registered or already removed)
- Gate list_changed notifications on the listChanged capability
Examples:
- threejs: propagate executeThreeCode failures to onSceneError/onSceneRendering; catch updateModelContext rejection
- wiki-explorer: search-article preserves graph on no-match
- budget-allocator: fix category/stage values in tool descriptions
* feat(app,app-bridge): guard requests sent before handshake completes (#620)
* feat(app,app-bridge): guard requests sent before handshake completes
Calling host-bound methods before the ui/initialize →
ui/notifications/initialized handshake completes can race the handshake
on strict hosts and leave the iframe permanently hidden.
App (View side):
- All 8 host-bound methods (callServerTool, readServerResource,
listServerResources, sendMessage, updateModelContext, openLink,
downloadFile, requestDisplayMode) now check that connect() has sent
the initialized notification.
- New AppOptions.strict (default false): console.error when false,
throw when true. AppOptions is now exported.
- useApp() forwards strict to the App constructor.
- Flag resets on reconnect.
AppBridge (Host side):
- replaceRequestHandler is overridden to wrap every host-bound handler
with a console.warn if the request arrives before
ui/notifications/initialized. Never throws. ui/initialize and ping
use setRequestHandler directly and are exempt; notifications are
unaffected.
- Catches Views that hand-roll postMessage without the SDK.
- Flag resets on reconnect.
Refs anthropics/claude-ai-mcp#61, anthropics/claude-ai-mcp#149.
* fix(docs): drop AppOptions from intentionallyNotExported now that it is exported
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: MCP app sampling support via stock SDK types
Alternative to #477. Instead of forking a stripped-down sampling schema,
this reuses the stock `CreateMessageRequest` / `CreateMessageResult` /
`CreateMessageResultWithTools` types from `@modelcontextprotocol/sdk`
directly — same pattern already used for `tools/call`.
- Spec: `sampling/createMessage` listed under `### Standard MCP Messages`,
`sampling?: { tools?: {} }` added to `HostCapabilities` (mirrors MCP
`ClientCapabilities.sampling` shape for easy pass-through).
- SDK: `App.createSamplingMessage()` with overloads that narrow the return
type based on whether `params.tools` is set; `AppBridge.oncreatesamplingmessage`
setter; `CreateMessageRequest`/`CreateMessageResult*` added to the
`AppRequest`/`AppResult` unions.
- Picks up SEP-1577 tool-calling support (`tools`, `toolChoice`, `tool_use`
content blocks, `stopReason: "toolUse"`, array content) for free — unblocks
the "nested agents" use case motivated by the original PR.
https://claude.ai/code/session_01ENGWTtsfcyP4S6fTWtUMAh
* feat(app): guard createSamplingMessage before handshake completes
---------
Co-authored-by: Claude <noreply@anthropic.com>
useApp() previously hardcoded autoResize: true. Expose it as a hook
option (default unchanged) so callers can disable the built-in
ResizeObserver and drive sizing themselves.
* feat(app,app-bridge): guard requests sent before handshake completes
Calling host-bound methods before the ui/initialize →
ui/notifications/initialized handshake completes can race the handshake
on strict hosts and leave the iframe permanently hidden.
App (View side):
- All 8 host-bound methods (callServerTool, readServerResource,
listServerResources, sendMessage, updateModelContext, openLink,
downloadFile, requestDisplayMode) now check that connect() has sent
the initialized notification.
- New AppOptions.strict (default false): console.error when false,
throw when true. AppOptions is now exported.
- useApp() forwards strict to the App constructor.
- Flag resets on reconnect.
AppBridge (Host side):
- replaceRequestHandler is overridden to wrap every host-bound handler
with a console.warn if the request arrives before
ui/notifications/initialized. Never throws. ui/initialize and ping
use setRequestHandler directly and are exempt; notifications are
unaffected.
- Catches Views that hand-roll postMessage without the SDK.
- Flag resets on reconnect.
Refs anthropics/claude-ai-mcp#61, anthropics/claude-ai-mcp#149.
* fix(docs): drop AppOptions from intentionallyNotExported now that it is exported
- link-self.mjs: skip when node_modules/@modelcontextprotocol/ext-apps is
a symlink (e.g. to the repo root). cpSync("dist", target/dist) was
failing with ERR_FS_CP_EINVAL because src and dest resolved to the
same path.
- .husky/pre-commit: capture staged files with --diff-filter=d so the
re-stage step doesn't fail with "pathspec did not match" on commits
(typically merges) that include deletions.
A host may interpose a long-running or user-interactive step between the
guest's tools/call and the server's response. Without this, the base
SDK's default request timeout in Protocol.request() fires and the guest
sees -32001 even though the host is still working.
Passing onprogress causes Protocol.request to include _meta.progressToken
in the outgoing request and register a progress handler;
resetTimeoutOnProgress makes incoming notifications/progress reset the
per-request timer. Hosts can then heartbeat during long-running steps.
Callers can still override both.
The initial fit-to-width in the pdf viewer measures container.clientWidth
immediately after showViewer() flips display:flex. Under CI load the reflow
can lag a tick, leaving widthFit >= 1.0 and zoom at 100% when first read,
which intermittently fails the toBeLessThan(100) assertion (~6 failures in
the 30 runs since #583).
Swap the single-shot read for expect.poll, matching the pattern already used
for the post-fullscreen assertion below, then capture inlineZoom for the
subsequent comparison.
* feat(pdf-server): rasterize imported annotations via annotationCanvasMap
Adds an 'imported' annotation type for anything in a loaded PDF that we
either don't model (Ink, Polygon, Caret, FileAttachment, ...) or can't
faithfully re-render (Stamp with an appearance stream, e.g. an image
signature). These now:
- Appear in the annotation panel as '<Subtype> (from PDF)'
- Render in our layer as a positioned div whose body is the per-
annotation canvas pdf.js produced via page.render({annotationCanvasMap})
- if pdf.js didn't divert it (no hasOwnCanvas), the box is transparent
over the main-canvas pixel and just captures clicks
- Are selectable and draggable (resize/rotate disabled - bitmap would
just stretch)
- Are skipped by addAnnotationDicts; getAnnotatedPdfBytes already
filters baseline ids, so save leaves the original in the PDF.
Move/delete are UI-only for now (documented).
Link (2) and Popup (16) are excluded - navigational/auxiliary, not
markup.
importPdfjsAnnotation tests added for unsupported-type and
appearance-stamp paths; computeDiff round-trip for 'imported'.
* fix(pdf-server): isEditing on page.render so stamps always divert to annotationCanvasMap
StampAnnotation.hasOwnCanvas defaults to noRotate; stamps without that
flag composited onto the main canvas, so deleting the 'imported' overlay
left an unclickable pixel. isEditing forces hasOwnCanvas=true for stamps
(via mustBeViewedWhenEditing) so the appearance lands in the per-id
canvas and our DOM element is the only render.
* fix(pdf-server): refresh annotation panel after save rebases baseline
setDirty(false) updated the title and save button but the panel kept
showing pending-change badges because it diffs against
pdfBaselineAnnotations/FormValues and was never re-rendered.
* fix(pdf-server): imported stamp canvases cropped/2x on retina
renderPage applied devicePixelRatio via ctx.scale(dpr,dpr) instead of
page.render's transform parameter. pdf.js sizes annotationCanvasMap
backing buffers as rectW * outputScaleX * viewport.scale, and
outputScaleX is read from transform[0] (defaults 1). So on retina the
per-annotation canvas got a 1x backing while its internal setTransform
(from the SVD of the already-dpr-scaled ctx) was 2x - the appearance
rendered at 2x into a half-sized buffer, showing only the top-left
quarter.
Pass dpr via transform: [dpr,0,0,dpr,0,0] so outputScaleX matches.
Also filter AnnotationLayer.render() to Widget annotations only so it
stops creating empty pointer-events:auto sections for stamps in
#form-layer that could steal clicks from our overlays.
* feat(pdf-server): strip deleted baseline annotations from /Annots on save
buildAnnotatedPdfBytes takes a removedRefs list and walks each page's
/Annots array (backwards) removing matching PDFRef entries.
getAnnotatedPdfBytes computes that list from baseline annotations no
longer in annotationMap, parsing the ref back from our id via
parseAnnotationRef (handles both pdf-<num>-<gen> and pdf-<num>R).
Panel: removed baseline annotations now stay listed as crossed-out cards
with a revert button (mirrors cleared form fields), so the user can see
and undo the pending deletion before save commits it.
* fix(pdf-server): re-render after seeding storage from AcroForm tree
buildFieldNameMap runs AFTER the first renderPage (perf: don't block the
canvas on an O(numPages) scan). When it detects a widget/field-tree
mismatch and pushes the field-tree value into annotationStorage, the
form layer has already rendered the stale widget value. Re-render once
when that happens so the input shows the AcroForm truth.
* fix(pdf-server): reload PDF after successful save instead of rebasing in place
Rebasing baselines while keeping the old pdfDocument drifts: subsequent
renders rasterize annotations that were just stripped from disk, and the
field/widget split pdf-lib's save can create isn't visible until reload.
Reload makes 'viewer == disk' an invariant. localStorage cleared first;
file_changed echo suppressed by lastSavedMtime as before.
* fix(pdf-server): one bad form field no longer aborts saving the rest
#577 dropped the per-field try/catch in buildAnnotatedPdfBytes when it
swapped to type-dispatch, so the first field whose pdf-lib write throws
(max-length text, missing /Yes appearance, radio buttonValue mapping to
neither label nor index, ...) bubbled to the outer catch and silently
dropped every subsequent field. Compounded by getAnnotatedPdfBytes
passing every baseline field, so even an untouched problematic field in
the PDF poisoned the whole save.
- Re-wrap each field write in its own try/catch (warn + continue).
- getAnnotatedPdfBytes now only sends fields whose value differs from
baseline (still includes explicit ''/false sentinels for cleared
fields so pdf-lib overwrites the original /V).
- Regression test: a maxLength=2 text field fed a long string, followed
by a normal field; assert the second one still lands.
* fix(pdf-server): radio groups misclassified as PDFCheckBox save the chosen widget
Some PDFs (e.g. third-party forms like the IRS f1040 family, and the
demo Form.pdf) omit the /Ff Radio flag bit on button fields, so pdf-lib
classifies a multi-widget radio as PDFCheckBox. The viewer stores
pdf.js's buttonValue (the widget's on-state name, e.g. '0'/'1') as a
string. The PDFCheckBox branch did 'if (value) field.check()', which
always sets the FIRST widget's on-state - so any choice saved as the
first option.
When the value is a string on a PDFCheckBox, treat it as a radio
on-value: write /V and per-widget /AS directly via the low-level
acroField (mirroring PDFAcroRadioButton.setValue minus its
first-widget-only onValues guard). Booleans keep check()/uncheck().
Test: build a radio fixture, clear the Radio flag so reload sees
PDFCheckBox, save with '1', assert /V = /1 and second widget /AS = /1.
* fix(pdf-server): clearing a radio from the panel unchecks all widgets
clearFieldInStorage wrote the same string clearValue to every widget's
storage, hitting pdf.js's inverted string-coercion bug (the one
setFieldInStorage already works around): the wrong widget rendered
checked. For radio, write {value:false} to every widget instead.
* fix(pdf-server): only treat multi-widget PDFCheckBox as misclassified radio
A single-widget checkbox getting a string value (e.g. 'Yes'/'Off' if
something upstream stores the export string instead of boolean) was
being routed through setButtonGroupValue, which no-ops when no widget's
on-state matches the value - leaving the box unchanged. Gate the
radio-path on widgets.length > 1; single-widget falls through to
check()/uncheck() with liberal truthy/'Off' semantics.
* fix(pdf-server): generate missing field appearances on save
PDFs whose checkbox/radio widgets lack an /AP/N/<onValue> stream: check()
sets /V and /AS but Preview/Acrobat can't render a state with no
appearance, so the field looks unchanged. Call form.updateFieldAppearances()
after the write loop so pdf-lib synthesizes defaults for dirty fields.
* feat(pdf-server): save PDFOptionList (multiselect listbox) values
Language field in Form.pdf is a PDFOptionList; the save loop skipped it.
Parse the viewer's comma-joined string, select() the subset that exists
in getOptions() (pdf-lib throws on unknowns and there's no edit-mode
fallback like Dropdown has).
* fix(pdf-server): capture all selected options on <select multiple>
target.value on a multiselect is only the first option; join
selectedOptions so the save path gets the full set.
* fix(pdf-server): import highlight/underline/strike from existing PDFs
importPdfjsAnnotation expected nested quadPoints arrays, but pdf.js
emits a flat Float32Array (8 numbers per quad). Iterating it yielded
numbers, qp.length was undefined, rects stayed empty, and the function
returned null - so every quad-based annotation in a loaded PDF was
dropped from annotationMap and rendered only as unselectable canvas
pixels. Shipped this way in #506; not a regression.
Parse the flat array in 8-stride chunks, fall through to ann.rect if
that yields nothing. Existing tests used the (wrong) nested fixture
shape; switched them to Float32Array and added a 2-quad case + a
rect-fallback case.
* fix(pdf-server): own markup annotations in our layer, not the canvas
page.render() defaulted to AnnotationMode.ENABLE, painting every
annotation's appearance stream onto the canvas. We ALSO import them into
annotationMap and render DOM versions in #annotation-layer, so the user
saw two representations: an unclickable canvas pixel that looked right,
and our clickable DOM element that looked like our styling. Clicking the
'real' one did nothing.
Set annotationMode: DISABLE so the canvas is page-content-only and our
layer is the single source of truth for markup. Imported annotations now
behave identically to user-created ones (select/drag/delete). Form
widgets are unaffected (#form-layer via AnnotationLayer.render).
get_screenshot keeps ENABLE_STORAGE so screenshots still show
annotations.
Known trade-offs:
- Stamps with image appearance streams degrade to our text-label render
(no rasterization yet).
- Annotation types we don't import (Ink, Polygon, Caret, ...) no longer
ghost on the canvas - they're invisible. They were never editable
anyway; loadBaselineAnnotations already logs them.
* Revert "fix(pdf-server): own markup annotations in our layer, not the canvas"
This reverts commit 2f989a757d3aa517fa427c7fd59b462326599bfc.
* feat(pdf-server): get_viewer_state interact action
New interact action that returns a JSON snapshot of the live viewer:
{currentPage, pageCount, zoom, displayMode, selectedAnnotationIds,
selection: {text, contextBefore, contextAfter, boundingRect} | null}.
The viewer already pushes selection passively via setModelContext as
<pdf-selection> tags, but not all hosts surface model-context. This gives
the model an explicit pull.
selection.boundingRect is a single bbox in PDF points (top-left origin,
y-down) so it can be fed straight back into add_annotations. selection is
null when nothing is selected or the selection is outside the text-layer.
Wiring: new PdfCommand variant -> processCommands case ->
handleGetViewerState -> submit_viewer_state (new app-only tool, mirrors
submit_save_data) -> waitForViewerState -> text content block.
Also fills a gap in the display_pdf description: it listed interact
actions but was missing save_as; added that and get_viewer_state.
e2e: two tests covering selection:null and a programmatic text-layer
selection.
* test(pdf-server): fix get_viewer_state e2e race + assertions
readLastToolResult clicked .last() before the interact result panel
existed (callInteract doesn't block), so it expanded the display_pdf
panel instead. Wait for the expected panel count first.
Also: basic-host renders the full CallToolResult JSON, with the state
double-escaped inside content[0].text. Parse instead of regex-matching.
playwright.config.ts: honor PW_CHANNEL env to use system Chrome locally
when the bundled chromium_headless_shell is broken.
Some hosts overlay UI on the iframe (chat composer, etc.) without
reporting it in safeAreaInsets.bottom, so computeFitScale's 'fit' fills
the iframe but the page bottom sits under the overlay. Flooring the
zoom-out button at fit removed the only escape hatch.
Pinch keeps the fit floor + rubber-band (it needs a defined snap point
for the exit-to-inline gesture). The button now goes to ZOOM_MIN as
before #587.
* fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout
Highlights rendered opaque: inline `background: <hex>` overrode the CSS
rgba(...,0.35). Now convert def.color via cssColorToRgb and force alpha
0.35. Added a toHaveCSS regression check in pdf-annotations.spec.ts.
Pinch-in while inline (wheel ctrlKey deltaY<0 or two-finger spread
>1.15x) now enters fullscreen. Pinch-out in fullscreen past 0.9x of
fit-scale, when already at/below fit, exits to inline and clears
userHasZoomed so refit sizes the inline view. previewScaleRaw tracks the
unclamped intent so the exit fires even when fit ~= ZOOM_MIN. A
modeTransitionInFlight latch (held 250ms post-toggle) keeps the gesture
tail from re-toggling or immediately zooming the new view.
Fullscreen toolbar: flex-wrap: nowrap and tighter 0.25rem vertical
padding (min-height 40px + safe-top instead of 48px + wrap). Search bar
top/right now follow --safe-top/--safe-right so it sits flush below the
toolbar instead of overlapping it. Base .canvas-container gets
touch-action: pan-x pan-y so the inline pinch is capturable on iOS.
* fix(pdf-server): drop --safe-top from fullscreen toolbar padding
Hosts wrap fullscreen in their own header (title + close), which already
clears the top safe-area. Adding --safe-top to our toolbar padding-top
double-dipped, leaving a visible gap between the host header and our
toolbar. Keep --safe-left/right (host header doesn't cover the sides).
* feat(pdf-server): horizontal swipe changes pages whenever page fits width
Previously gated on scale <= 1.0, which blocked page-nav in fullscreen
where fit-scale is often >100%. Now gate on actual horizontal overflow
(scrollWidth > clientWidth) so swipe works at any fit-to-width scale and
still defers to native panning once you zoom past it.
* fix(pdf-server): refit to inline width after pinch-out exits fullscreen
handleHostContextChanged calls refitScale() before the iframe has actually
shrunk, and the ResizeObserver's inline branch only refits on width
*growth* (to avoid a requestFitToContent shrink-loop). So the
fullscreen->inline shrink never triggered a refit and the page stayed at
the fullscreen scale.
Add a one-shot forceNextResizeRefit flag, set on fullscreen->inline (both
the pinch-out path and handleHostContextChanged), consumed by the
ResizeObserver on the next size change. One-shot keeps the shrink-loop
guard intact for ordinary inline resizes.
* feat(pdf-server): floor fullscreen zoom at fit-to-page
In fullscreen, pinch-out and the zoom-out button now floor at the
fit-to-page scale instead of ZOOM_MIN, so the page never shrinks below
fully-visible (no dead margin around it). previewScaleRaw stays unclamped
so a continued pinch-out past fit still triggers exit-to-inline.
* fix(pdf-server): wheel pinch-out can exit fullscreen again
The fit-floor pinned previewScale at fit, but the wheel handler multiplied
the *clamped* previewScale, so it could never accumulate below fit*0.9 to
trigger exit. Drive the wheel accumulator off previewScaleRaw instead, and
bound previewScaleRaw to [floor*0.7, ZOOM_MAX] so it can cross the 0.9
exit threshold without drifting unboundedly (which would make direction
reversal feel sticky). Touch path was unaffected (absolute ratio).
* fix(pdf-server): always refit to inline width on fullscreen exit
userHasZoomed stayed true after Escape/button/host-× exits, so refitScale()
bailed even though forceNextResizeRefit let the ResizeObserver call it.
Clear userHasZoomed in handleHostContextChanged whenever we land inline —
fullscreen zoom level is meaningless there. Dropped the now-dead
requestFitToContent fallback in the same block.
* refactor(pdf-server): rubber-band pinch-out instead of dual-tracked clamp
The previewScaleRaw side-channel made the gesture feel dead (page pinned
at fit, no feedback) and the wheel accumulator interaction was fragile.
New model: previewScale is the only tracked value. In fullscreen it may
overshoot down to 0.75*fit so the user *sees* the page pull away as they
pinch out. On commit:
- started near fit (<=1.05*fit) AND preview <0.9*fit -> exit to inline
- otherwise clamp committed scale to >=fit (overshoot snaps back)
beginPinch seeds fitScaleAtPinchStart synchronously from when
!userHasZoomed (the common enter-fullscreen-at-fit case) so the first
frame already has the right floor; the async computeFitScale refines it.
* test(pdf-server): update inline-pinch e2e for new fullscreen behavior
'trackpad pinch is ignored outside fullscreen' is no longer the contract.
Replace with two tests: pinch-in inline -> .main.fullscreen appears;
pinch-out inline -> zoom unchanged, no fullscreen. The two
pdf-annotations.spec.ts failures in the previous run were flaky (passed
on retry).
#585's `npm pkg set dependencies.@modelcontextprotocol/sdk` created a
root `dependencies` block — root previously had only devDependencies +
peerDependencies (it IS the published SDK, the upstream sdk is a peer).
Harmless at runtime but pulls sdk into the published tarball's deps.
The original 1.25.2 devDep pin (b9a459a6, Jan 26) was because sdk
1.25.3 depended on @hono/node-server@^1.19.9 which didn't exist on
npm yet — long since resolved.
* fix(pdf-server): stub DOMMatrix/ImageData/Path2D for npx without @napi-rs/canvas
pdfjs-dist/legacy/build/pdf.mjs does `new DOMMatrix()` at module scope.
Its own polyfill loads @napi-rs/canvas (an optionalDependency), but npx
frequently misses the platform-native binary (npm/cli#4828), so the
server crashed on `npx @modelcontextprotocol/server-pdf --stdio`:
ReferenceError: DOMMatrix is not defined
at pdfjs-dist/legacy/build/pdf.mjs:17078
The server only uses pdfjs for text/metadata/form-field extraction —
never canvas rendering — so no-op stubs suffice and avoid shipping
~130MB of native binaries.
The polyfill is a separate ESM module kept --external in the bun bundle
so it executes before pdfjs's hoisted static import; inlined body code
would run too late.
Broken since 1.3.0 (#506). Thanks to Bryan Thompson for the analysis
and fix shape from the npm-to-mcpb pipeline.
* fix(pdf-server): MCPB bundle missing runtime deps + display_name with spaces
`mcpb pack` zips whatever is on disk; in this monorepo all runtime deps
are hoisted to root node_modules, so the published .mcpb shipped without
pdfjs-dist/ajv/express/etc. and crashed on import in Claude Desktop.
build-mcpb.mjs stages dist/ + manifest into a clean temp dir, runs a
non-workspace `npm install --omit=dev --omit=optional` (the new
DOMMatrix polyfill makes @napi-rs/canvas's ~130MB of native binaries
unnecessary), syncs manifest.version to package.json, then packs.
Result: 12.3MB / 38.8MB unpacked, full MCP handshake + tools/list pass.
Also rename display_name "PDF (By Anthropic)" → "pdf-viewer" — spaces
in display_name break MCP App UI rendering for MCPBs in Claude Desktop.
CI now smoke-tests the extracted bundle starts (`dist/index.js --stdio`
prints "Ready") so a deps-less bundle can't ship again.
* fix(pdf-server): list interact in MCPB manifest tools (was stale since #491)
* feat(pdf-server): refit on inline→fullscreen + pinch-to-zoom in fullscreen
Two related fixes for the fullscreen reading experience:
1) Inline → fullscreen left the page at the cramped inline scale.
computeFitToWidthScale() returned null when the natural page
already fit (availableWidth >= pageWidth), so going from a
narrow inline iframe (~67%) to a wide fullscreen container
produced null → no rescale. Now caps at min(1.0, fit) so the
refit handler sees 1.0 ≠ 0.67 and re-renders. Display-mode
handling moves before the containerDimensions refit so the
.fullscreen padding lands before we measure clientWidth, and
the inline→fullscreen edge triggers refit explicitly (some
hosts send displayMode without containerDimensions).
2) Pinch-to-zoom in fullscreen. Two input paths:
- wheel + ctrlKey → trackpad pinch (Chrome/FF/Edge/Safari).
exp(-deltaY/100) per pdf.js; 150ms settle then commit.
- two-finger touch → mobile. Distance ratio against touchstart;
commit on touchend, revert on touchcancel.
Both apply a CSS transform to .page-wrapper for live feedback,
then commit one renderPage() at gesture end (renderPage is
heavy: PDF.js raster + TextLayer + AnnotationLayer rebuild).
touch-action: pan-x pan-y on the fullscreen canvas-container
keeps native panning working while we own the pinch.
Incidental: resetZoom (Cmd+0) now snaps to fit, not blindly 1.0,
so it doesn't overflow a narrow inline iframe; ZOOM_MIN/MAX
constants; shared pageWrapperEl ref.
* test(pdf-viewer-zoom): start narrow — basic-host doesn't emit containerDimensions on resize
The first test loaded at 1400w then resized to 500w expecting the
viewer to refit, but basic-host only sends containerDimensions at
init (implementation.ts:297) — never on window resize. Start narrow
so the initial fit lands <100%, then widen + fullscreen. Matches
the webtop verification flow that confirmed 67% → 100%.
* fix(pdf-server): drive fullscreen refit from ResizeObserver, not host messages
The displayMode hook ran refitToWidth() but Claude Desktop resizes
the iframe element AFTER (or independently of) sending the displayMode
update — clientWidth was still the inline width when we measured.
ResizeObserver on the canvas-container fires after the iframe has
actually laid out at the new size, so the measurement is fresh.
Gated to width-growth only: in inline mode renderPage() →
requestFitToContent() → host shrinks iframe to page width, which
would otherwise loop the scale down to ZOOM_MIN. Growth-only covers
inline→fullscreen and window-widen without the feedback loop.
* fix(basic-host): emit containerDimensions on iframe resize
Per spec, hosts notify views of container dimension changes via
host-context-changed. basic-host only sent containerDimensions once
at init — never on window resize or after the fullscreen panel
toggle. ResizeObserver on the iframe sends {width, maxHeight: 6000}
on every layout change; height stays flexible so views can keep
driving it via sendSizeChanged.
Doesn't replace the viewer-side ResizeObserver (defensive against
hosts we don't control), but makes the reference host spec-compliant.
* fix(pdf-server): scale UP to fill width in fullscreen, not just down
The previous fit cap of 1.0 meant a 612pt page in a 1400px fullscreen
container stayed at 100% — natural size, acres of empty space.
Fullscreen now caps at ZOOM_MAX so the page fills the available
width. Inline keeps the 1.0 cap (the iframe sizes to the page via
sendSizeChanged; growing past 1.0 would just make the iframe huge).
Exit-fullscreen refits back down to ≤1.0; if userHasZoomed (so
refit no-ops) fall back to requestFitToContent so the iframe still
shrinks to whatever scale they left it at.
* test(pdf-viewer-zoom): expect fullscreen to fill width (>100%), pinch out
Fullscreen fit now scales up past 100%, so the entering-fullscreen
assertion is >inlineZoom+50, not =100. The pinch test pinches OUT
(deltaY>0) so it doesn't immediately hit ZOOM_MAX from a ~220%
starting point.
* fix(pdf-server): fullscreen fit-to-PAGE, not fit-to-width
Fit-to-width on a wide screen filled the width but left the page
scrolling vertically — "fits" should mean the whole page is visible.
computeFitScale (renamed) now returns min(widthFit, heightFit) in
fullscreen so the page sits centred without scroll. Inline keeps
fit-to-width capped at 1.0 (height is host-driven via sendSizeChanged).
ResizeObserver now reacts to height changes too in fullscreen — the
inline width-growth-only gate stays (requestFitToContent loop guard),
but in fullscreen requestFitToContent early-returns so any size change
is safe to refit on.
e2e: assert no overflow in fullscreen instead of a magic zoom %.
* fix(pdf-server): respect safe-area insets in fullscreen fit-to-page
The host's chat input / home indicator overlays the bottom of the
fullscreen iframe (reported via safeAreaInsets). The toolbar already
honoured top/left/right; the canvas-container now adds the bottom +
side insets to its padding so the page sits above the overlay.
computeFitScale subtracts padding, so fit-to-page lands clear.
ResizeObserver watches the content box, so inset changes re-trigger
the fit.
* fix(pdf-server): clear pinch transform in same frame as canvas resize
commitPinch cleared the CSS transform BEFORE renderPage's first
await — for one frame the page snapped back to the old rendered
size, then jumped forward when the canvas resized. renderPage now
clears the transform immediately after setting canvas.style.width/
height, so the size handoff is atomic.
Also bump the wheel settle timer 150→200ms; slow trackpad pinches
can leave >150ms gaps and would commit mid-gesture.
* fix(pdf-server): no blank flash on pinch commit; clamp Ctrl+mousewheel; basic-host observer cleanup
Three review follow-ups:
1) Snap on pinch release: setting canvasEl.width clears the backing
store, so between the resize and page.render() completing the
page was blank. Snapshot the old bitmap first and drawImage it
scaled as a placeholder; the real render paints over it. Gated
on a transform being present so page-nav renders aren't affected.
2) Ctrl + physical mouse wheel sent deltaY≈±100 → exp(±1)≈2.7× per
notch, slamming straight to ZOOM_MIN/MAX. Clamp per-event delta
to ±25 (≈±28% per notch); trackpad pinch deltas are ~±1-10 so
the clamp is a no-op there.
3) basic-host's iframe ResizeObserver had no disposal path. Chained
disconnect() into appBridge.onclose (Protocol base class hook).
* simplify(pdf-server): pinch snap was transform-origin mismatch, not bitmap
The snap on pinch release was the page jumping vertically:
transform-origin: center center scales around the wrapper's middle,
but the flex container is align-items: flex-start so on commit the
canvas grows from the TOP. Preview top edge moved up; committed top
edge stays put → jump down. transform-origin: 50% 0 matches the
layout anchor, no jump.
Drop the canvas-snapshot placeholder (over-engineered, wrong fix),
the dead transition='' lines (.page-wrapper has no CSS transition),
and the containerDimensions→refit hook (ResizeObserver covers actual
size changes; the host message isn't load-bearing).
* fix(pdf-server): fail fast on never-polled viewer + surface batch errors
interact's get_screenshot/get_text waited the full 45s for a viewer that
never mounted (host went idle before iframe reached startPolling), then
timed out with a generic message. Now tracks viewsPolled and fails in ~8s
with a recovery hint. Batch errors were also ordered success-first —
hosts that flatten isError to content[0] showed "Queued: Filled N fields"
and dropped the actual failure entirely. Viewer side: poll even when PDF
load throws, and catch connect() rejection instead of silently blanking.
* fix(pdf-server): squash batch errors into one block, warn on phantom annotation updates
Three issues from a hung interact in production:
1. Batch error swallowed by host flattening. The earlier fix unshifted the
error before successes, but the prefix went on top of THAT — so
content[0] was just "Batch failed at step 2/2 (get_screenshot):" with
the actual timeout text in content[1], which the host dropped. Now all
text joins into one block; images survive after.
2. update_annotations silent no-op. The viewer's processCommands does
`if (!existing) continue` for unknown ids. If a prior add_annotations
failed mid-batch the model gets a happy "Queued" while nothing renders.
Track ids server-side; warn (not error — user might have drawn the
annotation manually in the iframe).
3. Per-def isolation in the viewer. A throw in addAnnotation killed the
whole processCommands batch including any get_pages later in the queue,
so submit_page_data never fired and the server waited the full 45s.
Now each def gets its own try/catch.
Also: stderr logging on interact failure (only — not chatty per-call) so
the next hang shows step timing in mcp-server-*.log instead of nowhere.
* fix(pdf-server): images first on success too, not just on error
The host's content[0] flattening drops images on successful batches just
the same: [add_annotations, get_screenshot] → content = ["Queued: ...",
<image>] → model sees only the text. Observed in audit logs where the
exact same iframe + viewUUID returned an image when the batch was
[get_screenshot, get_screenshot] but only text when add_annotations
came first.
Stable-sort images to the front. The Queued lines still arrive, just
after what the model actually asked to see.
* Revert "fix(pdf-server): images first on success too, not just on error"
This reverts commit 129a2dcd7a04e5bd116e59c4d6fc211db4203e2e.
* refactor(pdf-server): 1:1 batch results, drop isError when there's something to lose
LocalAgentMode SDK collapses isError:true results to a bare string of
content[0].text. The squash-everything-into-one-block defense from
0dbb8ed3 worked but lost the positional contract — the model couldn't
tell which command produced which result.
Cleaner: content[i] is the result of commands[i]. If a step fails the
batch stops there with that slot's text starting with "ERROR", and
content.length tells the model how far it got. No isError on multi-step
failures — there's no way to set it without the SDK eating the prior
images. Single-command failures keep isError:true since there's nothing
before content[0] to lose; the SDK's flatten-to-content[0].text is
exactly the ERROR text we want shown.
Tool description documents the contract.
* fix(pdf-server): isError only for single-command failures, not failedAt===0
failedAt===0 in a multi-step batch still has a positional contract to
honor — content.length=1 vs commandList.length=3 is a signal the model
loses if the SDK flattens the array to a string. The ERROR text encodes
"step 1/3" but the simpler rule ("batches never set isError") is
easier to document and reason about.
* fix(pdf-viewer): read live displayMode in requestFitToContent, guard tiny measurements
currentDisplayMode defaults to "inline" and handleHostContextChanged
only writes it `if (ctx.displayMode)`. If the host's initial context
omits the field — or the update lands one tick after a renderPage() —
requestFitToContent measures a near-empty pageWrapper and sends ~85px
(toolbar + padding only), shrinking a fullscreen iframe to a sliver.
Workaround was exit + re-enter fullscreen to force a context update.
Read app.getHostContext().displayMode directly (always fresh), and
separately refuse to send when pageWrapper measures smaller than the
toolbar — that's never a real layout, just an early render.
* revert(pdf-server): drop viewAnnotationIds — wrong about source of truth
The viewer's annotationMap is baseline (loadBaselineAnnotations from the
PDF) + diff restored from localStorage + manual edits + model adds.
viewAnnotationIds only tracks the last category, so the warning fires
for any update to an annotation the model didn't itself add — including
the entire restored state.
That's not "best-effort" miss rate, it's structurally wrong about
where the data lives. Model would learn to ignore the warning. The
viewer-side log.error on the silent skip (mcp-app.ts:4251) is the right
layer for this.
PR #506 added loadBaselineAnnotations() and buildFieldNameMap() between
showViewer() and renderPage(). Both iterate every page calling
getPage()+getAnnotations(), so the viewer sat with an unsized empty
canvas for ~400ms (15-page doc, scales linearly) before painting.
Neither scan blocks the canvas — page.render() only needs canvasContext
+ viewport. renderAnnotationsForPage() reads annotationMap (graceful
empty), AnnotationLayer.render() takes cachedFieldObjects (graceful
null). So: render first, scan after, re-render to overlay annotations
+ form values. Same fix at the reload path.
* pdf-server: add save_as interact action
Adds a model-callable save_as action to the interact tool that writes the
viewer's current annotated state (annotations + form values) to a new file.
Reuses the get_pages request/reply pattern: model's interact call blocks
while the viewer builds bytes via getAnnotatedPdfBytes() and posts them
back through a new submit_save_data app-only tool.
Validation runs synchronously before bothering the viewer:
- path must be absolute or file://
- isWritablePath gate (same as save_pdf)
- existence check; fails with a descriptive error unless overwrite: true
The viewer can report build errors back via submit_save_data({error}),
so the model gets a real message instead of the 45s timeout.
* pdf-server: warn model off app-only tools in descriptions
Hosts that don't filter visibility:["app"] tools from the model's tool
list will still surface these. The save_pdf one in particular is a
near-miss for the new save_as interact action — point the model at the
right one.
* pdf-server: save_as without path overwrites original
When path is omitted, save_as falls back to the displayed PDF's local
path (tracked per-viewUUID by display_pdf). Same writability gate as
the viewer's save button: isWritablePath + OS-level W_OK probe, so it
refuses on read-only mounts before the viewer roundtrip.
Requires explicit overwrite: true since the original always exists —
the model can't accidentally clobber it.
Remote PDFs have no source path stored; the error tells the model to
pass an explicit path.
* fix(pdf-server): fail fast on never-polled viewer + surface batch errors
interact's get_screenshot/get_text waited the full 45s for a viewer that
never mounted (host went idle before iframe reached startPolling), then
timed out with a generic message. Now tracks viewsPolled and fails in ~8s
with a recovery hint. Batch errors were also ordered success-first —
hosts that flatten isError to content[0] showed "Queued: Filled N fields"
and dropped the actual failure entirely. Viewer side: poll even when PDF
load throws, and catch connect() rejection instead of silently blanking.
* fix(pdf-server): radio button + dropdown handling in fill_form and save
Three related bugs around non-text form fields:
Viewer fill_form (mcp-app.ts): writing a string to annotationStorage on
every radio widget trips pdf.js's inverted string coercion
(`value !== buttonValue` → true) — the first-rendered widget checks
itself and clears the rest, so whatever you ask for, you get the first
option. Fix: extract a setFieldInStorage() helper that writes
{value: true} only on the widget whose buttonValue matches, {value: false}
on siblings, matching pdf.js's own change handler. Also covers the
syncFormValuesToStorage() path (localStorage restore).
Save (pdf-annotations.ts): buildAnnotatedPdfBytes only knew about
getTextField/getCheckBox — dropdowns and radios threw in the try block
and fell through the catch, so they were silently dropped on save.
Fix: instanceof dispatch on getFieldMaybe(). For radios, accept either
the option label or a numeric buttonValue (indexed into getOptions()),
since the viewer stores the latter for PDFs with /Opt.
display_pdf output (server.ts): radio groups showed as N identical
`name [Btn]` lines with no way to tell them apart; dropdowns didn't
list their options. Fix: surface buttonValue as exportValue and the
options array from the annotation data already in hand.
Transitive dep via express → router. Fixes:
- GHSA-27v5-c462-wpq7 (ReDoS via multiple wildcards)
- GHSA-j3q9-mxjg-w52f (DoS via sequential optional groups)
router@2.2.0 accepts ^8.0.0, so this is a clean lockfile-only bump.
* feat(events): DOM-model on* semantics with addEventListener/removeEventListener
Fixes#551 (useHostStyles broken: fonts clobber theme/CSS variables)
Fixes#225 (user onhostcontextchanged and SDK hooks mutually exclusive)
The on* setters now follow the DOM event model:
- Replace semantics (like el.onclick) with getters
- Coexist with addEventListener/removeEventListener listeners
- Both channels fire on dispatch: on* handler then listeners
Introduces ProtocolWithEvents base class between Protocol and
App/AppBridge. Each notification event gets a slot with two independent
channels (singular on* handler + addEventListener listener array).
useHostStyles hooks now use addEventListener with proper cleanup,
so they compose correctly with user on* handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix typedoc validation warnings
- Export AppEventMap, AppBridgeEventMap, ProtocolWithEvents so they
appear in generated docs (they're part of public addEventListener
signatures)
- Move AppBridgeEventMap above the AppBridge class JSDoc — it had been
inserted between the doc block and the class, orphaning the AppBridge
docs onto the wrong type
- Drop @param callback tags from on* getter/setter pairs — typedoc
attaches the doc block to the getter signature which has no params
- Add MethodSchema to intentionallyNotExported (internal zod helper
referenced by the now-documented eventSchemas field)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the fullscreen displayMode guard added in #567. Skipping
size-changed measurement during fullscreen caused apps to not receive
an initial size report after the transition, breaking layout for some
apps.
* Fix auto-resize destroying horizontal scroll positions
The setupSizeChangedNotifications method temporarily sets
html.style.width to 'fit-content' to measure intrinsic content width.
For responsive apps that derive width from their container (width: 100%,
flex: 1, etc.), fit-content resolves to 0px. The synchronous reflow at
0px causes browsers to clamp scrollLeft on all horizontal scroll
containers to 0, permanently destroying their scroll positions.
Replace the fit-content width measurement with window.innerWidth. The
height measurement (max-content) is preserved since hosts rely on it.
Neither the iOS nor web host uses the width value from size-changed
notifications, so this change has no functional impact on hosts while
fixing scroll position clamping for all MCP apps with horizontal scroll
views.
* Skip size-changed measurement during fullscreen
Hosts determine the container size in fullscreen mode, so size-changed
notifications are not needed. The temporary max-content height override
used for measurement causes visible vertical jitter during animated
fullscreen transitions by briefly reflowing the document on every frame.
This directory duplicated the Claude Code plugin that's being added to
the knowledge-work plugin marketplace (anthropics/knowledge-work-plugins#72).
Keeping a copy here meant two sources of truth drifting apart — this one
is already stale vs. the server's interact/annotation capabilities from #506.
The MCPB packaging (manifest.json) remains — that's the distributable
server artifact. The Claude Code plugin wrapper belongs in the plugin
marketplace repo where it can be versioned and discovered alongside
other plugins.
Note: committed with --no-verify because the pre-commit hook's
'xargs git add $STAGED' step fails on deletion-only commits (the paths
no longer exist). Hook fix tracked separately.
* fix(pdf-server): slice bytes before onDataRange to avoid detached buffer
PDF.js transfers the ArrayBuffer to its worker via postMessage, detaching
it in the main thread. When the same range is re-requested (common on
iOS/WKWebView under memory pressure during scroll/zoom/navigate), the
cached Uint8Array now wraps a detached buffer and onDataRange throws
'Buffer is already detached'.
Fix: pass result.bytes.slice() so the rangeCache entry stays valid.
* test(e2e): update pdf-annotations assertion for new display_pdf prompt text
The 91880a6d prompt rewrite changed the interact-enabled result text from
'Displaying PDF (viewUUID: ...)' to 'PDF opened. viewUUID: ...'. The e2e
test was still asserting the old string.
* pdf-server: add interact tool to allow model to modify existing view
* pdf-server: add annotations, download, form fill, and highlight_text
Add PDF annotation system with 7 annotation types (highlight, underline,
strikethrough, note, rectangle, freetext, stamp), text-based highlighting,
form filling, and annotated PDF download using pdf-lib.
- Server: annotation Zod schemas, extended interact tool with add/update/remove
annotations, highlight_text, and fill_form actions
- Client: annotation layer rendering with PDF coordinate conversion, persistence
via localStorage (using toolInfo.id key), pdf-lib-based download with embedded
annotations and form fills, uses app.downloadFile() SDK with <a> fallback
- Model context includes annotation summary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add get_pages tool for batch text/screenshot extraction
New tool `get_pages` lets the model get text and/or screenshots from
arbitrary page ranges without navigating the visible viewer.
- Server: `get_pages` tool with interval-based page ranges (optional
start/end, open ranges supported), `getText`/`getScreenshots` flags,
request-response bridge via `submit_page_data` app-only tool
- Client: offscreen rendering (hidden canvas, no visual interference),
text from cache or on-demand extraction, screenshots scaled to 768px
max dimension, results submitted back to server
- Max 20 pages per request, 60s timeout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: move get_pages into interact tool as an action
Fold get_pages into the interact tool to minimize tools requiring
approval. Now accessed via `interact(action: "get_pages", ...)`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve interact tool annotation discoverability
- Add concrete per-type schema docs with field names in tool description
- Add JSON example showing add_annotations with highlight + stamp
- Replace opaque z.record(z.string(), z.unknown()) with typed union
of all annotation schemas (full + partial forms) so the model sees
exact field names and types
- Remove redundant manual safeParse since Zod inputSchema validates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve annotation discoverability + add E2E tests
- display_pdf result text now explicitly lists annotation capabilities
(highlights, stamps, notes, etc.) instead of vague "navigate, search, zoom, etc."
- Restructured interact tool description: annotations promoted to top,
with clear type reference, JSON example, and bold section headers
- Added pdf-annotations.spec.ts with 6 E2E tests covering:
- Result text mentions annotation capabilities
- interact tool available in dropdown
- add_annotations renders highlight
- Multiple annotation types render (highlight, note, stamp, freetext, rectangle)
- remove_annotations removes from DOM
- highlight_text finds and highlights text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add Claude API prompt discovery tests for annotations
Tests that Claude can discover and use PDF annotation capabilities
by calling the Anthropic Messages API with the tool schemas and
simulated display_pdf result.
Disabled by default — skipped unless ANTHROPIC_API_KEY is set:
ANTHROPIC_API_KEY=sk-... npx playwright test tests/e2e/pdf-annotations-api.spec.ts
3 scenarios tested:
- Model uses highlight_text when asked to highlight the title
- Model discovers annotation capabilities when asked "can you annotate?"
- Model uses interact (add_annotations or get_pages) when asked to add notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add example prompts, testing docs, and updated tools table to README
- Example prompts for annotations, navigation, page extraction, stamps, forms
- Documents how to run E2E tests and API prompt discovery tests
- Updated tools table to include interact tool
- Updated key patterns table with annotations, command queue, file download
- Added pdf-lib to dependencies list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: simplify interact annotations schema (7802 → 2239 chars)
The typed Zod union (14 anyOf variants: 7 full + 7 partial annotation
types) produced a 5,817-char JSON schema for the annotations field alone.
This bloated the interact tool schema to 7,802 chars, which may cause
the model to struggle with or skip the tool.
Replace with z.record(z.string(), z.any()) — annotation types are
already fully documented in the tool description. Schema drops to
2,239 chars (71% reduction), annotations field to 254 chars (96% reduction).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: enumerate all interact actions in display_pdf result
The display_pdf result text now lists every action by name (navigate,
search, find, search_navigate, zoom, add_annotations, update_annotations,
remove_annotations, highlight_text, fill_form, get_pages) so the model
knows exactly what commands are available without needing to inspect the
interact tool schema.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: validate viewUUID in interact, clarify in description
The model was passing "pdf-viewer" instead of the actual UUID, causing
get_pages to timeout (commands queued under wrong key, client never
picks them up).
- Add activeViewUUIDs set tracking UUIDs issued by display_pdf
- Validate viewUUID at the top of interact handler with clear error
- Add "IMPORTANT: viewUUID must be the exact UUID returned by display_pdf"
to the interact tool description
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix annotation clickability and replace emoji icon with SVG
- Raise annotation-layer z-index above text-layer so note annotations
receive hover/click events (was z-index: 1, now 3; text-layer is 2)
- Replace memo emoji (data-icon attr) with CSS mask SVG document icon
that respects currentColor for consistent cross-platform rendering
* pdf-server: add annotation side panel with bidirectional linking
Right-side panel (250px) shows all annotations grouped by page with
expand/collapse cards. Clicking a card navigates to the page and pulses
the annotation; clicking a note icon on the PDF highlights its card in
the panel. Panel auto-shows on first annotation, remembers user toggle
preference via localStorage, and shows a badge count when collapsed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: replace 300ms interval polling with long-polling
Server now holds poll_pdf_commands requests open (up to 30s) until
commands arrive, waking waiters via enqueueCommand. Client loops
sequentially instead of using setInterval, with 2s backoff on errors.
Reduces idle RPC traffic from ~3 calls/sec to ~2 calls/min.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add interactive form filling via PDF.js AnnotationLayer
Render PDF.js AnnotationLayer with renderForms:true so form fields
appear as interactive HTML inputs. Build a fieldName→annotationID map
via getFieldObjects() to bridge fill_form (which uses field names) with
annotationStorage (which uses annotation IDs). Sync user input back to
formFieldValues for persistence and PDF download.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add elicit_form_inputs to display_pdf for upfront form filling
When enabled and the client supports elicitation, extracts form fields
from the PDF via pdf-lib and prompts the user to fill them before the
viewer loads. Elicited values are returned in content/structuredContent
and enqueued as a fill_form command for the viewer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: replace pdf-lib with pdfjs-dist for server-side form field extraction
Use pdfjs-dist's getDocument() + getFieldObjects() instead of pdf-lib's
PDFDocument.load() + getForm().getFields() in extractFormSchema(). This
removes the pdf-lib import from the server bundle (pdf-lib is still used
client-side for PDF modification in downloadAnnotatedPdf). Uses the legacy
pdfjs-dist build to avoid DOMMatrix dependency in Node.js.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form field font size and programmatic fill_form
Set --scale-factor/--total-scale-factor CSS variables on the form layer
so AnnotationLayer font-size rules resolve correctly instead of falling
back to browser defaults. Also update live DOM elements directly in
fill_form handler so values appear immediately without waiting for a
full re-render.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix listbox font size overflow in AnnotationLayer
After rendering the annotation layer, shrink select[size] font to fit
within the PDF rect height. The default AnnotationLayer CSS uses a fixed
9px * scale-factor which overflows when many options share a small rect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add trash icon to annotation sidebar cards
Show a delete button on each annotation card that appears on hover.
Clicking it removes the annotation from the PDF and persists the change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix space key captured in form fields
Skip keyboard navigation shortcuts (space, arrows, +/-) when any input,
textarea, or select element is focused, not just the search/page inputs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: show form fields in annotation sidebar with clear buttons
Form field values now appear in the sidebar under a "Form Fields" group,
with trash icons to clear individual values. The badge count and auto-show
logic include form fields. The panel open/close now calls
requestFitToContent with width to avoid overflow in inline layout mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: clarify Y coordinate system in interact tool description
Explicitly state that Y=0 is the bottom edge and Y=792 is the top for
US Letter, with concrete guidance on typical values for top/bottom
placement to prevent models from using top-down screen coordinates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: validate fill_form field names and report valid fields
Extract form field names during display_pdf and cache per viewer UUID.
fill_form now soft-fails on unknown field names (applies valid ones,
reports skipped ones). Both display_pdf and fill_form results include
the list of valid field names so the model can self-correct.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add compact annotation strip for inline mode
In inline mode, replace the 250px side panel with a slim bottom strip
that shows one annotation/form-field at a time with prev/next navigation.
The side panel is still used in fullscreen mode.
- Strip shows item swatch, label, preview + counter (N of M · Page P)
- Click item to navigate to its page; delete single or clear all
- requestFitToContent includes strip height in size calculation
- fieldNameToPage map built during init for form field page context
- Display mode toggle switches between strip and panel automatically
* pdf-server: improve display_pdf description for form/annotation use cases
Add explicit guidance on when to use the tool: filling out forms
(tax forms, applications), annotating PDFs, and interactive review.
This helps models route user requests like 'help me fill out this form'
to the display_pdf tool.
* pdf-server: only show download button when host supports downloadFile
* pdf-server: resizable sidebar + human-readable form field labels
- Sidebar panel is now resizable by dragging its left edge (150px min,
50vw max). Width is persisted in localStorage.
- Form fields show human-readable labels from PDF's TU (alternativeText)
field when available. Mechanical names like ').F1_01[0]' are replaced
with the generic label 'Field' when no TU text exists.
- Empty values are no longer shown in the sidebar.
* pdf-server: fix field label collection to use getAnnotations() API
getFieldObjects() doesn't include alternativeText. Instead, iterate
per-page annotations (only pages with known form fields) to collect
the TU/alternativeText labels that PDF authors set as tooltips.
* pdf-server: move strip below toolbar + follow form field focus
- Strip is now positioned below the toolbar (above search bar) instead
of at the bottom, for better visibility and proximity to the toolbar.
- Form field focus changes automatically sync the strip to show the
focused field's item.
- Search bar top offset adjusts dynamically when strip is shown/hidden.
* pdf-server: add Clear all to fullscreen sidebar + steer model to reuse views
- Add 'Clear all' button in the fullscreen sidebar panel header
- Update display_pdf description to instruct models to use the interact
tool with existing viewUUID rather than calling display_pdf again for
the same PDF (e.g. when user asks to annotate an already-open doc)
* pdf-server: add page dimensions to model context + improve coordinate docs
- Model context now includes 'Page size: 612x792pt (coordinates: origin
at bottom-left, Y increases upward)' so the model knows actual page
dimensions when placing annotations.
- Interact tool description expanded with clearer coordinate guidance,
margin hints, and a tip to use highlight_text for text annotations.
- Download button temporarily shown unconditionally (capability check
commented out with TODO for re-enabling).
* pdf-server: auto-close panel when empty + stronger display_pdf reuse guidance
- Sidebar/strip auto-closes when all items are cleared (via delete or
Clear all), and the annotations toolbar button hides.
- display_pdf description now uses CRITICAL warning to prevent models
from re-calling it to add annotations to an already-open PDF.
* pdf-server: skip elicitation when form fields have mechanical names
Only use elicitation if all editable fields have human-readable names.
Fields with brackets, dots, or ALL_CAPS_UNDERSCORE patterns (and no
alternativeText label from the PDF) are considered mechanical and
would be confusing in an elicitation form. Also use alternativeText
as the elicitation field title when available.
* pdf-server: paint annotations on screenshots + list coords in model context
Screenshots sent to the model (both updateModelContext and get_pages)
now include annotations drawn on top of the PDF canvas, so the model
can visually verify placement. The text context also lists each
annotation on the current page with its coordinates.
* pdf-server: style multiselect/listbox form fields to match viewer theme
* pdf-server: add command batching to interact tool
Extract command processing into a helper function and accept an optional
`commands` array for sequential batch execution. This lets the model
send multiple actions (e.g. add annotations + get screenshot) in a single
tool call, reducing round trips. Stops on first error in a batch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix multiselect rendering by removing font-size override
The custom font-size: inherit on #form-layer select was conflicting with
PDF.js AnnotationLayer's inline font-size, causing doubled/overlapping text
in select options. Remove the override and keep only the checked-option
highlight style.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form reset button by passing fieldObjects to AnnotationLayer
PDF.js AnnotationLayer needs the fieldObjects parameter to bind reset
form button actions. Cache the result of doc.getFieldObjects() and pass
it through to annotationLayer.render().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve annotation labels in sidebar and strip
Use human-readable labels instead of raw type names (e.g. "Rectangle"
instead of "rectangle", "Stamp: APPROVED" instead of "stamp"). Remove
redundant preview text for types that have no additional content (rectangle,
underline, strikethrough, stamp) to avoid "rectangle Rectangle" duplication.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: split get_pages into get_text and get_screenshot commands
Replace the combined get_pages action (with getText/getScreenshots flags)
with two focused commands:
- get_text: extract text from pages (single page or intervals)
- get_screenshot: capture a single page as PNG image
This makes the API cleaner and returns each command's result as its own
content block in batch mode. Also fix download function syntax error
from earlier capability check commenting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix download by using pdfDocument.getData() instead of re-fetching
The download was failing with "No PDF header found" because it re-fetched
the PDF via read_pdf_bytes tool calls, but stateless MCP transport creates
fresh server instances per request with empty caches. Using getData() from
the already-loaded pdf.js document avoids the round-trip entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: increase express body limit to fix get_screenshot hang
The submit_page_data tool sends base64 PNG screenshots that exceed the
default 100KB express.json() body limit from createMcpExpressApp. This
caused the request to silently fail, making waitForPageData timeout and
the get_screenshot command hang. Replaced createMcpExpressApp with manual
express setup using a 50MB body limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: use JPEG for screenshots to fix get_screenshot hang
Switch from PNG to JPEG (quality 0.85) for both offscreen screenshots
(get_screenshot) and model context images. JPEG is much smaller than PNG
for document pages, keeping the submit_page_data payload well within the
MCP SDK's default 100KB express body limit. This also reverts the
temporary workaround of replacing createMcpExpressApp with a custom
express setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add ' - edited' suffix to download filename when modified
Renames the download from '_annotated' to ' - edited' and also triggers
the suffix when form fields have been filled, not just when annotations
are present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: include viewUUID in display_pdf structuredContent
The viewUUID was only in _meta but not in structuredContent, so the model
couldn't retrieve it from the tool result to pass to interact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: move interact action list from display_pdf content to description
The content response was bloated with static instructions about interact
actions. Moved that to the tool description where it belongs, keeping
only the dynamic PDF URL and viewUUID in the content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form reset setting dropdowns to first item
PDF.js resetform handler deselects all options but doesn't re-add the
hidden empty placeholder, so the browser falls back to showing the first
real option. Work around this by listening for resetform on combo selects
and re-inserting a hidden blank option when nothing is selected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: sort form fields in sidebar/strip by intrinsic PDF order
Fields are now sorted by page number then top-to-bottom Y position on
the page, matching the visual reading order of the PDF form rather than
the arbitrary insertion order of formFieldValues.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add viewUUID to display_pdf outputSchema
The viewUUID was added to structuredContent but not to the outputSchema,
causing MCP validation error -32602.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: allow horizontal scrolling in inline mode
When the PDF is zoomed or wider than the container, users can now scroll
horizontally in inline mode instead of the content being clipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add viewUUID to model context, remove activeViewUUIDs validation
Include viewUUID in the updateModelContext header so the model can
reference it when calling interact. Remove the activeViewUUIDs set
validation since it breaks after server restarts — command queue TTL
pruning is sufficient cleanup. Also prune empty queues with no active
pollers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix delete button alignment in sidebar annotation cards
Annotations without preview text had the delete button hugging the type
label instead of right-aligning. Add margin-left: auto so the button is
consistently pushed to the right edge of the row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add interactive annotations (select, drag, resize, rotate, undo/redo)
Enable pointer-events on all annotation types so they can be clicked to
select. Draggable types (rectangle, freetext, stamp, note) support smooth
drag-to-move. Rectangles show corner resize handles, stamps show a rotate
handle. Delete/Backspace removes the selected annotation, Ctrl+Z/Shift+Z
provides unlimited undo/redo, and Escape deselects. Sidebar cards sync
with on-canvas selection state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: clicking sidebar card focuses form field input and scrolls into view
Sidebar annotation cards now scroll the annotation into view, and form
field cards navigate to the correct page and focus the input element.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form widget double-text by making inputs opaque
PDF.js AnnotationLayer renders interactive form inputs with a
semi-transparent background, letting the canvas's static form appearance
bleed through. Since the widget font (9px * scale, sans-serif) doesn't
match the PDF's actual font metrics, this creates a "double text" misalignment
at most zoom levels. Make text/choice widgets opaque so only the interactive
layer's text is visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add selection/focus to model context, double-click to send messages, toggle resize/rotate
- Model context now includes selected annotation ID and focused form
field name so the model knows what the user is interacting with.
- Double-clicking a sidebar annotation card sends a message prompting
the model to change that annotation.
- Double-clicking a sidebar form field card sends a message prompting
the model to fill that field.
- Double-clicking an annotation in the renderer toggles between resize
and rotate handles (for rectangles and stamps).
- Rectangles now support rotation via the rotate handle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: replace inline-mode strip with floating accordion sidebar
Replace the compact horizontal annotation strip with a floating panel
at top-right in inline mode. Uses accordion-style collapsible sections
(per-page annotations + form fields) with at most one section open at
a time. Refactors renderAnnotationPanel into reusable helpers
(appendAccordionSection, createAnnotationCard, createFormFieldCard).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: remove dead annotation strip code
The compact horizontal annotation strip is no longer used since inline
mode now uses a floating accordion sidebar. Remove all strip-related
HTML, CSS, JS (DOM refs, StripItem type, buildStripItems, renderStrip,
navigateToStripItem, deleteStripItem, event listeners, stripHeight
calculations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: multi-select annotations, natural prompts, form reset sync
- Convert selection from single ID to Set for multi-select (Shift+click)
- Deselect annotations when focusing a form input field
- Fix rotation to always use center origin (stamp was using left-bottom)
- Simplify double-click sendMessage prompts (e.g. "update Notes: ")
- Call updateModelContext before sendMessage to guarantee context is sent
- Listen for PDF.js resetform events to clear formFieldValues, update
sidebar badge/count, and hide icon when no items remain
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: draggable floating panel with magnetic corner anchoring
- Reduce floating panel width (220→180px), add border-radius
- Panel header is draggable; on drop, snaps to nearest corner
- Persists corner preference to localStorage
- Auto-docks to opposite side when overlapping selected annotations
- Accordion chevron: ▶ when collapsed, ▼ when expanded
- Slightly reduce section header size for compactness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: allow all accordion sections to be collapsed
Only auto-open the first section on initial render. Once the user has
interacted with accordion headers, respect their choice to collapse all
sections without forcing one back open.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: polish interactions and UI details
- Remove auto-show annotation panel; wait for user to open it
- Badge color: blue (accent) instead of red
- Search bar: offset floating panel below it when anchored top-right
- Text selection deselects annotations and clears field focus
- Click on text layer / empty area clears field focus too
- Zoom keeps selected annotation visible (scrollIntoView after render)
- Fullscreen button: distinct exit icon + tooltip with Esc hint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix text deselect, stamp dblclick, rotation direction, export
- Text selection and text-layer mousedown now deselect annotations
- Add user-select: none on draggable annotations (prevents stamp text
selection on double-click, allowing resize/rotate toggle to work)
- Fix rotation direction: remove CSS transform negation so clockwise
drag = clockwise rotation on screen
- Fix stamp export: rotate around center (matching CSS transformOrigin)
and negate rotation for PDF coordinate convention
- Add rotation support to rectangle PDF export
- Remove auto-show annotation panel; badge uses blue accent color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: allow any text as stamp label, not just predefined enum
The StampLabel was unnecessarily restricted to 6 predefined values
(APPROVED, DRAFT, etc). The PDF format supports arbitrary stamp text,
so open it up to z.string() while keeping common labels as examples
in the schema description.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: show resize + rotate handles simultaneously, dblclick sends message
Remove the interactionMode toggle — instead show both corner resize handles
and the rotate handle at the same time on selected annotations. Rectangle
gets resize corners + rotate handle; stamp gets rotate handle only.
Double-click on annotation elements now sends a message to modify the
annotation (matching sidebar card behavior) instead of toggling modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: SVG fullscreen icons, Y-coord docs, dirty flag
- Replace Unicode fullscreen chars (⛶/⊠) with proper SVG expand/collapse icons
- Add Cmd+Enter keyboard shortcut for fullscreen toggle
- Improve coordinate system docs: emphasize rectangle x,y is BOTTOM-LEFT corner, add worked examples
- Add isDirty state with * prefix on title for unsaved changes
- Track dirty state through persistAnnotations, suppress during restore
* pdf-server: diff-based annotation persistence, proper PDF annotation export, PDF annotation import
Extract annotation logic into pdf-annotations.ts module with comprehensive tests. Replace content-stream drawing (drawRectangle/drawText) with proper PDF annotation dicts (/Type /Annot) that are editable in Acrobat/Preview. Persistence now stores only a diff (additions/removals) relative to the PDF's native annotations, keeping localStorage small. On load, annotations are imported from the PDF via pdf.js and merged with the user's diff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: enhance page shadow for better depth perception
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: use floating panel in fullscreen, respect insets, direction-aware resize
Always use the floating annotations panel (both inline and fullscreen) instead
of the sidebar in fullscreen mode. Panel anchorage now respects safe area insets
(especially bottom). Resize handle repositions to the correct edge based on
which corner the panel is anchored to. Initial width reduced from 180px to 135px.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add save_pdf tool and save button for local files
Add server-side save_pdf tool that writes annotated PDF bytes back to the
original local file. Client shows a save button (floppy disk icon) next to
the download button for file:// URLs. Ctrl/Cmd+S shortcut saves when dirty.
Saving clears the dirty flag and localStorage diff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add circle and line annotation types synced with PDF.js
Add circle (ellipse) and line annotation support across all layers:
server Zod schemas, pdf-annotations module (import/export), client
renderers, canvas painting for screenshots, CSS, drag/resize handling,
and tool description documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add searchability keywords to display_pdf tool description
Include viewer, render, renderer, display, show, annotate, edit, and
form keywords so the tool is easier to discover by models.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: switch model-facing annotation API to top-left origin coordinates
Convert Y coordinates at the command boundary (add/update_annotations) so the
model uses intuitive top-left origin while internal storage stays in PDF's
native bottom-left origin. Also converts coordinates back to model space in
context strings for consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix stamp rotation in PDF export via appearance stream matrix
Stamp rotation was a no-op (empty if-block with a comment). Now computes
a proper rotation matrix around the bounding box center and applies it
to the appearance stream. Also adds verification tests for rectangle
stroke/fill colors and stamp color/rotation in exported PDF dicts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix screenshots to include filled form field values
Screenshots sent to the model were missing form field values because
the canvas render didn't use ENABLE_STORAGE mode. Now both offscreen
renders (model context and get_pages) pass annotationStorage to PDF.js
so filled fields appear in screenshots. Also reuses renderPageOffscreen
for model context screenshots to deduplicate rendering logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: list form fields with page, bounding box, and label in display_pdf result
The model needs field locations to fill forms accurately, especially for
fields with mechanical names or no name at all. Extracts per-page bounding
boxes in model coordinates (top-left origin) and includes labels where
available from PDF alternativeText.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: include full field listing in fill_form result and update UI after fill
fill_form results now include all form fields with names, types, pages, and
bounding boxes so the model can self-correct on unknown field names. Also
updates the annotation panel badge and visibility after programmatic fills.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add formFields to display_pdf structuredContent and output schema
Programmatic consumers need structured access to form field metadata
(names, types, pages, bounding boxes) without parsing text content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add image annotation type with URL/base64 support, drag-drop, and PDF export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix image annotation click/select, add Shift-resize for aspect ratio, add handle tooltips
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix image annotation to stretch to fill its bounding box
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix stamp PDF export to match on-screen appearance
Use non-standard /Name to prevent Preview.app from substituting built-in
stamp graphics, align padding with CSS (4pt/12pt), add ExtGState for 0.6
opacity, and fix text baseline positioning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve annotation panel UX and image resize behavior
- Page accordion click navigates to that page
- Form fields appear in their page's accordion section (before annotations)
- Selecting annotation or focusing form field auto-expands page accordion
- Image resize preserves aspect ratio by default (Shift for free resize)
- Add aspect field ("preserve"|"ignore") to ImageAnnotation type
- Fix selection UI regression: remove overflow:hidden that clipped handles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add signature/signing keywords and image example to tool descriptions
- Add signing/signature keywords to display_pdf description for discoverability
- Add image annotation example (signature placement) to interact tool description
- Clarify that imageUrl supports file paths and HTTP URLs with auto-fetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: discourage base64 encoding in image annotation, clarify imageUrl accepts file paths
Models tend to encode images as base64 themselves instead of passing file
paths. Make the schema descriptions and tool docs explicit: prefer imageUrl
with a file path, no data: URIs, don't encode images yourself.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: remove imageData from image annotation API, require imageUrl
Models were encoding images as base64 themselves instead of passing file
paths. Remove imageData from the model-facing Zod schema entirely — only
imageUrl (file path or HTTPS URL) is accepted. The server fetches and
embeds the image automatically. imageData remains as an internal field
for client-side rendering and drag-drop. Also mention drag-and-drop as
an alternative in the tool description.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add clipboard support (copy/cut/paste annotations and images)
- Request clipboardWrite permission in resource metadata
- Ctrl/Cmd+C: copy selected annotations as JSON to clipboard
- Ctrl/Cmd+X: cut (copy + delete) selected annotations
- Ctrl/Cmd+V / paste event: paste annotations from clipboard JSON,
or paste images from clipboard (e.g. screenshots, copied images)
- Refactor drag-drop image creation into shared addImageFromFile()
- Pasted annotations get new IDs and slight offset to avoid overlap
- Pasted images are centered on the current page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add disableInteract option for distributed deployments
Adds a `disableInteract` boolean to `createServer()` options that skips
registering the `interact`, `poll_pdf_commands`, and `submit_page_data`
tools (all relying on an in-memory command queue). Adjusts `display_pdf`
description and schema accordingly when disabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: flip interact to opt-in, enable only for --stdio
Rename disableInteract → enableInteract (default false) so existing
library consumers get read-only mode without code changes. The CLI
entry point passes enableInteract: true only when running with --stdio.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: log client-roots warning once at startup, not per request
* pdf-server: fix form widget transparency, poll spam, and pdf.js warnings
- Form widgets: PDF.js writes inline background-color (from the
annotation /BC entry, typically 'transparent'), defeating our CSS.
Force opaque with !important. Fixes double-text in listboxes and
text inputs at any zoom.
- Poll spam: view was calling poll_pdf_commands in a tight loop when
interact is disabled (HTTP mode) — server returns isError, which
doesn't throw, so the loop never backed off. Server now signals
interactEnabled via _meta; view only polls when true, and stops
entirely on isError results.
- Silence Helvetica / inset-border warnings: set verbosity=ERRORS
for server-side getDocument (only used for form introspection,
no rendering, so font fallback warnings are noise).
* pdf-server: set standardFontDataUrl for server-side pdf.js
Resolve pdfjs-dist/standard_fonts/ at runtime via createRequire. Works
from both source (bun main.ts) and bundled dist/ since pdfjs-dist is
--external. NodeStandardFontDataFactory reads via fs.readFile, so a
filesystem path with trailing separator is what's expected.
Keep verbosity=ERRORS as backstop for unrelated warnings (e.g.
'Unimplemented border style: inset').
* pdf-server: use unpkg CDN for pdf.js standard fonts, add to CSP
- Single CDN URL pinned to the bundled pdfjs-dist version, used in both
server (Node) and viewer (browser)
- Node: default NodeStandardFontDataFactory uses fs.readFile which can't
fetch URLs — pass a minimal FetchStandardFontDataFactory
- Browser: DOMStandardFontDataFactory already fetches, just pass the URL
- Add https://unpkg.com to CSP connectDomains (pdf.js loads font
binaries via fetch(), maps to connect-src)
* pdf-server: guard against missing totalBytes in viewer
Without this, an invalid totalBytes flows to PDFDataRangeTransport.length
→ worker ChunkedStream sized at 0 → opaque 'Bad end offset: N' error.
The guard turns that into an actionable message (rebuild hint).
* pdf-server: re-probe file size at load time, ignore stale totalBytes from history
The display_pdf result (including totalBytes) is baked into conversation
history. If the user saves the annotated PDF (grows the file) then
reloads the conversation, the host replays the stale size. pdf.js
initializes its ChunkedStream at that stale length, then the first live
read_pdf_bytes returns the current (larger) file — every chunk fails
the 'end === bytes.length' check with 'Bad end offset: N'.
Fix: drop the fileSizeBytes param and always probe via a live 1-byte
read_pdf_bytes call before setting up the range transport.
* pdf-server: file watching, save confirmation, disk-change reload
Server:
- fs.watch on local files when interact is enabled; pushes file_changed
via the poll_pdf_commands channel. Re-attaches on rename to survive
atomic writes (vim/vscode). Debounced 150ms. Skips unchanged mtime.
- save_pdf returns {filePath, mtimeMs} so the saving viewer can
recognise its own write's echo. Other viewers on the same file still
get notified (their content is genuinely stale).
- stopFileWatch in pruneStaleQueues TTL sweep.
Viewer:
- In-app confirm dialog (no window.confirm — would block the iframe).
- Save button: hidden until first edit, enabled while dirty, disabled
(but still visible) after save, re-enabled on next edit.
- savePdf() asks for confirmation, records lastSavedMtime.
- loadGeneration counter invalidates stale fetchChunk results and
aborts the preloader when the PDF is reloaded underneath them.
- reloadPdf() clears all per-document state (byte cache, annotations,
form fields, undo/redo, text caches, field maps, localStorage diff)
and loads fresh.
- file_changed handler: suppresses own-save echo via saveInProgress +
mtime match; auto-reloads when clean; prompts Keep/Discard when dirty.
* pdf-server: add file watching tests
Covers: external write → file_changed enqueued via poll_pdf_commands;
debounced rapid writes; stopFileWatch prevents further events; save_pdf
returns mtimeMs; watcher survives atomic rename (vim/vscode pattern).
* pdf-server: fix confirm-btn-primary hover, show filename only in save prompt
- .confirm-btn:hover's background cascaded to primary buttons too
(same specificity, primary has both classes) — turning the blue
button light grey on hover. Re-assert background in primary:hover.
- Save confirm: extract just the filename from pdfUrl (strip file://
prefix, take basename).
* pdf-server: match confirm dialog to host's native style
Use host-provided CSS variables (--color-*, --font-*, --border-radius-*,
--shadow-*) from applyHostStyleVariables, with local fallbacks.
- Larger border-radius (--border-radius-xl), softer shadow (--shadow-lg)
- Larger bold title (--font-heading-md-size, --font-weight-bold)
- Primary button uses inverse colors (dark bg + light text) like the
host's downloadFile dialog, not blue
- New .confirm-detail box: monospace bordered rect for filename
- Button order: Cancel first, primary last (native convention)
- Escape resolves to first non-primary button
* pdf-server: derive dirty from diff — undoing all changes clears save button
persistAnnotations() was unconditionally setDirty(true). It already
computes the diff vs baseline — use isDiffEmpty(diff) to decide instead.
Now undoing all the way back to the original state marks the viewer
clean again (save button disables, title loses asterisk).
* pdf-server: import saved form field values from PDF as baseline
getFieldObjects() returns the PDF's stored form values but we were only
reading field IDs/pages from it. After saving a filled form and reopening,
the panel showed nothing and there was no way to see what was filled.
- New pdfBaselineFormValues map, populated in buildFieldNameMap() from
each field's .value (skipping empty/Off/button values). Seeds
formFieldValues so the panel shows PDF-stored values on open.
- computeDiff takes an optional baselineFormFields param and only
includes values that differ — opening a filled PDF doesn't mark dirty,
editing a field does, reverting to the PDF's value marks clean again.
- importFieldValue() normalises radio-group value lookup (parent entry
has value=undefined, children have the real export value), checkbox→
true, listbox array→joined string.
* pdf-server: fix form Reset button; split panel into Reset vs Clear all
Reset-button regression: buildFieldNameMap() now seeds formFieldValues
from the PDF's stored values, but syncFormValuesToStorage() was pushing
them back into annotationStorage in our normalised repr (checkbox→true,
radio→export string). pdf.js's native repr may differ, and overwriting
it breaks the form's Reset button. Fix: skip syncing values that match
baseline — the PDF's own values are already in storage natively.
Panel buttons:
- Reset: revert to what's in the PDF file. Restores baseline annotations
and form values, clears undo/redo. Empty diff → clean.
Disabled when not dirty.
- Clear all: removes EVERYTHING including PDF-native items. Non-empty
diff (baseline items are 'removed') → dirty; saving writes stripped PDF.
Disabled when nothing to clear.
* pdf-server: fix Clear all to push defaultValue; gate save on writability
Clear all regression: annotationStorage.remove(id) only drops our
override — the widget reverts to the PDF's stored /V (the baseline
value). To actually clear, push each field's defaultValue (/DV) via
setValue instead, which is what the PDF's native Reset button does.
Save button visibility: server now checks fs.access(path, W_OK) and
reports via _meta.writable. Viewer gates save on that instead of just
'is this a local path'. Hides the button for read-only mounts and
Claude Desktop's uploads directory. Removed dead isLocalFileUrl().
* pdf-server: fix annotationStorage keys — use widget annotation IDs
Root cause: getFieldObjects() returns field-dictionary refs (the /T
tree, e.g. '86R'), but annotationStorage is keyed by WIDGET annotation
refs (what page.getAnnotations() returns, e.g. '9R'). These differ
when a PDF's field and its widget /Kids are separate objects.
fieldNameToIds was built from getFieldObjects().id, so EVERY
storage write was keyed wrong and silently no-op'd:
- syncFormValuesToStorage: wrote to dead keys
- fill_form: setValue + querySelector([data-element-id=...]) both missed
- individual field delete: remove() on wrong key
- clearAllItems: setValue to wrong keys
Fix: build fieldNameToIds from page.getAnnotations() which gives the
correct widget IDs. Keep cachedFieldObjects only for type/value/
defaultValue metadata (matched by name, not id) and for passing to
AnnotationLayer.render() where pdf.js uses it internally.
Also extract clearFieldInStorage() helper — pushes defaultValue for a
single field, used by both individual delete and Clear all.
* pdf-server: remap fieldObjects IDs to widget IDs for pdf.js Reset
pdf.js _bindResetFormAction iterates fieldObjects using each entry's .id
to (a) key annotationStorage and (b) querySelector([data-element-id=...]).
Both expect WIDGET annotation IDs. fieldObjects contains field-dict IDs.
Works only when field and widget share a PDF object — which pdf-lib's
save breaks (it splits merged objects on write).
This is a latent pdf.js bug, not a regression from our changes — it was
broken the moment the user first saved via save_pdf.
Fix: rebuild cachedFieldObjects with widget IDs (from fieldNameToIds)
before passing to AnnotationLayer.render({fieldObjects}). Skip parent
entries with no concrete id (radio group /T tree root). Preserve
type/defaultValue/exportValues which Reset needs.
* pdf-server: fix combo reset, scope writability, drop flaky CI assert
Combo reset: pdf.js's resetform handler sets all option.selected =
(option.value === defaultFieldValue); when defaultFieldValue is null
nothing matches. Chrome then synchronously normalises the non-multiple
<select> by auto-selecting option[0] — so by the time our fix-listener
ran, selectedIndex was 0 and the state-check failed. Now: capture
whether a real default exists before the event; if reset didn't land
on it, force-prepend a hidden blank and select it explicitly.
(Was latent — before the ID fix, resetform never dispatched on selects.)
Writability scope: only mark writable if the file is explicitly in
allowedLocalFiles (CLI arg = opt-in) OR strictly under an allowed
root directory (isAncestorDir already excludes the root itself via
rel !== ''), AND fs.access W_OK passes. A root passed by the client
is a boundary, not a target.
CI: drop the post-rename second-write assertion — fs.watch re-attach
semantics differ between kqueue and inotify, inherently racy in CI.
Only assert the rename itself is detected.
* pdf-server: fix Reset bug; show cleared baseline fields w/ revert
Reset-all bug: clearUserFormStorage skipped any field whose name was
in pdfBaselineFormValues — but if the user had EDITED a baseline field,
the edit sits in storage under that name. Skipping it left the widget
showing the stale edit while the panel showed the restored baseline.
Fix: remove ALL storage overrides on reset — every field reverts to the
PDF's /V, which IS baseline. Removed the now-dead helper.
Panel: cleared baseline fields now stay visible instead of vanishing.
State is derived per-field by comparing formFieldValues to
pdfBaselineFormValues (no new data structures — both maps already
exist, the comparison was just never rendered):
- unchanged: current === baseline → solid swatch, trash clears
- modified: baseline exists, current differs → solid swatch, revert
- cleared: baseline exists, current empty/absent → outlined cross
swatch, struck-out label/value, revert restores
- added: no baseline → solid swatch, trash removes
Panel iterates union(formFieldValues, pdfBaselineFormValues) so cleared
items don't disappear. sidebarItemCount uses the same union so the
toolbar button stays visible as long as there are baseline items OR
edits. Per-item revert: formFieldValues.set(name, baseline) +
storage.remove(id) → widget reverts to /V.
* pdf-server: import baseline from widget fieldValue, not field-dict
page.getAnnotations().fieldValue is what AnnotationLayer actually
renders. getFieldObjects().value reads the field-dict /V — which can
be out of sync in an internally-inconsistent PDF (e.g. after a pdf-lib
setText silently failed on a comb field, leaving the field-dict stale
while the widget still shows the older value from the raw bytes).
Your Form.pdf: ID widget shows 'eeeee', field-dict says 'eew2e'.
Panel was importing 'eew2e' → mismatch with what's on screen.
Fix: capture a.fieldValue during the page.getAnnotations scan (we're
already iterating there for widget IDs) and prefer it over the
field-dict value. Fall back to field-dict if the widget doesn't expose
one. normaliseFieldValue() handles the format differences (choice
widgets give arrays; field-dict gives strings).
* pdf-server: fix radio input → 'on'; use export value for consistency
pdf.js creates <input type=radio> without setting .value (pdf.mjs:
18138-18144), so target.value defaults to the HTML spec's 'on'. Our
input listener read that → panel showed 'on' after clicking a radio,
but baseline import correctly used the export value ('0') — so
clicking then reverting showed two different strings for the same
selection.
Fix: store each radio widget's buttonValue during the annotation scan
(we're already there for widget IDs), read it in the input listener
via data-element-id. Now baseline, click, revert, and save all agree
on the export value.
(Form.pdf happens to use '0'/'1' as export values instead of
'Male'/'Female' — that's the form's design; the labels are just PDF
text next to the widgets, not form data.)
* pdf-server: panel Reset/Clear-all as icon buttons, keep tooltips
* pdf-server: file roots from MCP client are read-only
allowedLocalFiles conflated two sources: CLI args (user explicitly
named the file when starting the server — overwriting is intentional)
and MCP file roots (client-uploaded copies in ad-hoc hidden folders
that the client doesn't expect to change).
New cliLocalFiles set tracks only the CLI-sourced files. Writable now
requires: file is in cliLocalFiles OR strictly under a directory root.
Directory roots are mounted folders where saving is expected.
save_pdf enforces the same scope server-side — the viewer hides the
button based on _meta.writable, but we must not trust the client.
* pdf-server: file root stays read-only even under a directory root
Extracted isWritablePath() with the full policy in one place (was
duplicated between display_pdf and save_pdf):
- CLI file → writable (user explicitly named it)
- MCP file root → read-only, ALWAYS. Even when the path happens to
fall inside a mounted directory root — the client sending it as a
file root is the stronger signal ('here's an upload')
- under a directory root at any depth → writable (mounted folder)
- the directory root itself → not writable (rel === '' excluded)
- no roots and no CLI files → nothing writable
8 unit tests cover each case.
* pdf-server: fix e2e (interact for HTTP test harness), skip rename test on Linux
E2E: tests/e2e/pdf-annotations.spec.ts expects 'interact' in the tool
dropdown and calls it for annotation tests. Since 3f2c209c interact is
stdio-only; e2e runs HTTP basic-host → tool absent → all tests fail
(broken since Mar 4). Add --enable-interact CLI flag for HTTP mode and
pass it in 'npm run serve'. The in-memory command queue works fine for
the e2e setup (one long-lived process); the stdio-only restriction is
about stateless multi-instance deployments.
Rename test: inotify watches inodes. When the directory entry is
atomically swapped to a NEW inode, the watcher on the OLD inode gets
no event. kqueue (macOS) watches paths and fires 'rename'. Gate with
skipIf(platform !== 'darwin').
* pdf-server: list_pdfs walks directory roots for *.pdf files
Recurse into allowedLocalDirs (depth ≤ 8, ≤ 500 files) and enumerate
actual .pdf files as file:// URLs alongside the explicitly-registered
ones. Skip dotfiles/dirs and node_modules. Dedup (a file may be both
explicitly registered and under a walked directory). Report truncation.
* pdf-server: fix stale e2e assertion after 2d47f14b
Commit 2d47f14b moved the interact action list from display_pdf's
content to the tool description, but the e2e test still asserted the
result text contained 'add_annotations', 'highlight_text', etc.
Now checks for viewUUID / interactEnabled / 'Displaying PDF' which
are the actual result contents.
* pdf-server: add unpkg to resourceDomains (font-src) for pdf.js FontFace
pdf.js in the browser doesn't just fetch() the .ttf bytes — it also
creates FontFace('name', 'url(https://unpkg.com/...)') which goes
through CSP font-src, not connect-src. Nest's CSP blocked it
('font-src self assets.claude.ai'). pdf.js fell back to system fonts
so nothing broke visually, but the CDN URL was doing nothing.
(Separate finding re: the 'bricked UI' report — logs show fill_form
processed cleanly at 16:27:18, then Nest's router navigated away 13s
later. Poll id=17 returned to an already-unmounted iframe. Normal
teardown, not a brick.)
* pdf-server: validate imageUrl scheme before <img src> assignment
Fixes CodeQL js/xss + js/client-side-unvalidated-url-redirection on
line 1994 (renderImageAnnotation) and the canvas-paint fallback.
The server resolves imageUrl to imageData before enqueueing, so
def.imageUrl only survives to the client if the server-side fetch
fails. safeImageSrc whitelists https/http/data/blob and returns
undefined for javascript: et al.
* pdf-server: fix get_pages timeout race; trim fill_form response
get_pages round-trip (interact → enqueue → poll → render → submit):
- GET_PAGES_TIMEOUT_MS 60s → 45s. The MCP SDK also defaults to 60s, so
if the round-trip got close, the client timed out first and our error
went nowhere. Server rejects first now.
- waitForPageData wires extra.signal. If the client cancels interact
(timeout/user abort), we clean up the pendingPageRequests entry
immediately instead of leaking until our own timeout. Also simplified
to a single settle callback instead of {resolve, reject, timer}.
- handleGetPages .catch() → submit empty pages. Was fire-and-forget
with no error path; any rejection before the inner try/catch meant
no submit_page_data → server waited the full timeout.
fill_form response: dropped the full field listing that was appended
on every call. display_pdf already returns it; echoing it back after
every successful fill is noise. Only list valid field names when the
model got one wrong.
* pdf-server: return URL.href from safeImageSrc so CodeQL sees sanitizer
The previous attempt returned the raw input string after a scheme
check, which CodeQL's taint tracker doesn't recognise as a sanitiser
(it sees tainted in → tainted out). Now parse with the URL constructor
and return the .href property — this is a canonical sanitiser pattern
CodeQL models as a barrier for js/xss and unvalidated-url-redirection.
Also folded the data: URI branch into safeImageSrc for a single call
site responsible for producing the <img src> string.
* feat(pdf-server): treat uploads-root dirs as read-only by default
Dir roots whose basename is 'uploads' (e.g. Claude Desktop's attachment
drop folder) are now treated as read-only unless --writeable-uploads-root
is passed. This prevents the save button from appearing for attached PDFs
that the client doesn't expect to be overwritten.
Also gates the _debug diagnostic block on --debug and adds save/restore
of writeFlags.allowUploadsRoot in test beforeEach/afterEach.
* feat(pdf-server): add --debug flag, fix interact timeout, skip persist for read-only cmds
- Wire --debug CLI flag through to createServer; _debug diagnostic block
in display_pdf _meta is now only emitted when --debug is set.
- Viewer: showDebugBubble renders _debug payload as a fixed overlay.
- Fix interact get_pages timeout: await handleGetPages instead of
fire-and-forget, so submit_page_data doesn't queue behind the next
30s long-poll on serialized host connections.
- Skip persistAnnotations for read-only commands (get_pages, file_changed).
* fix(pdf-server): rebase annotations baseline after save
After a successful save_pdf, update pdfBaselineAnnotations and
pdfBaselineFormValues to reflect what was written to disk. Without
this, removing all annotations after a save produced an empty diff
(compared to the stale pre-save baseline), incorrectly disabling
the save button even though the file on disk still had annotations.
* fix(pdf-server): sync annotation panel on canvas click, fix tooltip z-index
Clicking an annotation on the canvas now re-renders the panel to
expand the correct page section and scrolls the card into view.
Previously only the selection class toggled without updating the
accordion.
Raise tooltip z-index to 100 within the annotation layer and
promote the annotation layer above the form layer on note hover
(:has selector) so tooltips aren't clipped by the form overlay.
* debug(pdf-server): add __pdfDebug() helper + log skipped baseline annotations
Adds window.__pdfDebug() that dumps annotationMap, baseline,
layer children, and localStorage diff to diagnose ghost annotations
(visible on canvas but not in panel, not selectable).
Also logs when importPdfjsAnnotation returns null for non-widget
annotations, and surfaces any thrown errors during baseline import
instead of silently swallowing them.
* debug(pdf-server): expose pdfDocument.annotationStorage + window.__pdf internals
__pdfDebug() now also dumps PDF.js's annotationStorage contents
(editor stamps live there, invisible to our tracking) and all
localStorage keys matching pdf-annot pattern.
After running __pdfDebug(), internals are exposed as
window.__pdf.{pdfDocument, annotationMap, annotationLayerEl, formLayerEl}
for interactive console poking.
* fix(pdf-server): hide viewUUID when interact disabled, widen annotations panel
- display_pdf result text no longer leaks viewUUID when the interact
tool is not registered (model has no use for it).
- Annotations panel default width: 250px -> 500px (max-width:50vw
still caps on narrow viewports).
* refactor(pdf-server): extract src/commands.ts, dedupe PdfCommand type
PdfCommand was defined independently in server.ts and mcp-app.ts with
no compiler check to keep them in sync. Extract to src/commands.ts as
the single source of truth; both sides import the type.
Uses the TS interfaces from pdf-annotations.ts (not Zod-inferred types)
since those already describe the post-resolveImageAnnotation wire shape
(imageData present, x/y/width/height non-optional).
Also deletes ~150 lines of dead Zod schemas (PdfAnnotationDef and the
10 variant schemas that built it) -- they were never used as runtime
validators, only for z.infer<typeof> type inference. Input remains
z.record(z.any()) for model-API forgiveness.
* fix(pdf-server): correctness cleanup bundle
- findTextRectsFromCache: return [] instead of a hardcoded placeholder
rect when text-layer DOM isn't rendered. The placeholder was persisted
as real coordinates; caller already guards on empty.
- onteardown: bump loadGeneration to stop the preloader (reuses the
reload-abort check) and clear searchDebounceTimer.
- startPreloading: re-check loadGeneration inside the pause-wait loop
so teardown mid-pause exits instead of spinning.
- app.sendMessage calls: wrap in .catch(log.error) so host-unsupported
doesn't throw an unhandled rejection.
- Remove dead ternary (Highlight:Highlight), stale TODO and commented
downloadBtn line.
* docs(pdf-server): fix action names in example prompts, document all flags
- Example prompts referenced get_pages with getText/getScreenshots params;
actual model-facing actions are get_text (intervals) and get_screenshot
(single page). Fixed 3 prompts.
- Document --debug, --enable-interact, --writeable-uploads-root flags.
- Add image annotation type to the Annotation Types table.
- Note that interact is disabled in HTTP mode unless --enable-interact.
- Add Deployment section (stdio vs HTTP single-instance vs stateless).
* test(pdf-server): add interact tool unit tests
New describe("interact tool") block with 7 tests covering:
- enqueue -> poll_pdf_commands roundtrip
- missing-arg error paths for navigate, fill_form, add_annotations
- command queue isolation across distinct viewUUIDs
- fill_form passthrough when viewFieldNames not registered
- (skip) unknown-UUID poll (LONG_POLL_TIMEOUT_MS not exported, can't
bypass the 30s wait without changing server.ts)
Surprises found:
- interact never validates viewUUID exists; enqueues to any string
- batch-mode early-exits on first error, silently dropping later commands
* style: auto-fix prettier formatting
* refactor(pdf-server): extract annotation panel to src/annotation-panel.ts
Split ~930 lines from mcp-app.ts into two new modules:
- src/viewer-state.ts (50 lines): shared Maps/Sets/Arrays + types
(annotationMap, formFieldValues, selectedAnnotationIds, fieldNameTo*,
undoStack/redoStack, TrackedAnnotation, EditEntry). Only containers
with stable bindings — both modules mutate contents, never reassign.
- src/annotation-panel.ts (1084 lines): floating panel positioning,
accordion rendering, annotation/form-field cards, reset/clear-all.
Coupling to mcp-app flows through PanelDeps injected at init —
scalars like currentPage/pdfDocument reach the panel via a state()
getter so the ~100 use sites in mcp-app stay unwrapped.
mcp-app.ts: 5666 → 4721 lines. panelState.open replaces
annotationPanelOpen; panelState.openAccordionSection is written by
selectAnnotation. syncSidebarSelection moved to the panel module.
* style: auto-fix prettier formatting
* test(pdf-server): document computeDiff same-id modification behavior
Two new tests:
- User-added annotation modification: captured in diff.added (id not
in baseline, so latest content persists correctly).
- Baseline annotation in-place modification: KNOWN LIMITATION. Same-id
edit produces empty diff (id-set based), so the change vanishes on
reload. Viewer's addAnnotation() works around via remove+add, but
updateAnnotation() (interact tool's update_annotations) does not.
* chore(pdf-server): revert unrelated screenshot regenerations
Pre-commit hook runs build:all which regenerates all example
screenshots. These 24 PNGs have no semantic change; reverting
to keep the PR diff focused on pdf-server.
* fix(pdf-server): gate resolveImageAnnotation on validateUrl (arbitrary file read)
resolveImageAnnotation() did fs.readFile on whatever imageUrl the model
sent, bypassing the allowedLocalFiles/allowedLocalDirs machinery that
protects display_pdf and save_pdf. Model could request
{imageUrl:"/Users/x/.ssh/id_rsa"}, server would base64 the bytes into
the add_annotations command, iframe stores it, get_screenshot reads it
back.
Also: fetch() branch accepted plain http:// (SSRF to local network).
Fix: call validateUrl(imageUrl) before touching the filesystem or
network. Throws on rejection; caller converts to {isError:true} so the
model sees a clear error instead of silent skip.
3 tests: reject local path outside roots, reject http://, accept path
under allowed dir (with real readFile + imageData population).
* fix(pdf-server): deleted-baseline zombies, clear-all form strip, height-only coord shift
Three bugs from PR review, all in viewer state/coord handling:
1. Deleted baseline annotations zombied back on reload (mcp-app.ts:2512).
restoreAnnotations' apply loop was add-only. loadBaselineAnnotations
runs between the two restore calls and re-seeds annotationMap with
every baseline id, including the ones in diff.removed. The zombie
survives, and the next persistAnnotations sees it in currentIds ->
computeDiff produces removed=[] -> deletion permanently lost.
Fix: delete diff.removed ids after the merge loop.
2. Clear All didn't strip form values from the saved PDF (mcp-app.ts:2756).
clearAllItems() empties formFieldValues, but buildAnnotatedPdfBytes
gates on formFields.size > 0 and only writes entries present in the
map. Empty map -> zero setText/uncheck calls -> pdf-lib keeps the
original /V values. Fix: at getAnnotatedPdfBytes time, inject an
explicit clearing sentinel ("" or false by baseline type) for every
baseline field absent from formFieldValues.
3. update_annotations shifted rect/circle/image when patching height
without y (mcp-app.ts:4161). The old code spread existing.def
(internal coords, y = bottom edge) with update (model coords, y =
top-left), then key-filtered back to only update keys. But internal
y = pageHeight - modelY - height; patching height with top fixed
requires rewriting internal y, which the key-filter discarded.
Fix: convert existing to model coords, merge in model space, convert
back, pass the full merged def. Also uses the target page height
when update.page differs (fixes the low-severity page-move bug too).
* fix(pdf-server): sweep aux maps independently of commandQueues; line annotations sort
Two lower-severity findings from review:
Leak (server.ts:265): pruneStaleQueues iterated commandQueues to clean
up viewFieldNames/viewFieldInfo/viewFileWatches, but display_pdf
populates those maps without creating a commandQueues entry (only
enqueueCommand does), and dequeueCommands deletes the entry on every
poll. Net: the sweep found nothing; aux maps leaked every display_pdf.
viewFileWatches entries hold an fs.StatWatcher -> slow FD exhaustion
on HTTP --enable-interact. Fix: new viewLastActivity heartbeat map,
touched at display_pdf/enqueue/dequeue, swept on TTL. Dead second
loop (entry.commands.length===0 was unreachable) removed.
Line-annotation sort (annotation-panel.ts:400): getAnnotationY checked
'y' in def and 'rects' in def, but LineAnnotation has only x1/y1/x2/y2.
Fell through to return 0 -> all line annotations sorted to panel
bottom. Added y1 branch returning max(y1, y2).
* feat(pdf-server): rewrite display_pdf prompts to steer model toward interact
The model was calling display_pdf again to add annotations instead of
using interact with the existing viewUUID. Root cause: the description's
first two sentences invited exactly that --
'Use this to display, annotate, edit, and fill form fields'
'when the user wants to ... annotate, edit, sign, stamp'
-- then the CRITICAL warning arrived three paragraphs later, after the
model had already pattern-matched 'user wants to sign' -> display_pdf.
Three changes:
- First sentence is now narrow: 'Open a PDF. Call this ONCE per PDF.'
- Follow-up-actions-go-through-interact is sentence 2, not paragraph 3.
- Result text is directive at the decision point: tells the model to
call interact with the viewUUID, warns against re-calling display_pdf.
* fix(pdf-server): correct the 'discards state' claim in display_pdf prompts
Both viewers coexist; nothing is discarded. The actual failure mode is
the model now holds a new viewUUID and addresses the wrong viewer --
interact calls land on the fresh empty one, not the one with the user's
existing annotations. Rewording to say that.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Fix touch gesture conflicts in WebGL example apps on mobile
* Format with prettier
* fix(examples): improve mobile support for map, pdf, and threejs examples
- map-server: fullscreen button respects safe area insets, hover/opacity
pattern with always-visible on touch devices
- pdf-server: fit-to-width scale for narrow viewports, responsive toolbar
with compact controls on mobile, safe area in fullscreen toolbar via
CSS custom properties
- threejs-server: fullscreen button respects safe area and always visible
on touch, ResizeObserver for responsive width, host containerDimensions
for fullscreen height, no container padding in fullscreen, replace
useHostStyles with inline style application to avoid overwriting
onhostcontextchanged handler
Changes since 1.2.1:
- fix(schemas): import from zod/v4 to match SDK's zod API (#548)
- test(e2e): fix no-op security injection test via frames[] traversal (#540)
The generated schemas compose with schemas imported from
@modelcontextprotocol/sdk/types.js (e.g. z.array(ContentBlockSchema)).
The SDK constructs its schemas via `import * as z from 'zod/v4'`, but
our generated schema.ts used `import { z } from "zod"` — which returns
the v3 API when the consumer has zod 3.25.x installed.
Mixing v3-created and v4-created schema instances throws at parse time
with errors like `keyValidator._parse is not a function` because zod v4
dropped the `_parse` / `_parseSync` internals that v3 wrappers call.
This was masked before #534 because zod was bundled — all schema
creation shared the same inlined zod copy. Externalizing zod exposed
the mismatch on consumers pinned to zod 3.x (e.g. anthropics/apps).
The `zod/v4` subpath is exported by both zod 3.25+ and zod 4.x, so the
existing peerDependency range `^3.25.0 || ^4.0.0` is preserved.
https://claude.ai/code/session_013xQUegrvVGct5xpqeSL5av
Co-authored-by: Claude <noreply@anthropic.com>
The cross-app message injection test at security.spec.ts:313 has been a
no-op since it was added in 2411c71a. It attempted to reach the inner app
iframe via:
outerIframe.contentDocument?.querySelector('iframe')
but contentDocument is null across the 8080→8081 origin boundary (the test
at :247 in the same file explicitly asserts this). The ?. short-circuited,
postMessage never fired, and the test passed without exercising the
defense in message-transport.ts.
Fix:
- Use window.frames[0].frames[0] — cross-origin accessible per HTML spec,
and the actual attack path a malicious sibling app would use
- Return a sentinel from page.evaluate() and assert it, so the test fails
loudly if frame traversal ever breaks again
- Actually assert the rejection log (was previously captured but never
checked)
- Use expect.poll() instead of waitForTimeout(500)
- Reuse captureConsoleLogs helper
Also fixed: comment at :293 claimed 'Outer frame should NOT have
allow-same-origin' but the assertion checks allow-top-navigation. The
outer frame DOES have allow-same-origin (implementation.ts:167);
isolation comes from the separate port, not the sandbox attribute.
* chore: remove dead ./react-with-deps export (copy-paste bug from #221)
The react-with-deps build entry was introduced in PR #221 (e078250a) as a
sibling to app-with-deps for CDN/unpkg no-bundler use. But its external
array was duplicated without editing — it never dropped @modelcontextprotocol/sdk
from externals, so it has shipped byte-identical to ./react for 10 releases
(v0.3.1 → v1.2.0).
After #534 merged, both entries use ['react', 'react-dom', ...PEER_EXTERNALS]
— still identical, just smaller.
Delete rather than fix: React apps always use a bundler; the CDN/unpkg
use case (HTML-string apps like qr-server) doesn't apply to React hooks.
Zero consumers verified (examples/docs/tests/plugins).
* fix: bundle SDK+zod in react-with-deps (keep React external)
Reverts the deletion from the previous commit and applies the actual fix.
The react-with-deps external array was copy-pasted in PR #221 without
dropping PEER_EXTERNALS — now it mirrors app-with-deps: bundles
@modelcontextprotocol/sdk + zod for CDN/import-map consumers.
React and react-dom MUST remain external — bundling them would cause
dual-React-instance hooks errors. Consumer provides them via import map.
Before: index.js 26KB == react-with-deps.js 26KB (identical MD5)
After: index.js 26KB != react-with-deps.js 323KB (SDK+zod bundled)
* fix(readme): use picture element for theme-aware logo
The #gh-light-mode-only / #gh-dark-mode-only URL fragments only work
on github.com. On npm, VS Code preview, and other markdown renderers
both img tags render, showing two stacked logos.
Switch to <picture> with prefers-color-scheme, which GitHub recommends
and which degrades gracefully to a single logo elsewhere.
* ci: add Cloudflare Pages preview for docs PRs
Deploys a per-PR preview of the TypeDoc site to Cloudflare Pages project
mcp-ext-apps-docs-preview when docs, src JSDoc, README, or typedoc config
change. Uses the shared modelcontextprotocol/actions composite action that
injects noindex headers, posts a sticky PR comment with preview URLs, and
cleans up deployments on PR close. Fork PRs are skipped (no secret access).
* fix(docs): sync README logo with TypeDoc theme switcher
The README <picture> logo uses prefers-color-scheme, which only follows
the OS setting. TypeDoc's theme switcher sets data-theme on <html>, so
manually picking Light/Dark would show the wrong logo (e.g. white logo
on light background if OS is dark but user picked Light).
Rewrite the <picture> in the generated docs into two class-tagged <img>s
via the mcpstyle plugin, and hide the inactive one via CSS keyed on both
data-theme (explicit selection) and prefers-color-scheme (OS mode). The
README source is unchanged so GitHub/npm still get <picture>.
Also: prettier normalized mcp-theme.css (tabs to spaces, quote style).
The issue_comment trigger previously ran for any commenter on a PR.
On a public repo this lets drive-by users trigger a job with
contents:write that checks out the PR branch, runs playwright
--update-snapshots, and pushes a [skip ci] commit.
Gate the issue_comment path on author_association being OWNER,
MEMBER or COLLABORATOR. workflow_dispatch is unchanged (already
requires repo write access).
The override pinning @hono/node-server to 1.19.7 was added in 71976101
as a workaround when @modelcontextprotocol/sdk 1.25.3 required ^1.19.9,
which did not yet exist on the public npm registry.
That constraint is now obsolete: the SDK is pinned to 1.25.2 (requires
^1.19.7), and 1.19.8-1.19.11 have since been published. Removing the
override lets npm naturally resolve 1.19.11, which includes the fix for
the HIGH-severity auth bypass in the Serve Static middleware
(GHSA-wc8c-qw6v-h7f6, fixed in 1.19.10).
Lockfile regenerated against registry.npmjs.org (no artifactory URLs).
Bug 1: .husky/pre-commit line 21 ran:
git diff --name-only --diff-filter=M | xargs -r git add
which stages EVERY modified tracked file in the worktree. If a
contributor has unstaged WIP elsewhere, it gets silently swept
into the commit.
Fix: capture `git diff --name-only --cached` before running
build/prettier, then only re-add that set. src/generated/ is
staged explicitly since build:all regenerates it from
spec.types.ts.
Bug 2: no repo-local .npmrc meant the maintainer's global
artifactory registry leaked into package-lock.json, requiring
the Docker regen dance. Committing .npmrc with
registry=https://registry.npmjs.org/ overrides user-global
config. Docker regen is now optional (still useful for
linux-amd64 optionalDeps).
- Switch npm install -> npm ci in ci.yml (build, build-wsl) and docs.yml
to fail on lockfile drift rather than silently tolerating it
- Add cache: npm to all setup-node steps that have a lockfile to hash
(skipped test-git-install which has no checkout, and WSL which has no
setup-node action)
- Add TypeDoc link validation step after build (Linux x64 only, matching
existing single-OS gate pattern for MCPB). Verified passing on main.
- Align all workflows on Node 22 (active LTS) — previously CI used 20
but publish workflows used 22, so releases were tested on a different
Node than PRs
- Add engines.node >=20 to package.json so consumers on Node 18 get a
warning
Two packaging bugs:
1. The './schema.json' export pointed to dist/src/generated/schema.json
but nothing copied it there (tsc is emitDeclarationOnly, Bun.build
doesn't emit JSON). Consumers got ERR_MODULE_NOT_FOUND.
Fix: cpSync after tsc runs.
2. zod is a peerDependency but was bundled into every entry point
(~270KB each). This causes dual-instance bugs where consumer-side
'instanceof ZodError' and schema.extend() break across boundaries.
Fix: add zod to external list for all non-with-deps entries.
Bundle size impact:
app.js 293952 -> 24561 (-91.6%)
app-bridge.js 299517 -> 30112 (-89.9%)
react/index.js 295558 -> 26153 (-91.2%)
server/index.js 291500 -> 22135 (-92.4%)
app-with-deps 321814 -> 321814 (unchanged, intentional)
Bug 1: generate-grid-screenshots.spec.ts was running on every CI E2E
invocation because playwright.config.ts had no testIgnore. This spec
writes examples/*/grid-cell.png as a side effect — it's meant to be
invoked only via npm run generate:screenshots.
Added testIgnore gated on GENERATE_SCREENSHOTS env var (Playwright's
testIgnore is stronger than explicit CLI file args, so a plain ignore
would break generate:screenshots). Passed the env var through the
docker invocation in generate:screenshots.
Before: npx playwright test --list → 82 tests in 3 files
After: npx playwright test --list → 65 tests in 2 files
GENERATE_SCREENSHOTS=1 ... --list → 17 tests in 1 file (still works)
Bug 2: examples/pdf-server/server.test.ts (448 LOC, 34 tests) was
never run — root script was 'bun test src'. Changed to 'bun test src
examples'. Only one .test. file exists in examples/ so no noise.
Before: npm test → 87 pass across 3 files
After: npm test → 121 pass across 4 files
Change `sandbox.ts` imports from relative paths into `dist/` to use the
proper `@modelcontextprotocol/ext-apps/app-bridge` package entry point.
This aligns the example with how external consumers would import from
the package.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Move detailed implementation recipes (tool visibility, host styling, streaming,
fullscreen, etc.) out of the skill and point to `docs/patterns.md` instead.
This keeps the skill focused on essentials while the patterns doc provides
comprehensive examples.
Key changes:
- Fix protocol flow diagram: host renders UI before server returns result
- Update template file list to reflect actual contents (`main.ts`, `global.css`)
- Add critical CSP/CORS warnings to "Common Mistakes" section
- Replace "Advanced Examples" table with categorized patterns list
- Add note clarifying `basic-host` is for testing, not a host behavior guarantee
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On Windows, `tsx watch` can hang when running under `concurrently` due
to stdin handling conflicts (see [privatenumber/tsx#623][]). Adding the
`--raw` flag to `concurrently` avoids this by preventing stdin
interception.
Since `--raw` mode disables concurrently's automatic output prefixing, a
custom Vite logger is added to prefix messages with `[vite]` so users
can still distinguish which process produced each line of output.
[privatenumber/tsx#623]: https://github.com/privatenumber/tsx/issues/623
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Follow-up to #470. Matches MCP SDK Client.listResources(params?) signature
so callers can omit the empty {} for the common case.
Also reflows a line in app-bridge.test.ts that prettier flagged.
* Replace platform-specific optionalDependencies with bun devDependency
Fixes#222. The bun meta-package handles platform detection internally.
Rollup resolves its own platform binaries transitively. Removes ~400MB
of bloat for SDK consumers. Simplifies setup-bun.mjs accordingly.
* Remove setup-bun.mjs, simplify prepare script
No longer needed since the bun npm package handles binary
installation and PATH registration automatically.
* Add Security docs section, SEO improvements, and doc site enhancements
- Add Authorization guide covering per-server and per-tool OAuth patterns
- Add CSP & CORS guide extracted from Patterns doc
- Group both under a new Security section in the documentation nav
- Replace CSP & CORS section in Patterns with cross-reference to new guide
- Rename doc site title from package name to 'MCP Apps'
- Rename 'Modules' nav section to 'API Documentation'
- Ensure Security section appears before API Documentation in TOC
- Add SEO plugin with JSON-LD structured data (TechArticle/WebPage)
- Add per-page meta descriptions from frontmatter
- Add description frontmatter to all documentation pages
- Add hostedBaseUrl for sitemap.xml generation and canonical URLs
- Normalize document page slugs to lowercase hyphenated format
- Update compressed navigation and search index data to match new slugs
* Add favicons with light/dark mode support
- Download favicons from modelcontextprotocol.io (light + dark variants)
- Inject favicon link tags into every page with correct relative paths
- Support prefers-color-scheme media queries for light/dark mode
- Include apple-touch-icon for iOS
- Copy favicon assets to output directory during doc build
* Fix HTML sanitization in SEO plugin with proper parser
Replace regex-based HTML tag stripping with htmlparser2 for
extracting plain-text descriptions from rendered pages.
* Add custom theme to align with modelcontextprotocol.io styling
- Import Inter and JetBrains Mono fonts (matching MCP site)
- Override light mode colors: pure white backgrounds, softer gray borders
- Override dark mode colors: match MCP site dark background (#0f1117)
- Taller header (3.5rem) with backdrop-blur frosted glass effect
- Sidebar: adjusted spacing, font size, and hover/active states
- Content: increased line-height (1.6), more heading breathing room
- Code blocks: JetBrains Mono, subtle border, more padding
- Fix CSS load order: custom.css now loads after theme stylesheets
* Align sidebar nav styling with modelcontextprotocol.io
- Active item: tinted blue background with blue text (matching MCP's
primary-color tinted active state)
- Non-active items: muted gray text, subtle hover background
- Larger border-radius (0.75rem/rounded-xl) matching MCP sidebar
- Group headers: semibold with slight letter spacing
- Proper dark mode variants for all states
* Fix sidebar active item: dark gray background with bold text
* Make sidebar active item more prominent: bolder weight, stronger background
* Highlight current page in sidebar nav, normalize code font size
Inject a small script via the SEO plugin that marks the active sidebar
link with a "current" class (TypeDoc doesn't do this natively for
document pages). Uses a MutationObserver since the sidebar is populated
asynchronously. Pathname comparison strips .html extensions to support
clean-URL servers.
Also bumps code/pre font-size from 0.85em to 1em so code snippets match
the surrounding body text size.
* Split styling into dedicated plugin, fix auth terminology, improve breadcrumbs
Extract CSS cascade fix, breadcrumb labels, and sidebar nav highlighting
from the SEO plugin into a new typedoc-plugin-mcpstyle.mjs to keep
concerns separated.
Fix authorization doc: use "authorization" consistently instead of mixing
with "authentication", link to MCP spec and RFC 9728 for Protected
Resource Metadata, remove redundant spec link paragraph.
Rename "Documents" nav section to "Getting Started" and add explicit
group frontmatter to all doc pages. Breadcrumbs now show the section
name instead of duplicating the page title.
Switch local development MCP server config from `npm --prefix` to
`bash -c "cd DIR && npm --silent run start:stdio"`. The --prefix form
caused connection instability in Claude Desktop (reconnect loops with
orphan builds competing on the same dist/).
Also updated example paths from ~/code/ext-apps to ~/src/ext-apps.
fit-content clamps to the containing block's available size. For <html>,
that's the iframe viewport, so when content exceeds the viewport height
the SDK reported the viewport height instead of the true content height,
causing internal scrolling instead of iframe growth.
max-content has no available-space constraint and correctly reports the
intrinsic content height in both directions (shrink and grow). Width
stays fit-content to preserve responsive wrapping.
* docs: restore full MCP client JSON configs to CONTRIBUTING.md
Reinstate the copy-pasteable JSON configurations for running all examples
with MCP clients (npx-based and local development), which were removed
from README.md in 2af02ba. Placed in CONTRIBUTING.md as a better home
for contributor-facing setup instructions.
https://claude.ai/code/session_014pjt7bE4ckHns5QFPkdWQp
* Use start:stdio in local dev config, add missing scripts to 9 examples
Update the local development JSON config in CONTRIBUTING.md to use
`npm --silent run start:stdio` instead of manual bash commands.
Add `serve:stdio` and `start:stdio` scripts to the 9 examples that
were missing them (basic-server-*, sheet-music, transcript,
video-resource, debug) to match the pattern already in place for the
other 11 showcase examples.
https://claude.ai/code/session_014pjt7bE4ckHns5QFPkdWQp
* style: auto-fix prettier formatting
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* security: ignore client roots by default in pdf-server
When the pdf-server is started with --stdio, MCP clients may advertise
roots that refer to directories on the *client's* file system. Because
the server resolves those paths locally, accepting them by default would
give the remote client access to arbitrary directories on the server's
machine.
This commit makes client roots opt-in via the --use-client-roots flag.
Without the flag, the server logs a notice and skips roots setup
entirely. The createServer() function now accepts a CreateServerOptions
object with a `useClientRoots` boolean (defaults to false).
https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw
* Enable client roots by default for HTTP, keep off for stdio
HTTP mode serves a local client so roots are safe. Stdio mode may
have a remote client whose roots would resolve against the server's
filesystem, so roots stay off unless --use-client-roots is passed.
https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw
* fix: correct transport-aware defaults for useClientRoots
stdio = local client (e.g. Claude Desktop) → auto-enable roots
HTTP = remote client → ignore roots unless --use-client-roots passed
The previous commit had the logic inverted.
https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw
* refactor: inline useClientRoots per transport branch
Remove the intermediate effectiveUseClientRoots variable — just pass
`true` in the stdio branch and the flag value in the HTTP branch.
https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw
* docs: update README to match transport-aware client roots defaults
Stdio always enables roots (client is local); HTTP ignores them by
default. The previous README text had these reversed.
https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: bump ext-apps to 1.1.2
Changes since 1.1.1:
- feat: add ui/download-file method for host-mediated file downloads (#475)
- pdf-server: robust path validation + folder/file root support (#497)
- ci: auto-fix prettier formatting in pre-commit hook and CI (#498)
* fix: sync local SDK build into node_modules for examples
npm workspaces hoists example dependencies to the root node_modules,
but installs the published registry copy of @modelcontextprotocol/ext-apps
instead of linking to the local source. This causes type-check failures
when examples use features not yet published to npm.
Add scripts/link-self.mjs which copies the freshly-built dist/ and
package.json into the hoisted node_modules copy after each SDK build,
ensuring examples always type-check against the latest local types.
See: https://github.com/npm/feedback/discussions/774
* Revert "fix: sync local SDK build into node_modules for examples"
This reverts commit e3387e8434f3def030f5a223a70632092c929119.
npm workspaces hoists example dependencies to the root node_modules,
but installs the published registry copy of @modelcontextprotocol/ext-apps
instead of linking to the local source. This causes type-check failures
when examples use features not yet published to npm.
Add scripts/link-self.mjs which copies the freshly-built dist/ and
package.json into the hoisted node_modules copy after each SDK build,
ensuring examples always type-check against the latest local types.
See: https://github.com/npm/feedback/discussions/774
Pre-commit hook:
- Stage all files modified by prettier:fix, not just src/generated/
CI (GitHub Actions):
- Add prettier-fix job that runs on PRs from the same repo
- Auto-commits formatting fixes back to the PR branch
- Skips fork PRs (no write access to fork branches)
Co-authored-by: Claude <noreply@anthropic.com>
* pdf-server: support folders in cli + files in roots
* fix(pdf-server): use path.relative for robust ancestor dir matching
- Replace string prefix matching (startsWith) with path.relative-based
isAncestorDir() which correctly handles trailing slashes, path
normalization, and prevents both .. traversal and prefix attacks.
- Use path.resolve consistently when adding paths to allowedLocalFiles
and allowedLocalDirs (main.ts was missing this).
- Use resolved path for exact file match check in validateUrl.
- Improve error message formatting for easier debugging.
- Add tests for trailing slashes, grandparent dirs, and isAncestorDir.
* chore(pdf-server): remove JSONL interaction logging
Remove the withLogging transport wrapper, logInteraction helper,
InteractionLogger type, and all log parameter threading that were
added for debugging.
* fix(pdf-server): support computer:// URL scheme for local files
Some clients (e.g. Claude Code) use computer:// instead of file://
for local file references. Handle both schemes in isFileUrl and
fileUrlToPath.
* fix(pdf-server): add debug logging for rejected files, ignore .jsonl logs
* fix(pdf-server): accept bare file paths in addition to file:// URLs
Clients like Claude Desktop may pass raw absolute paths (e.g.
/Users/.../file.pdf) instead of file:// URLs. Handle these in
validateUrl and readPdfRange.
* debug(pdf-server): add hex char diagnostics for path mismatch
* fix(pdf-server): resolve symlinks to handle sandbox path remapping
Use fs.realpathSync to resolve symlinks/bind mounts when comparing
file paths against allowed directories. This fixes the path namespace
mismatch where Claude Desktop sends container paths (e.g. /sessions/...)
while MCP roots use host paths (e.g. /Users/...).
Both the file path and directory paths are resolved through symlinks
before comparison, so either side can be a symlink.
Also stores realpath of roots in allowedLocalDirs/Files so matching
works in both directions.
Removes verbose hex diagnostics (no longer needed).
* fix(pdf-server): decode percent-encoded bare paths (e.g. %20 for spaces)
Clients may send bare file paths with percent-encoded characters
(e.g., Application%20Support). Apply decodeURIComponent to bare paths
before resolving, matching the behavior already used for file:// URLs.
* fix(pdf-server): add full diagnostics to client-facing error message
Include url, resolved path, realpath, allowedFiles, and per-directory
path.relative results in the error returned to the client, so we can
diagnose why isAncestorDir fails when paths look identical.
* debug(pdf-server): add hex dump to diagnose invisible char differences
path.relative returns 10 levels of .. despite paths looking identical,
meaning they differ at the byte level. Add hex dump of first 30 chars
of both paths to catch invisible Unicode or encoding differences.
* fix(pdf-server): use inode comparison for bind-mount path matching
The cowork VM remaps paths via bind mounts: the tool input uses
VM-internal paths (/sessions/...) while MCP roots use host paths
(/Users/...). Since path.relative can't match across bind mounts,
compare directory inodes instead (dev+ino from fs.statSync).
Walks up the file's parent directories checking each against allowed
dirs by inode, which works regardless of path namespace differences.
* fix(pdf-server): resolve VM-internal paths via directory basename matching
When the MCP server runs on the host but receives unrewritten VM paths
(e.g., /sessions/name/mnt/uploads/file.pdf), the tool call url argument
isn't rewritten by the cowork VM proxy because it doesn't know which
arguments are file paths.
Fix: when path validation fails, try to match the file by finding the
directory basename (e.g., 'uploads') in the VM path and looking for
the relative path suffix under each allowed directory on the host.
Also returns the resolved host path so readPdfRange reads the correct
file on the host filesystem.
* revert(pdf-server): remove resolveVmPath hack
The VM path mismatch (VM-internal paths not rewritten in tool call
arguments) should be fixed in the cowork VM proxy layer, not worked
around in the MCP server.
Removes:
- resolveVmPath() function
- resolvedPath field from validateUrl return type
- effectiveUrl remapping in tool handlers
- VM path resolution test
* nit
Update all documentation links from `modelcontextprotocol.github.io/ext-apps`
to the new custom domain across README.md, docs/, and example READMEs.
Add `cname` to the `peaceiris/actions-gh-pages` deploy action in
`.github/workflows/docs.yml` so the CNAME file persists across deploys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug fixes / improvements since 1.1.0:
- fix: add audio/video support for basic-host (#460)
- fix(pdf-server): remove page-loading overlay (#493)
- pdf-server: remove domain allowlist, require HTTPS only (#487)
- Set audioSession.type to playback in sheet music app (#489)
- Add MCPB packaging and Claude Code plugin for pdf-server (#491)
Use the Audio Session API to declare audio as media playback so the
platform handles it correctly: background audio, lock screen controls,
Dynamic Island integration, and bypassing the iOS silent switch.
* Add MCPB packaging and Claude Code plugin for pdf-server
Two distribution paths for the pdf-server:
1. **MCPB bundle** (Claude Desktop MCP Directory):
- manifest.json: v0.3 manifest with tools, compatibility, privacy
- .mcpbignore: excludes source/tests from bundle
- icon.png: 64x64 PDF document icon
2. **Claude Code plugin** (plugin/):
- .mcp.json: declares local stdio server via npx
- commands: /pdf-research:read, /pdf-research:summarize
- skills: pdf-reading skill for document navigation
- Tested end-to-end: MCP server connects, all tools available
The plugin uses a local stdio MCP server pattern — the first known
instance in a knowledge-work plugin (all existing plugins use remote
HTTP connectors).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Run prettier:fix to satisfy CI formatting checks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add basic-server-preact, basic-server-solid, basic-server-svelte,
basic-server-vue, and debug-server to the publish workflows so they
are included in pkg-pr-new and npm deployments.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hardcoded list of allowed remote origins with a simple
HTTPS protocol check. Any HTTPS URL is now accepted for remote PDFs.
Local file allowlisting remains unchanged.
* fix(pdf-server): remove page-loading overlay
The overlay had multiple issues:
- Hardcoded white background broke dark mode
- Could get stuck visible, breaking sizing/layout
- Added little value since previous page stays visible during render
and the 150ms delay meant most renders completed before it showed
* prettier:fix
* chore: bump ext-apps to 1.1.0
Features:
- registerAppResource now returns the registered resource (#370)
- Accept UIResourceMeta in both resources/list and resources/read (#410)
- callServerTool throws helpful error when called with a string (#449)
- Add double-connect guard to prevent protocol message handling errors (#450)
Bug fixes:
- Only ignore messages that lack jsonrpc 2.0 in message-transport (#448)
- Align basic-host dark mode styles (#438)
Spec:
- Change Host <> Sandbox communication protocol to SHOULD (#435)
- Clarify UIResourceMeta in both list and read responses (#410)
* fix(debug-server): add missing npm package config (main, bin, types, exports)
Align debug-server package.json with all other server examples:
- main: dist/server.js (was server.ts)
- types: dist/server.d.ts
- bin: mcp-server-debug -> dist/index.js
- exports with types
- files: only dist (remove source server.ts)
* lazy PDF loading w/ PDFDataRangeTransport
* lint
* feat: lazy PDF loading via range transport + eager background preloading
Replace full-file download with PDFDataRangeTransport so PDF.js fetches
byte ranges on demand. First page renders immediately; remaining pages
load in background for search.
**Range transport (server + client)**
- server.ts: add read_pdf_bytes tool returning base64 chunks with offset
- mcp-app.ts: AppRangeTransport feeds PDF.js via fetchRange/fetchChunk
with parallel sub-requests for large ranges, Promise-based dedup of
concurrent requests, and a persistent range cache
**Background preloader**
- Sequential page 1→N text extraction, pauses during interactive
navigation (preloadPaused flag set in goToPage, cleared in renderPage)
- Debounced live search refresh (500ms) as pages load
- Search UI shows '(loading…)' suffix until all pages processed
**Loading indicators**
- SVG pie in toolbar: fills as pages load, fades on completion,
turns red on errors with tooltip listing failed pages
- Debounced page-loading overlay (150ms) for uncached page navigation
**Cleanup**
- Remove old full-file chunked download and progress bar
- Remove extractAllPageText (replaced by preloader)
- Remove verbose debug logs (Coverage, RangeReq, cache hit)
Add padding: 0 !important to .main.fullscreen so that
safe area insets applied via inline styles are overridden,
making the PDF viewer truly fullscreen edge-to-edge.
Also add .build directories to .prettierignore to fix
pre-commit hook failures from Swift build artifacts.
When the client advertises roots capability, the server queries roots/list
on initialization and subscribes to roots/list_changed notifications.
Any file:// roots pointing to existing directories are added to an
allowedLocalDirs set, enabling access to all PDFs under those directories
without needing to enumerate them via CLI args.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: revert unrelated changes from bad rebase
Restores files that were accidentally modified alongside the MCP roots
feature: README, pdf-server UI (search feature), threejs example,
SDK guards (app.ts, app-bridge.ts), typedoc theme, media assets,
and e2e snapshots.
Move the agent skills section to the top of the README so readers
immediately see how to install and start building. Keep Claude Code
install commands front and center with a brief pointer to the agent
skills guide for other agents. Specification section moves below
Examples.
Replace custom {filename, content, mimeType, encoding} params with
standard MCP resource types: EmbeddedResource for inline content and
ResourceLink for host-resolved references.
New params shape:
params: { contents: (EmbeddedResource | ResourceLink)[] }
This reuses existing MCP primitives instead of inventing new ones,
and enables hosts to show resource 'bubbles' or resolve ResourceLink
URIs via resources/read on the MCP server.
Add a new ui/download-file JSON-RPC request that allows MCP App views to
request file downloads through the host. Since MCP Apps run in sandboxed
iframes where direct downloads are blocked (allow-downloads not set),
this provides a host-mediated mechanism for file exports.
Spec changes (draft):
- Add ui/download-file request to Requests (View → Host) section
- Add downloadFile capability to HostCapabilities
- Document host behavior requirements (confirmation dialog, filename
sanitization, base64 decoding, size limits)
SDK changes:
- Add McpUiDownloadFileRequest/Result interfaces to spec.types.ts
- Add downloadFile() method to App class
- Add ondownloadfile setter to AppBridge class
- Add DOWNLOAD_FILE_METHOD constant
- Generate Zod schemas
Example:
- Add download demo to integration-server (exports JSON with server time)
- Capability check: only shows download button when host advertises
downloadFile capability
Based on developer feedback requesting file export capabilities for
visualization tools, document editors, and data analysis apps.
* Add PDF search feature to pdf-server
Adds full-text search functionality to the PDF viewer:
- Search button in toolbar (Ctrl/Cmd+F to open)
- Search bar with input, prev/next navigation, match count
- Text extraction and caching across all pages
- Highlight overlays using DOM Range API
- Case-insensitive search across entire document
- Keyboard shortcuts: Ctrl+F (open), Enter (next), Shift+Enter (prev), Escape (close)
Changes:
- mcp-app.html: Add search button, search bar, highlight layer
- src/mcp-app.css: Add search/highlight styles and PDF.js text layer rules
- src/mcp-app.ts: Add search state, functions, event listeners
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* update screenshots
* Add fullscreen button to threejs-server
* Fix prettier formatting
* Move search bar below toolbar, right-aligned
* Make search bar compact and right-aligned
* Float search bar over page content
* Ctrl/Cmd+F toggles search; pass through to browser when empty
* Hide search nav controls until query is entered
* Skip scroll when search highlight is already visible
* Fix search bar right-edge alignment with container border
* Prevent browser find when our search is open with text
* Cmd+F when search input is focused passes through to browser find
* Fix prettier formatting in README
* Remove unrelated formatting and threejs changes from PR
* prettier
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: guard against double-connect in App and AppBridge (#429)
Calling connect() twice on the same transport instance caused the MCP
SDK's Protocol class to chain onmessage handlers, so each incoming
message was processed twice. This made incoming responses get consumed
by the first processing (resolving the handler) and then trigger a
spurious "Received a response for an unknown message ID" error on the
second processing pass, since the handler was already removed from the
response map.
Add an explicit guard in both App.connect() and AppBridge.connect() that
throws if the instance is already connected. This converts the silent
message-corruption into a clear, actionable error and prevents
downstream confusion.
Adds three regression tests:
- AppBridge.connect() throws when already connected
- App.connect() throws when already connected
- Demonstrates the double-processing root cause and verifies it is fixed
Fixes#429https://claude.ai/code/session_01XU3FW1TpjBkyRbkBUH5gqR
* test: replace redundant regression test with cleaner same-transport guard check
The previous third test in "double-connect guard" never actually attempted
a double-connect — it created fresh instances and verified normal operation,
which every other test already covers. The stray bridge.onupdatemodelcontext
and bridgeTransport.close() at the top were leftover noise.
Replace it with a focused test that verifies the guard fires even when
the caller passes the *same* transport object (not just a different one),
which is the exact scenario described in #429.
https://claude.ai/code/session_01XU3FW1TpjBkyRbkBUH5gqR
---------
Co-authored-by: Claude <noreply@anthropic.com>
v0.4.0 includes the typography fixes from upstream PR
JulianWowra/typedoc-github-theme#7, so the local `customCss` override
(`scripts/typedoc-github-theme-fixes.css`) added in 0bbbfee is no
longer needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users frequently call callServerTool("tool_name", args) instead of the
correct callServerTool({ name: "tool_name", arguments: args }), resulting
in a silent/confusing failure. Detect this and throw immediately with
a message that shows the correct call shape.
Also fixes the incorrect example in examples/pdf-server/README.md that
demonstrated this same wrong usage pattern.
Fixes#386https://claude.ai/code/session_01GqAyN4Ux7svWoU2HqsSLZF
Co-authored-by: Claude <noreply@anthropic.com>
In ChatGPT, we have our own sandbox <> host communication protocol that doesn't match the names of the protocol enforced by the "MUST" in the spec.
This changes that by marking the specific messages sent back and forth as a "SHOULD".
Tighten the parse-failure handler: silently ignore postMessage frames
where event.data?.jsonrpc !== "2.0" (i.e. not JSON-RPC at all — null,
undefined, a primitive, or an object from the host environment that
doesn't carry the jsonrpc field). Messages that do carry jsonrpc: "2.0"
but are otherwise malformed still surface via onerror so genuine protocol
errors aren't swallowed.
https://claude.ai/code/session_01DaJpUtSJf4hA7tnu3nJz8s
Co-authored-by: Claude <noreply@anthropic.com>
The skill name spec requires lowercase letters, numbers, and hyphens
only. Change `name` from "Migrate from OpenAI App" to `migrate-oai-app`.
Companion to #443, which does the same for the `create-mcp-app` skill.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
As per the spec (https://agentskills.io/specification) `name` should be:
"Max 64 characters. *Lowercase letters*, numbers, and *hyphens* only. Must not start or end with a hyphen."
Backport the `.tsd-typography` CSS fixes from upstream PR
JulianWowra/typedoc-github-theme#7 so document pages render with
GitHub-accurate heading spacing, list bullets, line-height, and code
block overflow. The override file and `customCss` config can be removed
once the PR is merged and the dependency is updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clarify spec and SDK: accept UIResourceMeta in both resources/list and resources/read
The spec defines UIResource with _meta.ui, implicitly as an extension of the Resource type (resources/list), but examples and practice place _meta.ui on TextResourceContents (resources/read). The registerAppResource helper accepts _meta.ui in its config (which ends up in resources/list), but examples only return said _meta.ui from resources/read — and current hosts might be just ignoring it.
This change:
- Adds a 'Metadata Location' subsection to the draft spec explicitly stating that _meta.ui is valid on both the resource listing entry AND the content item, with content-item taking precedence.
- Updates the spec example to show _meta on the resource declaration (resources/list).
- Updates the CSP construction example to reference both locations.
- Clarifies McpUiAppResourceConfig JSDoc: _meta here appears in resources/list and serves as a listing-level fallback for when content items omit _meta.ui.
- Fixes the map-server example comment that incorrectly stated _meta must only be on the content item.
* Add McpUiReadResourceResult type for UI metadata in resources/read
* basic-host: support UIResourceMeta fallback from resources/list
---------
Co-authored-by: Claude <noreply@anthropic.com>
The term "external" is misleading for MCP Apps because developers
typically interpret it as "third-party." However, MCP App HTML runs in
a sandboxed iframe with no same-origin server, so *all* origins must be
declared—including where your own bundled JS/CSS is served from. Using
"network" instead avoids this confusion and aligns with the detailed
explanations in `patterns.md` and `spec.types.ts`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Single quotes in `concurrently` commands work on Unix shells but not on
Windows Command Shell (`cmd.exe`), causing the `INPUT` environment
variable to not be set and npm scripts to fail with errors like:
'cross-env' is not recognized as an internal or external command
Error: INPUT environment variable is not set
Replace single quotes with escaped double quotes (`\"`) in npm scripts
in all example `package.json` files.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds a new section to the patterns guide explaining how to configure
Content Security Policy and CORS for MCP Apps that make network requests:
- CSP via `_meta.ui.csp` controls browser-side permissions (`connectDomains`,
`resourceDomains`)
- CORS via `_meta.ui.domain` provides a stable origin for API servers to
allowlist
Includes a type-checked example showing `computeAppDomainForClaude()` helper
that hashes an MCP server URL to create a stable `*.claudemcpcontent.com`
subdomain. Also adds `@see` links to `McpUiResourceMeta` and `McpUiResourceCsp`
in `registerAppResource` JSDoc.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(pdf-server): handle HTTP 200 fallback in remote range requests
When a remote server ignores the Range header and returns HTTP 200
with the full body, readPdfRange previously passed the entire file
(potentially 10MB+) through as a single chunk. This bypassed the
512KB limit because:
1. The error check `!response.ok && status !== 206` short-circuits
on 200 (ok is true), so the full body is read via arrayBuffer()
2. No Content-Range header on a 200 response leaves totalBytes at 0
3. hasMore becomes `offset + fullSize < 0` = false, so the client
stops after one oversized message
Fix: detect HTTP 200, cache the full body in memory (to avoid
re-downloading on every subsequent chunk request), then slice to the
requested range. The 512KB per-message limit is now enforced for all
remote URLs regardless of Range request support.
* Add session-based PDF caching with timeout cleanup and size limits
- Add dual timeout strategy for cache cleanup:
- 10s inactivity timeout (resets on each access)
- 60s max lifetime (absolute timeout from creation)
- Add 50MB max size limit with both Content-Length and actual size checks
- Add unit tests for caching behavior including:
- Cache on HTTP 200 response (no range support)
- No cache on HTTP 206 response (range supported)
- Slice cached data for subsequent range requests
- Reject PDFs exceeding size limits
- Export getCacheSize() and clearCache() for testing
* refactor(pdf-server): make PDF cache session-local via factory pattern
- Replace module-level global cache with createPdfCache() factory
- Each server instance now gets its own isolated cache
- Export PdfCache interface for type-safe usage
- Update tests to use per-test cache instances
- Add test verifying cache isolation between sessions
---------
Co-authored-by: Claude <noreply@anthropic.com>
Update `mcp-app.css`, `mcp-app.module.css`, and Svelte/Vue component
styles across all `basic-server-*` examples (and `integration-server`)
to use CSS variables instead of hardcoded values:
- Remove component-local color variables (`--color-primary`, etc.)
- Add derived spacing variables to `global.css` based on `--spacing-unit`
- Replace hardcoded spacing with `--spacing-xs/sm/md/lg` variables
- Add app-defined accent color variables (`--color-accent`,
`--color-text-on-accent`) to `global.css` for brand customization
- Use app accent colors for button styling
- Update button hover to use `color-mix()` for theme-adaptive lightening
- Change focus-visible to use `--color-ring-primary` outline
This makes the templates more instructive for users and AI coding agents
building new MCP Apps by showing how to consume host-provided variables
semantically, define app-specific accent colors, and create adaptive
styling that integrates with host themes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move the inline example from the `getUiCapability()` JSDoc to
`index.examples.ts` for type checking, following the pattern used
by other examples in the codebase.
The example now uses the correct MCP SDK API:
- `server.server.oninitialized` callback for post-initialization hook
- `server.server.getClientCapabilities()` to retrieve capabilities
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update `global.css` in all `basic-server-*` examples (and `integration-server`
for consistency) to better serve as templates for AI coding agents scaffolding
new MCP Apps.
Changes:
- Use CSS custom properties (`--font-sans`, `--color-text-primary`, etc.) with
sensible fallbacks, enabling host design system integration
- Add `color-scheme: light dark` and `light-dark()` fallbacks for theme support
- Include comprehensive typography: h1-h6, body text, `code`/`pre`/`kbd`, `b`/`strong`
- Move `#server-time` styles from global CSS to component stylesheets (classes
instead of IDs for better CSS practices)
Updates E2E test selectors in `DYNAMIC_MASKS` to match the new class-based
selectors.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* docs: fix basic-host setup instructions in quickstart
Add missing root-level npm install and build steps before running
the basic-host example. Without these steps, the example fails because
it imports from dist/src/ which doesn't exist until the root package
is built.
Fixes setup failure when following quickstart tutorial.
* Update quickstart.md
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
---------
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
Clarifies when to use `localStorage` vs server-side storage for view state:
- Scope section intro to "recoverable" view state (easy to recreate)
- Add guidance for state representing user effort (bookmarks, annotations,
custom configurations) to use app-only tools instead
- Note that `viewUUID` should also be passed to app-only tools for scoping
- Clarify that example links are specifically for `localStorage` approach
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document how to serve binary content (e.g., video) via MCP resources
as base64-encoded blobs. Includes server-side registration using the
`blob` field and client-side fetching with conversion to data URIs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents the recommended approach for real-time dashboards that need to
poll server tools at regular intervals. Includes both Vanilla JS and
React examples with proper cleanup handling via `app.onteardown` and
`useEffect` cleanup functions respectively.
Also updates the "Tools that are private to Apps" section to mention
polling as a use case for app-only visibility.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert italic `_See example..._` text to `> [!NOTE]` callouts for better
visual prominence, and expand cross-references to include additional
relevant examples for each pattern.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The two-tool architecture previously had both tools returning identical
`SystemStats` data. This refactors them to properly separate concerns:
- `get-system-info` (Model-visible): Returns static system configuration
(hostname, platform, CPU model/count, total memory) sent once via
`ontoolresult`
- `poll-system-stats` (App-only): Returns dynamic metrics (per-core CPU
timing, memory usage, uptime) polled every 2 seconds
Moves `formatBytes()` and `formatUptime()` utilities to the client since
the server now returns raw values for client-side formatting.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename "host styling" section to "host context" and add safe area
insets handling
- Replace abstract `renderLoadingUI`/`renderFinalUI` example with
concrete code preview pattern for `ontoolinputpartial`
- Rename callback parameter `params` → `ctx` for clarity across all
`onhostcontextchanged` handlers
- Add CSS snippet for fullscreen border-radius handling
- Expand display mode example to show `availableDisplayModes` check
- Fix minor typos and wording consistency
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The `extractRegion()` function in `sync-snippets.ts` used `indexOf()` to
find region markers like `//#region persistData`. This caused incorrect
matching when one region name was a prefix of another—searching for
`persistData` would match `//#region persistDataServer` first if it
appeared earlier in the file.
The fix detects the file's line ending style and appends it to region
markers, ensuring exact matches:
- `//#region persistData\n` no longer matches `//#region persistDataServer\n`
This corrects `docs/patterns.md` where the `persistData` code fence was
incorrectly showing server-side code instead of client-side localStorage
helpers.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tool input JSON examples for Gradient and Kaleidoscope had literal
newlines in the string values, making them invalid JSON. Changed to use
`\n` escape sequences to match the Interactive Julia Set example.
Fixes regression from 574a768.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply the same pattern as map-server: capture the PDF canvas as a
scaled-down screenshot and include it in the model context update
when the host supports image content.
- Check host capability via getHostCapabilities().updateModelContext.image
- Scale canvas to max 768px dimension to reduce token usage
- Convert to base64 PNG and include with text content
* chore: bump ext-apps to 1.0.1
* chore: update examples to depend on ext-apps ^1.0.1
* fix: revert examples to ext-apps ^1.0.0 and fix cheerio version
- Revert ext-apps dependency to ^1.0.0 (works with 1.0.1 once published)
- Fix accidental cheerio bump in wiki-explorer-server
- Remove broken override attempt
* fix: allow compatible semver ranges in version check
The check-versions script now accepts any compatible semver range,
not just exact version matches. For example, ^1.0.0 is allowed when
root version is 1.0.1, since semver compatibility is maintained.
* style: fix indentation in package.json
* fix(examples): use npm reference for ext-apps dependency
Replace local path '../..' with '^1.0.0' npm reference in all example
packages. The local path was accidentally published to npm, causing
npm install to hang when installing server-* packages.
Bump versions to 1.0.1 for all public packages to fix the published
versions.
Fixes npm install hanging issue in dependent projects like
example-remote-server.
Note: npm workspaces automatically resolves the dependency to the
local package during development, so no overrides are needed.
* fix: update zod to 4.x in transcript-server and fix update-lock:docker script
- Update transcript-server to use zod ^4.1.13 for compatibility
- Fix update-lock:docker script to properly clean workspace node_modules
- Regenerate package-lock.json with clean npmjs.org URLs
The main branch of the MCP Python SDK has removed `FastMCP` (likely
moving it to the standalone `fastmcp` package), causing import errors.
Pin to mcp>=1.26.0 which is the latest release that still includes
`mcp.server.fastmcp`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(qr-server): use stable mcp SDK from PyPI
- Replace git-based mcp dependency with mcp>=1.26.0 from PyPI for stability
- Remove TransportSecuritySettings that was blocking browser connections
- Keep simple FastMCP initialization like other Python examples
Fixes ModuleNotFoundError and 403 Forbidden errors when connecting from basic-host
* fix(qr-server): use stable mcp SDK from PyPI
- Replace git-based mcp dependency with mcp>=1.26.0 from PyPI
- Add stateless_http=True for proper HTTP transport handling
- Remove TransportSecuritySettings that blocked browser connections
* chore: add stable spec release 2026-01-26
- Use git mv to copy specification from draft to specification/2026-01-26/apps.mdx (preserves history)
- Update status from 'Draft' to 'Stable (2026-01-26)'
- Add specification version table to README
- Update Resources section with links to both stable and draft specs
* chore: bump version to 1.0.0
- Update main package version from 0.4.2 to 1.0.0
- Update all example package versions to 1.0.0
- Regenerate package-lock.json
* prettier
* chore: align protocol version with spec release 2026-01-26
- Add 'Protocol Version: 2026-01-26' to spec header
- Update LATEST_PROTOCOL_VERSION from '2025-11-21' to '2026-01-26'
- Update spec examples to use '2026-01-26' protocol version
- Add '2025-11-21' to SUPPORTED_PROTOCOL_VERSIONS for backwards compatibility
* chore: update spec references to stable 2026-01-26
- Update typedoc.config.mjs navigation link
- Update docs/overview.md spec links (10 occurrences)
- Update plugins/mcp-apps/skills/create-mcp-app/SKILL.md spec link
- Update src/spec.types.ts @see links
- Update AGENTS.md spec path
- Regenerate src/generated/schema.ts with updated links
* chore: simplify versioning - first stable release
- Remove backwards compatibility version (2025-11-21) - this is the first stable release
- Remove redundant 'Protocol Version' header - spec version IS the protocol version
* chore: add history reference comment to stable spec
Update links from `claude.ai/docs/` to `claude.com/docs/` for the
remote and local MCP server configuration documentation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add local development MCP client configuration to READMEs
Add instructions for configuring MCP clients to use locally-cloned
and modified servers. This allows developers to test local changes
with MCP clients like Claude Desktop or VS Code.
- Top-level README: Added 'Local Development' section after npx config
- All TypeScript examples: Added 'Local Development' subsection with
bash one-liner that builds and runs the server
- Python examples (qr-server, say-server): Added local path config
* docs: include all servers in local development config
* docs: use bash wrapper for Python server configs to enable tilde expansion
* Adding a simple (bad) pdf icon for directory (#330)
* chore: bump version to 0.4.2
- Update package version from 0.4.1 to 0.4.2
- Pin @types/node to 22.10.0 for dev compatibility
- Override seroval to ^1.4.1 to fix security vulnerabilities
- Use '*' for workspace SDK dependency (works with unpublished versions)
* fix(e2e): update Map Server display name to CesiumJS Map Server (#324)
* fix(test): update map-server test to expect 'CesiumJS Map Server'
The server name was renamed but the E2E test config was not updated.
* fix: change in another file
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
* feat(server): add getUiCapability helper for capability negotiation (#313)
* feat(server): add hasUiSupport/getUiCapability for experimental+extensions
Support double-tagging for MCP Apps capability negotiation:
- Check both experimental and extensions fields in client capabilities
- Add hasUiSupport() to easily check if client supports MCP Apps
- Add getUiCapability() to retrieve the capability settings
- Update spec to document both capability locations
This enables forward compatibility as MCP transitions from experimental
to the extensions field (SEP-1724).
Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
* fix: update @types/node version in package-lock.json
Replace invalid 22.19.5/22.19.6/22.19.7 versions with valid 22.19.3
* docs(spec): clarify hosts SHOULD double-tag, servers SHOULD check both
- Hosts SHOULD advertise capabilities in both experimental and extensions
- Servers SHOULD check both locations, preferring extensions when present
- Updated example to show double-tagging pattern
* chore: remove double-tagging recommendation, keep hasUiSupport helper
Reverts the spec changes that recommended clients double-tag capabilities
in both experimental and extensions fields. The helper functions remain
as they're useful for checking capability in either location.
* refactor(server): simplify to only getUiCapability, remove hasUiSupport
- Remove hasUiSupport function (use getUiCapability directly)
- Remove double-tagging logic (only check extensions field)
- Add ClientCapabilitiesWithExtensions type using SDK's ClientCapabilities
- Update spec to use getUiCapability
- Simplify tests
* fix: update @see link to getUiCapability
* refactor(server): inline ClientCapabilitiesWithExtensions type in getUiCapability
* refactor: move McpUiClientCapabilities to spec.types.ts
* fix: update playwright docker image to v1.58.0
* fix: use -i instead of -it for docker commands (no TTY needed)
* fix: Python server dependency resolution and Docker host validation (#333)
* fix(say-server): add --index flag to uv run for PyPI resolution
The pocket-tts dependency wasn't resolving without explicit --index flag
when invoked via npm scripts.
* fix(qr-server): allow Docker bridge IP in transport security
Add 172.17.0.1:* to allowed_hosts to fix 421 Misdirected Request errors
when the server is accessed from Docker containers.
* feat(basic-host): Add theme toggle and MCP style variables (#336)
* feat(basic-host): add theme toggle and MCP style variables
Add theme support to basic-host example:
- theme.ts: Global theme manager with light/dark toggle
- host-styles.ts: Full set of MCP CSS variables using light-dark()
- ThemeToggle component with sun/moon icons
- Theme-aware CSS using CSS custom properties
- Pass theme + styles to apps via hostContext
- Notify apps when theme changes via sendHostContextChange
Apps now receive:
- hostContext.theme: 'light' | 'dark'
- hostContext.styles.variables: All 60+ MCP CSS variables
- onhostcontextchanged notifications when theme changes
* test: update e2e goldens for theme changes
* [MCP Apps] Add appCapabilities.availableDisplayModes documentation to spec (#331)
* Add appCapabilities.availableDisplayModes documentation to spec
Document the availableDisplayModes field in McpUiAppCapabilities that allows
apps to declare which display modes they support. This enables hosts to
compute the intersection of host and app capabilities and only offer
appropriate display mode controls.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Address PR review feedback
- Change "Guest UI" to "View" terminology
- Add "View" prefix to availableDisplayModes heading for disambiguation
- Remove intersection logic - host only checks its own supported modes
- Remove assumption that apps must support inline mode
- Simplify host behavior to honor/reject based on its own capabilities
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add appCapabilities.availableDisplayModes to types
- Add `availableDisplayModes` field to `McpUiAppCapabilities` so apps can
declare which display modes they support
- Change `HostContext.availableDisplayModes` type from `string[]` to
`McpUiDisplayMode[]` for type safety
- Regenerate schemas
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Address Olivier's feedback - consolidate display mode docs
- Remove separate "App Capabilities" section for availableDisplayModes
- Add display mode requirements to existing ui/request-display-mode section:
- Host MUST NOT switch View to mode not in appCapabilities.availableDisplayModes
- Host MAY decline requests from Views that didn't declare modes
- View MUST declare supported modes during initialization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Restore McpUiAppCapabilities docs and fix display mode clause
- Restore McpUiAppCapabilities interface documentation with availableDisplayModes
- Restore "View MUST check if the requested mode is in availableDisplayModes
from host context" clause that was accidentally removed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Create unified Display Modes section
Consolidate all display mode documentation into a single section:
- Display mode types (inline, fullscreen, pip)
- Declaring support from View (appCapabilities.availableDisplayModes)
- Declaring support from Host (HostContext.availableDisplayModes)
- Requesting changes (ui/request-display-mode)
- Notifying changes (ui/notifications/host-context-changed)
- All MUST/SHOULD requirements for View and Host behavior
The ui/request-display-mode in Requests section now references
the Display Modes section for behavior details.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove mode coercion clause from display modes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Regenerate schemas with correct version
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(say-server): add light/dark theme support (#338)
- Add importmap entry for main SDK exports (applyDocumentTheme, etc.)
- Import and apply host theme and styles on context changes
- Apply initial host context theme/styles when app connects
- Update CSS to use [data-theme="dark"] selector for host-provided themes
- Keep @media prefers-color-scheme as fallback for standalone mode
- Add color-scheme meta tag for native element theming
* fix: followup app -> view terminology (#339)
* chore: husky env fix, version pins, update-lock:docker script (#341)
* fix(husky): load Node.js env for GUI apps + add update-lock:docker script
Cherry-picked non-e2e changes from ochafik/fix-e2e-flaky-tests:
- Load nvm/fnm/volta/homebrew paths in pre-commit for GitHub Desktop
- Add update-lock:docker script to regenerate lockfile with public registry
* pin package versions for dev compatibility
- solid-js: 1.9.10
- caniuse-lite: 1.0.30001763
- cheerio: 1.1.2
- electron-to-chromium: 1.5.267
- @playwright/test: 1.57.0
- playwright: 1.57.0
- playwright-core: 1.57.0
* regen
* fix: pin @types/node to existing version and regenerate lockfile
- Fix @types/node 20.19.28 -> 20.19.27 (typo, version didn't exist)
- Regenerate package-lock.json to resolve versions available on both
public npm and Anthropic artifactory registries
* fix(basic-server-solid): bump solid-js 1.9.10 -> 1.9.11 to fix seroval vulnerabilities
* fix: pin seroval to 1.4.1 for dev compatibility
* Refactor server startup functions
Rename `startServer` to `startStreamableHTTPServer` and add a new
`startStdioServer` helper to make the transport type explicit. Remove
the `ServerOptions` interface in favor of reading `PORT` directly from
the environment inside the function (defaulting to `3001`).
Also update Python examples to default to port `3001` for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Support full-file inclusion in synced code snippets
Previously, the script only supported extracting regions from `.ts/.tsx`
files using the `#regionName` syntax. Now it also supports including
entire files without a region specifier, enabling sync of any file type
(JSON, YAML, shell scripts, etc.) into documentation.
Changes:
- Make `#regionName` optional in the `source=""` attribute
- Accept any fence language (not just `ts`/`tsx`)
- Simplify cache to flat map with composite keys
- Add validation error for region extraction on non-TypeScript files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rewrite Quickstart guide with type-checked code examples
Rewrites the Quickstart tutorial to provide a more focused, step-by-step
introduction to building MCP Apps. The guide now uses synced code fences
that pull from `examples/quickstart/`, ensuring all code shown in the
documentation is type-checked and tested.
Key improvements:
- Clearer project setup instructions with explicit commands
- Streamlined explanations of the tool + resource registration pattern
- Added screenshot showing the completed app
- E2E test coverage validates the example works end-to-end
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add MCP Apps Overview document
Adds `docs/overview.md` covering the core concepts: architecture,
lifecycle, UI resources, bidirectional communication, theming, display
modes, and security. Includes mermaid flowchart and sequence diagrams.
Also adds the `@boneskull/typedoc-plugin-mermaid` TypeDoc plugin for
mermaid diagram support. However, the plugin handles HTML entities from
TypeDoc incorrectly, so `scripts/typedoc-plugin-fix-mermaid-entities.mjs`
decodes HTML entities before the plugin processes them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Agent Skills installation guide
Add `docs/agent-skills.md` with installation instructions for multiple AI
coding agents including Claude Code, VS Code/Copilot, Gemini CLI, Cline,
and Goose. The guide provides three installation methods: Claude Code
plugin, Vercel Skills CLI (`npx skills add`), and manual installation.
Update `README.md` to link to the new guide and reframe the plugin
section around the broader "Agent Skills" concept.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Polish OpenAI migration reference for readability
Add framing prose to the migration guide to improve navigation:
- Expanded intro describing document purpose and how to use it
- Added note about features marked "Not yet implemented"
- Added brief transition sentences before Server-Side and Client-Side sections
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix mermaid diagram arrowheads not rendering
The `@boneskull/typedoc-plugin-mermaid` creates both dark and light theme
variants of each diagram, each with identical marker IDs (e.g., `#arrowhead`).
When mermaid renders SVGs at runtime, lines reference `url(#arrowhead)`, but
the browser resolves this to the first match in document order — the hidden
dark-theme SVG — causing arrows to be invisible.
The fix removes dark-theme `<div class="mermaid dark">` elements at build time,
leaving only one SVG per diagram with unique marker IDs. A CSS filter
(`filter: invert(1) hue-rotate(180deg)`) handles dark mode styling instead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(docs): improve OpenAI migration guide accuracy (#349)
- Add missing setOpenInAppUrl API to 'Not Yet in MCP Apps' section
- Clarify Event Handling table: specify exact OpenAI properties
(toolInput/toolOutput) instead of vague 'window.openai.*'
- Improve notes to distinguish sync property reads vs callbacks
* Add Testing MCP Apps guide
Document how to test MCP Apps using `basic-host` and MCP Apps-compatible
hosts like Claude.ai and VS Code Copilot. Cover debugging features for
`basic-host`, and using `cloudflared` to expose local servers for remote
hosts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix `onhostcontextchanged` handler invocation in examples
`App.onhostcontextchanged` is a setter-only property with no getter.
TypeScript doesn't prevent reading from setter-only properties, so
`app.onhostcontextchanged?.(ctx)` compiles but silently returns
`undefined` at runtime.
Extract the handler into a named `handleHostContextChanged()` function
and call it directly after `connect()`, matching the pattern used in
`basic-server-vanillajs` and other examples.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Expand Agent Skills guide
Add intro explaining what Agent Skills are and link to the two skill
files (`create-mcp-app`, `migrate-oai-app`). Reorganize headings and add
"Invoke a Skill" and "Test Your App" sections. Update `README.md` link
text and add tip to quickstart suggesting agent skills as an alternative.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(e2e): improve test stability and developer experience (#337)
* fix(e2e): add masks for dynamic content and increase timeouts for slow servers
- Add HOST_MASKS for Tool Input/Result panels with dynamic timestamps
- Add say-server to SLOW_SERVERS (10s) for TTS model download
- Add say-server to EXTRA_WAIT_MS (30s) in generate-grid-screenshots
* feat: add update-lock:docker script to regenerate lockfile with public registry
* pin package versions for dev compatibility
* feat(basic-host): add ?theme=hide query param to hide theme toggle in screenshots
- Add hideThemeToggle param to getQueryParams()
- Conditionally render ThemeToggle based on ?theme=hide
- Update e2e tests to use ?theme=hide for consistent screenshots
* update screenshots
* update screenshots
* fix(husky): load Node.js environment for GUI apps like GitHub Desktop
* fix(e2e): increase map-server wait time to 15s for tile loading
* fix(e2e): increase waitForAppLoad timeout to 30s for nested iframe loading
* ci: add workflow to update e2e snapshots
* ci: trigger snapshot update on push to this branch
* chore: update e2e snapshots [skip ci]
* ci: trigger CI after snapshot update
* style: fix prettier formatting in update-snapshots workflow
* fix: pin seroval, seroval-plugins, and solid-js for dev compatibility
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Improve README structure and clarity
Add "Why MCP Apps?" section to explain the motivation upfront. Reorganize
the "How It Works" section to lead with tool definition. Rename "Overview"
to "Using the SDK" and add "For" prefix to audience headings.
Restructure the Examples section with clearer subsections for running
examples with `basic-host` vs MCP clients using stdio transport. Move the
`qr` server note outside the collapsed details block.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Codex and Goose to documentation
Add Codex to the list of agents supporting skills in `agent-skills.md`.
Add Goose to the list of supported clients in `testing-mcp-apps.md` and
rename "VS Code Copilot" to "VS Code (Insiders)" for accuracy.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: regenerate lockfile with public registry, use ^0.4.2 for ext-apps deps
* fix: update quickstart example to use workspace reference
* chore: remove internal registry references from lockfile
* feat(ci): add /update-snapshots comment trigger for PRs
- Comment '/update-snapshots' on any PR to update E2E snapshots
- Adds rocket reaction to acknowledge the command
- Comments back with result (updated or no changes needed)
- Removes hardcoded branch trigger
- Keeps workflow_dispatch for manual runs
* docs: document /update-snapshots PR comment trigger
* fix: pin @hono/node-server to 1.19.7 for dev compatibility
SDK 1.25.3 requires @hono/node-server@^1.19.9 which doesn't exist
on the public npm registry (latest is 1.19.7).
* fix: pin @modelcontextprotocol/sdk to 1.25.2
SDK 1.25.3 has broken dependency on @hono/node-server@^1.19.9
which doesn't exist on public npm registry.
* fix: restore flexible SDK peerDep range (^1.24.0)
devDependencies stays pinned to 1.25.2 for our builds,
but peerDependencies should allow users flexibility.
* fix: pin @types/node to avoid unpublished versions
- Pin quickstart @types/node to 22.19.5 (latest public 22.x)
- Add @types/node override to root package.json
- Regenerate lockfile with public registry only
---------
Co-authored-by: Tobin South <tobin.south@gmail.com>
Co-authored-by: Liad Yosef <liady@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: martinalong <martinalong10@gmail.com>
Co-authored-by: Ido Salomon <idosalomon@gmail.com>
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(map-server): add screenshot to updateModelContext and remove rounded corners
- Remove border-radius and overflow: hidden from cesiumContainer CSS
- Capture map canvas as base64 PNG and include in updateModelContext
- Only include image if host supports image content blocks
* feat(map-server): reduce screenshot size for fewer tokens
- Scale down to max 256px (longest dimension)
- Use PNG for better quality with map graphics
* prettier
Add Codex to the list of agents supporting skills in `agent-skills.md`.
Add Goose to the list of supported clients in `testing-mcp-apps.md` and
rename "VS Code Copilot" to "VS Code (Insiders)" for accuracy.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add "Why MCP Apps?" section to explain the motivation upfront. Reorganize
the "How It Works" section to lead with tool definition. Rename "Overview"
to "Using the SDK" and add "For" prefix to audience headings.
Restructure the Examples section with clearer subsections for running
examples with `basic-host` vs MCP clients using stdio transport. Move the
`qr` server note outside the collapsed details block.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(e2e): add masks for dynamic content and increase timeouts for slow servers
- Add HOST_MASKS for Tool Input/Result panels with dynamic timestamps
- Add say-server to SLOW_SERVERS (10s) for TTS model download
- Add say-server to EXTRA_WAIT_MS (30s) in generate-grid-screenshots
* feat: add update-lock:docker script to regenerate lockfile with public registry
* pin package versions for dev compatibility
* feat(basic-host): add ?theme=hide query param to hide theme toggle in screenshots
- Add hideThemeToggle param to getQueryParams()
- Conditionally render ThemeToggle based on ?theme=hide
- Update e2e tests to use ?theme=hide for consistent screenshots
* update screenshots
* update screenshots
* fix(husky): load Node.js environment for GUI apps like GitHub Desktop
* fix(e2e): increase map-server wait time to 15s for tile loading
* fix(e2e): increase waitForAppLoad timeout to 30s for nested iframe loading
* ci: add workflow to update e2e snapshots
* ci: trigger snapshot update on push to this branch
* chore: update e2e snapshots [skip ci]
* ci: trigger CI after snapshot update
* style: fix prettier formatting in update-snapshots workflow
* fix: pin seroval, seroval-plugins, and solid-js for artifactory compatibility
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Add intro explaining what Agent Skills are and link to the two skill
files (`create-mcp-app`, `migrate-oai-app`). Reorganize headings and add
"Invoke a Skill" and "Test Your App" sections. Update `README.md` link
text and add tip to quickstart suggesting agent skills as an alternative.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
`App.onhostcontextchanged` is a setter-only property with no getter.
TypeScript doesn't prevent reading from setter-only properties, so
`app.onhostcontextchanged?.(ctx)` compiles but silently returns
`undefined` at runtime.
Extract the handler into a named `handleHostContextChanged()` function
and call it directly after `connect()`, matching the pattern used in
`basic-server-vanillajs` and other examples.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document how to test MCP Apps using `basic-host` and MCP Apps-compatible
hosts like Claude.ai and VS Code Copilot. Cover debugging features for
`basic-host`, and using `cloudflared` to expose local servers for remote
hosts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The `@boneskull/typedoc-plugin-mermaid` creates both dark and light theme
variants of each diagram, each with identical marker IDs (e.g., `#arrowhead`).
When mermaid renders SVGs at runtime, lines reference `url(#arrowhead)`, but
the browser resolves this to the first match in document order — the hidden
dark-theme SVG — causing arrows to be invisible.
The fix removes dark-theme `<div class="mermaid dark">` elements at build time,
leaving only one SVG per diagram with unique marker IDs. A CSS filter
(`filter: invert(1) hue-rotate(180deg)`) handles dark mode styling instead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add framing prose to the migration guide to improve navigation:
- Expanded intro describing document purpose and how to use it
- Added note about features marked "Not yet implemented"
- Added brief transition sentences before Server-Side and Client-Side sections
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add `docs/agent-skills.md` with installation instructions for multiple AI
coding agents including Claude Code, VS Code/Copilot, Gemini CLI, Cline,
and Goose. The guide provides three installation methods: Claude Code
plugin, Vercel Skills CLI (`npx skills add`), and manual installation.
Update `README.md` to link to the new guide and reframe the plugin
section around the broader "Agent Skills" concept.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds `docs/overview.md` covering the core concepts: architecture,
lifecycle, UI resources, bidirectional communication, theming, display
modes, and security. Includes mermaid flowchart and sequence diagrams.
Also adds the `@boneskull/typedoc-plugin-mermaid` TypeDoc plugin for
mermaid diagram support. However, the plugin handles HTML entities from
TypeDoc incorrectly, so `scripts/typedoc-plugin-fix-mermaid-entities.mjs`
decodes HTML entities before the plugin processes them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rewrites the Quickstart tutorial to provide a more focused, step-by-step
introduction to building MCP Apps. The guide now uses synced code fences
that pull from `examples/quickstart/`, ensuring all code shown in the
documentation is type-checked and tested.
Key improvements:
- Clearer project setup instructions with explicit commands
- Streamlined explanations of the tool + resource registration pattern
- Added screenshot showing the completed app
- E2E test coverage validates the example works end-to-end
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, the script only supported extracting regions from `.ts/.tsx`
files using the `#regionName` syntax. Now it also supports including
entire files without a region specifier, enabling sync of any file type
(JSON, YAML, shell scripts, etc.) into documentation.
Changes:
- Make `#regionName` optional in the `source=""` attribute
- Accept any fence language (not just `ts`/`tsx`)
- Simplify cache to flat map with composite keys
- Add validation error for region extraction on non-TypeScript files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rename `startServer` to `startStreamableHTTPServer` and add a new
`startStdioServer` helper to make the transport type explicit. Remove
the `ServerOptions` interface in favor of reading `PORT` directly from
the environment inside the function (defaulting to `3001`).
Also update Python examples to default to port `3001` for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add importmap entry for main SDK exports (applyDocumentTheme, etc.)
- Import and apply host theme and styles on context changes
- Apply initial host context theme/styles when app connects
- Update CSS to use [data-theme="dark"] selector for host-provided themes
- Keep @media prefers-color-scheme as fallback for standalone mode
- Add color-scheme meta tag for native element theming
* Add appCapabilities.availableDisplayModes documentation to spec
Document the availableDisplayModes field in McpUiAppCapabilities that allows
apps to declare which display modes they support. This enables hosts to
compute the intersection of host and app capabilities and only offer
appropriate display mode controls.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Address PR review feedback
- Change "Guest UI" to "View" terminology
- Add "View" prefix to availableDisplayModes heading for disambiguation
- Remove intersection logic - host only checks its own supported modes
- Remove assumption that apps must support inline mode
- Simplify host behavior to honor/reject based on its own capabilities
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add appCapabilities.availableDisplayModes to types
- Add `availableDisplayModes` field to `McpUiAppCapabilities` so apps can
declare which display modes they support
- Change `HostContext.availableDisplayModes` type from `string[]` to
`McpUiDisplayMode[]` for type safety
- Regenerate schemas
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Address Olivier's feedback - consolidate display mode docs
- Remove separate "App Capabilities" section for availableDisplayModes
- Add display mode requirements to existing ui/request-display-mode section:
- Host MUST NOT switch View to mode not in appCapabilities.availableDisplayModes
- Host MAY decline requests from Views that didn't declare modes
- View MUST declare supported modes during initialization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Restore McpUiAppCapabilities docs and fix display mode clause
- Restore McpUiAppCapabilities interface documentation with availableDisplayModes
- Restore "View MUST check if the requested mode is in availableDisplayModes
from host context" clause that was accidentally removed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Create unified Display Modes section
Consolidate all display mode documentation into a single section:
- Display mode types (inline, fullscreen, pip)
- Declaring support from View (appCapabilities.availableDisplayModes)
- Declaring support from Host (HostContext.availableDisplayModes)
- Requesting changes (ui/request-display-mode)
- Notifying changes (ui/notifications/host-context-changed)
- All MUST/SHOULD requirements for View and Host behavior
The ui/request-display-mode in Requests section now references
the Display Modes section for behavior details.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove mode coercion clause from display modes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Regenerate schemas with correct version
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(basic-host): add theme toggle and MCP style variables
Add theme support to basic-host example:
- theme.ts: Global theme manager with light/dark toggle
- host-styles.ts: Full set of MCP CSS variables using light-dark()
- ThemeToggle component with sun/moon icons
- Theme-aware CSS using CSS custom properties
- Pass theme + styles to apps via hostContext
- Notify apps when theme changes via sendHostContextChange
Apps now receive:
- hostContext.theme: 'light' | 'dark'
- hostContext.styles.variables: All 60+ MCP CSS variables
- onhostcontextchanged notifications when theme changes
* test: update e2e goldens for theme changes
* fix(say-server): add --index flag to uv run for PyPI resolution
The pocket-tts dependency wasn't resolving without explicit --index flag
when invoked via npm scripts.
* fix(qr-server): allow Docker bridge IP in transport security
Add 172.17.0.1:* to allowed_hosts to fix 421 Misdirected Request errors
when the server is accessed from Docker containers.
* feat(server): add hasUiSupport/getUiCapability for experimental+extensions
Support double-tagging for MCP Apps capability negotiation:
- Check both experimental and extensions fields in client capabilities
- Add hasUiSupport() to easily check if client supports MCP Apps
- Add getUiCapability() to retrieve the capability settings
- Update spec to document both capability locations
This enables forward compatibility as MCP transitions from experimental
to the extensions field (SEP-1724).
Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
* fix: update @types/node version in package-lock.json
Replace invalid 22.19.5/22.19.6/22.19.7 versions with valid 22.19.3
* docs(spec): clarify hosts SHOULD double-tag, servers SHOULD check both
- Hosts SHOULD advertise capabilities in both experimental and extensions
- Servers SHOULD check both locations, preferring extensions when present
- Updated example to show double-tagging pattern
* chore: remove double-tagging recommendation, keep hasUiSupport helper
Reverts the spec changes that recommended clients double-tag capabilities
in both experimental and extensions fields. The helper functions remain
as they're useful for checking capability in either location.
* refactor(server): simplify to only getUiCapability, remove hasUiSupport
- Remove hasUiSupport function (use getUiCapability directly)
- Remove double-tagging logic (only check extensions field)
- Add ClientCapabilitiesWithExtensions type using SDK's ClientCapabilities
- Update spec to use getUiCapability
- Simplify tests
* fix: update @see link to getUiCapability
* refactor(server): inline ClientCapabilitiesWithExtensions type in getUiCapability
* refactor: move McpUiClientCapabilities to spec.types.ts
* fix(test): update map-server test to expect 'CesiumJS Map Server'
The server name was renamed but the E2E test config was not updated.
* fix: change in another file
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
Migrate JSDoc and markdown documentation from `{@includeCode ./file#region}`
tags to inline code fences with `source="./file#region"` attributes.
Code examples are now visible directly in source files while remaining
type-checked via their `.examples.ts` source files.
Add `scripts/sync-snippets.ts` to extract code from `//#region` blocks
and sync it into labeled fences. Run with `npm run sync:snippets`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update spec: drop Intents reference, rename Notify to Log
- Remove "intent" from the list of MCP-UI message types in the rationale
section (Intents concept is being dropped)
- Rename "Notify" to "Log" in the lifecycle diagram to better reflect
that notifications/message is for debugging/telemetry logging, not
for sending messages to the chat (which is now ui/message)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* spec: remove Intents reference from rationale
Remove 'intent' from the list of MCP-UI message types - Intents concept was dropped in earlier discussions.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(examples): add debug-server example
A comprehensive debug/development server for testing MCP Apps SDK features:
- Vanilla TypeScript UI with collapsible panels
- Message visualization (sent, received, errors)
- Server tool calling interface
- Multiple tool endpoints for testing
* refactor(debug-server): convert to React
- Replace vanilla TypeScript with React components
- Add comprehensive Host Styles panel showing all CSS variables grouped by category
- Use React hooks (useApp, useHostStyles, useAutoResize)
- Cleaner state management with useReducer for event log
- Collapsible sections for better organization
- CSS modules for scoped styling
* fix(debug-server): correct server name in main.ts
* Revert "refactor(debug-server): convert to React"
This reverts commit 5d22d2daa9a98ba412892447202ef10fbdb62f5b.
* fix(debug-server): update default values for multipleBlocks and includeMeta
* fix(debug-server): correct serve script and server name
- serve script now runs main.ts instead of server.ts
- Updated server name to 'Debug Server'
- Fixed npx command in comment
* fix: increase maxHeight to 6000px and add tools capability to debug app
- basic-host: Increase maxHeight from 600 to 6000px
- debug-server: Add tools capability to enable oncalltool/onlisttools handlers
* fix(debug-server): use consistent callback names for event tracking
Event types now match callback names (ontoolinput, ontoolresult, etc.)
so the Callback Status panel shows correct counts and payloads.
* feat(debug-server): add Update (Image) button for model context
* feat(debug-server): add expandable event log entries
- Click on any event log row to expand/collapse
- Arrow indicator rotates when expanded
- Full JSON payload shown indented in pre block
- Max height with scroll for large payloads
* style(debug-server): format long line in fullPayload function
* test(debug-server): add to E2E tests and screenshot specs
* chore: regenerate package-lock.json for cross-platform compatibility
* update dep
* fix(map-server): simplify location update message format
* fix(say-server): pass host to streamable_http_app for non-localhost deployments
* refactor(pdf-server): simplify to stateless range-query architecture
- Remove caching/indexing in favor of direct HTTP Range requests
- Use HEAD requests to get file size (no full download)
- Use Range requests to proxy byte chunks (no caching)
- Local files: fs.read with offset/length
- Remote files: HTTP Range header
- Zero memory footprint (no PDF caching)
- Truly stateless (no session management needed)
- Single server.ts (~350 lines) + main.ts CLI entry point
- Instant startup (no preloading)
- Security: whitelist local files, allowed origins
Removed files:
- src/pdf-indexer.ts
- src/pdf-loader.ts
- src/types.ts
* feat(pdf-server): add dark mode support
- Add CSS variables with light-dark() function for automatic theme switching
- Sync theme with host via applyDocumentTheme()
- Apply host CSS variables via applyHostStyleVariables()
* feat(pdf-server): add list_pdfs tool back
Lists local files and allowed remote origins.
Also moves CHUNK_SIZE to module level constant.
* feat(pdf-server): improve tool descriptions and increase context limit
- display_pdf: list all allowed domains in description
- Increase MAX_MODEL_CONTEXT_LENGTH to 15000
- Move CHUNK_SIZE to module level constant
* chore(say-server): remove deployment docs from repo
Keep server.py fix for non-localhost deployments.
Deployment docs kept locally but not in repo.
* refactor(pdf-server): simplify updateModelContext format
- Add widget ID for multi-widget disambiguation
- Use concise header: 'PDF viewer (id) | title | Current Page: X/Y'
- Add 'Page content:' prefix before text
- Keep selection markers (<pdf-selection>...</pdf-selection>)
* refactor: rename widgetUUID to viewUUID for consistency
Aligns with main's standardization on 'view' terminology (PR #325).
Also dynamically generate allowed domains in display_pdf description.
* viewUUID
* fix(map-server): fix syntax error in location update message
* fix(pdf-server): restore package.json, improve server docs
- Restore --external pdfjs-dist in build
- Rewrite server.ts header for newcomers (list tools, not implementation details)
- Minimize main.ts diff
* chore(pdf-server): remove unused addAllowedOrigin/addAllowedLocalFile
* docs(pdf-server): update README for new architecture
* docs(pdf-server): restore full README, add theming section, remove get_pdf_info
* rm
* fix(pdf-server): restore createMcpExpressApp and error handling
- Use createMcpExpressApp for DNS rebinding protection
- Restore error callback in app.listen()
* fix(say-server): pass host to streamable_http_app for non-localhost deployments
* chore(say-server): remove deployment docs from repo
* widgetId->tool id
* fix(map-server): add null check for center in location update
chore(pdf-server): dedupe and sort allowed origins
* fix(map-server): use transparent background for rounded corners
* prettier
* docs: standardize UI terminology to use "view" consistently
Replaces "widget" and "Guest UI" terminology with "view" across
the codebase as decided in the community survey.
This change updates:
- Specification document
- SDK source code and JSDoc comments
- Example servers and documentation
- Plugin documentation and skills
Closes#71
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add explicit backtick-wrapped display text to all `{@link}` tags for
monospace rendering in TypeDoc output. This improves readability by:
- Using monospace font for code references (types, methods, functions)
- Stripping module prefixes from display text (e.g., `app!App.connect`
displays as `App.connect`)
Updated files across the SDK including `App`, `AppBridge`,
`PostMessageTransport`, React hooks, server helpers, types, and
documentation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Configure TypeDoc to use the GitHub-styled theme for better visual
consistency with the project's GitHub presence. Also reorganize the
config options and enable group navigation for improved documentation
structure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(qr-server): embed widget HTML for self-contained usage
Embed the widget HTML directly in server.py following the same pattern
as the say-server. This allows the QR server to be run without any
local file dependencies using `uv run <url>`.
The get_widget_html() function prefers the built version from dist/
for local development but falls back to the embedded HTML for unbundled
usage.
* refactor(qr-server): remove widget.html and simplify embedded HTML
Address PR review feedback:
- Remove separate widget.html file (no longer needed)
- Remove get_widget_html() function and dist/mcp-app.html reference
- Inline EMBEDDED_WIDGET_HTML directly in widget() resource
- Remove unused Path import
The server is now fully self-contained in a single file.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Update the spec to clarify that:
- ui.domain format and validation rules are host-dependent
- Servers must consult host-specific documentation for expected format
- Common patterns: hash-based subdomains, URL-derived subdomains
- Updated examples to reflect realistic host-controlled domains
This addresses the gap between the spec's previous implied flexibility
(arbitrary domains like weather-widget.example.com) and real-world
constraints where hosts must control sandbox domains for security.
Each pattern section now includes a "See examples/..." link pointing
to a working implementation in the repository. This helps readers
quickly find real code demonstrating each pattern.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new pattern showing how to combine `updateModelContext()` with
`sendMessage()` when data exceeds message size limits. The example
demonstrates offloading a large transcript to model context first,
then sending a brief trigger message.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents `updateModelContext` for keeping the model informed about user
interactions. The example shows a shopping cart use case with YAML frontmatter
for structured, parseable context.
Also refines the `updateModelContext` JSDoc to clarify the difference from
`sendMessage` and removes the `structuredContent` example.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add `IntersectionObserver` pattern for pausing expensive operations
(animations, WebGL, polling) when widgets scroll out of view.
- Add `visibilityBasedPause` example to `patterns.tsx` with play/pause
callbacks and `onteardown` cleanup
- Document pattern in `patterns.md` with link to MDN docs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replaces the TODO stub with complete server-side and client-side examples
based on the `pdf-server` implementation pattern:
- Server: shows returning `widgetUUID` via `CallToolResult._meta`
- Client: shows receiving UUID via `ontoolresult`, using it as localStorage
key with generic `saveState<T>()` / `loadState<T>()` helpers
Also clarifies the distinction between widget state persistence (UI state
surviving page reloads) and `updateModelContext()` (model-visible state).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds JSDoc example `App_updateModelContext_reportError` showing how to
report runtime errors (e.g., microphone permission denied) back to the
model using `updateModelContext`.
Updates patterns.md with clearer framing:
- Server-side (tool handler): return `{ isError: true, ... }`
- Client-side (app): use `updateModelContext` for runtime errors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Expand `App_ontoolinputpartial_progressiveRendering` example in
`app.examples.ts` to show full state management pattern with
`toolInputs` and `toolInputsPartial` variables
- Add healed JSON caveat to `ontoolinputpartial` JSDoc in `app.ts`
- Update `patterns.md` to reference the expanded example and document
the streaming preview pattern
- Remove empty `lowerPerceivedLatency` placeholder from `patterns.tsx`
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add `App_onhostcontextchanged_respondToDisplayMode` example showing how to
listen for display mode changes and update UI with `classList.toggle()`.
Update `App_requestDisplayMode_fullscreen` to `App_requestDisplayMode_toggle`
to show the common toggle pattern checking `availableDisplayModes` first.
Refactor full examples (pdf-server, shadertoy-server) to use
`classList.toggle(class, condition)` instead of add/remove branches.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Merge the separate "Support native host styling using CSS variables" and
"Reacting to light/dark theme changes" sections into a single "Matching
host styling (CSS variables, theme, and fonts)" pattern. This provides
clearer guidance by covering all three concerns together.
- Rename `docs/patterns.ts` to `docs/patterns.tsx` for JSX support
- Add `hostStylingVanillaJs` and `hostStylingReact` consolidated examples
- Enhance `applyHostFonts_fromHostContext` to apply initial fonts after
`connect()`, not just on context change
- Add CSS usage examples to `applyHostStyleVariables()` JSDoc
- Update font example to use `var(--font-sans, system-ui, sans-serif)`
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reference existing JSDoc examples instead of maintaining a separate one in
`docs/patterns.ts`:
- Vanilla JS: `applyDocumentTheme_fromHostContext` from `styles.examples.ts`
- React: `useDocumentTheme_conditionalRender` for reactive theme reading
Expand `applyDocumentTheme_fromHostContext` example to show both the
`onhostcontextchanged` handler and initial theme application after `connect()`.
Note in the docs that `useHostStyles()` applies theme automatically, so React
apps don't need additional setup unless they want to read the theme reactively
for conditional rendering.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
References existing type-checked examples instead of a stub:
- Vanilla JS: `applyHostStyleVariables_fromHostContext` from `src/styles.examples.ts`
- React: `useHostStyles_basicUsage` from `src/react/useHostStyles.examples.tsx`
Expanded `applyHostStyleVariables_fromHostContext` to show the full pattern:
using CSS variables in styles, applying on `onhostcontextchanged`, and
applying initial styles after `connect()`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document the `_meta.ui.visibility: ["app"]` feature in `docs/patterns.md`
by referencing the existing type-checked example from
`src/server/index.examples.ts#registerAppTool_toolVisibility`.
- Fix typo: `["apps"]` → `["app"]` (singular)
- Remove placeholder function from `docs/patterns.ts`
- Expand description to explain use cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces a patterns documentation page for common MCP Apps recipes.
The skeleton includes TODO sections for: perceived latency, iframe/MCP Apps
coexistence, authenticated calls, private tools, theme changes, fullscreen,
host styling, data persistence, OpenAI migration, and error handling.
- Add `docs/patterns.md` to `projectDocuments` in `typedoc.config.mjs`
- Add `docs/patterns.ts` companion file with region-delimited code stubs
- Include `docs/**/*.ts` in `tsconfig.json` for type-checking
- Document the standalone docs pattern in `CLAUDE.md`
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After commit 65a62a9f, the server startup code was moved from server.ts
to main.ts, but the serve:http script was not updated accordingly.
This caused 9 example servers to fail to start when running npm start:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- integration-server
- map-server
- scenario-modeler-server
- system-monitor-server
- threejs-server
- wiki-explorer-server
The server.ts file now only exports createServer(), while main.ts
contains the actual server startup logic.
This enables users to install the `mcp-apps` plugin via:
/plugin marketplace add modelcontextprotocol/ext-apps
/plugin install mcp-apps@modelcontextprotocol-ext-apps
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Follow-up to the examples extraction: pass dependencies as function
parameters instead of using global stub declarations, rename region
names to match method names (e.g., `AppBridge_guestCapabilities_*` →
`AppBridge_getAppCapabilities_*`), and simplify examples using optional
chaining.
Also clarifies `RESOURCE_URI_META_KEY` documentation to indicate it's
for tool definition metadata (from `tools/list`), not tool result
metadata, and removes the redundant `setNotificationHandler` example
in favor of the simpler event setter approach.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract inline code examples from JSDoc comments into companion
`.examples.ts`/`.examples.tsx` files. Examples are now:
- Type-checked by wrapping them in functions
- Referenced via `{@includeCode ./file.examples.ts#regionName}` tags
- Organized using `//#region` and `//#endregion` markers
This keeps the main source files cleaner while ensuring examples
compile correctly. Updated TypeDoc config with `exampleTag: false`
to support title text on `@example` tags.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Relative directory links like `](examples/map-server)` caused TypeDoc
warnings ("relative path is not a file") and resulted in broken links
in the generated API documentation. Converted all 20 example directory
links to full GitHub URLs while keeping image paths relative.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add screenshot reference
- Remove GPU prerequisite (not needed, wasn't requested)
- Use GitHub raw URLs instead of local file paths
- Simplify Docker section
- Fix Claude Desktop config to use remote URL
* Add say-server: streaming TTS MCP App with karaoke UI
- Streaming TTS MCP App with real-time word highlighting
- React widget with pause/resume controls
- Debounced model context updates
- Self-contained server.py for standalone uv run
Co-authored-by: Claude <noreply@anthropic.com>
* Update server.py
* fix(say-server): fix React/Zod version conflicts in widget
- Pin Zod to v3.25.1 via esm.sh deps param (fixes .custom() error)
- Add external=react,react-dom to prevent duplicate React instances
- Update React from 19.1.0 to 19.2.0
- Import React for Babel's classic JSX transform
- Consolidate imports: only use @modelcontextprotocol/ext-apps/react
- Use Babel from unpkg.com instead of esm.sh
- Simplify resource decorator with meta= parameter
- Update CSP to allow both esm.sh and unpkg.com
* fix(say-server): show play button when paused, restart when finished
* feat(say-server): add fullscreen button, remove inline scroll
- Add fullscreen toggle button (appears on hover when available)
- Remove max-height/scroll in inline mode for cleaner display
- Enable scrolling only in fullscreen mode
- Track displayMode and fullscreenAvailable state
- Handle onHostContextChanged to detect fullscreen support
* fix(say-server): fix fullscreen button visibility and prevent multiple audio streams
- Check initial host context for fullscreen availability (not just updates)
- Add audioOperationInProgressRef guard to prevent concurrent audio operations
- Prevents race conditions from rapid clicks/double-clicks
* fix(qr-server): use meta parameter instead of manual handler patching
Simplified CSP metadata handling to match say-server's approach:
- Use meta parameter on @mcp.resource decorator directly
- Removed custom _read_resource_with_meta handler and handler replacement
* fix(say-server): simplify controls to prevent multiple audio streams
- Remove onClick from text display (was conflicting with double-click)
- Always show overlay button (with pause icon when playing)
- Lower opacity (0.3) when playing, full on hover
- Button shows: ▶️ (play), ⏸️ (pause), 🔄 (restart)
- Double-click text to restart still works
- Eliminates click/dblclick race condition
* fix(say-server): only show pause button on hover
- Play/restart buttons visible normally
- Pause button hidden until hover (doesn't block karaoke text)
* fix(say-server): fix partial input handling and session management
- Add promise lock to initTTSQueue to prevent concurrent initialization
- Reset queueIdRef and lastTextRef in ontoolresult for next tool call
- Close existing AudioContext when starting new session
- Reset all playback state (chunks, timings, position) on new session
- Fixes: text deltas not sent when new tool call has shorter text
* fix(say-server): detect new sessions by text comparison, not queueIdRef reset
- Don't reset queueIdRef in ontoolresult - audio should keep playing
- Detect new sessions in ontoolinputpartial/ontoolinput by checking if
new text starts with lastTextRef (continuation) or not (new session)
- Only reset and create new queue when genuinely new session detected
- Fixes: pause/play was creating new streams because queueIdRef was reset
* feat(say-server): add theming, floating button, and keyboard shortcuts
Theming:
- Use CSS variables (--font-sans, --color-text-primary/secondary) with fallbacks
- Apply host fonts via applyHostFonts() on connect and context changes
- Apply host style variables via applyHostStyleVariables()
- Apply document theme via applyDocumentTheme()
Floating button:
- Button follows cursor position (below current line being spoken)
- Uses invisible cursor marker element for accurate positioning
- Shows ⏸️ when playing, ▶️ when paused, 🔄 when finished
- Low opacity when playing, full on hover
Keyboard shortcuts:
- Space: toggle play/pause
- Enter: toggle fullscreen (when available)
UI changes:
- Large overlay only shown for initial play (idle state)
- Floating button used during playback for less intrusive controls
* docs(say-server): add TODO items for future improvements
* fix(say-server): simplify to fixed play button at top right
- Remove floating cursor-following button (was too complex)
- Fixed play button at top-right corner
- Low opacity when playing, full on hover
- Keep theming and keyboard shortcuts (Space/Enter)
- Add say-server to E2E tests
* test(e2e): add say-server to tests but skip in CI
Say-server requires Pocket TTS model (~500MB) and GPU support,
which isn't available in the Docker CI environment. Skip it for now.
* test(e2e): add say-server and qr-server to screenshot generation
- Remove say-server from SKIP_SERVERS (widget works without TTS model)
- Add say-server masks for play buttons
- Add both say-server and qr-server to grid screenshot generator
* fix(say-server): restore working play/pause overlay, keep theming and keyboard shortcuts
Reverted to working commit 413beca and carefully reapplied:
- CSS variables for theming (--font-sans, --color-text-primary/secondary)
- Dark mode support via @media (prefers-color-scheme)
- Host theming via applyHostStyleVariables/applyHostFonts/applyDocumentTheme
- Keyboard shortcuts: Space = play/pause, Enter = fullscreen
Preserved working behavior:
- Play overlay visible when not playing, hover to show when playing
- Pause on click works correctly
- Karaoke highlighting works
* fix(say-server): minimal CSS-only theming, no JS changes from working version
Only CSS changes from working commit 413beca:
- Added CSS variables for theming foundation
- No JavaScript changes (reverted theming calls and keyboard shortcuts)
This isolates whether the issue is CSS or JS related.
* feat(say-server): add autoPlay param and Reset button
- Add autoPlay parameter (default: true) with Pydantic Field description
- Description visible in MCP tool schema: 'browsers may block autoplay'
- Add Reset button next to Fullscreen button (bottom right)
- Both buttons share controlBtn base styles, shown on hover
- Widget reads autoPlay from tool input params
* fix(say-server): update FastMCP API for new SDK version
Python SDK refactored transport params from constructor to run():
- Remove port, stateless_http from FastMCP() constructor
- Pass stateless_http=True to streamable_http_app() instead
* debug(say-server): add console logs to trace audio stream issue
Logging added to:
- togglePlayPause (all branches)
- initTTSQueue (entry and AudioContext creation)
- restartPlayback (entry)
- ontoolinputpartial/ontoolinput (entry and new session detection)
Open browser DevTools console and reproduce the issue - logs will show what's being called.
* feat(say-server): add play/pause buttons in top-left and bottom-right corners
- Top left: playPauseTopBtn for quick access
- Bottom right: playPauseBtn alongside reset and fullscreen
- Both show on hover with same styling as other control buttons
- Icons: ▶️ (play), ⏸️ (pause), 🔄 (restart when finished)
* fix(say-server): simplify UI and fix pause/resume audio duplication
- Remove central play overlay, use single toolbar in top-right
- Fix audio duplication on pause/resume (only defer to pendingChunks for initial autoplay)
- Remove double-click to restart (use toolbar restart button)
- Sync displayMode when host changes it externally
- Click on text to play/pause
* feat(say-server): add voice selection support
- Add list_voices tool to show available voices
- Add voice parameter to say tool (predefined names, HuggingFace URLs, or local paths)
- Predefined voices: alba, marius, javert, jean, cosette, eponine, azelma, fantine
- Update README with voice documentation
* docs(say-server): improve tool description for better LLM triggering
- Add English-only constraint prominently in first line and note
- List common trigger patterns (say, speak, read aloud, etc.)
- Update text parameter description to mention English
- Keep description concise for quick LLM scanning
* feat(say-server): add speak lock for multi-widget coordination
When multiple TTS widgets exist, only one plays at a time:
- Widget announces to localStorage when starting playback
- Playing widgets poll localStorage every 200ms
- If another widget starts, previous one pauses automatically
- Lock is cleared on pause/finish/teardown
Uses _meta.widgetUUID in tool result for widget identification.
* update package.json of say
* feat(say-server): add screenshots and update README gallery
- Add say-server to demo gallery with 300x300 grid-cell.png
- Fix qr-server grid-cell.png dimensions (was 672x668, now 300x300)
- Add golden snapshot for say-server E2E tests
- Add full screenshot and grid-cell thumbnail for say-server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style(say-server): apply prettier formatting
- Add blank lines in README for better readability
- Add trailing newline to package.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(say-server): add Escape key to exit fullscreen and update docs
- Add onKeyDown handler to main element for Escape key
- Update README with full list of MCP App features:
- Model context updates
- Native theming
- Fullscreen mode
- Multi-widget speak lock
- Update server.py docstring with feature documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(say-server): add queue timeout to prevent infinite polling
The TTS queue could hang forever if:
- The LLM stops generating mid-stream (no end signal)
- User cancels before tool completes
- Network interruption prevents end_tts_queue call
Changes:
- Add last_activity timestamp to track queue activity
- Add 30-second timeout (QUEUE_TIMEOUT_SECONDS)
- Poll with 5-second timeout, checking for stale queues
- Mark stale queues as "error" so widget stops polling
- Return error message in poll_tts_audio response
- Add error logging in widget polling loop
This prevents tool calls from running forever when queues are abandoned.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* debug(say-server): add logging to track queue lifecycle
Add console logging to debug why end_tts_queue might not be called:
Widget-side:
- Log queueId in ontoolinputpartial, ontoolinput, ontoolresult
- Log when new session is detected (queue reset)
- Log when initTTSQueue fails
- Log when end_tts_queue is called/not called
Server-side:
- Log when end_tts_queue is called
- Log warnings for unknown queues
- Log info for already-ended queues
This will help identify if the issue is:
- User interruption (ontoolresult never called)
- Session reset (queueId becomes null)
- Host bug (callback dropped)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(say-server): add initial delay before polling
Add 150ms delay before first poll to give TTS model time to generate
the first audio chunk. This reduces unnecessary polling and server load.
Current backoff after that:
- 30ms if chunks received (keep streaming fast)
- 80ms if no chunks (wait longer for generation)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(say-server): use adaptive polling instead of fixed initial delay
Previous approach: Always wait 150ms before first poll
Problem: Even when chunks are ready, we wait unnecessarily
New approach: Adaptive polling
- Start polling immediately (no initial delay)
- 20ms backoff when receiving chunks (fast streaming)
- Exponential backoff when no chunks: 50ms → 100ms → 150ms max
- Reset backoff when chunks start flowing
This reduces latency when TTS is fast while being polite when it's slow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(say-server): add speaker icon to server info
Uses SVG data URI for a speaker with sound waves icon.
* docs(say-server): add comprehensive third-party licensing information
- Expand License section in README with component attribution table
- Add voice collection license matrix with commercial use indicators
- Add warning about non-commercial expresso/ears voice collections
- Clarify NON-COMMERCIAL restriction in list_voices() output
- Use consistent CC-BY formatting (hyphenated)
* Update last activity to prevent timeout during active polling
* feat(say-server): add info button, SVG icons, and UI fixes
- Replace emoji play/pause buttons with neutral white SVG icons
- Add (i) info button in bottom-right with attribution popup
- Use app.openLink() API for external links in popup
- Add dynamic padding to fit popup when shown
- Make create_tts_queue async to fix AnyIO event loop error
- Add better error logging for TTS queue initialization
- Update README with openLink feature documentation
- Remove unnecessary HuggingFace login prerequisites
* docs(say-server): document multi-widget speak lock mechanism
* prettier:fix
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(shadertoy): add proper mouse/touch interaction support
- Fix iMouse uniform to match official Shadertoy specification:
- iMouse.xy only updates while button/touch is held down
- iMouse.zw uses positive values when down, negative when released
- Enables proper click detection via iMouse.z > 0
- Add touch event support for mobile devices:
- touchstart/touchmove/touchend/touchcancel handlers
- Same behavior as mouse events
- Update tool description with mouse interaction documentation
- Update README with:
- Mouse & Touch Interaction feature
- Improved Julia Set example with click detection
- iMouse specification table and usage examples
The iMouse uniform is a core Shadertoy feature used by popular shaders
like Star Nest, raymarching examples, and particle systems.
* style: fix Prettier formatting in shadertoy README
* update qr-server w/ python sdk fix
* feat(qr-server): use Python SDK meta parameter for resource CSP metadata
- Update qr-server to use the new meta parameter on @mcp.resource decorator
instead of manually overriding the read_resource handler
- Add ui.resourceUri to tool meta alongside legacy ui/resourceUri for forward compatibility
- Simplify server.py by removing the custom _read_resource_with_meta handler
- Add qr-server to grid screenshot generation tests
- Update grid-cell.png screenshot
This leverages the Python SDK's native support for resource metadata,
eliminating the need for low-level handler manipulation.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The spec document incorrectly defined permission fields (camera, microphone,
geolocation, clipboardWrite) as boolean types, but the SDK implementation
uses empty object types ({}).
This caused validation errors for MCP servers (like Hex) that followed
the spec and sent boolean values like `clipboardWrite: true`, when the
Zod schema expects `clipboardWrite: {}`.
The empty object design was intentional to allow future extensibility
(e.g., adding options like `{ required: true }`). This commit aligns
the spec documentation with the actual TypeScript types in spec.types.ts.
Fixes validation error:
`{ code: 'invalid_type', expected: 'object', received: 'boolean',
path: ['permissions', 'clipboardWrite'] }`
* fix(e2e): make server-time element fill width for consistent mask
- Add flex layout to .action p to make server-time stretch
- Add #server-time flex: 1 rule across all basic-server variants
- Update golden screenshots with full-width masked time field
This ensures the Playwright mask covers a consistent width regardless
of the timestamp content, fixing screenshot comparison flakiness.
* fix(e2e): wait for select to be enabled in screenshot generator
Match the logic in servers.spec.ts - wait for select to be enabled
(servers connected) instead of just visible.
* chore: trigger CI re-run
Adds a new skill (`plugins/mcp-apps/skills/migrate-oai-app/SKILL.md`)
that guides developers through migrating OpenAI Apps SDK applications
to the MCP Apps SDK. Includes API mapping tables, CSP configuration
guidance, and a "Before Finishing" checklist.
Also fixes the `registerAppResource()` JSDoc example in `src/server/index.ts`
to show CSP configuration in the correct location (on the content item,
not the resource config).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add visibility-based animation pause (IntersectionObserver + wrapped rAF)
- Zero CPU usage when widget scrolls out of view
- Apply host style variables via useHostStyles
- Support transparent WebGL background (alpha: true)
- Use light-dark() for theme-aware fallback colors
- Tool no longer echoes code back in result
- Document performance features in README
Shadertoy server improvements:
- Add ontoolinputpartial handler to show code preview during generation
- Add IntersectionObserver to pause/play shader based on visibility
- Apply host style variables (theme, colors, fonts)
- Add border-radius with fullscreen mode support
- Use gradient background and ghost text color for code preview
Skill documentation updates:
- Add streaming partial input section with use cases
- Add visibility-based resource management pattern
- Add fullscreen mode API and CSS patterns
- Expand host styling with CSS variable usage examples
Implement the conventional MCP pattern for transport backward compatibility:
try `StreamableHTTPClientTransport` first (modern), then fall back to
`SSEClientTransport` (deprecated but needed for older servers).
This follows the approach documented in the MCP TypeScript SDK's
`streamableHttpWithSseFallbackClient.ts` example.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add stdio transport configuration snippets near the top of each example
README to help users quickly set up the servers in their MCP clients.
- Node.js examples use npx with the published npm packages
- Python qr-server uses uv with local path
- Removed Claude Desktop-specific references from qr-server
* feat(shadertoy): add fullscreen support
Add fullscreen toggle button that appears when the host supports fullscreen
display mode. Uses the same pattern as pdf-server:
- Check availableDisplayModes in host context
- Show/hide button based on capability
- Toggle between inline and fullscreen via requestDisplayMode()
- Update UI state on host context changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(shadertoy): button visibility after exiting fullscreen + Escape key
- Only update button visibility when availableDisplayModes is present
in context (partial updates don't include all fields)
- Add Escape key handler to exit fullscreen mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(examples): resolve DIST_DIR path for npm package execution
When servers are compiled to dist/server.js and published to npm,
`import.meta.dirname` already points to dist/, so joining with "dist"
results in dist/dist/ which doesn't exist.
This fix detects whether we're running from TypeScript source or
compiled JavaScript and adjusts the path accordingly:
- From source (server.ts): use dist/ subdirectory
- From compiled (dist/server.js): use current directory
Fixes ENOENT errors like:
"ENOENT: no such file or directory, open '.../dist/dist/mcp-app.html'"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(pdf-server): mark pdfjs-dist as external in bundle
The pdf.worker.mjs file wasn't being found when running from npm
because bun bundled pdfjs-dist but didn't include the worker file.
By marking pdfjs-dist as external, it uses the installed package
from node_modules at runtime where the worker is available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* map-server: fix async/await in ontoolresult, remove unused getViewStorageKey
* pdf-server: fix scrolling gap in inline mode
- Set html/body background to match .main background color
This hides any gap between .main and iframe border
- Change .main from height:100% to min-height:100%
Ensures .main fills container properly
* feat: compile example servers to JavaScript for npm consumption
Add TypeScript compilation for server code so the example packages can be
imported and used by consumers without requiring TypeScript compilation.
Changes:
- Add tsconfig.server.json to each example for compiling server code
- Update package.json exports to point to compiled dist/server/server.js
- Add types field pointing to dist/server/server.d.ts
- Update build scripts to include server compilation step
- Fix .ts import extensions to .js in customer-segmentation-server
This enables importing the createServer function from published packages:
import { createServer } from '@modelcontextprotocol/server-basic-react';
* fix: add type assertion for Nominatim API response
* chore: regenerate schemas
* refactor: split server examples into library and binary entry points
- server.ts: exports createServer() only (library entry point)
- main.ts: entry point with main() that runs the server (binary)
- package.json: adds bin field for npx support, uses inline bun build
- tsconfig.server.json: emitDeclarationOnly, outputs to dist/
This allows:
1. Importing the server as a library: import { createServer } from '@modelcontextprotocol/server-basic-react'
2. Running with npx: npx @modelcontextprotocol/server-basic-react [--stdio]
Added migration script at scripts/migrate-server-examples.ts
* refactor: migrate remaining server examples to library/binary structure
Apply the same refactoring to all 13 remaining server examples:
- scenario-modeler-server, shadertoy-server, map-server
- cohort-heatmap-server, customer-segmentation-server, video-resource-server
- system-monitor-server, budget-allocator-server, sheet-music-server
- threejs-server, integration-server, transcript-server, wiki-explorer-server
Each now has:
- server.ts: exports createServer() (library)
- main.ts: runs the server (binary with shebang)
- bin field in package.json for npx support
* chore: remove unused server-utils.ts from video-resource-server/src
* update
* fix(build): make Windows-compatible by removing chmod and fixing bun externals
- Remove 'chmod +x dist/index.js' which doesn't exist on Windows
- Add '--external ./server.js' to prevent bun from treating the import as a second entry point
* fix(build): use .ts import for bun compatibility
- Change import from './server.js' to './server.ts' in main.ts files
- Remove --external flag since bun should inline everything
- Fixes Windows build where bun couldn't resolve .js to .ts properly
* fix(build): use --external for Windows bun compatibility
Bun on Windows incorrectly treats imported .ts files as entry points.
Use --external './server.js' to work around this, creating a runtime
dependency between index.js and server.js.
* style: fix prettier formatting in pdf-server/main.ts
* fix(build): remove quotes from --external for Windows cmd.exe compatibility
* fix(build): use glob pattern for --external to work on all platforms
The literal path --external ./server.js was not matching the import.
Using glob pattern --external "./server*" correctly externalizes the server module.
* fix(build): use */server.js pattern for --external
The "./server*" pattern was not matching on Windows.
Using "*/server.js" matches any path ending in /server.js which works cross-platform.
* fix(build): use *server.js pattern without slash for Windows compatibility
* update package-lock
* chore: regenerate package-lock.json for cross-platform compatibility
* fix(build): use ./server.js instead of glob pattern for Windows compatibility
* fix(build): use double quotes for banner to fix Windows cmd.exe compatibility
---------
Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
* map-server: fix async/await in ontoolresult, remove unused getViewStorageKey
* fix(map-server): improve updateModelContext and fix initialization
- Use structured YAML frontmatter format for model context (like pdf-server)
- Add tool name, altitude, and cleaner data format
- Fix initialization deadlock: app.connect() was inside ontoolresult handler
which could never fire until connected
- Fix view state persistence: widgetUUID now properly extracted from
ontoolresult and used to restore persisted camera position
- Server now includes widgetUUID in tool result _meta for widget identification
* fix(pdf-server): use widgetUUID for page persistence
- Add widgetUUID to tool result _meta for widget identification
- Use widgetUUID instead of generated key (pdfUrl+toolId) for localStorage
- Enables reliable page restoration when revisiting PDF widgets
* fix(transcript-server): use structured YAML frontmatter for model context
- Add tool name, listening status, and unsent entry count in frontmatter
- Consistent format with pdf-server and map-server
* update names
* drop bogus altitude
* Update mcp-app.ts
- Replace requirements.txt with inline script dependencies in server.py
- Add package.json with npm scripts for run-all.ts integration
- Update CI workflow to install uv via astral-sh/setup-uv@v5
- Update Docker commands to install uv for e2e tests
- Add Playwright golden screenshot for qr-server
- Update README with uv installation and usage instructions
* feat(pdf-server): Interactive PDF viewer example
PDF viewer with PDF.js featuring:
- Chunked binary loading with progress bar
- Text extraction for AI context
- arXiv paper support (fetch by ID)
- Page navigation with keyboard shortcuts
- Zoom controls (including Ctrl+0 reset)
- Fullscreen mode support
- Horizontal swipe for page changes (disabled when zoomed)
- Page persistence in localStorage
- Text selection via PDF.js TextLayer
- Clickable title link to source URL
- Rounded corners and subtle border styling
* chore: Add pdf-server to screenshot generation list
* refactor(pdf-server): Simplify and generalize PDF loading
- Accept any HTTP(s) URLs instead of ArXiv-only
- Use HTTP Range requests for chunked binary loading
- Remove ArXiv-specific code (arxiv.ts, metadata fetching)
- Remove CLAUDE.md index generation
- Flatten hierarchical folder structure to simple entries list
- Remove dead code: getPdfSummary, httpFileSizes
- Simplify base64 encoding using Buffer
- Simplify chunk extraction using slice()
- Consolidate DEFAULT_PDF_URL constant
The server now works with any PDF URL, not just arXiv papers.
HTTP Range requests stream chunks on-demand when supported.
* feat(pdf-server): Include title and selection in model context
- Add pdfTitle to updateModelContext structuredContent
- Include selection position (text, start, end) when text is selected
- Add debounced selectionchange listener to update context on selection
* fix(pdf-server): Restore default URL in view_pdf schema
The UI needs the default value in the schema to show it properly.
* refactor(pdf-server): Further simplifications
- Remove hard-coded test paths from main()
- Remove unused resources: pdfs://metadata/{pdfId}, pdfs://content/{pdfId}
- Remove unused metadata fields: subject, creator, producer, creationDate, modDate
- Remove unused entry fields: relativePath, estimatedTextSize
- Remove filterEntriesByFolder and folder filter from list_pdfs
- Remove redundant output schema validation (trust typed returns)
- Simplify scanDirectory and createLocalEntry signatures
Total: 1836 → 1666 lines (-170 lines, -9%)
* refactor(pdf-server): Major simplification for didactic focus
Simplified the example to focus on key MCP Apps SDK patterns:
- Chunked data through size-limited tool calls
- Model context updates (page text + selection)
- Display modes (fullscreen vs inline)
- External links (openLink)
Changes:
- Remove local file support (HTTP URLs only)
- Restrict dynamic URLs to arxiv.org for security
- Simplify types: url instead of sourcePath/sourceType
- Simplify indexer: 168 → 44 lines
- Simplify loader: 318 → 171 lines
- Simplify server: 337 → 233 lines
- Fix selection text normalization
- Rewrite README with didactic focus
Total: 1836 → 1236 lines (-33%)
* feat(pdf-server): Add file:// URL support for local files
- Local paths are converted to file:// URLs on startup
- file:// URLs must be in the initial list (strict validation)
- Dynamic URLs still restricted to arxiv.org only
- Updated README with local file examples
* fix(pdf-server): Improve selection detection with logging
- Add logging to selectionchange handler to verify it fires
- Add fallback matching without spaces (TextLayer spans may lack spaces)
- Log selection detection success/failure for debugging
The issue: PDF.js TextLayer renders text as positioned spans without
space characters between them. When selecting across spans:
- pageText has spaces (items joined with ' ')
- sel.toString() may not have spaces
- indexOf fails to match
The fix tries exact match first, then falls back to spaceless matching.
* feat(pdf-server): Format model context as markdown with front matter
Model context now looks like:
```markdown
---
url: https://arxiv.org/pdf/...
page: 5/144
---
Page text with <pdf-selection>selected text</pdf-selection> inline.
```
This is cleaner for the model to parse and includes the source URL.
* refactor(pdf-server): Extract smart truncation helpers
Added two well-designed helpers:
formatPageContent(text, maxLength, selection?)
- Centers truncation window around selection if present
- Adds <truncated-content/> markers at elision points
- Wraps selection in <pdf-selection> tags
- Allocates 60% context before, 40% after for readability
findSelectionInText(pageText, selectedText)
- Tries exact match first
- Falls back to spaceless match for TextLayer quirks
- Returns { start, end } or undefined
Example output with selection:
```
<truncated-content/>
...context before... <pdf-selection>selected text</pdf-selection> ...context after...
<truncated-content/>
```
* fix(pdf-server): Truncate inside selection tags when selection too long
When selection is too large for the budget:
<truncated-content/><pdf-selection><truncated-content/>start...end<truncated-content/></pdf-selection><truncated-content/>
This keeps the selection structure intact while showing beginning and end.
* refactor(pdf-server): Remove unused read_pdf_text, use Attention paper as default
- Remove read_pdf_text tool (viewer extracts text client-side with pdfjs)
- Remove PdfTextChunk and ReadPdfTextInput types
- Remove loadPdfTextChunk from pdf-loader
- Change default PDF to 'Attention Is All You Need' (1706.03762)
- Update README with modest language
* refactor(pdf-server): Simplify to use URL as ID, rename view_pdf to display_pdf
Major simplifications:
- Use URL directly as identifier (no hashing)
- Remove displayName - show elided URL with full URL as tooltip
- Rename view_pdf to display_pdf with better description
- Update all references from pdfId to url
- Simplify storage key and model context
The tool description now explains it displays an interactive viewer in the chat.
* feat(pdf-server): Normalize arxiv URLs to PDF format
arxiv.org/abs/... -> arxiv.org/pdf/...
Applied both at startup and when loading dynamic URLs.
* docs(pdf-server): Add prompt engineering to display_pdf description
* fix(pdf-server): Sharp rendering on retina displays
Account for devicePixelRatio when rendering canvas:
- Scale canvas dimensions by dpr
- Scale context by dpr
- Keep CSS size at logical pixels
* fix(pdf-server): Normalize arxiv URLs in read_pdf_bytes too
* add to e2e spec
* add to e2e spec
* add to e2e spec
* add to e2e spec
* regen
* chore: regenerate package-lock.json and fix hono vulnerability
* docs: add pdf-server screenshot to READMEs
* regen
* ci: add missing examples to pkg-pr-new publish
* ci: add pdf-server to npm publish examples
* Update README.md
* pdf-server: improve tool response text for better model context
* revert unrelated screenshot changes
* pdf-server: dynamically add arxiv URLs in read_pdf_bytes
Fixes 'PDF not found' error when server restarts between display_pdf
(which adds the entry) and read_pdf_bytes (which previously only looked
up existing entries). Now read_pdf_bytes mirrors display_pdf's logic
and dynamically adds arxiv URLs to the index.
* cap length of context update in pdf-server
* format
* feat(pdf-server): Interactive PDF viewer example
PDF viewer with PDF.js featuring:
- Chunked binary loading with progress bar
- Text extraction for AI context
- arXiv paper support (fetch by ID)
- Page navigation with keyboard shortcuts
- Zoom controls (including Ctrl+0 reset)
- Fullscreen mode support
- Horizontal swipe for page changes (disabled when zoomed)
- Page persistence in localStorage
- Text selection via PDF.js TextLayer
- Clickable title link to source URL
- Rounded corners and subtle border styling
* chore: Add pdf-server to screenshot generation list
* refactor(pdf-server): Simplify and generalize PDF loading
- Accept any HTTP(s) URLs instead of ArXiv-only
- Use HTTP Range requests for chunked binary loading
- Remove ArXiv-specific code (arxiv.ts, metadata fetching)
- Remove CLAUDE.md index generation
- Flatten hierarchical folder structure to simple entries list
- Remove dead code: getPdfSummary, httpFileSizes
- Simplify base64 encoding using Buffer
- Simplify chunk extraction using slice()
- Consolidate DEFAULT_PDF_URL constant
The server now works with any PDF URL, not just arXiv papers.
HTTP Range requests stream chunks on-demand when supported.
* feat(pdf-server): Include title and selection in model context
- Add pdfTitle to updateModelContext structuredContent
- Include selection position (text, start, end) when text is selected
- Add debounced selectionchange listener to update context on selection
* fix(pdf-server): Restore default URL in view_pdf schema
The UI needs the default value in the schema to show it properly.
* refactor(pdf-server): Further simplifications
- Remove hard-coded test paths from main()
- Remove unused resources: pdfs://metadata/{pdfId}, pdfs://content/{pdfId}
- Remove unused metadata fields: subject, creator, producer, creationDate, modDate
- Remove unused entry fields: relativePath, estimatedTextSize
- Remove filterEntriesByFolder and folder filter from list_pdfs
- Remove redundant output schema validation (trust typed returns)
- Simplify scanDirectory and createLocalEntry signatures
Total: 1836 → 1666 lines (-170 lines, -9%)
* refactor(pdf-server): Major simplification for didactic focus
Simplified the example to focus on key MCP Apps SDK patterns:
- Chunked data through size-limited tool calls
- Model context updates (page text + selection)
- Display modes (fullscreen vs inline)
- External links (openLink)
Changes:
- Remove local file support (HTTP URLs only)
- Restrict dynamic URLs to arxiv.org for security
- Simplify types: url instead of sourcePath/sourceType
- Simplify indexer: 168 → 44 lines
- Simplify loader: 318 → 171 lines
- Simplify server: 337 → 233 lines
- Fix selection text normalization
- Rewrite README with didactic focus
Total: 1836 → 1236 lines (-33%)
* feat(pdf-server): Add file:// URL support for local files
- Local paths are converted to file:// URLs on startup
- file:// URLs must be in the initial list (strict validation)
- Dynamic URLs still restricted to arxiv.org only
- Updated README with local file examples
* fix(pdf-server): Improve selection detection with logging
- Add logging to selectionchange handler to verify it fires
- Add fallback matching without spaces (TextLayer spans may lack spaces)
- Log selection detection success/failure for debugging
The issue: PDF.js TextLayer renders text as positioned spans without
space characters between them. When selecting across spans:
- pageText has spaces (items joined with ' ')
- sel.toString() may not have spaces
- indexOf fails to match
The fix tries exact match first, then falls back to spaceless matching.
* feat(pdf-server): Format model context as markdown with front matter
Model context now looks like:
```markdown
---
url: https://arxiv.org/pdf/...
page: 5/144
---
Page text with <pdf-selection>selected text</pdf-selection> inline.
```
This is cleaner for the model to parse and includes the source URL.
* refactor(pdf-server): Extract smart truncation helpers
Added two well-designed helpers:
formatPageContent(text, maxLength, selection?)
- Centers truncation window around selection if present
- Adds <truncated-content/> markers at elision points
- Wraps selection in <pdf-selection> tags
- Allocates 60% context before, 40% after for readability
findSelectionInText(pageText, selectedText)
- Tries exact match first
- Falls back to spaceless match for TextLayer quirks
- Returns { start, end } or undefined
Example output with selection:
```
<truncated-content/>
...context before... <pdf-selection>selected text</pdf-selection> ...context after...
<truncated-content/>
```
* fix(pdf-server): Truncate inside selection tags when selection too long
When selection is too large for the budget:
<truncated-content/><pdf-selection><truncated-content/>start...end<truncated-content/></pdf-selection><truncated-content/>
This keeps the selection structure intact while showing beginning and end.
* refactor(pdf-server): Remove unused read_pdf_text, use Attention paper as default
- Remove read_pdf_text tool (viewer extracts text client-side with pdfjs)
- Remove PdfTextChunk and ReadPdfTextInput types
- Remove loadPdfTextChunk from pdf-loader
- Change default PDF to 'Attention Is All You Need' (1706.03762)
- Update README with modest language
* refactor(pdf-server): Simplify to use URL as ID, rename view_pdf to display_pdf
Major simplifications:
- Use URL directly as identifier (no hashing)
- Remove displayName - show elided URL with full URL as tooltip
- Rename view_pdf to display_pdf with better description
- Update all references from pdfId to url
- Simplify storage key and model context
The tool description now explains it displays an interactive viewer in the chat.
* feat(pdf-server): Normalize arxiv URLs to PDF format
arxiv.org/abs/... -> arxiv.org/pdf/...
Applied both at startup and when loading dynamic URLs.
* docs(pdf-server): Add prompt engineering to display_pdf description
* fix(pdf-server): Sharp rendering on retina displays
Account for devicePixelRatio when rendering canvas:
- Scale canvas dimensions by dpr
- Scale context by dpr
- Keep CSS size at logical pixels
* fix(pdf-server): Normalize arxiv URLs in read_pdf_bytes too
* add to e2e spec
* add to e2e spec
* add to e2e spec
* add to e2e spec
* regen
* chore: regenerate package-lock.json and fix hono vulnerability
* docs: add pdf-server screenshot to READMEs
* regen
* ci: add missing examples to pkg-pr-new publish
* ci: add pdf-server to npm publish examples
* Update README.md
* pdf-server: improve tool response text for better model context
* revert unrelated screenshot changes
* pdf-server: dynamically add arxiv URLs in read_pdf_bytes
Fixes 'PDF not found' error when server restarts between display_pdf
(which adds the entry) and read_pdf_bytes (which previously only looked
up existing entries). Now read_pdf_bytes mirrors display_pdf's logic
and dynamically adds arxiv URLs to the index.
- Add `.gitignore` files to all 19 examples (`node_modules/`, `dist/`)
- Replace custom `log` objects with standard `console` methods
- Add `ontoolcancelled` handler to all `basic-server-*` examples
- Add host styling integration (`applyDocumentTheme`, `applyHostFonts`,
`applyHostStyleVariables`) to non-React `basic-server-*` examples
- Add section headers for better code organization in feature examples
- Update READMEs with Build System section explaining single-file
bundling
These changes make examples more consistent and easier to adapt.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a Claude Code plugin (`plugins/mcp-apps/`) for distribution via the
plugin marketplace. The plugin provides the "Create MCP App" skill that
guides the agent through building MCP Apps with interactive UIs.
The skill includes:
- Instructions to clone SDK repo for reference templates
- Framework selection guidance (React, Vanilla JS, Vue, Svelte, etc.)
- Tool visibility configuration (`_meta.ui.visibility`)
- Host styling integration patterns
- Testing workflow using `basic-host`
- Common mistakes to avoid
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Key improvements:
- Add `{@link}` cross-references throughout the codebase to make API
documentation navigable and help AI models understand relationships
- Use `{@link module!Export}` syntax for cross-module references
- Add backticks for inline code formatting (`ResizeObserver`, `App`, etc)
- Fix `PostMessageTransport` examples to show required two-argument form
- Clarify `onteardown` callback return type requirement
- Document React hooks cleanup behavior and Strict Mode considerations
- Add tool visibility example for `registerAppTool`
- Add CSP example for `registerAppResource`
- Fix notification direction documentation in `spec.types.ts`
TypeDoc config migrated from `typedoc.json` to `typedoc.config.mjs` to:
- Support custom `@description` block tag
- Add `intentionallyNotExported` for internal types
- Include `src/server/index.ts` entry point
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add script to generate 300x300 grid-cell.png screenshots for examples
- Add generate-grid-screenshots.spec.ts Playwright test to capture screenshots
- Add npm run generate:screenshots command (Docker-based for Santa compatibility)
- Add sharp dependency for image resizing with centered-fill mode
- Generate grid-cell.png for 14 example servers (only React for basic-server-* series)
The screenshots are 300x300 PNG images cropped with centered fill mode,
suitable for use in a grid layout showcasing the examples.
* docs: add example gallery grid to README
Add a 3-column visual grid showcasing 14 example MCP Apps with:
- Thumbnail screenshots (300x300 grid-cell.png)
- Clickable links to example directories
- Hover tooltips with brief descriptions
Organized by visual impact: 3D/WebGL examples first, then
visualizations, charts, monitoring, and starter templates.
* fix: use aspect-fit instead of crop for screenshots
Change from fit: 'cover' (crops) to fit: 'contain' (letterbox)
so the full app content is visible without cropping.
* feat: improve screenshot generation
- Constrain viewport to 500px width for consistent rendering
- Wait 5 seconds for full app load (tiles, animations, data)
- Screenshot just the inner iframe (no extra whitespace)
- Save both screenshot.png (full-size) and grid-cell.png (300x300 thumbnail)
- Use contain mode to preserve aspect ratio
* fix: use cover mode with top alignment for grid thumbnails
No letterboxing - crops to fill 300x300, aligned to top so headers
and important content are always visible.
* feat: improve screenshot gallery
- Remove integration-server from gallery (it's for E2E testing only)
- Add 30s extra wait for Map and Video servers to load fully
- Improve Three.js default scene: isometric rotation + better lighting
- Update README: Starter Templates row with links to all framework variants
(React, Vue, Svelte, Preact, Solid, Vanilla JS)
- Regenerate all screenshots with updated settings
* fix: improve README gallery layout
- Fix uneven column widths by using minimal separator
- Move Starter Templates to separate 2-cell table with description
- Add screenshot.png to top of each example README (13 examples)
- Shorten tooltips for cleaner table
* revert to document.write again; cesiumjs doesn't like srcdoc, period.
* fix: map-server screenshot now renders properly
- Merged ochafik/post-fixes which reverts sandbox from srcdoc to document.write
(CesiumJS doesn't work with srcdoc iframes)
- Fixed map-server name in screenshot test ('Map Server' not 'CesiumJS Map Server')
- Simplified screenshot generation script (removed complex debugging code)
- Increased map-server wait time to 45s for reliable tile loading
- Regenerated all 13 screenshots
* fix: remove redundant screenshots from READMEs
Remove the simple \ line from READMEs that
already had nicer HTML table screenshots. Keep it only for examples
that didn't have existing screenshots.
* docs: move gallery to Examples section
- Move screenshot gallery from top of README to Examples section
- Remove redundant list of basic examples (now shown visually in gallery)
- Clean up duplicate run instructions
* fix: remove empty header row from starter templates table
* fix: remove empty header rows from both tables
* fix: add prettier-ignore to prevent table header expansion
* feat: use custom screenshot for video-resource-server
- Use better screenshot from PR #253 comment showing actual video playback
- Skip video-resource-server in screenshot generation script
- Add SKIP_SERVERS set for manually maintained screenshots
* fix: revert unrelated map-server name change
* fix: use correct server name 'CesiumJS Map Server' in screenshot test
* basic-host: filter non-ui tools
* basic-host: filter app-only tools, prioritize UI tools
- Filter out tools with visibility: ['app'] from host tool list
- Sort tools to show UI-enabled tools first, then alphabetically
- Add local isToolVisibleToModel helper in basic-host
* chore: release 0.4.0
* chore: add new examples to npm publish workflow
Add map-server, shadertoy-server, and transcript-server to the
publish-examples matrix.
* chore: remove tamper-detection.ts (not meant to be submitted)
Follow-up to PR #234. This file was accidentally included.
* revert to srcdoc
Follow-up to PR #234 per domfarolino's review feedback.
* regen
* regen
* feat: add transcript-server example with live speech transcription
- New example: transcript-server with Web Speech API transcription
- Start/stop recording with timer display
- Live transcript with interim results
- Send button to send transcript as ui/message to host
- Copy button to copy full transcript
- Sent messages marked with divider, greyed out
- Experimental ui/update-model-context for live context updates
- Transparent background, respects host theme
- Enable microphone and clipboard in basic-host sandbox iframes
- Add allow="microphone; clipboard-write" to both outer and inner iframes
- Required for Web Speech API, audio capture, and clipboard access
- Add alert popup in basic-host for ui/message from apps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: format transcript-server files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add e2e test and golden for transcript-server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: use @types/dom-speech-recognition instead of hand-rolled types
Removes ~50 lines of manually defined Web Speech API types in favor of
the community-maintained type package.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: add newlines between transcript chunks when copying to clipboard
* refactor: unify transcript formatting with timestamps
- Add formatEntry() helper that includes [timestamp] prefix
- Add formatEntries() helper that joins entries with newlines
- Use same format for clipboard copy, send message, and model context
- DRY up duplicate code in send button handler
* fix: log model context update failures instead of silently swallowing
* fix: log model context updates for debugging
* refactor: pass permissions through loadSandboxProxy like csp
- Add permissions parameter to loadSandboxProxy in implementation.ts
- Build iframe allow attribute from permissions instead of hardcoding
- Update sandbox.ts to not hardcode allow attribute (set via notification)
- Add microphone + clipboardWrite permissions to transcript-server resource
- Update README to reflect permission configuration via resource _meta.ui
* fix(transcript-server): auto-expand instead of scroll
- Remove min-height: 100vh and overflow-y: auto
- Let content grow naturally with autoResize (default)
- Remove unnecessary scrollTop calls
* test: remove transcript timer mask (no longer dynamic)
* refactor: pass permissions through loadSandboxProxy like csp
* docs: document updateModelContext for transitional speech
* test: update e2e screenshots
* refactor: use app.updateModelContext() instead of raw request
* package-lock.json
* src/generated/schema.json
* test: update e2e screenshots from CI
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(map): add keyboard shortcuts for fullscreen control
- Escape: exit fullscreen when in fullscreen mode
- Ctrl/Cmd+Enter: toggle fullscreen from any mode
* feat(map): persist view state to localStorage
- Save camera position/orientation on moveEnd (debounced 500ms)
- Restore persisted view when no explicit tool input is received
- Key by toolInfo.id (tool invocation ID) - localStorage is per-conversation
- Tool input (explicit bbox) takes precedence over persisted state
* refactor(map): use app.updateModelContext() instead of adhoc request
Provides context about this repository including:
- Common commands for building, testing, and running examples
- SDK entry points and their purposes
- Communication flow between Guest UI, Host, and MCP Server
- Key source files and their responsibilities
- Protocol lifecycle from initialization to teardown
- Type generation workflow
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(basic-host): use SDK types for ModelContext and AppMessage
- Import McpUiUpdateModelContextRequest and McpUiMessageRequest from SDK
- Replace custom ModelContext with McpUiUpdateModelContextRequest['params']
- Replace custom AppMessage with McpUiMessageRequest['params']
- Pass full message params instead of extracting text
- Format messages by extracting text from content blocks in display
* feat(basic-host): handle non-text content blocks in display
- Add formatContentBlock helper for text, image, audio, resource types
- Show placeholders like <image: image/png> for non-text content
- Apply to both messages and model context display
* test: update shadertoy screenshot
* test: update screenshots from CI (linux x64)
Update all example servers to use the new recommended metadata format
for associating UI resources with tools. The old `RESOURCE_URI_META_KEY`
constant is deprecated in favor of the nested `_meta.ui.resourceUri`
structure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces a new MCP App example that renders ShaderToy-compatible GLSL
fragment shaders in real-time using WebGL 2.0 and ShaderToyLite.js.
Features:
- Standard ShaderToy uniforms (`iTime`, `iResolution`, `iMouse`, etc.)
- Multi-pass rendering with buffers A-D for feedback effects
- Shader compilation error overlay
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
PR #184 accidentally removed/reverted educational comments from
`basic-server-react` and `basic-server-vanillajs` that were established
in PR #182. This restores consistency with the other basic-server
examples (Vue, Svelte, Preact, Solid).
Changes:
- Add back the three-comment pattern explaining two-part registration
- Restore full tool description ("as an ISO 8601 string")
- Change vanillajs to return plain text instead of JSON (matching #182)
- Use local `resourceUri` variable instead of top-level constant
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The `mcp-app.ts` UI was calling a `refresh-stats` tool that didn't exist
on the server, causing the example to fail. This was introduced in
PR #131 when the squash merge included the client-side change but lost
the server-side tool registration.
Fixes#200
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: Add CesiumJS map server example
A WebGL-based 3D globe example that demonstrates:
- CesiumJS integration with MCP Apps
- CSP configuration for external tile servers
- worker-src directive usage for tile decoding
Tools:
- geocode: Search places via OpenStreetMap Nominatim
- show-map: Display interactive 3D globe at coordinates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* comment out app.registerTool call for now
* prettier
* fix error handling in server-utils.ts
* refactor: rename cesium-map-server to map-server
- Rename examples/cesium-map-server to examples/map-server
- Update package name to @modelcontextprotocol/server-map
- Add README.md with documentation
* style: add min-height of 400px to map container
* feat(map-server): add fullscreen support, clean UI, sharp rendering
- Add fullscreen toggle button (only shown if host supports it)
- Use autoResize: false and manually send height of 400px
- Hide Cesium UI controls (home, scene mode picker)
- Remove location label (gets stale quickly)
- Use full device pixel ratio for sharp rendering on high-DPI displays
- Resize Cesium viewer when display mode changes
* feat(map-server): add reverse geocoding (logging only) and rounded corners
- Add camera move end listener for reverse geocoding
- Debounce Nominatim API calls (1.5s) to respect rate limits
- Log location name to console on camera move
- Add nominatim.openstreetmap.org to CSP connectDomains
- Add 8px rounded corners to map container
* fix(map-server): fix pixelated rendering on high-DPI displays
- Set canvas imageRendering to 'auto' (CesiumJS defaults to pixelated)
- Set resolutionScale to window.devicePixelRatio for native resolution
* tweaks
* update model context w/ current location
* fix(map-server): improve rendering quality and add visible extent logging
Rendering fixes:
- Remove incorrect resolutionScale = devicePixelRatio which was doubling
the scaling factor on Retina displays (2x2=4x), causing blurriness
- Disable FXAA anti-aliasing which can cause blurriness on high-DPI displays
- Keep useBrowserRecommendedResolution: false to use native devicePixelRatio
Reverse geocoding improvements:
- Add getVisibleExtent() using camera.computeViewRectangle() to get the
visible bounding box (west, south, east, north)
- Add getScaleDimensions() to calculate visible area in km
- Add calculateNominatimZoom() to map visible extent to appropriate
Nominatim zoom level (3=country to 18=building)
- Log visible extent with dimensions on each camera move
- Pass zoom parameter to Nominatim for context-aware geocoding results
- Update model context with visible area dimensions
* fix(map-server): direct positioning + wait for tiles before showing
Changes:
- Remove zoom granularity logic from reverse geocoding (was not working)
- Keep extent logging in both console and model context
- Replace flyTo animation with instant setView positioning
- Add waitForTilesLoaded() to detect when tiles finish loading
- Keep loading spinner visible until:
1. Tool input positions camera to destination
2. Tiles at destination are fully loaded
- Add 5-second fallback timeout if no tool input received
- Add 10-second timeout for tile loading to prevent infinite wait
* feat(map-server): use viewbox search to find visible places
Replace center-point reverse geocoding with Nominatim viewbox search:
- Add searchPlacesInBox() that queries Nominatim with bounded viewbox
- Uses layer=address&featuretype=settlement to find cities/towns
- Returns up to 5 visible places in the current view
- Update model context with 'Visible places: City1, City2, ...'
- Still includes center coordinates and extent dimensions
This provides better context about what's actually visible on the map
rather than just the address at the center point.
* fix(map-server): use CARTO @2x retina tiles for sharp rendering
Root cause: Standard OSM tiles are 256x256 pixels. On Retina displays
(devicePixelRatio=2), these get scaled up 2x, making them look pixelated.
Solution: Switch to CARTO Voyager tiles which provide @2x retina support:
- Detect high-DPI displays using window.devicePixelRatio
- Use 512x512 tiles with @2x suffix on Retina displays
- Use 256x256 tiles on standard displays
- Set tileWidth/tileHeight to match actual tile size
- Use CARTO subdomains (a,b,c,d) for load balancing
- Update CSP to allow CARTO tile domains
CARTO Voyager provides clean OSM-based tiles with free @2x retina support,
no API key required.
* fix(map-server): use OSM tiles and fix reverse geocoding
- Switch back from CARTO to standard OSM tiles (tile.openstreetmap.org)
for better detail and label rendering
- Fix geocoding: use reverse geocode API instead of search API
(search API requires a q= parameter, returns 400 without it)
- Update CSP domains for OSM tile server
* fix(map-server): use *.openstreetmap.org wildcard in CSP
Simplify CSP to use wildcard for both tiles and geocoding:
- connectDomains: *.openstreetmap.org covers tile.* and nominatim.*
- resourceDomains: *.openstreetmap.org for tile images
* feat(map-server): multi-point sampling for visible places
- Sample multiple points in visible extent to discover places
- Adaptive sampling based on extent size:
- < 30km: center only (city zoom)
- 30-100km: center + 2 corners
- > 100km: center + 4 quadrants
- Proper rate limiting for Nominatim (1.1s between requests)
- Shows 'Visible places: City1, City2, ...' in model context
- Fix: move geocode tool registration inside createServer()
* format
* fix(map-server): fix Express listen callback + add e2e tests
- Fix incorrect Express listen() callback signature (err param doesn't exist)
- Add map-server to e2e test suite with SLOW_SERVERS timeout (5s for tiles)
- Improve comment about experimental ui/update-model-context API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, if app.listen() failed (e.g., port already in use), the
server would fail silently. This adds proper error handling to log
the error and exit with a non-zero status code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: Move CSP to HTTP headers + add worker-src, frameDomains, baseUriDomains, permissions
Security improvements:
- CSP is now set via HTTP headers in serve.ts instead of meta tags
(meta tag CSP can be tampered with by same-origin content)
- CSP passed as query param to sandbox.html for header-based enforcement
New CSP/permissions features (borrowed from PR #158):
- frameDomains: control frame-src directive for nested iframes
- baseUriDomains: control base-uri directive
- permissions: camera, microphone, geolocation via iframe allow attribute
WebGL fix:
- Use document.write() instead of srcdoc for inner iframe content
(srcdoc creates opaque origin that breaks WebGL canvas updates)
- Add worker-src directive with blob: support (critical for WebGL apps
like CesiumJS/Three.js that use workers for tile decoding, terrain
processing, image processing)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: Remove permissions support (defer to PR #158)
Keep this PR focused on CSP security fixes only.
Permissions (camera, microphone, geolocation) will be
handled in #158.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* cleanup McpUiResourceCsp type usage / duplicate defs
* fix: guard against CSP injection in domain parameters
Validate CSP domain entries to reject characters that could:
- Break out of CSP directives (semicolons, newlines)
- Inject CSP keywords like 'unsafe-eval' (quotes)
- Inject multiple sources in one entry (spaces)
This prevents injection attacks where malicious domains could
override the security policy.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: ensure lib is built before examples:build
Add preexamples:build hook to automatically run `npm run build` before
`examples:build`. This ensures examples always type-check against the
latest library code, catching breaking changes during development.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add version consistency check for examples
Add scripts/check-versions.mjs to verify that example package.json files
reference the correct version of @modelcontextprotocol/ext-apps.
- Allows "../.." (local dev) or "^{root-version}" as valid dependencies
- Runs in CI before npm install to catch mismatches early
- Available locally via `npm run check:versions`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: regenerate package-lock.json for cross-platform compatibility
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: bump version to 0.3.0
Breaking changes:
- `viewport` replaced with `containerDimensions` in host context
New examples:
- video-resource-server
- sheet-music-server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(video-resource-server): add missing server-utils and fix import path
The video-resource-server was importing from a non-existent
../shared/server-utils.js path. Added the server-utils.ts file
and fixed the import to use ./src/server-utils.js like other examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: update ext-apps version refs to 0.3.0
---------
Co-authored-by: Claude <noreply@anthropic.com>
1. video-resource-server: Add missing server-utils.ts and fix import path
- Was importing from non-existent ../shared/server-utils.js
- Now correctly imports from ./src/server-utils.js
2. basic-host: Be resilient to individual server connection failures
- Use Promise.allSettled instead of Promise.all
- Failed connections are logged as warnings but don't crash the UI
- Only throws if ALL servers fail to connect
3. threejs-server: Add id to canvas for reliable screenshot masking
- Added id="threejs-canvas" to the canvas element
- Updated e2e test masks to use #threejs-canvas and .threejs-container
- Fixes flaky Three.js screenshot comparisons in CI
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: publish example servers to npm
Make example MCP App servers publishable to npm under the
@modelcontextprotocol scope:
- server-basic-react
- server-basic-vanillajs
- server-budget-allocator
- server-cohort-heatmap
- server-customer-segmentation
- server-scenario-modeler
- server-system-monitor
- server-threejs
- server-wiki-explorer
Changes:
- Remove `private: true` from example package.json files
- Add proper npm metadata (description, repository, license, files)
- Update ext-apps dependency from relative to ^0.2.2
- Copy server-utils.ts into each package for standalone use
- Add publish-examples job to npm-publish.yml workflow
The examples will be published automatically when a GitHub release
is created, after the main SDK package is published.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: publish examples with pkg-pr-new for PR testing
* feat: export createServer from example servers
* style: format integration-server/server.ts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update e2e snapshots for basic-server examples
Update golden screenshots for basic-react and basic-vanillajs after
the tool output was simplified from JSON to plain text in #182.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* merge
* refactor: update sheet-music-server and video-resource-server to new publishing pattern
- Move server-utils.ts from src/ to root level
- Add npm publishing metadata (scoped name, repository, license, files)
- Change ext-apps dependency from local to versioned ^0.2.2
- Update server.ts to handle stdio mode inline
* update package-lock
* fix: update package-lock.json for renamed server packages
* feat: add NASA public domain video to video-resource-server
Added 'nasa-earth' option - NASA ISS Earth observations (6.4MB).
Source: archive.org/details/NASA-Ultra-High-Definition (public domain)
* fix: add default videoId for video-resource-server
* fix: use existing bunny-1mb as default video, remove nasa-earth
* refactor: update new framework examples to publishing pattern
Add publishing metadata for:
- @modelcontextprotocol/server-basic-preact
- @modelcontextprotocol/server-basic-solid
- @modelcontextprotocol/server-basic-svelte
- @modelcontextprotocol/server-basic-vue
Changes:
- Move server-utils.ts from src/ to root
- Add npm publishing metadata
- Update ext-apps dep to versioned ^0.2.2
- Add stdio mode support in main()
* fix: add missing @rollup/rollup-win32-arm64-msvc to optionalDependencies
This fixes the Windows ARM64 CI build failure caused by npm's optional
dependency bug. The package.json was missing this platform-specific
rollup binding.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
Provides a concise 4-step overview of the MCP Apps architecture:
tool call → `ui://` resource → sandboxed iframe → bidirectional
communication via notifications.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Reorganize prerequisites to emphasize MCP concepts first
- Add tip linking to official MCP quickstart for newcomers
- Update install command to use published npm package
- Remove zod dependency and `structuredContent` usage
- Use `RESOURCE_MIME_TYPE` constant for consistency
- Add clearer comments explaining tool/resource registration
- Simplify UI to extract time from text content
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add basic-server-vue example
Demonstrates MCP App SDK usage with Vue 3, providing framework
parity alongside the existing React and vanilla JS examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add basic-server-svelte example
Demonstrates MCP App SDK usage with Svelte, providing parity
with the existing basic-server-react and basic-server-vue examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add basic-server-preact example
Demonstrates MCP App SDK usage with Preact, providing another
lightweight framework option alongside the existing React, Vue,
and Svelte examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add basic-server-solid example
Demonstrates MCP App SDK usage with Solid.js, providing another
reactive framework option alongside the existing React, Vue,
Svelte, and Preact examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add framework GitHub links to README
Link each framework name to its official GitHub repo in basic examples
list.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Apply host-provided safe area insets as padding on main containers
across all examples. This ensures content doesn't render under
system UI elements like phone notches.
React apps use `useState` + `onhostcontextchanged` handler with
initial fetch via `getHostContext()` in `useEffect`. Vanilla JS
apps apply padding in the handler and invoke it after `connect()`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The spec (apps.mdx) defines toolInfo.id as optional (id?: RequestId),
but the TypeScript types and generated schemas had it as required.
This allows hosts to pass just the tool definition without needing
a request ID, which is useful when the tool name is the primary
information needed by the widget.
Apply host-provided safe area insets as padding on the main container,
and update `tsconfig.json` to use `moduleResolution: "bundler"` for
correct resolution of `McpUiHostContext` type with Vite.
Also adds `tsc --noEmit` to the build script to catch type errors, and
fixes pre-existing type safety issue with `blob` property access by
using proper type narrowing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(app): add source validation to default PostMessageTransport
App.connect() now passes window.parent as both eventTarget and eventSource,
enabling source validation by default. This ensures apps only accept
messages from their parent window, preventing potential cross-app
message spoofing attacks.
Previously, the default transport only specified the target but not the
source for validation, meaning apps would accept messages from ANY window.
* fix(sandbox): add origin validation for host messages
The sandbox proxy now validates that messages from the parent window
come from the expected host origin (derived from document.referrer).
This prevents malicious pages from sending spoofed messages to the sandbox.
Changes:
- Extract EXPECTED_HOST_ORIGIN from document.referrer
- Validate event.origin against expected origin for parent messages
- Use specific origin instead of '*' when sending to parent
- Reject and log messages from unexpected origins
This addresses the TODO comment that was previously in the code.
* validate messages from app come from same origin as sandbox proxy
* fix(basic-host): be resilient to individual server connection failures
Use Promise.allSettled instead of Promise.all when connecting to
servers, so that a single server failure doesn't crash the entire UI.
Failed connections are logged as warnings but the UI continues with
the servers that connected successfully.
Also fixes video-resource-server missing server-utils.ts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The server was importing from ../shared/server-utils.js which doesn't exist.
Added the standard server-utils.ts file (matching other examples) and updated
the import path to ./src/server-utils.js.
Demonstrates serving binary content (video) via MCP resources using the
base64 blob pattern:
1. Server fetches video from CDN
2. Returns as base64 blob via `videos://{id}` resource template
3. Widget fetches resource and plays in `<video>` element
Includes multiple video sizes for testing (1MB to 150MB).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Demo MCP App showcasing ABC notation rendering and audio playback with:
- Server-side ABC notation validation
- Built-in audio player with play/pause and loop controls
- Sheet music visualization using abcjs library
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove 'server.ts' argument from 'npx tsc --noEmit' command.
When a file is specified directly, TypeScript ignores tsconfig.json,
causing skipLibCheck and other options to not apply. This results in
errors from node_modules (especially zod v4 locale files).
The correct command lets tsc use the project's tsconfig.json settings.
* Centralize example server transport logic in shared utils
Move stdio/HTTP transport handling into `server-utils.ts` so example
servers only need to call `startServer(createServer)`. Switch HTTP
transport to stateless mode and remove legacy SSE endpoints.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make example servers self-contained with local server-utils
Copy `server-utils.ts` into each example's `src/` directory so examples
can be used standalone without the shared directory. Updates imports
from `"../shared/server-utils.js"` to `"./src/server-utils.js"`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add `integration-server` example for E2E testing
Add dedicated `integration-server` example (duplicated from
`basic-server-react`) to preserve E2E testing capabilities while
allowing `basic-server-react` to be simplified as a minimal example.
Changes:
- Add `examples/integration-server` exercising SDK communication APIs
- Update E2E tests to use `integration-server` instead of
`basic-server-react` and `basic-server-vanillajs`
- Use server name labels instead of indices for more robust test
selection
- Remove `basic-react.png` and `basic-vanillajs.png` golden snapshots
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify `basic-server-*` examples
- Clarify comments
- Return plain text from `get-time` tool instead of JSON
- Remove simulated cleanup delay from `onteardown`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Allows running `npm start` as a shorthand for `npm run examples:start`,
providing a conventional entry point to launch the example servers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update `package.json`
* Add TypeScript type checking to example build scripts
Add `tsc --noEmit &&` before `vite build` in all example `package.json`
files to catch type errors during builds. Previously, type errors
in `server.ts` files were not detected because Vite/Bun transpile
without type checking.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make `_meta` optional in `McpUiAppResourceConfig`
The `_meta` and `ui` properties were required, causing TypeScript errors
when calling `registerAppResource()` without providing metadata.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Zod type compatibility in `registerAppTool()`
Use `ZodRawShapeCompat | AnySchema` from SDK's `zod-compat` module
instead of raw `ZodRawShape` to resolve type errors when examples
pass `ZodObject` schemas to `registerAppTool()`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused imports in example apps
- `basic-server-react`: Remove unused `McpUiResourceTeardownResult`
- `customer-segmentation-server`: Remove unused `PostMessageTransport`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI git install test for fork PRs
Use `github.event.pull_request.head.repo.full_name` to correctly
reference the fork repository when testing git installs from PRs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: make generated Zod schemas version-agnostic (v3.25+/v4)
- Change generated schema imports from `zod/v4` to standard `zod`
- Replace v4-only `z.looseObject()` with `z.object().passthrough()`
- Works with both Zod v3.25+ and v4, matching MCP TS SDK approach
- Bump version to 0.2.2
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: regenerate package-lock.json in Docker
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: widen @oven/bun-* version range to improve install compatibility
Lower the minimum version of @oven/bun-* optional dependencies from
^1.3.4 to ^1.2.21. This is the lowest common version available across
all platform-specific packages, improving compatibility for contributors
who may not have access to the latest package versions.
The caret range (^) still allows npm to install newer versions when
available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: bump version to 0.2.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(threejs-server): use server.registerTool for non-UI tool
The learn_threejs tool doesn't have a UI component - it only returns
documentation text. Using registerAppTool without _meta causes a
runtime error when the function tries to access config._meta.ui.
Fixes crash: "Cannot read property 'ui' of undefined"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): add missing registerAppTool and registerAppResource imports
All example servers were using registerAppTool and registerAppResource
but not importing them from @modelcontextprotocol/ext-apps/server,
causing "ReferenceError: registerAppTool is not defined" at runtime.
Affected servers:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- scenario-modeler-server
- system-monitor-server
- threejs-server
- wiki-explorer-server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: make MCP SDK a peer dependency
- Move @modelcontextprotocol/sdk from dependencies to peerDependencies (^1.20.0)
- Add to devDependencies for local development (^1.24.3)
- Externalize SDK from all build entry points (app, app-bridge, react, server)
Benefits:
- Consumers control their SDK version
- Deduplication when consumer already has SDK
- Follows pattern of react/zod peer deps
* fix: require SDK ^1.24.0 for Zod 4 compatibility
Earlier SDK versions have Zod type incompatibilities that cause build errors.
* chore: update examples to SDK ^1.24.0 to match peer dep
* refactor: rename request methods to remove send prefix
Following the MCP TS SDK convention where request methods don't have
the "send" prefix (reserved for notifications):
- App.sendMessage() → App.message()
- App.sendOpenLink() → App.openLink()
- AppBridge.sendResourceTeardown() → AppBridge.resourceTeardown()
The old method names are kept as deprecated aliases for backwards
compatibility.
Updated all examples to use the new method names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test: add tests for deprecated method aliases
- Update existing tests to use new method names (message, openLink)
- Add tests verifying deprecated aliases are same reference as new methods
- Add tests verifying deprecated aliases still work functionally
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename message→sendMessage, resourceTeardown→teardownResource
- App.message() → App.sendMessage() (with deprecated message alias)
- AppBridge.resourceTeardown() → AppBridge.teardownResource() (with deprecated aliases)
This better follows the pattern where methods that send data have 'send' or action verb prefix.
* fix: update examples to use sendMessage instead of message
* fix: update basic-host to use teardownResource
* fix: update threejs-server to use sendMessage
* refactor: remove deprecated app.message alias
The app.message alias was removed since sendMessage is the correct name.
sendResourceTeardown already exists in AppBridge.
* refactor: remove resourceTeardown alias, keep only sendResourceTeardown
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add tool visibility and restructure _meta.ui
- Restructure tool metadata: `_meta["ui/resourceUri"]` → `_meta.ui.resourceUri`
- Add `visibility` array field: ["model"], ["apps"], or ["model", "apps"]
- Default ["model"] preserves standard MCP behavior
- ["apps"] enables widget-only tools hidden from agent
- Add McpUiToolMeta interface for type safety
- Add Design Decision #4 explaining approach vs OpenAI's two-field model
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* address PR review comments
- Change default visibility to ["model", "apps"]
- Add deprecation notice for flat ui/resourceUri format
- Add McpUiToolMeta and McpUiToolVisibility to spec.types.ts
- Improve tools/list and tools/call behavior wording
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: regenerate schemas for McpUiToolMeta types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: tool visibility and nested _meta.ui format
- Rename visibility "apps" → "app" in McpUiToolVisibility type
- Add _meta.ui.resourceUri nested format (deprecate flat format)
- Add getToolUiResourceUri() utility with backward compatibility
- Add visibility demo to system-monitor-server:
- get-system-stats: visibility ["model"] with resourceUri
- refresh-stats: visibility ["app"] (app-only polling)
- Update all example servers to use new _meta.ui format
- Add 11 unit tests for getToolUiResourceUri()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: regenerate schemas with updated dependencies
* feat: export McpUiToolMeta type and add type annotations
- Export McpUiToolMeta and McpUiToolVisibility types from types.ts
- Export corresponding Zod schemas (McpUiToolMetaSchema, McpUiToolVisibilitySchema)
- Add `as McpUiToolMeta` type annotations to all example servers
- Update docs/quickstart.md with proper typing
Ensures type safety for `_meta.ui` tool metadata across the codebase.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add server helpers and optional connect() transport
Add `src/server/` with convenience functions for registering MCP App tools
and resources:
- `registerAppTool(server, name, config, handler)`
- `registerAppResource(server, name, uri, config, callback)`
The `transport` parameter in `App.connect()` is now optional, defaulting to
`PostMessageTransport(window.parent)`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update src/server/index.ts
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* feat: update McpUiAppToolConfig to support both _meta.ui and flat formats
* refactor: simplify server helper imports
* docs: add deprecation notice and improve resourceUri documentation
* feat: add backward compat normalization in registerAppTool
- If _meta.ui.resourceUri is set, also set legacy flat key
- If legacy flat key is set, also set _meta.ui.resourceUri
- Preserves existing visibility when merging
- Does not overwrite if both formats already set
* refactor: avoid mutating config arg in registerAppTool
* style: format with prettier
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* feat: add server helpers and optional connect() transport
Add `src/server/` with convenience functions for registering MCP App tools
and resources:
- `registerAppTool(server, name, config, handler)`
- `registerAppResource(server, name, uri, config, callback)`
The `transport` parameter in `App.connect()` is now optional, defaulting to
`PostMessageTransport(window.parent)`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update src/server/index.ts
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* fix: refine server helper types
- Make RESOURCE_URI_META_KEY required in McpUiAppToolConfig (tools need a UI resource)
- Make _meta optional in McpUiAppResourceConfig (not all resources need custom metadata)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify RESOURCE_URI_META_KEY re-export
* refactor: consolidate export statements
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
The spec used `ui/notifications/sandbox-ready` but the implementation
uses `ui/notifications/sandbox-proxy-ready`. Updated the spec to match.
Also fixed a typo: `ui/sandbox-resource-ready` → `ui/notifications/sandbox-resource-ready`
Fixes#156🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Move react and react-dom from dependencies to peerDependencies with
support for React 17, 18, and 19. This allows consumers to use their
preferred React version instead of forcing v19.
- Added peerDependencies for react/react-dom (^17 || ^18 || ^19)
- Marked react dependencies as optional in peerDependenciesMeta
- Moved react/react-dom to devDependencies for local development
- React is already externalized in the build (build.bun.ts)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: Windows compatibility for npm scripts and cross-platform CI
Fixes#142 - Windows PowerShell install bug
Changes:
- Replace single quotes with escaped double quotes in generate:schemas
(cmd.exe passes single quotes literally, breaking prettier glob matching)
- Simplify prettier scripts: remove redundant --ignore-path flags
(prettier auto-loads .gitignore and .prettierignore by default)
- Remove $(pwd) from prettier scripts (bash-only syntax)
- Add cross-platform CI matrix: Linux x64, Windows x64, macOS ARM64, macOS x64
- Use shell: bash for grep commands in CI (not available in Windows cmd.exe)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: remove bun npm package from all workspaces
The bun npm package tries to install platform-specific binaries which
fails on Windows CI. Since bun is installed via oven-sh/setup-bun action
in CI (and developers install it globally), we don't need the npm package.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: skip examples:build on Windows (uses Unix-only INPUT=... syntax)
The examples use `INPUT=mcp-app.html vite build` which is bash syntax
for setting environment variables. This doesn't work on Windows cmd.exe.
Since examples aren't published to npm, we can skip building them on Windows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: force LF line endings for cross-platform prettier consistency
Git on Windows auto-converts line endings to CRLF, causing prettier to
report formatting issues. Force LF for all text files.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: add test for git-based npm install (cross-platform)
This verifies that `npm install git+https://...` works on all platforms,
which triggers the prepare script and requires bun to be available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: add Linux ARM64 and Windows ARM64 to CI matrix
- Added ubuntu-24.04-arm (Linux ARM64)
- Added windows-11-arm (Windows ARM64)
Note: WSL is not available on GitHub-hosted runners (requires nested
virtualization which isn't supported).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: disable Windows ARM64 (Bun lacks stable ARM64 binaries)
The oven-sh/setup-bun action fails with 404 when trying to download
Bun for Windows ARM64. Re-enable when Bun adds proper support.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: add Windows WSL build test
Uses Vampire/setup-wsl action to install Ubuntu-24.04 in WSL and run
the full build/test cycle. This tests the Linux-in-Windows scenario
that was reported in issue #142.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: fix WSL build - install unzip for Bun installer
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: add setup-bun to publish.yml workflow
The pkg-pr-new publish workflow was missing bun setup, causing
"bun: not found" errors during npm ci (which triggers prepare script).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: enable Windows ARM64 with x64-baseline Bun (via emulation)
Windows ARM64 can run x64 binaries via emulation. Use bun-download-url
to force x64-baseline build since native ARM64 Bun isn't available yet.
Reference: https://github.com/oven-sh/bun/issues/9824🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: remove macOS x64 (macos-13 runner retired)
The macos-13 based runner images are now retired.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add Bun installation instructions per platform
Documents that Bun is required for development, with platform-specific
install commands in collapsible sections.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: note Bun requirement in README examples section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add bun as optional dependency (no global install required)
- Add @oven/bun-* platform packages as optionalDependencies
- Create postinstall script to set up bun binary from optional deps
- Handle Windows ARM64 by downloading x64-baseline for emulation
- Remove setup-bun from CI workflows (npm install handles it now)
- Remove manual Bun install instructions from docs
This alternative approach eliminates the need for global Bun installation
or setup-bun action in CI. npm install automatically installs the
platform-appropriate bun binary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: add debug logging to setup-bun script
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: force stdout flush in setup-bun for CI debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: add more debugging to setup-bun
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove accidental test artifacts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: run setup-bun in prepare script (before build)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: use cross-env for Windows-compatible example builds
- Add cross-env to devDependencies
- Update all example package.json scripts to use cross-env for INPUT and NODE_ENV
- Enable examples:build on Windows in CI (no longer Unix-only)
- Add examples:build to WSL CI job
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove unused imports in setup-bun.mjs
Address github-code-quality bot comments.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use run-bun.mjs wrapper for prepare hook compatibility
During npm install's prepare hook, node_modules/.bin is not in PATH,
so calling 'bun' directly fails. The new run-bun.mjs wrapper finds
bun at node_modules/.bin/bun and runs it with the correct path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: restore setup-bun action for build jobs
- Add setup-bun action to build and test-git-install jobs
- Disable Windows ARM64 (Bun doesn't support this platform)
- Add bun installation to WSL build
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: remove bun devDep, add libc detection for Linux ARM64
- Remove 'bun' from devDependencies (its postinstall fails on Windows ARM64)
- Add libc detection (glibc vs musl) to setup-bun.mjs
- Only try compatible binaries based on detected libc
- Re-enable Windows ARM64 in CI (now uses x64 emulation fallback)
- Remove setup-bun action from CI (no longer needed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: simplify WSL build - let setup-bun.mjs handle bun installation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Files in `src/generated/` are produced by `scripts/generate-schemas.ts`
and should be collapsed by default in PR diffs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Same check as in CI to catch issues before push.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Updated screenshots to reflect UI changes from styles addition.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Prevents iframe from automatically applying a default background color.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove platform-specific browser channel (was using Chrome on macOS,
Chromium in CI) - now uses default Chromium everywhere
- Add Docker-based test scripts for CI-identical local testing:
- `npm run test:e2e:docker` - run tests in Docker
- `npm run test:e2e:docker:update` - regenerate golden screenshots
- Regenerate golden screenshots using Docker/Linux environment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
According to spec, mimeType should be present in both the resource
declaration and the resource content. This adds the missing mimeType
to the registerResource() declaration (third parameter) in all example
servers and the quickstart documentation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ochafik <olivier.chafik@gmail.com>
* Add setter-based handlers for MCP request/notification forwarding
- Add oncalltool, onlistresources, onlistresourcetemplates, onreadresource, onlistprompts setters for Guest UI → Host requests
- Add sendToolListChanged, sendResourceListChanged, sendPromptListChanged methods for Host → Guest UI notifications
- Refactor connect() to use new setters with inlined callbacks
- Update documentation to clarify optional client parameter behavior
- Remove unused forwardRequest/forwardNotification helper methods
- Add comprehensive tests for AppBridge without MCP client (manual handlers)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Potential fix for pull request finding 'Unused variable, import, function or class'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: move prettier to dependencies for git-based installs
* fix: add explicit prettierrc to prevent inheriting parent configs during git-based installs
* Apply suggestions from code review
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* refactor(tests): factor out test values and add missing assertions
Address PR review feedback:
- Factor out test value objects (toolCall, resultContent, etc.) for clearer tests
- Add missing assertions on receivedRequests[0] for list handlers
- Improve test readability by defining expected values at the top of each test
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: improve TypeDoc formatting and use `as const` for notification methods
- Use proper TypeDoc syntax with `@param callback.params` and `@param callback.extra`
- Add {@link ResultType} references for return types
- Replace `as Notification` with `as const` on method literals where possible
(sendResourceListChanged, sendPromptListChanged)
- Simplify sendHostContextChange cast syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(types): use explicit type unions for AppBridge protocol types
- Add AppBridgeRequest, AppBridgeNotification, AppBridgeResult type unions
that explicitly list all valid types for the protocol
- Replace generic SDK Request/Notification/Result with our specific types
- Remove all type casts by using `as const` on method literals
- Add index signature to McpUiHostContext for forward compatibility
This improves type safety and documents exactly what the AppBridge supports.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move AppRequest/AppNotification/AppResult to types.ts
Move protocol type unions from app-bridge.ts to types.ts where they
belong with other type definitions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: regenerate schemas with clean npm install
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* Add Playwright E2E tests with screenshot golden testing
- Add E2E tests for all 8 MCP server examples
- Screenshot golden images for visual regression testing
- CI workflow for running E2E tests
- npm scripts: test:e2e, test:e2e:update, test:e2e:ui
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add default demo code for Three.js example
When no code is provided, show a rotating green cube demo.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add E2E testing documentation to CONTRIBUTING.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update Three.js golden screenshot with 3D cube
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add explicit permissions to CI workflow
Set minimal read-only permissions for security best practices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix test.setTimeout() to be inside describe blocks
Playwright requires setTimeout to be called within a test or describe block.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify E2E tests - remove excessive timeouts
- Remove explicit setTimeout calls (default 30s is sufficient)
- Replace waitForTimeout(5000/6000) with proper waitForAppLoad()
- Wait for inner iframe visibility instead of fixed delays
- Keep only 500ms stabilization for screenshot animations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Auto-generate missing snapshots in CI
Use updateSnapshots: 'missing' in CI to handle cross-platform
screenshot differences (macOS vs Linux).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use platform-agnostic golden screenshots
- Remove -chromium-darwin suffix from snapshot filenames
- Configure snapshotPathTemplate for cross-platform compatibility
- Increase tolerance to 5% for rendering differences between platforms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor tests to use forEach instead of for-of loop
May help with Playwright test discovery issues in CI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use npm ci and list reporter in CI
- Use npm ci for exact package versions
- Use --reporter=list instead of html to avoid potential re-evaluation issues
- Only upload test-results on failure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Exclude e2e tests from bun test
Run bun test only on src/ directory to avoid running Playwright tests
with Bun's test runner.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix prettier formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add interaction tests for basic server apps
Test that clicking buttons in the MCP App triggers the corresponding
host callbacks:
- Send Message → host logs "Message from MCP App"
- Send Log → host logs "Log message from MCP App"
- Open Link → host logs "Open link request"
Tests both React and Vanilla JS implementations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Mask dynamic content in E2E screenshot tests
Add Playwright mask option to handle servers with dynamic/random content:
- basic-react/vanillajs: mask server time display
- system-monitor: mask CPU chart, memory stats, uptime
- cohort-heatmap: mask heatmap grid (random data)
- customer-segmentation: mask scatter chart (random data)
This addresses PR feedback about handling examples with non-deterministic
output. Masking replaces the previous 10% tolerance with proper exclusion
of dynamic elements, allowing tighter 1% tolerance for the rest of the UI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add wiki-explorer to E2E tests with default URL param
- Add default URL "https://en.wikipedia.org/wiki/Model_Context_Protocol" to
wiki-explorer-server's get-first-degree-links tool inputSchema
- Update basic-host to automatically populate input field with tool defaults
extracted from inputSchema.properties
- Add wiki-explorer-server to E2E test suite with dynamic masking for the
force-directed graph
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use .default() for threejs tool schema instead of .optional()
This exposes the default code and height values in the JSON Schema,
allowing basic-host to auto-populate the input field with defaults.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Enable parallel E2E tests with timeouts and canvas masking
- Enable fullyParallel with 4 workers locally (2 in CI)
- Add 30s timeout per test
- Mask threejs canvas for stable screenshots
- Update snapshots to reflect default values in input fields
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add pre-commit check for private registry URLs in package-lock.json
Same check as in CI to catch issues before push.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Increase screenshot diff tolerance to 6% for cross-platform rendering
Font rendering differs between macOS and Linux, causing ~5% pixel differences.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert pre-commit artifactory check (moved to separate PR #133)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Format threejs server.ts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update CONTRIBUTING.md to reflect platform-agnostic screenshots
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(e2e): use factory pattern for MCP servers to support parallel connections
McpServer only supports one transport at a time. When multiple browser
contexts connected in parallel, calling server.connect(transport) for
each session overwrote the previous transport's callbacks, causing
connection corruption and test timeouts.
Changes:
- server-utils.ts: accept factory function instead of single instance
- All 9 example servers: wrap server creation in createServer() function
- playwright.config.ts: re-enable parallel execution (4 workers)
- servers.spec.ts: add toBeEnabled wait for server connection
- Update screenshot baselines for basic-vanillajs and cohort-heatmap
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Changed from min-width: 425px to:
- width: 100%
- max-width: 425px
- box-sizing: border-box
This allows the apps to shrink on narrow viewports (like mobile
or when embedded in native WebViews) instead of causing horizontal
scrolling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat(examples): add SSE transport support to all example servers
Add backwards-compatible SSE transport support alongside the current
Streamable HTTP transport. Each server now exposes:
- /mcp (GET, POST, DELETE) - Streamable HTTP transport (current spec)
- /sse (GET) - Legacy SSE transport stream endpoint
- /messages (POST) - Legacy SSE transport message endpoint
This enables older clients using the deprecated HTTP+SSE protocol
(version 2024-11-05) to connect to the example servers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(examples): migrate 6 servers to use shared server utility
Refactored the following example servers to use the shared startServer utility from examples/shared/server-utils.ts:
- threejs-server
- system-monitor-server
- cohort-heatmap-server
- budget-allocator-server
- customer-segmentation-server
- scenario-modeler-server
Changes for each server:
- Removed direct imports: SSEServerTransport, StdioServerTransport, StreamableHTTPServerTransport, cors, express
- Removed PORT constant (now handled by shared utility)
- Added import for startServer from ../shared/server-utils.js
- Replaced entire async main() function with single startServer() call
- Preserved all business logic (tool registration, resource registration, helper functions)
This reduces code duplication by ~700 lines and ensures consistent transport handling across all example servers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(examples): migrate remaining servers to shared utility
Complete the migration of example servers to use the shared
server-utils.ts module for transport setup:
- basic-server-react
- basic-server-vanillajs
- wiki-explorer-server
- Add shared/server-utils.ts
This centralizes all transport handling (stdio, Streamable HTTP, SSE)
in one place, reducing code duplication across ~500 lines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): improve server-utils with stateful sessions and security
Address critical issues in the shared server utility:
1. Stateful sessions: Use sessionIdGenerator + onsessioninitialized
to persist StreamableHTTPServerTransport across requests
2. Unified session store: Single Map for both transport types with
proper type discrimination
3. Error handling: Try/catch on all endpoints with JSON-RPC errors
4. DNS rebinding protection: Use SDK's createMcpExpressApp helper
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): allow connections from any host for development
Use host: "0.0.0.0" in createMcpExpressApp to disable DNS rebinding
protection, allowing connections from Android emulators and other
devices on the network. Also re-add CORS middleware.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(examples): use getPort() helper for transport selection
- Add getPort() helper that returns undefined for --stdio, else port
- Caller now explicitly passes port to startServer()
- port: undefined → stdio mode, port: number → HTTP mode
This makes the transport selection explicit at the call site.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): startServer returns promise that resolves/rejects on listen
Use httpServer.on('listening') and httpServer.on('error') events
to properly resolve or reject the returned promise.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): add error handling to startServer calls
Exit with code 1 if server fails to start (e.g., port in use).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(examples): update server files to use main() pattern with stdio support
Update all example server files to use the new main() pattern that supports both HTTP and stdio transports:
- Add StdioServerTransport import
- Remove getPort import (replaced with direct env var parsing)
- Replace direct startServer call with async main() function
- Add --stdio flag detection to choose between transports
- Use PORT env var with default 3001 for HTTP mode
Updated servers:
- basic-server-vanillajs
- wiki-explorer-server
- threejs-server
- system-monitor-server
- cohort-heatmap-server
- budget-allocator-server
- customer-segmentation-server
- scenario-modeler-server
Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): use correct default ports matching run-all.ts
Each server now defaults to its assigned port from run-all.ts:
- basic-server-react: 3101
- basic-server-vanillajs: 3102
- budget-allocator-server: 3103
- cohort-heatmap-server: 3104
- customer-segmentation-server: 3105
- scenario-modeler-server: 3106
- system-monitor-server: 3107
- threejs-server: 3108
- wiki-explorer-server: 3109
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(examples): expose mcp-session-id header for CORS requests
Without exposedHeaders in the CORS config, browsers block JavaScript
from reading the mcp-session-id response header. This caused the SDK's
StreamableHTTPClientTransport to never capture the session ID, breaking
all subsequent requests with "Bad request: not initialized" errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* pretty:fix
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: prettify generated files on the fly
- Add prettier --write to generate:schemas script to format generated
files immediately after generation
- Remove redundant prettier:fix from CI schema verification step
(now that generation includes formatting)
This fixes the release CI failure where generated files weren't
formatted. Previously, PR CI masked this by running prettier:fix
before the final prettier check, while release CI ran the check
directly on freshly-generated (unformatted) files.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* format
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: store and expose hostContext in App class
Fixes#129 - The App class now stores the initial hostContext received
during initialization and exposes it via getHostContext().
Changes:
- Add _hostContext private property to App class
- Store result.hostContext in connect() method
- Add getHostContext() getter method (parallel to getHostCapabilities())
- Update onhostcontextchanged setter to merge partial updates into
stored context
- Add default notification handler to update context even when
user hasn't set onhostcontextchanged
This enables apps to:
- Access toolInfo immediately after connection (only in initial context)
- Render with correct theme/locale without waiting for first notification
- Synchronously access viewport, timezone, and other initial state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use sendHostContextChange for partial update accumulation test
Address PR review feedback: The test was using setHostContext with full
context spreads which didn't clearly demonstrate partial update
accumulation. Now uses sendHostContextChange directly to send truly
partial updates (only theme, then only viewport), making it clear
the App correctly merges multiple partial notifications.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement ui/notifications/tool-cancelled throughout SDK
Wire the tool-cancelled notification that hosts MUST send when tool
execution is cancelled (user action, timeout, classifier intervention, etc.):
- Add McpUiToolCancelledNotification type in spec.types.ts
- Generate Zod schema via ts-to-zod
- Add AppBridge.sendToolCancelled() method for hosts
- Add App.ontoolcancelled setter for guest UIs
- Export type and schema from types.ts
- Add tests for notification with/without reason
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add App.onteardown handler for graceful shutdown
Wire the guest UI side of ui/resource-teardown request:
- Add onteardown setter in App class for guest UIs to handle teardown
- Allow ui/resource-teardown in assertRequestHandlerCapability
- Import McpUiResourceTeardownRequest/Result types
- Add tests for sync and async cleanup handlers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(example): wire tool-cancelled and teardown in basic-host
Update basic-host example to properly use the new notifications:
- Send sendToolCancelled when tool call promise rejects
- Send sendResourceTeardown when React component unmounts
- Store appBridge ref for cleanup access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(example): implement graceful teardown per spec recommendation
Address review feedback: the host now waits for guest's teardown
response before unmounting, following the spec: "Host SHOULD wait
for a response before tearing down the resource (to prevent data loss)."
Changes:
- Add close button to tool call panels
- Two-phase unmount: isDestroying state dims panel and disables events
- sendResourceTeardown() completes before component removal
- Non-app tool results close immediately
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(examples): add onteardown handlers to demo apps
Demonstrate graceful shutdown with simulated 500ms cleanup work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Replace custom spawn-based orchestration with concurrently's API.
This fixes zombie processes on Ctrl+C - concurrently uses tree-kill
internally to properly terminate entire process trees.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Interactive Wikipedia link graph visualization using force-graph with:
- Force-directed graph layout for exploring page connections
- Node expansion to reveal first-degree Wikipedia links
- Visual state tracking (blue=default, green=expanded, red=error)
- Direct browser access to Wikipedia pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Replace `force_orphan` with `keep_files` in `peaceiris/actions-gh-pages`
to prevent wiping manually-added files (like screenshots) when deploying
generated documentation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add ts-to-zod schema generation from spec.types.ts
Add automated Zod schema generation using ts-to-zod:
- New npm script `generate:schemas` to regenerate schemas from spec.types.ts
- Post-processing script for Zod v4 compatibility and MCP SDK type imports
- src/spec.types.ts: Pure TypeScript interface definitions (source of truth)
- src/schemas.generated.ts: Auto-generated Zod schemas
- src/schemas.ts: Re-exports with PascalCase naming for backwards compatibility
The generated schemas provide a simpler alternative to the hand-written schemas
in types.ts, useful for lighter validation or reference implementations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: types.ts now re-exports from spec.types.ts and schemas.generated.ts
- types.ts imports and re-exports types from spec.types.ts (source of truth)
- types.ts imports and re-exports schemas from schemas.generated.ts
- Added compile-time VerifySchemaMatches checks to flag mismatches between
interfaces and generated schemas
- Removed redundant schemas.ts (types.ts now handles all re-exports)
- Applied prettier formatting to schemas.generated.ts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: use ts-to-zod as library with integration tests
Replace CLI-based schema generation with library API:
- scripts/generate-schemas.ts: New script using ts-to-zod library API
- Detailed documentation of why each post-processing step is needed
- Generates integration tests via getIntegrationTestFile()
- Better error reporting and control
- src/schemas.generated.test.ts: Auto-generated integration tests
- Compile-time verification that schemas match TypeScript types
- Uses expectType<> pattern for bidirectional type checking
- Removed scripts/postprocess-schemas.ts (merged into generate-schemas.ts)
- Fixed McpUiResourceTeardownResult to include index signature for MCP SDK
Protocol compatibility (detected by the new integration tests)
Post-processing rationale (documented in generate-schemas.ts):
1. zod → zod/v4: Explicit v4 subpath for version clarity
2. z.any() → MCP SDK schemas: External types need real validation
3. z.record().and() → z.looseObject(): Index signatures need Zod v4 idiom
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: regenerate schemas during build + CI check
- build script now runs generate:schemas before bun build
- CI workflow verifies generated schemas are up-to-date
(fails if spec.types.ts changed but schemas weren't regenerated)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: generate PascalCase schemas, move verification to tests
- Add getSchemaName option to generate PascalCase schema names directly
(McpUiOpenLinkRequestSchema instead of mcpUiOpenLinkRequestSchema)
- Remove redundant compile-time verification from types.ts
(already handled by generated test file's bidirectional expectType checks)
- Simplify types.ts from 235 to 57 lines (just re-exports)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add missing csp field, simplify schema generation
- Add missing csp field to McpUiSandboxResourceReadyNotification (regression from merge)
- Simplify generate-schemas.ts - remove complex .describe() conversion
- Document that ts-to-zod supports @description JSDoc tags for .describe()
- Improve z.looseObject() replacement with brace-counting for nested objects
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add @description JSDoc tags for .describe() on schemas
Added @description tags to all interfaces and fields in spec.types.ts.
ts-to-zod now generates .describe() calls on all schema fields, making
descriptions available at runtime for introspection and JSON Schema generation.
Example:
/** @description URL to open in the host's browser */
url: string;
Generates:
url: z.string().describe("URL to open in the host's browser")
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: generate JSON Schema from Zod schemas
Added JSON Schema generation to the schema generation pipeline:
- Uses Zod v4's built-in toJSONSchema() to convert schemas
- Generates src/schema.json with all message types in $defs
- Includes descriptions from @description JSDoc tags
- Added export "./schema.json" to package.json
- Updated CI to verify schema.json is up-to-date
Use cases:
- Language-agnostic validation (Python, Go, Rust)
- Documentation generation (Redoc, Swagger UI)
- Code generation (quicktype)
- IDE autocomplete for JSON/YAML
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename schema.json to schema.generated.json
Renamed to match other generated files naming convention.
Export path unchanged: ./schema.json -> ./dist/src/schema.generated.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename schemas.generated.* to schema.generated.* (singular)
Consistent singular naming for all generated files:
- schema.generated.ts
- schema.generated.test.ts
- schema.generated.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename schemas.generated.* to schema.generated.* (singular)
Consistent singular naming for all generated files:
- schema.generated.ts
- schema.generated.test.ts
- schema.generated.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move generated files to src/generated/ with simpler names
Moved:
- src/schema.generated.ts → src/generated/schema.ts
- src/schema.generated.test.ts → src/generated/schema.test.ts
- src/schema.generated.json → src/generated/schema.json
Benefits:
- Cleaner organization (generated files isolated)
- Simpler names without .generated suffix
- Easier to gitignore if desired later
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add missing type exports for backwards compatibility
Added missing exports that existed in main:
- McpUiTheme, McpUiThemeSchema
- McpUiDisplayMode, McpUiDisplayModeSchema
- McpUiResourceCsp, McpUiResourceCspSchema
- McpUiResourceMeta, McpUiResourceMetaSchema
Also fixed theme/displayMode to match original values:
- theme: 'light' | 'dark' (removed 'system')
- displayMode: 'inline' | 'fullscreen' | 'pip' (removed 'carousel')
Verified: all 47 exports from main are preserved with compatible types.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: stage generated files after prettier in pre-commit hook
The pre-commit hook runs build (which generates schemas) then prettier
(which formats them). The formatted generated files weren't being staged,
causing CI to fail with 'Generated schemas are out of date'.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update generated files
* fix: run prettier after regenerating schemas in CI verification
CI was comparing unformatted generated output against formatted committed
files, causing false 'out of date' failures.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
This adds the media-src directive to the Content Security Policy to enable
MCP Apps to play video and audio content.
Changes:
- Add media-src to the restrictive default CSP in the spec
- Add media-src to the CSP construction from metadata in the spec
- Update resourceDomains documentation in spec to include media
- Update resourceDomains JSDoc in src/types.ts to include media-src
Fixesmodelcontextprotocol/ext-apps#106
- Fix RESOURCE_MIME_TYPE to match spec: "text/html;profile=mcp-app"
- Add CSP field to McpUiSandboxResourceReadyNotification type and schema
- Update basic-host to extract and pass CSP metadata from resources
- Update sandbox.ts to dynamically inject CSP meta tag into inner iframe
- Remove static CSP from sandbox.html (now dynamic based on resource metadata)
- Update QR server to use custom read_resource handler for _meta injection
- Update requirements.txt to require mcp[cli]>=1.23.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* release: fix NPM_SECRET reference
* release: 0.0.5
* Add repository field to package.json
Required for npm provenance verification to link the package
to the GitHub repository.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* release: 0.0.6
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add QR code Python MCP server example
A minimal Python MCP server that generates customizable QR codes with an
interactive widget UI. Features:
- Generate QR codes from any text or URL
- Customizable colors, size, border, and error correction
- Interactive widget using MCP Apps SDK protocol
- Supports both HTTP (for web clients) and stdio (for Claude Desktop)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Potential fix for code scanning alert no. 10: Client-side cross-site scripting
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* Add semicolon
* Bind to 0.0.0.0 for Docker compatibility
* Use SDK from unpkg + add CSP resourceDomains for external scripts
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Automatically publish documentation when a new release is created,
ensuring docs stay in sync with published package versions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Restructure threejs-server README to match marketing examples
- Update title format to "Example: Three.js App"
- Constrain screenshot width using table layout
- Add Features section with key capabilities
- Reorganize Running section with Tool Input subsection
- Add Architecture section describing server and app components
- Remove implementation-focused sections (WidgetProps, Creating a New Widget)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add bun as devDependency to example packages
npm install now automatically installs bun, eliminating the need for
separate installation instructions. Removed manual bun install step
from customer-segmentation-server README.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add start:http and start:stdio npm scripts to examples
For the 6 examples supporting --stdio transport, add explicit scripts:
- serve:http / serve:stdio for running the server
- start:http / start:stdio for build + serve
- start now aliases to start:http for backward compatibility
Update READMEs to document both transport options.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Default basic-host SERVERS to localhost:3001/mcp
- Change threejs-server default port from 3109 to 3001 for consistency
- Add default SERVERS value in basic-host serve.ts
- Document SERVERS env var in basic-host README
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve Examples section in README
Frame the three basic examples as foundational starting points and
add a link to the examples directory for additional demo apps.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Extract server dropdown into ServerSelect component and simplify
ToolCallInfoPanel to read server name from toolCallInfo.serverInfo.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace hardcoded port assignments and npm scripts with run-all.ts that:
- Auto-discovers example directories and assigns ports
- Passes server URLs to basic-host via SERVERS env var
- Supports start, dev, and build commands
Update basic-host to fetch server list from /api/servers endpoint
and display server names from MCP server metadata.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 16:34:52 -06:00
473 changed files with 82827 additions and 6949 deletions
- `@modelcontextprotocol/ext-apps/app-bridge` - SDK for hosts (`AppBridge` class)
- `@modelcontextprotocol/ext-apps/server` - Server helpers (`registerAppTool`, `registerAppResource`)
### Key Source Files
- `src/app.ts` - `App` subclasses `Protocol` from `@modelcontextprotocol/client`, handles View initialization, tool calls, and messaging
- `src/app-bridge.ts` - `AppBridge` subclasses `Protocol` from `@modelcontextprotocol/client` for the iframe channel and proxies through a separate outer `Client`
- `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata
- `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build)
- `src/message-transport.ts` - `PostMessageTransport` for iframe communication
View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate MCP Client--> MCP Server
```
1. Host creates iframe with view HTML
2. View creates `App` instance and calls `connect()` with `PostMessageTransport`
3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps capabilities and context
4. Host sends `sendToolInput()` with tool arguments after initialization
5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
6. Host sends `sendToolResult()` when tool execution completes
7. Host calls `teardownResource()` before unmounting iframe
## Documentation
JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `app.ts` → `app.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks; region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`). For whole-file inclusion (any file type), omit the `#regionName`. Run `npm run sync:snippets` to sync.
Standalone docs in `docs/` (listed in `typedoc.config.mjs``projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same pattern.
## Full Examples
Uses npm workspaces. Full examples in `examples/` are separate packages:
- `basic-server-*` - Starter templates (vanillajs, react, vue, svelte, preact, solid). Use these as the basis for new examples.
- `basic-host` - Reference host implementation
- Other examples showcase specific features (charts, 3D, video, etc.)
## Claude Code Plugin
The `plugins/mcp-apps/` directory contains a Claude Code plugin distributed via the plugin marketplace. It provides the following Claude Code skills files:
- `plugins/mcp-apps/skills/create-mcp-app/SKILL.md` — for creating an MCP App
- `plugins/mcp-apps/skills/migrate-oai-app/SKILL.md` — for migrating an app from the OpenAI Apps SDK to the MCP Apps SDK
@ -25,6 +25,15 @@ We welcome contributions to the MCP Apps SDK! This document outlines the process
- Update documentation as needed
- Keep changes focused and atomic
- Provide a clear description of changes
- **Keep "Allow edits by maintainers" checked** when opening your PR — this lets maintainers rebase your branch and lets the [`/update-snapshots` workflow](#updating-snapshots-in-ci) push updated screenshots to it
## Examples Policy
The examples in `examples/` are maintained by the core team. **We do not accept pull requests that add new example servers from outside contributors.** Each example carries an ongoing maintenance cost — keeping it building, tested, and up to date with SDK changes — so we are deliberate about what we add.
If you have an idea for an example that would provide significant educational value, please [open an issue](https://github.com/modelcontextprotocol/ext-apps/issues) describing what the example would demonstrate and why it would be useful, and maintainers will evaluate the request.
PRs that add new examples without a prior approved issue may be closed.
## Running Examples
@ -40,9 +49,460 @@ Or build and run examples:
npm run examples:start
```
### With MCP Clients
To use these examples with MCP clients that support the stdio transport (such as Claude Desktop or VS Code), add this MCP server configuration to your client's settings:
<details>
<summary>MCP client configuration for all examples (using stdio)</summary>
> The `qr` server requires cloning the repository first. See [qr-server README](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) for details.
### Local Development
To test local modifications with MCP clients, first clone and install the repository:
Then configure your MCP client to build and run the local server. Replace `~/src/ext-apps` with your actual clone path.
Most example servers have a `start:stdio` script that builds and launches in stdio mode:
<details>
<summary>MCP client configuration for local development (all examples)</summary>
```json
{
"mcpServers": {
"basic-react": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/basic-server-react && npm --silent run start:stdio"
]
},
"basic-vanillajs": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/basic-server-vanillajs && npm --silent run start:stdio"
]
},
"basic-vue": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/basic-server-vue && npm --silent run start:stdio"
]
},
"basic-svelte": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/basic-server-svelte && npm --silent run start:stdio"
]
},
"basic-preact": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/basic-server-preact && npm --silent run start:stdio"
]
},
"basic-solid": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/basic-server-solid && npm --silent run start:stdio"
]
},
"budget-allocator": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/budget-allocator-server && npm --silent run start:stdio"
]
},
"cohort-heatmap": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/cohort-heatmap-server && npm --silent run start:stdio"
]
},
"customer-segmentation": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/customer-segmentation-server && npm --silent run start:stdio"
]
},
"map": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/map-server && npm --silent run start:stdio"
]
},
"pdf": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/pdf-server && npm --silent run start:stdio"
]
},
"scenario-modeler": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/scenario-modeler-server && npm --silent run start:stdio"
]
},
"shadertoy": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/shadertoy-server && npm --silent run start:stdio"
]
},
"sheet-music": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/sheet-music-server && npm --silent run start:stdio"
]
},
"system-monitor": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/system-monitor-server && npm --silent run start:stdio"
]
},
"threejs": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/threejs-server && npm --silent run start:stdio"
]
},
"transcript": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/transcript-server && npm --silent run start:stdio"
]
},
"video-resource": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/video-resource-server && npm --silent run start:stdio"
]
},
"wiki-explorer": {
"command": "bash",
"args": [
"-c",
"cd ~/src/ext-apps/examples/wiki-explorer-server && npm --silent run start:stdio"
]
},
"qr": {
"command": "bash",
"args": [
"-c",
"uv run ~/src/ext-apps/examples/qr-server/server.py --stdio"
]
},
"say": {
"command": "bash",
"args": [
"-c",
"uv run --index https://pypi.org/simple ~/src/ext-apps/examples/say-server/server.py --stdio"
]
}
}
}
```
</details>
This configuration rebuilds each server on launch, ensuring your local changes are picked up.
## Testing
### Unit Tests
Run unit tests with Bun:
```bash
npm test
```
### E2E Tests
E2E tests use Playwright to verify all example servers work correctly with screenshot comparisons.
```bash
# Run all E2E tests
npm run test:e2e
# Run a specific server's tests
npm run test:e2e -- --grep "Budget Allocator"
# Run tests in interactive UI mode
npm run test:e2e:ui
```
### Updating Golden Screenshots
When UI changes are intentional, update the golden screenshots:
```bash
# Update all screenshots
npm run test:e2e:update
# Update screenshots for a specific server
npm run test:e2e:update -- --grep "Three.js"
```
**Note**: Golden screenshots are platform-agnostic. Tests use canvas masking and tolerance thresholds to handle minor cross-platform rendering differences.
#### Updating Snapshots in CI
If E2E tests fail in CI due to screenshot mismatches, you can update snapshots directly from your PR:
1. Comment `/update-snapshots` on the PR
2. The workflow will update snapshots and push to your branch
3. A comment will confirm when complete
Alternatively, use the [workflow dispatch](https://github.com/modelcontextprotocol/ext-apps/actions/workflows/update-snapshots.yml) to manually trigger updates for any branch.
## Code of Conduct
This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please review it before contributing.
This project follows our [Code of Conduct](https://github.com/modelcontextprotocol/.github/blob/main/CODE_OF_CONDUCT.md). Please review it before contributing.
This repository uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) with OIDC - no secrets required.
Before publishing releases, ensure the following are configured:
1. **NPM_TOKEN secret**: Add an npm automation token to the repository secrets
- Go to Settings <20> Secrets and variables <20> Actions
- Create a new secret named `NPM_TOKEN`
- Value: an npm automation token with publish permissions for `@modelcontextprotocol/ext-apps`
1. **Trusted publisher on npm**: Configure the package to trust this GitHub repository
- Go to https://www.npmjs.com/package/@modelcontextprotocol/ext-apps/access
- Under "Trusted Publishers", click "Add trusted publisher"
- Select "GitHub Actions"
- Repository: `modelcontextprotocol/ext-apps`
- Workflow filename: `npm-publish.yml`
- Environment: `Release` (optional, for additional protection)
2. **`release` environment** (optional): Create a protected environment for additional safeguards
- Go to Settings <20> Environments <20> New environment
- Name it `release`
2. **`Release` environment** (optional): Create a protected environment for additional safeguards
- Go to Settings > Environments > New environment
- Name it `Release`
- Add required reviewers or other protection rules as needed
### Publishing a Release
Releases are published automatically via GitHub Actions when a GitHub Release is created.
#### Steps to publish:
1. **Update the version** in `package.json`:
1. **Bump the version** across the root and all workspace packages:
```bash
# For a regular release
npm version patch # or minor, or major
# For a beta release
npm version prerelease --preid=beta
npm run bump -- minor # or: patch | major | prerelease --preid=beta | 1.7.0
```
2. **Commit the version bump** (if not done by `npm version`):
Commit and open a PR with a grouped changelog in the body (see prior `chore: bump …` PRs for the format).
```bash
git add package.json
git commit -m "Bump version to X.Y.Z"
git push origin main
```
2. **Merge the PR**, then [draft a GitHub Release](https://github.com/modelcontextprotocol/ext-apps/releases/new) — create a `vX.Y.Z` tag on `main`, paste the changelog, publish.
3. **Create a GitHub Release**:
- Go to [Releases](https://github.com/modelcontextprotocol/ext-apps/releases)
- Click "Draft a new release"
- Create a new tag matching the version (e.g., `v0.1.0`)
- Set the target branch (usually `main`)
- Write release notes describing the changes
- Click "Publish release"
4. **Monitor the workflow**:
- The [npm-publish workflow](https://github.com/modelcontextprotocol/ext-apps/actions/workflows/npm-publish.yml) will trigger automatically
- It runs build and test jobs before publishing
- On success, the package is published to npm with provenance
3. **Approve the deployment** — publishing the Release triggers the [npm-publish workflow](https://github.com/modelcontextprotocol/ext-apps/actions/workflows/npm-publish.yml); all publish jobs wait together for a single "Review deployments" approval.
#### npm Tags
@ -131,7 +573,7 @@ To release a patch for an older version:
### Testing Pre-releases
Every commit and PR automatically publishes a preview package via [pkg-pr-new](https://github.com/pkg-pr-new/pkg-pr-new). Check the PR comments or workflow logs for the install command.
Every commit and PR automatically publishes a preview package via [pkg-pr-new](https://github.com/stackblitz-labs/pkg.pr.new). Check the PR comments or workflow logs for the install command.
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
No rights beyond those granted by the applicable original license are conveyed for such contributions.
---
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on behalf
of the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
This repo contains the SDK and [specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) for MCP Apps Extension ([SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)).
<br><em>Excalidraw built with MCP Apps, running in Claude</em>
</p>
MCP Apps are a proposed standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients / chatbots.
## Table of Contents
## Overview
- [Build with Agent Skills](#build-with-agent-skills)
- [Supported Clients](#supported-clients)
- [Why MCP Apps?](#why-mcp-apps)
- [How It Works](#how-it-works)
- [Getting Started](#getting-started)
- [Using the SDK](#using-the-sdk)
- [Examples](#examples)
- [Specification](#specification)
- [Resources](#resources)
- [Contributing](#contributing)
This SDK serves two audiences:
## Build with Agent Skills
### App Developers
The fastest way to build an MCP App is to let your AI coding agent do it. This
repo ships four [Agent Skills](https://agentskills.io/) — install them once,
then just ask:
Build interactive UIs that run inside MCP-enabled chat clients.
| [`create-mcp-app`](./plugins/mcp-apps/skills/create-mcp-app/SKILL.md) | Scaffolds a new MCP App with an interactive UI from scratch | _"Create an MCP App"_ |
| [`migrate-oai-app`](./plugins/mcp-apps/skills/migrate-oai-app/SKILL.md) | Converts an existing OpenAI App to use MCP Apps | _"Migrate from OpenAI Apps SDK"_ |
| [`add-app-to-server`](./plugins/mcp-apps/skills/add-app-to-server/SKILL.md) | Adds interactive UI to an existing MCP server's tools | _"Add UI to my MCP server"_ |
| [`convert-web-app`](./plugins/mcp-apps/skills/convert-web-app/SKILL.md) | Turns an existing web app into a hybrid web + MCP App | _"Add MCP App support to my web app"_ |
- **SDK for Apps**: `@modelcontextprotocol/ext-apps` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/app.html)
**Claude Code** — install via the plugin marketplace:
Embed and communicate with MCP Apps in your chat application.
- **SDK for Hosts**: `@modelcontextprotocol/ext-apps/app-bridge` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html)
There's no _supported_ host implementation in this repo (beyond the [examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example).
We have [contributed a tentative implementation](https://github.com/MCP-UI-Org/mcp-ui/pull/147) of hosting / iframing / sandboxing logic to the [MCP-UI](https://github.com/idosal/mcp-ui) repository, and expect OSS clients may use it, while other clients might roll their own hosting logic.
## Installation
This repo is in flux and isn't published to npm yet. When it is, it will use the `@modelcontextprotocol/ext-apps` package.
In the meantime you can depend on the SDK library in a Node.js project by installing it with its git URL:
| `@modelcontextprotocol/ext-apps/app-bridge` | Embed and communicate with Views in your chat client | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/app-bridge.html) |
| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/server-helpers.html) |
There's no _supported_ host implementation in this repo (beyond the
| [](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) | The same app built with different frameworks — pick your favorite!<br><br>[React](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) · [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) · [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) · [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) · [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) · [Vanilla JS](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) |
> The build tools (`esbuild`, `tsx`, `typescript`) are in `dependencies` rather than `devDependencies`. This is intentional: it allows the `prepare` script to run when the package is installed from git, since npm doesn't install devDependencies for git dependencies.
>
> Once the package is published to npm with pre-built `dist/`, these can be moved back to `devDependencies`.
For example, to add the map server: `@modelcontextprotocol/server-map`. The
Python examples (`qr-server`, `say-server`) use `uv run` instead — see their
READMEs for details.
## Examples
#### Local Development
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — Example MCP server with tools that return UI Apps (React)
- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — Example MCP server with tools that return UI Apps (vanilla JS)
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — Bare-bones example of hosting MCP Apps
To test local modifications with an MCP client, clone the repo, install, then
- **`viewport` replaced with `containerDimensions`** — The `viewport` property in host context has been replaced with `containerDimensions`, which provides a clearer distinction between fixed dimensions and maximum constraints. The new type allows specifying either `height` or `maxHeight`, and either `width` or `maxWidth` by @martinalong in [#153](https://github.com/modelcontextprotocol/ext-apps/pull/153)
### New Examples
- **Video resource server** — Demonstrates video resource handling with proper mimeType declarations by @antonpk1 in [#175](https://github.com/modelcontextprotocol/ext-apps/pull/175)
- **Sheet music server** — Interactive sheet music notation example by @jonathanhefner in [#196](https://github.com/modelcontextprotocol/ext-apps/pull/196)
### Developer Experience
- **`npm start` alias** — Added `npm start` as alias for `npm run examples:start` by @jonathanhefner in [#183](https://github.com/modelcontextprotocol/ext-apps/pull/183)
- **Examples cleanup** — Improved consistency across example servers by @jonathanhefner in [#182](https://github.com/modelcontextprotocol/ext-apps/pull/182)
- **Documentation fixes** — Fixed tsc command in docs to use tsconfig.json by @blackgirlbytes in [#188](https://github.com/modelcontextprotocol/ext-apps/pull/188)
### Bug Fixes
- **Move prettier to dev dependency** — Fixed incorrect dependency classification by @niclim in [#179](https://github.com/modelcontextprotocol/ext-apps/pull/179)
- **Fix build errors in examples** — Resolved build issues across example servers by @jonathanhefner in [#180](https://github.com/modelcontextprotocol/ext-apps/pull/180)
- **Server helpers** — New `registerAppTool()` and `registerAppResource()` simplify server setup with proper type safety. `connect()` now defaults to `PostMessageTransport(window.parent)`, enabling simpler initialization with just `await app.connect()` by @ochafik in [#165](https://github.com/modelcontextprotocol/ext-apps/pull/165)
- **Tool visibility control** — New `visibility` array field controls whether tools are visible to the agent, apps, or both. Restructured `_meta` format from flat `"ui/resourceUri"` to nested `_meta.ui.resourceUri` by @jonathanhefner in [#131](https://github.com/modelcontextprotocol/ext-apps/pull/131)
- **Host-provided theming** — Apps receive 36 standardized CSS variables for colors, typography, and spacing via `styles.variables`, enabling visual consistency with the host by @martinalong in [#127](https://github.com/modelcontextprotocol/ext-apps/pull/127)
- **Display mode requests** — Apps can request display mode changes (e.g., fullscreen) via `requestDisplayMode`, with hosts able to accept or reject by @martinalong in [#152](https://github.com/modelcontextprotocol/ext-apps/pull/152)
- **Custom fonts support** — Apps can receive custom fonts via `styles.css.fonts` and apply them using `applyHostFonts()` or `useHostFonts()` helpers by @martinalong in [#159](https://github.com/modelcontextprotocol/ext-apps/pull/159)
### API Changes
- **MCP SDK as peer dependency** — Consumers control their SDK version, reducing duplication by @ochafik in [#168](https://github.com/modelcontextprotocol/ext-apps/pull/168)
- **React as peer dependency** — Supports React 17, 18, and 19 by @ochafik in [#164](https://github.com/modelcontextprotocol/ext-apps/pull/164)
- **Renamed request methods** — Removed `send` prefix: `openLink()` and `teardownResource()`. Deprecated aliases maintained by @ochafik in [#161](https://github.com/modelcontextprotocol/ext-apps/pull/161)
- **Optional Client in AppBridge** — Enables custom forwarding scenarios without direct MCP client access by @ochafik in [#146](https://github.com/modelcontextprotocol/ext-apps/pull/146)
- **Zod v3 and v4 support** — Bring your own Zod by @alpic-ai in [#49](https://github.com/modelcontextprotocol/ext-apps/pull/49)
- **Zod schemas now version-agnostic** — Generated schemas work with both Zod 3.25+ and v4, using `z.object().passthrough()` instead of v4-only `z.looseObject()` by @ochafik in [#178](https://github.com/modelcontextprotocol/ext-apps/pull/178)
### Platform & DX
- **Windows compatibility** — Bun as optional dependency with automatic setup, `cross-env` for examples. Just run `npm install` by @ochafik in [#145](https://github.com/modelcontextprotocol/ext-apps/pull/145)
- **Widened `@oven/bun-*` version range** — Lowered the minimum from `^1.3.4` to `^1.2.21`, helping contributors whose registries may not have the latest packages by @ochafik in [#176](https://github.com/modelcontextprotocol/ext-apps/pull/176)
- **SSE transport support** — All examples now support both stdio and HTTP transports with SSE endpoints for older clients by @ochafik in [#136](https://github.com/modelcontextprotocol/ext-apps/pull/136)
- **Playwright E2E tests** — Screenshot golden testing across all 9 example servers with parallel execution (~28s) by @ochafik in [#115](https://github.com/modelcontextprotocol/ext-apps/pull/115)
- **hostContext exposure** — Access `hostContext` directly from the `App` class by @ochafik in [#139](https://github.com/modelcontextprotocol/ext-apps/pull/139)
### Bug Fixes
- **Responsive UIs** — Fixed narrow viewport handling for mobile and sidebar experiences by @ochafik in [#135](https://github.com/modelcontextprotocol/ext-apps/pull/135)
- **Sandbox notification fix** — Fixed `sandbox-ready` notification name to match implementation by @ochafik in [#160](https://github.com/modelcontextprotocol/ext-apps/pull/160)
- **Fixed non-UI tool registration** — Use `server.registerTool` for tools without UI and fix missing imports across examples by @ochafik in [#173](https://github.com/modelcontextprotocol/ext-apps/pull/173)
Thank you for helping keep the Model Context Protocol and its ecosystem secure.
## Reporting Security Issues
If you discover a security vulnerability in this repository, please report it through
the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
for this repository.
Please **do not** report security vulnerabilities through public GitHub issues, discussions,
or pull requests.
## What to Include
To help us triage and respond quickly, please include:
- A description of the vulnerability
- Steps to reproduce the issue
- The potential impact
- Any suggested fixes (optional)
Some files were not shown because too many files have changed in this diff
Show more