Compare commits

...
Author SHA1 Message Date
Olivier Chafik
6d9bdc7bab
Merge pull request #773 from modelcontextprotocol/repo-url-fix
Some checks failed
CI / Auto-fix formatting (push) Has been cancelled
CI / Build (Linux ARM64) (push) Has been cancelled
CI / Build (Linux x64) (push) Has been cancelled
CI / Build (Windows ARM64) (push) Has been cancelled
CI / Build (Windows x64) (push) Has been cancelled
CI / Build (macOS ARM64) (push) Has been cancelled
CI / Dependency isolation (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / Build (Windows WSL) (push) Has been cancelled
CI / Test git install (Linux ARM64) (push) Has been cancelled
CI / Test git install (Linux x64) (push) Has been cancelled
CI / Test git install (Windows ARM64) (push) Has been cancelled
CI / Test git install (Windows x64) (push) Has been cancelled
CI / Test git install (macOS ARM64) (push) Has been cancelled
Publish Any Commit / pkg-publish (push) Has been cancelled
Use the canonical git+https form for repository.url
2026-09-09 10:39:35 +01:00
Olivier Chafik
2be172defb Use the canonical git+https form for repository.url
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.
2026-09-08 19:37:47 +01:00
Olivier Chafik
352f6ced4d
CI: build the WSL job on ext4, skip the unused Chromium download, list tsconfig types (#772)
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.
2026-09-08 19:18:07 +01:00
Olivier Chafik
ae4f7e0ad0 Sync the quickstart tsconfig snippets 2026-09-08 18:39:50 +01:00
Olivier Chafik
a0dcf16843 CI: build the WSL job on ext4, skip the unused Chromium download, list tsconfig types
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).
2026-09-08 18:38:58 +01:00
Olivier Chafik
ae0fe55b84
Migration guide fixes and editor-visible deprecation for the 1.x handler form (#771)
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.
2026-09-08 18:15:07 +01:00
Olivier Chafik
853c9f749c Migration guide fixes and editor-visible deprecation for the 1.x handler form
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.
2026-09-08 18:01:18 +01:00
Olivier Chafik
12aa50b623
Add cross-version interop test against the published ext-apps 1.7.5 (#770)
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.
2026-09-08 16:17:27 +01:00
Olivier Chafik
218aef167e
Keep the 1.x handler registration forms as deprecated overloads (#769)
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.
2026-09-08 16:17:02 +01:00
Olivier Chafik
5f6346ef86 Add cross-version interop test against published ext-apps 1.7.5
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.
2026-09-08 15:32:20 +01:00
Olivier Chafik
4eab52e372 Export the legacy handler types from the root entry
They appear in App's public setRequestHandler / setNotificationHandler
signatures, so TypeDoc's link validation needs them reachable from the
entry point.
2026-09-08 15:31:03 +01:00
Olivier Chafik
728b0bf38e Keep the 1.x handler registration forms as deprecated overloads
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.
2026-09-08 14:24:25 +01:00
Olivier Chafik
4cd4273947
SDK 2.0 fixups: lockfile, peers, handler guards, docs, wire-compat tests (#768)
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.
2026-09-08 14:20:31 +01:00
Olivier Chafik
6d083cd69f Address review: MCPB build, guard the SDK's own handlers, doc fixes
- 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.
2026-09-08 13:59:09 +01:00
Olivier Chafik
f722753c8f Finish .js import extensions and drop the zod 3 fallback test
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.
2026-09-08 13:59:09 +01:00
Olivier Chafik
cfdc0c725f fix: restore double-registration guard on App and AppBridge handlers
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
2026-09-08 13:59:09 +01:00
Olivier Chafik
ea6b89c9e2 Add wire compatibility tests against captured 1.x traffic
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.
2026-09-08 13:58:37 +01:00
Olivier Chafik
c5b2543115 Document the 2.x install matrix and add a migration guide
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.
2026-09-08 13:58:37 +01:00
Olivier Chafik
09bf2aae3c Drop the zod 3 JSON Schema fallback
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.
2026-09-08 13:58:37 +01:00
Olivier Chafik
0a477bb25d Use explicit .js extensions in relative imports
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.
2026-09-08 13:58:36 +01:00
Olivier Chafik
7f686ff2b4 Update dependency isolation check for the required client peer
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.
2026-09-08 13:58:36 +01:00
Olivier Chafik
d6ebcca9a0 Accept raw zod shapes in registerAppTool as a deprecated overload
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.
2026-09-08 13:58:36 +01:00
Olivier Chafik
040942b3af Import shared wire types from @modelcontextprotocol/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.
2026-09-08 13:58:22 +01:00
Olivier Chafik
50eb30ec8d Bump to 2.0.0 and widen SDK peer ranges to ^2.0.0
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.
2026-09-08 13:58:22 +01:00
Olivier Chafik
f28f5ddb3c Harden dependency isolation check and run it in CI
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.
2026-09-08 13:58:21 +01:00
Olivier Chafik
90123733bb test: regenerate lockfile (re-add nested @types/node entries) 2026-09-08 13:58:21 +01:00
Olivier Chafik
f9c671c323
Add explicit .js extensions to relative imports for NodeNext consumers (#705)
Emitted declarations used extensionless relative imports, so types failed to resolve under moduleResolution NodeNext/Node16 (#704).

Closes #704
2026-09-08 13:57:46 +01:00
Ken Jo
264d9cae44 fix: add explicit .js extensions to relative imports so declarations resolve under NodeNext/Node16
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
2026-09-08 13:40:58 +01:00
Olivier Chafik
1a713173cc
Migrate to MCP SDK 2.0 with official Protocol and isolated role peers (#720)
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
2026-09-08 12:44:49 +01:00
Quentin Churet
10195ad918
docs: add Alpic Playground to supported clients (#729)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 18:04:54 +03:00
Enrico
4e4886c5ff chore: update MCP SDK packages to stable 2.0.0 2026-07-28 15:02:48 -07:00
Olivier Chafik
92f46a5745
chore: bump ext-apps to 1.7.5 (#724) 2026-07-23 12:25:07 +01:00
Enrico
c847bc0887 docs: align Protocol and role-specific setup 2026-07-22 15:37:22 -07:00
Enrico
e7ffe7c738 chore: update MCP SDK packages to beta.5 2026-07-22 15:24:49 -07:00
Olivier Chafik
7d498079e8
chore: clear npm audit findings (15 -> 3) (#725)
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.
2026-07-22 19:34:30 +01:00
victor-openai
c55a3a231f
fix: preserve experimental capability settings (#721) 2026-07-22 19:28:57 +03:00
Enrico
7664bb1010 test: isolate optional SDK role dependencies 2026-07-20 11:29:43 -07:00
Felix Weinberger
43eed45404 Point SDK packages at the pkg.pr.new preview of typescript-sdk#2501
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.
2026-07-20 11:14:56 -07:00
Felix Weinberger
ccae093d1a Port App and AppBridge to the public v2 Protocol base class
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.
2026-07-20 11:14:56 -07:00
Dominic Farolino
2ca6a59d2f
spec: sync object-src with default-src (#715) 2026-07-17 03:15:10 +03:00
Mel Ludowise
9778cf97a2
Make lazy-auth-server usable on mobile (#714)
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)
2026-07-16 17:00:17 +01:00
Andrew Khadder
f50ec6ebfb
fix: regenerate lockfile for npm 10 2026-07-14 17:16:27 -07:00
Andrew Khadder
f94cb38b0b
refactor: finish MCP SDK v2 migration 2026-07-14 17:07:30 -07:00
Andrew Khadder
a76e3a7691
refactor: migrate shared SDK surfaces to v2 2026-07-14 15:46:56 -07:00
Andrew Khadder
d19a1fb83a
refactor: migrate AppBridge to SDK v2 server 2026-07-14 15:34:33 -07:00
Andrew Khadder
59e6ffc6d8
refactor: migrate App to SDK v2 client 2026-07-14 15:02:25 -07:00
Andrew Khadder
209cfc6241
refactor: compose protocol-neutral event dispatch 2026-07-14 13:49:10 -07:00
Andrew Khadder
b383847bf2
chore: prepare SDK v2 dependencies 2026-07-14 13:35:51 -07:00
Copilot
cf87f2a2c2
ci: fix README prettier build failure (#698)
* Initial plan

* fix: apply Prettier formatting to README table row

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-07 21:29:16 +03:00
Andrew Khadder
cd969dbc31
docs: add mcp-use inspector badge to supported clients (#650) 2026-07-07 21:05:17 +03:00
Cameron Yick
f0d9582eaa
spec: add missing HostCapabilities fields (#653) 2026-07-07 21:02:07 +03:00
Ensar Gök
ea8775dcb5
docs: API Docs link for ext-apps/server (#697)
Updated API documentation link for ext-apps/server.
The link `/server.html` does not exists.
2026-07-07 20:43:37 +03:00
Aqil Aziz
fa12744908
docs: fix map-server readme and code of conduct links (#657) 2026-06-17 18:51:57 +03:00
Olivier Chafik
ca1d29894f
chore: bump ext-apps to 1.7.4 (#685)
* chore: bump ext-apps to 1.7.4

* fix: resolve all npm audit vulnerabilities

- Remove unused vitest devDependency from basic-host (critical advisory,
  package was never referenced by any test or config)
- systeminformation ^5.31.5 -> ^5.31.6 (command injection, GHSA-hvx9-hwr7-wjj9)
- Lockfile-only bumps: devalue 5.8.1, fast-uri 3.1.2, hono 4.12.23,
  ip-address 10.2.0, express-rate-limit 8.5.2, postcss 8.5.15, qs 6.15.2,
  svelte 5.56.1

npm audit now reports 0 vulnerabilities.
2026-06-04 22:10:06 +01:00
Olivier Chafik
0ffe16781e
lazy-auth-server: support mounting under a base path (#683)
* 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.
2026-06-04 19:47:57 +01:00
Mel Ludowise
a990780293
Require PKCE and bind redirect_uri in lazy-auth-server token exchange (#681)
* 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).
2026-06-02 13:30:49 +01:00
Olivier Chafik
7d4434e991
chore: bump ext-apps to 1.7.3 (#680)
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).
2026-06-01 12:10:47 +01:00
Mel Ludowise
fcc4b03bde
Add lazy-auth-server example (#679)
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
2026-06-01 11:19:53 +01:00
Olivier Chafik
9a37ad7182
chore: bump ext-apps to 1.7.2 (#660)
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)
2026-05-15 19:01:25 +01:00
Olivier Chafik
1d091e2132
chore: 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.
2026-05-08 15:55:06 +01:00
Olivier Chafik
3cf5b588a1
chore(pdf-server): switch pdf-lib to maintained @cantoo/pdf-lib fork (#651)
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.
2026-05-08 13:01:54 +01:00
Olivier Chafik
0008d3b78f
chore: bump ext-apps to 1.7.1 (#643) 2026-04-27 16:16:33 +01:00
Olivier Chafik
ba31be1f91
perf(pdf-server): lazy form extraction via range transport + incremental viewer scans (#639)
* 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
2026-04-27 15:32:53 +01:00
Olivier Chafik
30a78b60b4
perf(pdf-server): share cache across server instances and dedupe form parsing (#637)
* 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.
2026-04-24 15:57:58 +01:00
Cameron Yick
7344d4c966
fix(docs): update contributing guide on package preview (#601) 2026-04-24 11:36:43 +01:00
Suriya Prakash
0f6d653b4a
test: add unit tests for buildAllowAttribute (#541) 2026-04-24 11:36:00 +01:00
Maurizio Albari
0b83679909
fix(qr-server): pass host/port to FastMCP for Docker compatibility (#372)
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.
2026-04-24 11:35:13 +01:00
Olivier Chafik
77b8daa1e8
test: add unit coverage for PostMessageTransport source validation (#536)
* 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
2026-04-24 11:27:32 +01:00
Olivier Chafik
64b4fa16de
chore: bump ext-apps to 1.7.0 (#628) 2026-04-21 16:19:22 +01:00
Olivier Chafik
3e9950062b
build: add npm run bump; parallel npm-publish approval (#568)
* 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>
2026-04-21 16:03:27 +01:00
Olivier Chafik
cc02e17e21
fix(react): StrictMode cleanup + relax late-handler guard for re-reg (#631)
* 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.
2026-04-21 15:44:57 +01:00
Olivier Chafik
9658678eaa
refactor(app): unify pre-handshake guard to console.warn (#630)
_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.
2026-04-21 15:06:49 +01:00
Olivier Chafik
6fa5d4e6ed
feat(app): warn when one-shot handlers are registered after connect() (#629)
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.
2026-04-21 14:50:17 +01:00
Olivier Chafik
9d68315720
feat(app): default Zod to jitless; add allowUnsafeEval opt-out (#618)
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).
2026-04-21 14:49:09 +01:00
Olivier Chafik
5dfc88f113
docs(types): drop stale resourceUri JSDoc referencing deprecated flat key (#626) 2026-04-21 14:48:52 +01:00
Jonathan Hefner
b170150a82
Add examples policy to CONTRIBUTING.md (#550)
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>
2026-04-21 13:53:58 +01:00
Olivier Chafik
989ddc7568
chore: bump vite, hono, @hono/node-server to patched versions (#616)
Lockfile-only bumps to address npm audit findings:
- vite 6.4.1 -> 6.4.2 (high)
- hono 4.12.9 -> 4.12.14 (moderate)
- @hono/node-server 1.19.11 -> 1.19.14 (moderate)

All within existing semver ranges; no package.json changes needed.
2026-04-21 13:53:20 +01:00
Olivier Chafik
9a75b891b2
chore: drop GitHub issue URLs from pre-handshake guard log messages (#625)
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.
2026-04-21 13:50:10 +01:00
Olivier Chafik
4995746759
fix(types): forbid csp/permissions on McpUiToolMeta via never (#624)
* 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
2026-04-21 13:49:07 +01:00
Olivier Chafik
3fb521b71f
spec: Add tool registration for Apps, to be called by Host (WebMCP-style!) (#72)
* 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>
2026-04-21 13:46:13 +01:00
Olivier Chafik
926dcb4fe9
feat: MCP app sampling support via stock SDK types (alt to #477) (#530)
* 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>
2026-04-21 13:29:09 +01:00
Olivier Chafik
4f89a6f8ea
feat(react): forward autoResize from useApp to App (#622)
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.
2026-04-21 11:51:04 +01:00
Olivier Chafik
94c94216d4
feat(app,app-bridge): guard requests sent before handshake completes (#620) (#623)
* 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
2026-04-21 11:45:14 +01:00
Olivier Chafik
739c0e049d
build: fix pre-commit hook for symlinked self-dep and staged deletions (#621)
- 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.
2026-04-21 11:28:46 +01:00
Olivier Chafik
01d826aea0
chore: bump ext-apps to 1.6.0 (#613) 2026-04-14 13:58:12 +01:00
Roy Li
c59c6060cd
callServerTool: opt into progress-based timeout reset (#600)
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.
2026-04-14 13:06:22 +01:00
Olivier Chafik
4f4dd8b6f0
test(e2e): poll for initial shrink-to-fit zoom before asserting (#607)
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.
2026-04-09 13:51:40 +01:00
Olivier Chafik
621a70a724
chore: bump ext-apps to 1.5.0 (#588)
* chore: bump ext-apps to 1.5.0

* style: auto-fix prettier formatting

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-02 10:09:49 +01:00
Olivier Chafik
61fe39b4db
feat(pdf-server): rasterize imported annotations + form/save consistency follow-ups (#593)
* 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.
2026-04-02 10:03:35 +01:00
Olivier Chafik
3af019144f
fix(pdf-server): form-field save robustness (#591)
* 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.
2026-04-02 10:01:23 +01:00
Olivier Chafik
f3bf1065db
fix(pdf-server): import highlight/underline/strike from existing PDFs (#592)
* 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.
2026-04-02 10:00:53 +01:00
Olivier Chafik
4fc95131bf
feat(pdf-server): get_viewer_state interact action (#590)
* 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.
2026-04-02 08:47:48 +01:00
Olivier Chafik
2e55c842ba
fix(pdf-server): let zoom-out button go below fit (revert button floor) (#589)
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.
2026-04-02 08:09:24 +01:00
Olivier Chafik
721ccd04b8
fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout (#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).
2026-04-02 06:44:56 +01:00
Olivier Chafik
9da4a75ff6
chore: bump ext-apps to 1.4.0 (#582)
* chore: bump ext-apps to 1.4.0

Changes since 1.3.2:

SDK
- feat: add addEventListener/removeEventListener with DOM-model on* semantics (#573)

pdf-server
- feat: add save_as interact action (#580)
- feat: fit-to-page on fullscreen + pinch-to-zoom (#583)
- fix: npx DOMMatrix crash + broken MCPB bundle (#584)
- fix: viewer liveness, 1:1 batch results, fullscreen jitter (#579)
- fix: render page before O(numPages) annotation scans (#581)
- fix: radio + dropdown in fill_form/save (#577)

Dependencies
- chore: npm audit fix — sdk 1.29.0, systeminformation 5.31.5, +13 transitives (#585, #586)
- fix: bump path-to-regexp 8.3.0 → 8.4.1 to patch ReDoS CVEs (#576)

* chore: update e2e snapshots [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-02 04:33:13 +01:00
Olivier Chafik
9757f35fd1
chore: drop spurious root dependencies block from #585 (#586)
#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.
2026-04-02 04:27:49 +01:00
Olivier Chafik
8e7206cb15
chore(deps): npm audit fix — sdk 1.29.0, systeminformation 5.31.5, +13 transitives (#585)
Clears all 15 npm audit findings (8 high / 6 moderate / 1 low):

- @modelcontextprotocol/sdk 1.25.2 → 1.29.0 (GHSA-345p-7cg4-v4c7
  cross-client data leak via shared transport reuse). Root had it
  pinned exact in devDependencies which blocked audit fix.
- systeminformation 5.30.6 → 5.31.5 (GHSA-5vv4-hvf7-2h46,
  GHSA-9c88-49p5-5ggf command injection)
- hono, undici, rollup, picomatch, minimatch, lodash-es, svelte,
  devalue, ajv, brace-expansion, markdown-it, yaml, qs — lockfile
  bumps via npm audit fix
2026-04-02 04:25:00 +01:00
Olivier Chafik
04fed524a9
fix(pdf-server): npx DOMMatrix crash + broken MCPB bundle (#584)
* 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)
2026-04-02 04:16:33 +01:00
Olivier Chafik
68c58b60b7
feat(pdf-server): fit-to-page on fullscreen + pinch-to-zoom (#583)
* 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).
2026-04-02 03:43:47 +01:00
Olivier Chafik
800f6d08fd
fix(pdf-server): viewer liveness, 1:1 batch results, fullscreen jitter (#579)
* 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.
2026-04-02 00:49:29 +01:00
Olivier Chafik
9d55050a00
fix(pdf-server): render page before O(numPages) annotation scans (#581)
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.
2026-04-01 22:45:45 +01:00
Olivier Chafik
4b3f6252a4
pdf-server: add save_as interact action (#580)
* 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.
2026-04-01 22:45:00 +01:00
Olivier Chafik
38700569e7
fix(pdf-server): radio + dropdown in fill_form/save (#577)
* 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.
2026-04-01 22:28:28 +01:00
Olivier Chafik
f987bc43b2
fix(deps): bump path-to-regexp 8.3.0 → 8.4.1 to patch ReDoS CVEs (#576)
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.
2026-04-01 18:04:00 +01:00
Olivier Chafik
0266171b92
Add addEventListener/removeEventListener with DOM-model on* semantics (#573)
* 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>
2026-03-30 18:48:33 +01:00
Olivier Chafik
14969c5da2
chore: bump ext-apps to 1.3.2 (#569)
Changes since 1.3.1:
- Revert fullscreen size-changed skip (#570)
- Fix auto-resize destroying horizontal scroll positions (#567)
- Remove examples/pdf-server/plugin/ — canonical copy is upcoming in knowledge-work-plugins (#565)
2026-03-27 17:45:09 +00:00
Mel Ludowise
fd1186cc2d
Revert fullscreen size-changed skip (#570)
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.
2026-03-26 16:46:48 +00:00
Mel Ludowise
a7273bbf42
Fix auto-resize destroying horizontal scroll positions (#567)
* 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.
2026-03-26 10:08:18 +00:00
Olivier Chafik
8961b03163
Remove examples/pdf-server/plugin/ — canonical copy is upcoming in knowledge-work-plugins (#565)
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.
2026-03-25 15:40:19 +00:00
Olivier Chafik
e8ca859429
chore: bump ext-apps to 1.3.1 (#562)
Changes since 1.3.0:
- fix(pdf-server): detached-buffer on range re-request + stale e2e assertion (#561)
2026-03-23 20:19:06 +00:00
Olivier Chafik
e8a4ea5f89
fix(pdf-server): detached-buffer on range re-request + stale e2e assertion (#561)
* 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.
2026-03-23 20:17:25 +00:00
Olivier Chafik
6723c8ff10
chore: bump ext-apps to 1.3.0 (#560)
Changes since 1.2.2:
- feat: add ui/notifications/request-teardown for view-initiated termination (#215)
- feat(pdf-server): annotations, interact tool, page extraction & prompt engineering (#506)
- fix(examples): improve mobile support for map, pdf, and threejs examples (#555)
2026-03-23 20:09:27 +00:00
Frédéric Barthelet
c026b398a1
Add ui/notifications/request-teardown notification for view-initiated termination (#215)
* Add ui/close-resource request for UI to initiate termination

* Add missing METHOD export

* Fix typo in remaining ui/request-close

* Align spec terminology to teardown/termination instead of close/clearnup

* Implement final comments

* Removing view behavior on initiation of request termination
2026-03-19 22:01:59 +02:00
Olivier Chafik
296b5921fb
pdf-server: annotations, interact tool, page extraction & prompt engineering (#506)
* 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>
2026-03-19 17:41:55 +00:00
Mel Ludowise
83d82a1caa
fix(examples): improve mobile support for map, pdf, and threejs examples (#555)
* 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
2026-03-18 13:47:47 +00:00
Olivier Chafik
704b6e0af9
chore: bump ext-apps to 1.2.2 (#549)
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)
2026-03-10 19:19:15 +00:00
Olivier Chafik
eeb81c08bc
fix(schemas): import from zod/v4 to match SDK's zod API (#548)
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>
2026-03-10 19:10:36 +00:00
Anton Pidkuiko
1aa3c9c106
Merge pull request #547 from modelcontextprotocol/ochafik/bump-1.2.1
chore: bump ext-apps to 1.2.1
2026-03-10 17:44:28 +00:00
Olivier Chafik
e13732ad38
chore: bump ext-apps to 1.2.1
Changes since 1.2.0:
- fix: bundle SDK+zod in react-with-deps (was byte-identical to ./react) (#539)
- fix(build): copy schema.json to dist and externalize zod (#534)
- fix: skip debug log for high-frequency tool-input-partial notifications (#546)
- fix(deps): drop @hono/node-server override to patch GHSA-wc8c-qw6v-h7f6 (#535)
- fix(readme): use picture element for theme-aware logo (#545)
- fix(ci): require maintainer association for /update-snapshots trigger (#532)
- fix: pre-commit stages only originally-staged files; add .npmrc (#538)
- ci: use npm ci with caching, validate typedoc links, align Node versions (#533)
- test: exclude screenshot-gen from default E2E run; wire pdf-server tests (#537)
2026-03-10 17:36:49 +00:00
Olivier Chafik
5e4839d1f1
test(e2e): fix no-op security injection test via frames[] traversal (#540)
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.
2026-03-10 17:36:15 +00:00
Olivier Chafik
22205e6d03
fix: bundle SDK+zod in react-with-deps (was byte-identical to ./react) (#539)
* 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)
2026-03-10 17:33:49 +00:00
Den Delimarsky
a870456893
fix(readme): use picture element for theme-aware logo (#545)
* 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).
2026-03-10 17:33:40 +00:00
Anton Pidkuiko
a2d2d8c6d0
Merge pull request #546 from modelcontextprotocol/antonp/quiet-partial-log
fix: skip debug log for high-frequency tool-input-partial notifications
2026-03-10 17:33:19 +00:00
Anton Pidkuiko MacBook
20d623f20c
fix: skip debug log for high-frequency tool-input-partial notifications
These fire dozens of times per second during streaming and flood the
console. All other JSON-RPC messages still log at debug level.
2026-03-10 17:25:12 +00:00
Olivier Chafik
2b0d11e64c
fix(ci): require maintainer association for /update-snapshots trigger (#532)
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).
2026-03-06 17:44:08 +00:00
Olivier Chafik
bf623f832d
fix(deps): drop @hono/node-server override to patch GHSA-wc8c-qw6v-h7f6 (#535)
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).
2026-03-06 17:22:34 +00:00
Olivier Chafik
7eb72c7b4c
fix: pre-commit stages only originally-staged files; add .npmrc (#538)
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).
2026-03-06 17:22:25 +00:00
Olivier Chafik
b093e39560
ci: use npm ci with caching, validate typedoc links, align Node versions (#533)
- 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
2026-03-06 17:13:58 +00:00
Olivier Chafik
79744afd41
fix(build): copy schema.json to dist and externalize zod (#534)
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)
2026-03-06 17:13:48 +00:00
Olivier Chafik
1ea1e9ef43
test: exclude screenshot-gen from default E2E run; wire pdf-server tests (#537)
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
2026-03-06 17:13:40 +00:00
Olivier Chafik
6b12fff1d0
chore: bump ext-apps to 1.2.0 (#527)
* chore: bump ext-apps to 1.1.3

Changes since 1.1.2:
- fix: autoResize reporting viewport height when content is taller (#525)
- fix: remove duplicate --color-text-ghost from McpUiStyleVariableKey (#523)
- fix: examples servers' npm run start:stdio (#507)
- pdf-server: ignore client roots by default (#510)
- docs: add Security documentation section and SEO improvements (#524)
- docs: add MCP client configuration examples to CONTRIBUTING.md (#508, #526)
- docs: use apps.extensions.modelcontextprotocol.io domain for docs URLs (#499)
- docs: add SECURITY.md with GitHub Security Advisories guidance (#472)

* chore: switch to minor bump 1.2.0 (new APIs in #470)
2026-03-06 15:43:55 +00:00
Andrew
9228662557
fix CI (#456)
Co-authored-by: Andrew Lyu <andrewlyu@Andrews-MacBook-Air.local>
2026-03-06 15:32:21 +00:00
Olivier Chafik
7046d7b1dc
docs: ask contributors to allow maintainer edits on PRs (#529)
Enables maintainers to rebase stale fork branches and allows the
/update-snapshots workflow to push updated screenshots.
2026-03-06 15:18:42 +00:00
Jonathan Hefner
5d6fff2910
Update basic-host imports to use package exports (#400)
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>
2026-03-06 14:38:12 +00:00
Hryhorii (Greg) Avcharov
38e1f7af14
refactor: remove unnecessary type assertions (#471) 2026-03-06 14:35:04 +00:00
Jonathan Hefner
61951a5c78
Refactor create-mcp-app skill to reference patterns.md (#416)
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>
2026-03-06 14:32:47 +00:00
Jonathan Hefner
c000ba6dd5
Work around tsx watch hang on Windows in quickstart (#428)
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>
2026-03-06 14:32:30 +00:00
Hryhorii (Greg) Avcharov
19fe73a08a
Replace deprecated RESOURCE_URI_META_KEY usage with modern format (#457) 2026-03-06 14:32:04 +00:00
Olivier Chafik
ef8fc6e2f0
fix: make listServerResources() params optional (#528)
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.
2026-03-06 14:30:36 +00:00
Hryhorii (Greg) Avcharov
4f9a7ebc74
feat: add readServerResource() and listServerResources() api to App (#470)
* feat: add readServerResource() and listServerResources() api to App class

* test: add listServerResources and readServerResource integration tests
2026-03-06 14:20:45 +00:00
Antoine Lévêque
62c75203b6
Replace platform-specific optionalDependencies with bun devDependency (#518)
* 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.
2026-03-06 13:56:34 +00:00
Suriya Prakash
0af4cdc305
fix: remove duplicate --color-text-ghost from McpUiStyleVariableKey (#523) 2026-03-06 13:46:04 +00:00
Den Delimarsky
76f2de7b19
Add Security documentation section and SEO improvements (#524)
* 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.
2026-03-06 13:45:15 +00:00
Olivier Chafik
b8a1349c07
docs: use cd-based MCP config in CONTRIBUTING local dev examples (#526)
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.
2026-03-06 13:44:57 +00:00
Mel Ludowise
26928b8622
Fix autoResize reporting viewport height when content is taller (#525)
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.
2026-03-06 13:30:13 +00:00
Olivier Chafik
134c71c29c
Add MCP client configuration examples to CONTRIBUTING.md (#508)
* 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>
2026-03-06 13:16:42 +00:00
Olivier Chafik
30f79b9e3f
ignore client roots by default in pdf-server (#510)
* 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>
2026-02-28 00:42:11 +00:00
Den Delimarsky
87e30edb03
Merge pull request #472 from modelcontextprotocol/localden/sec-doc-update
Update SECURITY.md to use GitHub Security Advisories
2026-02-26 22:48:01 -08:00
Olivier Chafik
627ead3805
fix examples servers' npm run start:stdio (#507)
* fix examples servers' npm run start:stdio

* rm mistakenly added file
2026-02-26 04:00:23 +00:00
Jonathan Hefner
37533ff4c7
Merge pull request #499 from jonathanhefner/update-docs-url
Use `apps.extensions.modelcontextprotocol.io` domain for docs URLs
2026-02-25 08:36:18 -06:00
Olivier Chafik
5373f01d6f
chore: bump ext-apps to 1.1.2 (#503)
* 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.
2026-02-25 01:43:51 +00:00
Olivier Chafik
02737db779
fix: sync local SDK build into node_modules for examples (#504)
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
2026-02-24 21:20:59 +00:00
Olivier Chafik
1728617194
ci: auto-fix prettier formatting in pre-commit hook and CI (#498)
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>
2026-02-24 18:37:57 +00:00
Anton Pidkuiko
e4a9766592
Merge pull request #475 from modelcontextprotocol/feat/download-file
feat: add ui/download-file method for host-mediated file downloads
2026-02-24 18:30:54 +00:00
Olivier Chafik
281a26b021
pdf-server: robust path validation + folder/file root support (#497)
* 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
2026-02-24 18:23:57 +00:00
Jonathan Hefner
b4642b4338
Use apps.extensions.modelcontextprotocol.io domain for docs URLs
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>
2026-02-24 11:38:54 -06:00
Anton Pidkuiko MacBook
9340bd800e
test: add App → AppBridge integration test for downloadFile 2026-02-24 16:57:18 +00:00
Olivier Chafik
ec389f473b
chore: bump ext-apps to 1.1.1 (#496)
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)
2026-02-24 02:39:24 +00:00
Mel Ludowise
82b1f29085
Set audioSession.type to playback in sheet music app (#489)
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.
2026-02-24 01:58:32 +00:00
Olivier Chafik
0be854b244
ci: build and publish MCPB bundle for pdf-server (#495)
- Add *.mcpb to .gitignore (built artifacts)
- Validate MCPB pack in CI (Linux x64 only)
- Build and upload .mcpb as GitHub release asset on publish
2026-02-24 01:48:17 +00:00
Bryan Thompson
3de035dc8d
Add MCPB packaging and Claude Code plugin for pdf-server (#491)
* 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>
2026-02-24 00:51:43 +00:00
Olivier Chafik
47e437b631
ci: add missing examples to deploy workflows (#494)
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>
2026-02-24 00:31:37 +00:00
Olivier Chafik
4d9ba1ea0e
pdf-server: remove domain allowlist, require HTTPS only (#487)
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.
2026-02-23 21:27:15 +00:00
Olivier Chafik
219b4b1605
fix(pdf-server): remove page-loading overlay (#493)
* 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
2026-02-23 21:25:25 +00:00
Anton Pidkuiko MacBook
f07bdfbe27
docs: remove redundant resource-type bullets from download-file spec
Remove bullets that restate standard EmbeddedResource/ResourceLink behavior
already defined in the MCP resource spec. Keep download-specific host behavior
(confirmation dialog, filename derivation, security policy, path sanitization).
2026-02-23 13:08:34 +00:00
Den Delimarsky
b4ea8c53a2
Merge pull request #486 from modelcontextprotocol/readme-updates
Adding ChatGPT to the readme
2026-02-21 18:12:15 -08:00
Conor Kelly
6e40513dbf Adding ChatGPT to the readme
Added ChatGPT as a supporting client in the readme. Also updated links to re-direct to the relevant MCP Apps docs on respective client pages
2026-02-21 18:09:18 -08:00
Hryhorii (Greg) Avcharov
c49b4b5e5b
fix: add audio/video support for basic-host (#460) 2026-02-22 01:21:22 +00:00
Olivier Chafik
5bbf277882
chore: bump ext-apps to 1.1.0 (#485)
* 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)
2026-02-22 01:18:47 +00:00
Olivier Chafik
d185099ead
feat(pdf-server): lazy range-based loading + eager background preloading (#480)
* 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)
2026-02-21 16:33:48 -08:00
Olivier Chafik
90ba41720e
pdf: ignore safe area insets in fullscreen mode (#484)
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.
2026-02-21 16:25:58 -08:00
Den Delimarsky
32f7f40f57
Merge pull request #483 from modelcontextprotocol/localden-patch-1
Update README.md
2026-02-21 13:04:00 -08:00
Den Delimarsky
5710a7e462
Update README.md 2026-02-21 13:03:42 -08:00
Den Delimarsky
221e372a28
Merge pull request #478 from modelcontextprotocol/localden/readme-ext
Lead README with agent skills for faster onboarding
2026-02-21 13:03:03 -08:00
Den Delimarsky
910c417fae
Format README with prettier 2026-02-21 08:33:29 +00:00
Den Delimarsky
da4406edc2
Update skill 2026-02-21 08:22:47 +00:00
Olivier Chafik
96c0af577d
feat(pdf-server): use MCP roots to auto-allow local directories (#479)
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.
2026-02-21 01:37:47 +00:00
Den Delimarsky
7e0fee6b03
Use GitHub note admonition for clients note 2026-02-20 22:12:20 +00:00
Den Delimarsky
2281bcf176
Add supported clients section to README 2026-02-20 22:10:43 +00:00
Den Delimarsky
7eec75164a
Format README with prettier 2026-02-20 20:53:10 +00:00
Den Delimarsky
2af02baf46
Update README hero gif to Excalidraw, add new agent skills 2026-02-20 20:47:02 +00:00
Den Delimarsky
8543d8d5dc Lead README with agent skills for faster onboarding
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.
2026-02-20 19:37:58 +00:00
Anton Pidkuiko MacBook
3757271641
refactor: use MCP resource types for ui/download-file params
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.
2026-02-20 12:00:35 +00:00
Anton Pidkuiko MacBook
494c73e7ca
feat: add ui/download-file method for host-mediated file downloads
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.
2026-02-19 21:25:48 +00:00
Olivier Chafik
37e127034f
Add fullscreen button to threejs-server (#446)
* Add fullscreen button to threejs-server

* Simplify wrapper: pass app instance directly instead of useCallback wrappers

* Update examples/threejs-server/src/threejs-app.tsx

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>

* Update examples/threejs-server/src/threejs-app.tsx

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>

* prettier

---------

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
2026-02-19 19:17:04 +00:00
Olivier Chafik
8cad9f04c0
Add PDF search feature to pdf-server (#445)
* 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>
2026-02-19 18:56:40 +00:00
Den Delimarsky
05b1f22cd0 Add SECURITY.md with GitHub Security Advisories guidance 2026-02-18 20:54:05 +00:00
Olivier Chafik
f615123711
Add double-connect guard to prevent protocol message handling errors (#450)
* 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 #429

https://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>
2026-02-17 18:15:24 +00:00
Jonathan Hefner
8dbc06a355
Merge pull request #462 from jonathanhefner/remove-typedoc-theme-css-patch
Upgrade `typedoc-github-theme` to v0.4.0 and remove local CSS workaround
2026-02-17 08:06:34 -06:00
Anton Pidkuiko
0b48dfab7d
Merge pull request #463 from modelcontextprotocol/localden/readme
README update
2026-02-17 10:45:25 +00:00
Den Delimarsky
5eab6da3fc
README update 2026-02-16 23:03:52 -08:00
Den Delimarsky
62e1a5a0ae
README update 2026-02-16 23:01:59 -08:00
Den Delimarsky
512016053d
README update 2026-02-16 23:00:57 -08:00
Den Delimarsky
c2a0b5ccad
Update with media 2026-02-16 22:59:17 -08:00
Den Delimarsky
291006ab73
Update with media 2026-02-16 22:57:28 -08:00
Den Delimarsky
a427e7127d
Update with media 2026-02-16 22:52:59 -08:00
Den Delimarsky
22bffa8782
Update with media 2026-02-16 22:51:16 -08:00
Den Delimarsky
8242496f3e
Update README 2026-02-16 21:26:21 -08:00
Den Delimarsky
716d7c1c5c
Update README 2026-02-16 21:23:08 -08:00
Den Delimarsky
09e1684db4
Update README 2026-02-16 21:19:48 -08:00
Den Delimarsky
c372c1f6a1
Update README 2026-02-16 21:16:47 -08:00
Jonathan Hefner
5c4000bafd
Upgrade typedoc-github-theme to v0.4.0 and remove local CSS workaround
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>
2026-02-16 13:27:39 -06:00
Jonathan Hefner
a8ff280145
Merge pull request #461 from Avcharov/fix/flaky-e2e-tests
fix: update e2e test snapshots for shadertoy and wiki-explorer images
2026-02-16 11:15:33 -06:00
Hryhorii Avcharov
91c135350d fix: update e2e test snapshots for shadertoy and wiki-explorer images 2026-02-15 12:01:16 +01:00
Olivier Chafik
666952980f
feat(app): throw helpful error when callServerTool is called with a string (#449)
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 #386

https://claude.ai/code/session_01GqAyN4Ux7svWoU2HqsSLZF

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 21:05:21 +00:00
Jonathan Hefner
0bbbfee8c2
Merge pull request #442 from jonathanhefner/typedoc-theme-css-patch
Apply `typedoc-github-theme` typography fixes locally via `customCss`
2026-02-12 10:22:47 -06:00
Salman Chishti
a4fda18caa
Upgrade GitHub Actions for Node 24 compatibility (#392)
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-02-12 15:31:10 +00:00
Salman Chishti
93b619cae6
Upgrade GitHub Actions to latest versions (#393)
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-02-12 15:30:57 +00:00
Max Stoiber
c60ee352ae
Change Host <> Sandbox communication protocol to "SHOULD" (#435)
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".
2026-02-12 15:29:03 +00:00
Hryhorii (Greg) Avcharov
10d1f9dc6d
fix: align basic-host dark mode styles (#438) 2026-02-12 15:28:51 +00:00
Health Kowshik
3b15f063fe
Update theme toggle button position in CSS from top to bottom (#441) 2026-02-12 15:28:44 +00:00
Olivier Chafik
fd8cc978d4
fix(message-transport): only ignore messages that lack jsonrpc: "2.0" (#448)
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>
2026-02-12 15:28:32 +00:00
Jonathan Hefner
a815f31392
Merge pull request #444 from jonathanhefner/fix-migrate-oai-app-skill-name
Rename `migrate-oai-app` skill `name` to align with spec
2026-02-10 09:02:19 -06:00
Jonathan Hefner
f9c96d973c
Merge pull request #443 from vgshenoy/patch-1
Rename skill to 'create-mcp-app' to align with spec
2026-02-10 09:01:58 -06:00
Jonathan Hefner
6d79123b97
Rename migrate-oai-app skill name to align with spec
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>
2026-02-10 08:50:49 -06:00
Vignesh Shenoy
0ea69081a0
Rename skill to 'create-mcp-app' to align with spec
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."
2026-02-10 11:23:20 +01:00
Jonathan Hefner
faabb6d1af
Apply typedoc-github-theme typography fixes locally via customCss
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>
2026-02-09 11:15:30 -06:00
Olivier Chafik
bcfffb6585
Clarify spec and SDK: accept UIResourceMeta in both resources/list and resources/read (#410)
* 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>
2026-02-04 14:26:09 +00:00
Jonathan Hefner
2dd56c3dce
Merge pull request #422 from jonathanhefner/everything-is-external
Replace "external" with "network" in CSP documentation
2026-02-03 10:29:52 -06:00
Jonathan Hefner
78b319e29a
Merge pull request #421 from jonathanhefner/use-double-quotes-in-npm-scripts
Use escaped double quotes in npm scripts for Windows compatibility
2026-02-03 09:41:28 -06:00
Anton Pidkuiko
398f5b4e4a
Merge pull request #294 from matteo8p/create-helper-functions-tool-visibility
Add `isToolVisibilityModelOnly` and `isToolVisibilityAppOnly` helper functions
2026-02-02 23:15:46 +00:00
Jonathan Hefner
c39b40e7c8
Replace "external" with "network" in CSP documentation
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>
2026-02-02 10:50:06 -06:00
Jonathan Hefner
27196b3b05
Use escaped double quotes in npm scripts for Windows compatibility
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>
2026-02-02 09:05:40 -06:00
Jonathan Hefner
ec0f2174f3
Merge pull request #415 from jonathanhefner/csp-and-cors-example
Add CSP and CORS section to patterns docs
2026-01-30 14:47:37 -06:00
Jonathan Hefner
13b6d06399
Add CSP and CORS section to patterns docs
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>
2026-01-30 14:33:57 -06:00
Olivier Chafik
bb4e5c57f3
Cache full PDF bodies from servers without Range support (#411)
* 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>
2026-01-30 20:24:18 +00:00
Ido Salomon
0278af6a2a
examples: remove arcade (#414) 2026-01-30 22:06:50 +02:00
Jonathan Hefner
5c4f4969b1
Merge pull request #406 from jonathanhefner/extract-getUiCapability-example
Extract `getUiCapability` example to type-checked examples file
2026-01-30 13:52:31 -06:00
Jonathan Hefner
d7ff999d40
Merge pull request #404 from jonathanhefner/basic-server-css-variables
Improve example CSS files to better demonstrate host style integration
2026-01-30 13:52:17 -06:00
Jonathan Hefner
d37a789693
Merge pull request #409 from jonathanhefner/spec-types-api-docs
Improve documentation for CSP and permissions types
2026-01-30 13:51:42 -06:00
Jonathan Hefner
c02eb4d006
Improve documentation for CSP and permissions types
Add detailed JSDoc comments to `McpUiResourceCsp`, `McpUiResourcePermissions`,
and `McpUiResourceMeta` interfaces:

- Document CSP directive mappings for `connectDomains`, `resourceDomains`,
  `frameDomains`, and `baseUriDomains`
- Add usage examples showing typical domain patterns
- Clarify Permission Policy mappings for `camera`, `microphone`, `geolocation`,
  and `clipboardWrite`
- Explain `domain` field's host-dependent behavior with concrete examples
- Document `prefersBorder` semantics including true/false/omitted behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 11:44:31 -06:00
Jonathan Hefner
c3d91a13ad
Improve example CSS files to better demonstrate host style integration
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>
2026-01-30 09:32:26 -06:00
Jonathan Hefner
52cc193e1b
Extract getUiCapability example to type-checked examples file
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>
2026-01-29 19:55:33 -06:00
Jonathan Hefner
6905aa8edc
Improve global.css as scaffolding template (#403)
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>
2026-01-29 16:55:24 -06:00
Rafael Thayto
90b5ac520b
docs: fix basic-host setup instructions in quickstart (#398)
* 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>
2026-01-29 14:40:54 -06:00
Ido Salomon
adf0bbd611
examples: add arcade example to servers (#399) 2026-01-29 20:55:57 +02:00
Ido Salomon
2187bd4498
examples: add MCP Apps arcade server (#346) 2026-01-29 19:15:32 +02:00
Jonathan Hefner
e93f8a0d8b
Merge pull request #397 from jonathanhefner/patterns-guide-server-side-view-state
Add server-side persistence guidance to view state pattern
2026-01-29 10:28:56 -06:00
Jonathan Hefner
e4f1a56c76
Add server-side persistence guidance to view state pattern
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>
2026-01-29 10:04:05 -06:00
Jonathan Hefner
ef46849c09
Merge pull request #391 from jonathanhefner/add-blob-resource-to-patterns-guide
Add binary blob resource pattern to patterns guide
2026-01-28 15:05:15 -06:00
Jonathan Hefner
13615613d4
Add binary blob resource pattern to patterns guide
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>
2026-01-28 14:50:34 -06:00
Jonathan Hefner
246e1ab138
Merge pull request #389 from jonathanhefner/add-polling-to-patterns-guide
Add polling pattern to patterns guide
2026-01-28 14:30:10 -06:00
Gilad Shoham
7f617b94c1
docs: add root npm install step for basic-host setup (#371)
* docs: add root npm install step for basic-host setup

* Remove not needed npm install command
2026-01-28 21:39:23 +02:00
Jonathan Hefner
818bd71c9f
Add polling pattern to patterns guide
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>
2026-01-28 12:42:34 -06:00
Jonathan Hefner
6a2905d17d
Use note callouts for pattern guide example references
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>
2026-01-28 12:42:34 -06:00
Jonathan Hefner
55f0ef0198
Merge pull request #388 from jonathanhefner/system-monitor-static-vs-dynamic
Separate static and dynamic data in `system-monitor-server` tools
2026-01-28 12:41:38 -06:00
Jonathan Hefner
9acebd7f9f
Separate static and dynamic data in system-monitor-server tools
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>
2026-01-28 11:04:35 -06:00
Anton Pidkuiko
6028cf255f
Merge pull request #373 from modelcontextprotocol/ochafik/pdf-server-screenshot-context
feat(pdf-server): send screenshot to updateModelContext
2026-01-28 13:56:57 +00:00
Jonathan Hefner
782756c833
Merge pull request #381 from jonathanhefner/improve-patterns-guide
Improve patterns guide with concrete examples and safe area insets
2026-01-27 17:43:31 -06:00
Jonathan Hefner
c6c2428e0b
Improve patterns guide with concrete examples and safe area insets
- 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>
2026-01-27 17:32:41 -06:00
Jonathan Hefner
6c8379ff3e
Merge pull request #376 from jonathanhefner/fix-code-snippets-sync
Fix `extractRegion()` matching region names that are substrings of others
2026-01-27 13:09:38 -06:00
Jonathan Hefner
1215bf33c3
Fix extractRegion() matching region names that are substrings of others
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>
2026-01-27 12:57:11 -06:00
Jonathan Hefner
f90e0bd090
Merge pull request #375 from jonathanhefner/fix-shadertoy-readme-json
Use escaped newlines in ShaderToy README JSON examples
2026-01-27 11:57:00 -06:00
Jonathan Hefner
9b1da10da1
Use escaped newlines in ShaderToy README JSON examples
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>
2026-01-27 11:37:40 -06:00
Olivier Chafik
d6705d970d
feat(pdf-server): send screenshot to updateModelContext
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
2026-01-27 14:05:18 +00:00
Konstantin Konstantinov
47a5787ec3
return registered app resource from method (#370) 2026-01-27 09:58:19 +00:00
Olivier Chafik
605e31b6c5
chore: bump ext-apps to 1.0.1 (#369)
* 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
2026-01-26 22:32:34 +00:00
Olivier Chafik
ae9b96e1fd
chore: bump to 1.0.1 - fix npm reference for ext-apps dependency in examples + update zod to 4.x (#368)
* 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
2026-01-26 21:55:15 +00:00
Olivier Chafik
01fa6287bf
fix(say-server): pin mcp to released version instead of main branch (#366)
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>
2026-01-26 18:55:26 +00:00
Maurizio Albari
f9abb74fc0
fix(qr-server): use stable mcp SDK from PyPI (#360)
* 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
2026-01-26 18:52:10 +00:00
Ido Salomon
aec5240f22
docs: update MCP-UI SDK description in README (#365)
Clarified the description of the MCP-UI client SDK and its usage.
2026-01-26 19:44:48 +02:00
Olivier Chafik
298e884ec3
chore: stable spec release 2026-01-26 and SDK v1.0.0 (#362)
* 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
2026-01-26 17:31:33 +00:00
Jonathan Hefner
dc948d1af9
Merge pull request #363 from jonathanhefner/fix-claude-docs-links
Fix Claude documentation URLs
2026-01-26 11:08:47 -06:00
Jonathan Hefner
9c0ba003b4
Fix Claude documentation URLs
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>
2026-01-26 11:02:25 -06:00
Olivier Chafik
d03123bb5c
docs: add local development MCP client configuration to READMEs (#361)
* 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
2026-01-26 15:22:00 +00:00
Matthew Wang
d27acc42b1 Add and helper functions 2026-01-16 21:20:10 -08:00
317 changed files with 39533 additions and 4894 deletions

View file

@ -10,6 +10,36 @@ permissions:
contents: read
jobs:
prettier-fix:
name: Auto-fix formatting
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
token: ${{ github.token }}
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm run prettier:fix
- name: Commit formatting changes
run: |
git diff --quiet && exit 0
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "style: auto-fix prettier formatting"
git push
build:
strategy:
fail-fast: false
@ -30,7 +60,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Verify no private URLs in package-lock.json
shell: bash
@ -40,14 +70,19 @@ jobs:
shell: bash
run: node scripts/check-versions.mjs
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "20"
node-version: "22"
cache: npm
- run: npm install
- run: npm ci
- run: npm run build
- name: Validate TypeDoc links
if: runner.os == 'Linux' && matrix.name == 'Linux x64'
run: npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
- run: npm run examples:build
- name: Verify generated schemas are up-to-date
@ -66,31 +101,62 @@ jobs:
- run: npm run prettier
- name: Build MCPB bundle (pdf-server)
if: runner.os == 'Linux' && matrix.name == 'Linux x64'
run: node build-mcpb.mjs
working-directory: examples/pdf-server
- name: Smoke-test MCPB bundle starts
if: runner.os == 'Linux' && matrix.name == 'Linux x64'
run: |
unzip -q pdf-server.mcpb -d .mcpb-smoke
node .mcpb-smoke/dist/index.js --stdio < /dev/null 2>&1 | tee /tmp/mcpb-smoke.log
grep -q "Ready" /tmp/mcpb-smoke.log
working-directory: examples/pdf-server
dependency-isolation:
name: Dependency isolation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm run test:dependency-isolation
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "20"
node-version: "22"
cache: npm
- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@v7
- run: npm ci
# Headless runs without a channel use chromium-headless-shell, so the
# full Chromium build (about 165 MB) would only be downloaded, never run.
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
run: npx playwright install --with-deps --only-shell chromium
- name: Run E2E tests
run: npx playwright test --reporter=list
- name: Upload test results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: failure()
with:
name: test-results
@ -103,9 +169,9 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: Vampire/setup-wsl@v5
- uses: Vampire/setup-wsl@v6
with:
distribution: Ubuntu-24.04
@ -113,13 +179,18 @@ jobs:
shell: wsl-bash {0}
run: |
sudo apt-get update
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# The checkout lives on the Windows drive, which WSL reaches over 9P;
# writing node_modules there is what makes this job slow (npm ci alone
# took about 3 minutes). Copy the tree onto the distro's ext4 filesystem
# first, as Microsoft's WSL guidance recommends for anything I/O heavy.
- name: Build and test in WSL
shell: wsl-bash {0}
run: |
npm install
cp -r "$(pwd)" ~/ext-apps && cd ~/ext-apps
npm ci
npm run build
npm run examples:build
npm test
@ -146,17 +217,20 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "20"
node-version: "22"
- name: Create test project and install from git
shell: bash
env:
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
HEAD_REF: ${{ github.head_ref || github.ref_name }}
run: |
mkdir test-project
cd test-project
npm init -y
# Install from the PR branch (use head repo for fork PRs)
npm install "git+https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }}#${{ github.head_ref || github.ref_name }}"
npm install "git+https://github.com/${HEAD_REPO}#${HEAD_REF}"
# Verify the package is usable (ESM import)
node --input-type=module -e "import { App } from '@modelcontextprotocol/ext-apps'; console.log('Import successful:', typeof App)"

65
.github/workflows/docs-preview.yml vendored Normal file
View file

@ -0,0 +1,65 @@
name: Docs Preview
on:
pull_request:
types: [opened, synchronize, reopened, closed]
paths:
- "docs/**"
- "src/**"
- "README.md"
- "typedoc.config.mjs"
- "scripts/typedoc-*.mjs"
- ".github/workflows/docs-preview.yml"
permissions:
contents: read
pull-requests: write
concurrency:
group: docs-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build-and-deploy:
# Refuse to run for fork PRs — forks do not have access to the Cloudflare
# secrets, so deploys would fail anyway.
if: >-
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm run build
- run: npm run docs
- name: Deploy preview
uses: modelcontextprotocol/actions/cloudflare-pages-preview/deploy@main
with:
directory: docs
project-name: mcp-ext-apps-docs-preview
api-token: ${{ secrets.CF_PAGES_PREVIEW_API_TOKEN }}
account-id: ${{ secrets.CF_PAGES_PREVIEW_ACCOUNT_ID }}
comment-title: "📖 Docs Preview Deployed"
comment-marker: "<!-- docs-preview-comment -->"
cleanup:
if: >-
github.event.action == 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: modelcontextprotocol/actions/cloudflare-pages-preview/cleanup@main
with:
project-name: mcp-ext-apps-docs-preview
api-token: ${{ secrets.CF_PAGES_PREVIEW_API_TOKEN }}
account-id: ${{ secrets.CF_PAGES_PREVIEW_ACCOUNT_ID }}
comment-marker: "<!-- docs-preview-comment -->"

View file

@ -12,12 +12,13 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "20"
- run: npm install
node-version: "22"
cache: npm
- run: npm ci
- run: npm run build
- run: npm run docs
- uses: peaceiris/actions-gh-pages@v4
@ -26,3 +27,4 @@ jobs:
publish_dir: ./docs
publish_branch: gh-pages
keep_files: true
cname: apps.extensions.modelcontextprotocol.io

View file

@ -14,11 +14,11 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
@ -31,17 +31,21 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm test
# All publish jobs depend directly on [build, test] (not on each other) so
# they all enter "waiting for approval" together and can be approved in a
# single "Review deployments" click.
publish:
runs-on: ubuntu-latest
if: github.event_name == 'release'
@ -53,11 +57,11 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
@ -88,7 +92,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: Release
needs: [publish]
needs: [build, test]
permissions:
contents: read
@ -107,6 +111,8 @@ jobs:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- debug-server
- lazy-auth-server
- map-server
- pdf-server
- scenario-modeler-server
@ -119,11 +125,11 @@ jobs:
- wiki-explorer-server
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
@ -133,7 +139,45 @@ jobs:
- name: Build example
run: npm run build --workspace examples/${{ matrix.example }}
- name: Determine npm tag
id: npm-tag
run: |
VERSION=$(node -p "require('./package.json').version")
if [[ "$VERSION" == *"-"* ]]; then
echo "tag=--tag beta" >> $GITHUB_OUTPUT
else
echo "tag=" >> $GITHUB_OUTPUT
fi
- name: Publish example
run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public
run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public ${{ steps.npm-tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
publish-mcpb:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: Release
needs: [build, test]
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- run: npm ci
- name: Build pdf-server + MCPB bundle
run: npm run build:mcpb --workspace examples/pdf-server
- name: Upload MCPB to release
run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber
env:
GH_TOKEN: ${{ github.token }}

View file

@ -13,8 +13,8 @@ jobs:
pkg-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
@ -24,11 +24,17 @@ jobs:
- run: |
npx pkg-pr-new publish \
. \
./examples/basic-server-preact \
./examples/basic-server-react \
./examples/basic-server-solid \
./examples/basic-server-svelte \
./examples/basic-server-vanillajs \
./examples/basic-server-vue \
./examples/budget-allocator-server \
./examples/cohort-heatmap-server \
./examples/customer-segmentation-server \
./examples/debug-server \
./examples/lazy-auth-server \
./examples/map-server \
./examples/pdf-server \
./examples/scenario-modeler-server \

View file

@ -16,16 +16,20 @@ permissions:
jobs:
update-snapshots:
# Run on workflow_dispatch OR when someone comments "/update-snapshots" on a PR
# Run on workflow_dispatch OR when a maintainer comments "/update-snapshots" on a PR.
# author_association check prevents arbitrary commenters from triggering a job with
# contents:write that pushes a [skip ci] commit to the PR branch.
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.issue.pull_request && contains(github.event.comment.body, '/update-snapshots'))
(github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-snapshots') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
steps:
- name: Get PR branch
if: github.event_name == 'issue_comment'
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const pr = await github.rest.pulls.get({
@ -38,7 +42,7 @@ jobs:
- name: Add reaction to comment
if: github.event_name == 'issue_comment'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
await github.rest.reactions.createForIssueComment({
@ -48,7 +52,7 @@ jobs:
content: 'rocket'
});
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.branch || steps.pr.outputs.ref || github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
@ -57,11 +61,12 @@ jobs:
with:
bun-version: latest
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "20"
node-version: "22"
cache: npm
- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@v7
- run: npm ci
@ -88,7 +93,7 @@ jobs:
- name: Comment on PR
if: github.event_name == 'issue_comment'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const changed = '${{ steps.commit.outputs.changed }}' === 'true';

7
.gitignore vendored
View file

@ -12,4 +12,9 @@ intermediate-findings/
playwright-report/
test-results/
__pycache__/
*.pyc
*.pyc
# MCPB bundles (built artifacts)
*.mcpb
.mcpb-stage/
.mcpb-smoke/

View file

@ -14,8 +14,16 @@ if grep -E '"resolved": "https?://' package-lock.json | grep -v registry.npmjs.o
exit 1
fi
# Capture staged files so we only re-stage what the user intended to commit
# (avoids sweeping unrelated WIP into the commit). --diff-filter=d skips
# deletions so the xargs git add below doesn't fail on removed paths.
STAGED=$(git diff --name-only --cached --diff-filter=d)
npm run build:all
npm run prettier:fix
# Stage any changes to generated files (they may have been reformatted by prettier)
# Re-stage originally-staged files (prettier may have reformatted them)
echo "$STAGED" | xargs -r git add
# Also stage generated files (regenerated by build:all from spec.types.ts)
git add src/generated/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
registry=https://registry.npmjs.org/

View file

@ -4,4 +4,6 @@ examples/basic-server-*/**/*.ts
examples/basic-server-*/**/*.tsx
examples/quickstart/**/*.ts
**/vendor/**
**/.build/**
**/.claude/**
SKILL.md

View file

@ -10,7 +10,7 @@ Key abstractions:
- **Host** - Chat client embedding the iframe, uses `AppBridge` class to proxy MCP requests
- **Server** - MCP server that registers tools/resources with UI metadata
Specification (draft): `specification/draft/apps.mdx`
Specification (stable): `specification/2026-01-26/apps.mdx`
## Commands
@ -39,7 +39,10 @@ npm test
# Check JSDoc comment syntax and `{@link}` references
npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
# Regenerate package-lock.json (especially on setups w/ custom npm registry)
# Regenerate package-lock.json
# Note: repo .npmrc pins registry to npmjs.org, so a plain `npm i` is safe even
# if your global npm config points elsewhere. The Docker step below is optional
# — it locks linux-amd64 optionalDependencies (sharp, rollup, bun) for CI.
rm -fR package-lock.json node_modules && \
docker run --rm -it --platform linux/amd64 -v $PWD:/src:rw -w /src node:latest npm i && \
rm -fR node_modules && \
@ -57,8 +60,8 @@ rm -fR package-lock.json node_modules && \
### Key Source Files
- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
- `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
@ -67,12 +70,12 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow
```
View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
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` request, receives host capabilities and context
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

View file

@ -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,6 +49,408 @@ 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>
```json
{
"mcpServers": {
"basic-react": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-react",
"--stdio"
]
},
"basic-vanillajs": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-vanillajs",
"--stdio"
]
},
"basic-vue": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-vue",
"--stdio"
]
},
"basic-svelte": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-svelte",
"--stdio"
]
},
"basic-preact": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-preact",
"--stdio"
]
},
"basic-solid": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-solid",
"--stdio"
]
},
"budget-allocator": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-budget-allocator",
"--stdio"
]
},
"cohort-heatmap": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-cohort-heatmap",
"--stdio"
]
},
"customer-segmentation": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-customer-segmentation",
"--stdio"
]
},
"map": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-map",
"--stdio"
]
},
"pdf": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-pdf",
"--stdio"
]
},
"scenario-modeler": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-scenario-modeler",
"--stdio"
]
},
"shadertoy": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-shadertoy",
"--stdio"
]
},
"sheet-music": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-sheet-music",
"--stdio"
]
},
"system-monitor": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-system-monitor",
"--stdio"
]
},
"threejs": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-threejs",
"--stdio"
]
},
"transcript": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-transcript",
"--stdio"
]
},
"video-resource": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-video-resource",
"--stdio"
]
},
"wiki-explorer": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-wiki-explorer",
"--stdio"
]
},
"qr": {
"command": "uv",
"args": [
"run",
"/path/to/ext-apps/examples/qr-server/server.py",
"--stdio"
]
},
"say": {
"command": "uv",
"args": [
"run",
"--default-index",
"https://pypi.org/simple",
"https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/refs/heads/main/examples/say-server/server.py",
"--stdio"
]
}
}
}
```
</details>
> [!NOTE]
> 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:
```bash
git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps
npm install
```
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
@ -91,7 +502,7 @@ Alternatively, use the [workflow dispatch](https://github.com/modelcontextprotoc
## 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.
## Reporting Issues
@ -128,40 +539,17 @@ Before publishing releases, ensure the following are configured:
### 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
@ -185,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.
---

472
README.md
View file

@ -1,67 +1,185 @@
# @modelcontextprotocol/ext-apps
<!-- LOGO -->
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="media/mcp-white.svg">
<source media="(prefers-color-scheme: light)" srcset="media/mcp.svg">
<img src="media/mcp.svg" alt="MCP Apps" width="128">
</picture>
<h1>MCP Apps</h1>
<p>
Build interactive UIs for MCP tools — charts, forms, dashboards — that render inline in Claude, ChatGPT and any other compliant chat client.
<br /><br />
<a href="#why-mcp-apps">Why</a>
·
<a href="https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html">Quickstart</a>
·
<a href="https://apps.extensions.modelcontextprotocol.io/api/">API Docs</a>
·
<a href="https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx">Spec</a>
·
<a href="CONTRIBUTING.md">Contributing</a>
</p>
</div>
[![npm version](https://img.shields.io/npm/v/@modelcontextprotocol/ext-apps.svg)](https://www.npmjs.com/package/@modelcontextprotocol/ext-apps) [![API Documentation](https://img.shields.io/badge/docs-API%20Reference-blue)](https://modelcontextprotocol.github.io/ext-apps/api/)
<p align="center">
<a href="https://github.com/modelcontextprotocol/ext-apps/actions/workflows/ci.yml"><img src="https://github.com/modelcontextprotocol/ext-apps/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue" alt="License: Apache 2.0"></a>
<a href="https://www.npmjs.com/package/@modelcontextprotocol/ext-apps"><img src="https://img.shields.io/npm/v/@modelcontextprotocol/ext-apps.svg" alt="npm version"></a>
<a href="https://www.npmjs.com/package/@modelcontextprotocol/ext-apps"><img src="https://img.shields.io/npm/dm/@modelcontextprotocol/ext-apps.svg" alt="npm downloads"></a>
<a href="https://github.com/modelcontextprotocol/ext-apps"><img src="https://img.shields.io/github/stars/modelcontextprotocol/ext-apps" alt="GitHub stars"></a>
<a href="https://apps.extensions.modelcontextprotocol.io/api/"><img src="https://img.shields.io/badge/docs-API%20Reference-blue" alt="API Documentation"></a>
</p>
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)).
<p align="center">
<img src="media/excalidraw.gif" alt="MCP Apps demo" width="600">
<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
## Why MCP Apps?
- [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)
MCP tools return text and structured data. That works for many cases, but not when you need an interactive UI, like a chart, form, or video player.
## Build with Agent Skills
MCP Apps provide a standardized way to deliver interactive UIs from MCP servers. Your UI renders inline in the conversation, in context, in any compliant host.
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:
## How It Works
| Skill | What it does | Try it |
| --------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------- |
| [`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"_ |
MCP Apps extend the Model Context Protocol by letting tools declare UI resources:
### Install the Skills
1. **Tool definition** — Your tool declares a `ui://` resource containing its HTML interface
2. **Tool call** — The LLM calls the tool on your server
3. **Host renders** — The host fetches the resource and displays it in a sandboxed iframe
4. **Bidirectional communication** — The host passes tool data to the UI via notifications, and the UI can call other tools through the host
## Using the SDK
This SDK serves two audiences:
### For App Developers
Build interactive UIs that run inside MCP-enabled chat clients.
- **SDK for Apps**: `@modelcontextprotocol/ext-apps` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/app.html)
- **React hooks**: `@modelcontextprotocol/ext-apps/react` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/_modelcontextprotocol_ext-apps_react.html)
### For Host Developers
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
```bash
npm install -S @modelcontextprotocol/ext-apps
```
### Install Agent Skills
This repository provides two [Agent Skills](https://agentskills.io/) for building MCP Apps. You can install the skills as a Claude Code plugin:
**Claude Code** — install via the plugin marketplace:
```
/plugin marketplace add modelcontextprotocol/ext-apps
/plugin install mcp-apps@modelcontextprotocol-ext-apps
```
For more information, including instructions for installing the skills in your favorite AI coding agent, see the [agent skills guide](./docs/agent-skills.md).
**Other agents** — any AI coding agent that supports
[Agent Skills](https://agentskills.io/) can use these skills. See the
[agent skills guide](./docs/agent-skills.md) for manual installation
instructions.
Once installed, verify by asking your agent _"What skills do you have?"_ — you
should see `create-mcp-app`, `migrate-oai-app`, `add-app-to-server`, and
`convert-web-app` in the list. Then just ask it to create or migrate an app and
it will guide you through the rest.
## Supported Clients
<p align="center">
<a href="https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt/"><img src="https://img.shields.io/badge/ChatGPT-docs-74aa9c?logo=openai&logoColor=white" alt="ChatGPT"></a>
<a href="https://claude.com/docs/connectors/building/mcp-apps/getting-started"><img src="https://img.shields.io/badge/Claude-docs-d97706?logo=claude&logoColor=white" alt="Claude"></a>
<a href="https://code.visualstudio.com/blogs/2026/01/26/mcp-apps-support"><img src="https://img.shields.io/badge/VS_Code-docs-007ACC?logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
<a href="https://block.github.io/goose/docs/tutorials/building-mcp-apps/"><img src="https://img.shields.io/badge/Goose-docs-000000?logo=goose&logoColor=white" alt="Goose"></a>
<a href="https://learning.postman.com/docs/postman-ai/mcp-requests/interact"><img src="https://img.shields.io/badge/Postman-docs-FF6C37?logo=postman&logoColor=white" alt="Postman"></a>
<a href="https://www.mcpjam.com/blog/mcp-apps-example"><img src="https://img.shields.io/badge/MCPJam-docs-8B5CF6" alt="MCPJam"></a>
<a href="https://manufact.com/inspector"><img src="https://img.shields.io/badge/mcp--use-inspector-E5F3FD" alt="mcp-use"></a>
<a href="https://docs.alpic.ai/distribution/playground"><img src="https://img.shields.io/badge/Alpic-playground-E90060?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAKlBMVEUKDhz%2F%2F%2F%2BjpaogJDFBRE%2Fw8PHHyMvh4eNmaHF1d4CUlpuFho2ztLhWWWP5YkbJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB1klEQVR42u2Y0ZKDIAxFBQQF8f9%2FdyW7dhAu7U7Iy87mzvShGfXU3CTGLotKpVKpVCqV6h%2FI2VuxfF3tUwcdVEccA2LNS%2BWCq2mUykG%2BCtg5iNmv76mllOApCPHrFQgNxJf8HHIQs10XdFtDKcEKPQ0x4brg6htKWGr0PIR8jq0tj4oQgJiIzD8rtATErMAWMj%2FxIYcBPjuPgoENcW3Jks%2BdLeXS32jLmise%2BZxQ8GRD%2BkkCbSlByi0Pgm3p0P6nJ1meXJ%2F8K1tKo7qUOFM4wdxEMMS4iSp9sqFJQrZ4hOZBTEa5gbaQ%2BTwI%2FcCu8TOq482xIWZkSx8MfAi0xcOEJTZkULKohXjm29epGU2SIGK%2BfT9J%2Bjre%2BRBYsiUYBSHkaEI2B0EInRx%2BkbApiB89EaMgBNdx7u5vDoJH79ne3yQElqxvEzYLKY%2Bl81PCZiFUsvlDwqYhw80uSkJg4z8Ttsss3OltwgQgMGGPlpSAwITVM0wCgius6iARCGzJcn%2BHJCSMXraCIATPsPW2SggCh364a1sIgveUew%2BTgtBGsuE9zDspyLBZ3M5aVCOCUDFlqXUbrm8j7%2FnvJ2B9G3kf%2BZBlt0iu%2Fr%2BNxHqRU6lUKpVKpVL9UX0BOrAPfcbTY8UAAAAASUVORK5CYII%3D" alt="Alpic Playground"></a>
</p>
> [!NOTE]
> MCP Apps is an extension to the
> [core MCP specification](https://modelcontextprotocol.io/specification). Host
> support varies — see the
> [clients page](https://modelcontextprotocol.io/clients) for the full list.
## Why MCP Apps?
MCP tools return text and structured data. That works for many cases, but not
when you need an interactive UI, like a chart, form, design canvas or video player.
MCP Apps provide a standardized way to deliver interactive UIs from MCP servers.
Your UI renders inline in the conversation, in context, in any compliant host.
## How It Works
MCP Apps extend the Model Context Protocol by letting tools declare UI
resources:
1. **Tool definition** — Your tool declares a `ui://` resource containing its
HTML interface
2. **Tool call** — The LLM calls the tool on your server
3. **Host renders** — The host fetches the resource and displays it in a
sandboxed iframe
4. **Bidirectional communication** — The host passes tool data to the UI via
notifications, and the UI can call other tools through the host
## Getting Started
Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of
`ext-apps` (`@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it for you).
For a View or host:
```bash
npm install -S @modelcontextprotocol/ext-apps \
@modelcontextprotocol/client@^2.0.0 \
zod@^4.2.0
```
For an MCP server, add the server package (and, for HTTP transports, the Node
and Express adapters):
```bash
npm install -S @modelcontextprotocol/ext-apps \
@modelcontextprotocol/client@^2.0.0 \
@modelcontextprotocol/server@^2.0.0 \
@modelcontextprotocol/node@^2.0.0 \
@modelcontextprotocol/express@^2.0.0 \
zod@^4.2.0
```
The wire protocol is unchanged between `ext-apps` 1.x and 2.x: a 2.x View works
in a 1.x host and a 2.x host renders 1.x Views. See the
[migration guide](https://apps.extensions.modelcontextprotocol.io/api/documents/migrate-to-v2.html)
when upgrading from 1.x.
**New here?** Start with the
[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
to build your first MCP App.
## Using the SDK
The SDK serves three roles: app developers building interactive Views, host
developers embedding those Views, and MCP server authors registering tools with
UI metadata.
| Package | Purpose | Docs |
| ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `@modelcontextprotocol/ext-apps` | Build interactive Views (App class, PostMessageTransport) | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/app.html) |
| `@modelcontextprotocol/ext-apps/react` | React hooks for Views (useApp, useHostStyles, etc.) | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/_modelcontextprotocol_ext-apps_react.html) |
| `@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
[examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host)
example).
The [MCP-UI](https://github.com/idosal/mcp-ui) client SDK offers a
fully-featured MCP Apps framework used by a few hosts. Clients may choose to use
it or roll their own implementation.
## Examples
The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains demo apps showcasing real-world use cases.
The
[`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples)
directory contains demo apps showcasing real-world use cases.
<!-- prettier-ignore-start -->
| | | |
@ -88,7 +206,9 @@ The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/exa
#### With basic-host
To run all examples locally using [basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) (the reference host implementation included in this repo):
To run all examples locally using
[basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host)
(the reference host implementation included in this repo):
```bash
git clone https://github.com/modelcontextprotocol/ext-apps.git
@ -101,234 +221,64 @@ Then open http://localhost:8080/.
#### 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>
Every Node.js example is published as `@modelcontextprotocol/server-<name>`. To
add one to an MCP client that supports stdio (Claude Desktop, VS Code, etc.),
use this pattern:
```json
{
"mcpServers": {
"basic-react": {
"<name>": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-<name>", "--stdio"]
}
}
}
```
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.
#### Local Development
To test local modifications with an MCP client, clone the repo, install, then
point your client at a local build:
```json
{
"mcpServers": {
"<name>": {
"command": "bash",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-react",
"--stdio"
]
},
"basic-vanillajs": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-vanillajs",
"--stdio"
]
},
"basic-vue": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-vue",
"--stdio"
]
},
"basic-svelte": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-svelte",
"--stdio"
]
},
"basic-preact": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-preact",
"--stdio"
]
},
"basic-solid": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-basic-solid",
"--stdio"
]
},
"budget-allocator": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-budget-allocator",
"--stdio"
]
},
"cohort-heatmap": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-cohort-heatmap",
"--stdio"
]
},
"customer-segmentation": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-customer-segmentation",
"--stdio"
]
},
"map": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-map",
"--stdio"
]
},
"pdf": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-pdf",
"--stdio"
]
},
"scenario-modeler": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-scenario-modeler",
"--stdio"
]
},
"shadertoy": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-shadertoy",
"--stdio"
]
},
"sheet-music": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-sheet-music",
"--stdio"
]
},
"system-monitor": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-system-monitor",
"--stdio"
]
},
"threejs": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-threejs",
"--stdio"
]
},
"transcript": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-transcript",
"--stdio"
]
},
"video-resource": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-video-resource",
"--stdio"
]
},
"wiki-explorer": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-wiki-explorer",
"--stdio"
]
},
"qr": {
"command": "uv",
"args": [
"run",
"/path/to/ext-apps/examples/qr-server/server.py",
"--stdio"
]
},
"say": {
"command": "uv",
"args": [
"run",
"--default-index",
"https://pypi.org/simple",
"https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/refs/heads/main/examples/say-server/server.py",
"--stdio"
"-c",
"cd ~/code/ext-apps/examples/<name>-server && npm run build >&2 && node dist/index.js --stdio"
]
}
}
}
```
</details>
## Specification
> [!NOTE]
> 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.
<div align="center">
| Version | Status | Link |
| -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **2026-01-26** | Stable | [specification/2026-01-26/apps.mdx](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx) |
| **draft** | Development | [specification/draft/apps.mdx](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) |
</div>
## Resources
- [Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html)
- [API Documentation](https://modelcontextprotocol.github.io/ext-apps/api/)
- [Draft Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
- [API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)
- [Specification (2026-01-26)](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx)
([Draft](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx))
- [SEP-1865 Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)
## Contributing
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for
guidelines on how to get started, submit pull requests, and report issues.

21
SECURITY.md Normal file
View file

@ -0,0 +1,21 @@
# Security Policy
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)

View file

@ -1,9 +1,18 @@
#!/usr/bin/env bun
import { $ } from "bun";
import { cpSync, mkdirSync, rmSync } from "node:fs";
// Avoid publishing artifacts left behind by an earlier build or branch.
rmSync("dist", { recursive: true, force: true });
// Run TypeScript compiler for type declarations
await $`tsc`;
// Copy schema.json (tsc is emitDeclarationOnly, Bun.build doesn't emit JSON assets).
// Needed for the "./schema.json" package export.
mkdirSync("dist/src/generated", { recursive: true });
cpSync("src/generated/schema.json", "dist/src/generated/schema.json");
const isDevelopment = Bun.env.NODE_ENV === "development";
// Build all JavaScript/TypeScript files
@ -25,10 +34,20 @@ function buildJs(
});
}
// Peer dependencies stay external in the standard entry points so consumers
// share one base MCP SDK and Zod instance. The *-with-deps entry points keep
// bundling these dependencies for standalone browser use.
const PEER_EXTERNALS = [
"@modelcontextprotocol/client",
"@modelcontextprotocol/core",
"@modelcontextprotocol/server",
"zod",
];
await Promise.all([
buildJs("src/app.ts", {
outdir: "dist/src",
external: ["@modelcontextprotocol/sdk"],
external: PEER_EXTERNALS,
}),
buildJs("src/app.ts", {
outdir: "dist/src",
@ -36,19 +55,19 @@ await Promise.all([
}),
buildJs("src/app-bridge.ts", {
outdir: "dist/src",
external: ["@modelcontextprotocol/sdk"],
external: PEER_EXTERNALS,
}),
buildJs("src/react/index.tsx", {
outdir: "dist/src/react",
external: ["react", "react-dom", "@modelcontextprotocol/sdk"],
external: ["react", "react-dom", ...PEER_EXTERNALS],
}),
buildJs("src/react/index.tsx", {
outdir: "dist/src/react",
external: ["react", "react-dom", "@modelcontextprotocol/sdk"],
external: ["react", "react-dom"],
naming: { entry: "react-with-deps.js" },
}),
buildJs("src/server/index.ts", {
outdir: "dist/src/server",
external: ["@modelcontextprotocol/sdk"],
external: PEER_EXTERNALS,
}),
]);

View file

@ -1,15 +1,19 @@
---
title: Agent Skills
group: Getting Started
description: Use Agent Skills to build, migrate, and extend MCP Apps with AI coding agents. Install skills that scaffold new apps, convert OpenAI Apps, and add UI to existing servers.
---
# Agent Skills
[Agent Skills](https://agentskills.io/) are instruction sets that guide AI coding agents through tasks. When you invoke a skill, the agent takes the lead — it asks clarifying questions, makes decisions based on your codebase, and executes the work.
This repository provides two skills:
This repository provides four skills:
- [**create-mcp-app**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/create-mcp-app/SKILL.md) — scaffolds a new MCP App with an interactive UI
- [**migrate-oai-app**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/migrate-oai-app/SKILL.md) — migrates an existing OpenAI App to the MCP Apps SDK
- [**add-app-to-server**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/add-app-to-server/SKILL.md) — adds interactive UI to an existing MCP server's tools
- [**convert-web-app**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/convert-web-app/SKILL.md) — converts an existing web application into an MCP App
## Install the Skills
@ -51,7 +55,7 @@ Then copy the skills from `plugins/mcp-apps/skills/` to your agent's skills dire
## Verify Installation
Ask your agent "What skills do you have?" — you should see `create-mcp-app` and `migrate-oai-app` among the available skills.
Ask your agent "What skills do you have?" — you should see `create-mcp-app`, `migrate-oai-app`, `add-app-to-server`, and `convert-web-app` among the available skills.
## Invoke a Skill
@ -59,6 +63,8 @@ Try invoking the skills by asking your agent:
- "Create an MCP App" — scaffolds a new MCP App with an interactive UI
- "Migrate from OpenAI Apps SDK" — converts an existing OpenAI App to use the MCP Apps SDK
- "Add UI to my MCP server" — adds interactive UI to an existing MCP server's tools
- "Convert my web app to an MCP App" — converts an existing web application into an MCP App
The agent will guide you through the process, asking clarifying questions as needed.

211
docs/authorization.md Normal file
View file

@ -0,0 +1,211 @@
---
title: Authorization
group: Security
description: Learn how to protect MCP App tools with OAuth authorization, including per-server and per-tool auth patterns, token verification, and UI-initiated auth escalation.
---
# Authorization
MCP Apps can protect tools behind OAuth-based authorization, as defined in the [MCP specification](https://modelcontextprotocol.io/specification/latest/basic/authorization). There are two approaches:
- **Per-server authorization** — The entire MCP server requires authorization at connection time. Every request must include a valid token, regardless of which tool is being called. This is the simpler model when all tools are sensitive.
- **Per-tool authorization** — Only specific tools require authorization. Public tools work without a token, and the OAuth flow is triggered only when the user calls a protected tool. This lets you mix public and protected tools in the same server.
## Shared setup
Regardless of which approach you choose, you need OAuth discovery metadata and token verification. These are the same for both.
### OAuth discovery metadata
The MCP specification requires servers to implement [authorization server discovery](https://modelcontextprotocol.io/specification/latest/basic/authorization#authorization-server-discovery) so clients know how to obtain authorization. Two well-known endpoints are needed:
**[Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)** (`/.well-known/oauth-protected-resource`) — describes the resource server and identifies which authorization server(s) can issue tokens for it. The MCP SDK's `mcpAuthRouter` handles this automatically.
**Authorization Server Metadata** (`/.well-known/oauth-authorization-server`) — advertises the authorization and token endpoints, supported scopes, and whether [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/latest/basic/authorization#client-id-metadata-documents) (CIMD) is supported:
```ts
app.get("/.well-known/oauth-authorization-server", (_req, res) => {
res.json({
...oauthMetadata,
client_id_metadata_document_supported: true,
});
});
```
Setting `client_id_metadata_document_supported: true` tells MCP clients to use CIMD instead of [Dynamic Client Registration](https://modelcontextprotocol.io/specification/latest/basic/authorization#dynamic-client-registration) (DCR). With CIMD, the `client_id` is a URL that serves the client's metadata document, removing the need for a registration endpoint. See [Client Registration Approaches](https://modelcontextprotocol.io/specification/latest/basic/authorization#client-registration-approaches) in the spec for the full list of options and priority order.
### Token verification
Verify access tokens as JWTs against the identity provider's JWKS endpoint. The `jose` library handles key fetching and caching:
```ts
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL(`${IDP_DOMAIN}/.well-known/jwks.json`));
const { payload } = await jwtVerify(token, JWKS, {
issuer: IDP_DOMAIN,
});
```
MCP servers must validate that tokens were issued specifically for them — see [Token Handling](https://modelcontextprotocol.io/specification/latest/basic/authorization#token-handling) and [Access Token Privilege Restriction](https://modelcontextprotocol.io/specification/latest/basic/authorization#access-token-privilege-restriction) in the spec for the full requirements.
## Per-server authorization
With per-server authorization, every request to the `/mcp` endpoint must include a valid Bearer token. Any unauthorized request receives HTTP `401`, and the host must complete the OAuth flow before the client can use any tools. This is the right choice when all tools are sensitive and there's no value in allowing unauthorized access.
The TypeScript MCP SDK supports this out of the box via `mcpAuthRouter` and `ProxyOAuthServerProvider` — no custom HTTP handler logic is needed. See the [MCP SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk) for setup details.
## Per-tool authorization
With per-tool authorization, the `/mcp` endpoint handler inspects the raw JSON-RPC request body, checks whether any message targets a protected tool, and only enforces authorization for those calls. Public tools pass through without a token.
### How it works
1. The server maintains a set of tool names that require authorization
2. When a JSON-RPC request arrives at the `/mcp` endpoint, the server inspects the request body to determine if any message is a `tools/call` targeting a protected tool
3. If a protected tool is being called and no valid Bearer token is present, the server returns HTTP `401` with a [`WWW-Authenticate` header](https://modelcontextprotocol.io/specification/latest/basic/authorization#protected-resource-metadata-discovery-requirements) pointing to its [Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
4. The MCP host (e.g., Claude Desktop) sees the `401`, discovers the authorization server via the metadata URL, runs the [OAuth flow](https://modelcontextprotocol.io/specification/latest/basic/authorization#authorization-flow-steps) with the user, and retries the request with the acquired token
5. On retry, the server verifies the token, extracts the user identity, and creates a per-request MCP server instance with that auth context
6. Unprotected tools pass through without any token check — they work for everyone
This design means authorization is enforced at the HTTP boundary (as [required by the spec](https://modelcontextprotocol.io/specification/latest/basic/authorization#access-token-usage)), not as a tool-level error. The MCP server itself never sees unauthorized requests for protected tools.
### Enforcing HTTP 401
The [MCP auth specification](https://modelcontextprotocol.io/specification/latest/basic/authorization#access-token-usage) requires protected resources to return HTTP `401` responses — not tool-level errors.
Start by defining which tools require authorization. Then, in the `/mcp` endpoint handler, inspect the raw JSON-RPC request body, check whether any message targets a protected tool, and either verify the Bearer token or return `401` before the request ever reaches the MCP server:
```ts
/** Tools that require a valid Bearer token — checked at the HTTP level for proper 401. */
const PROTECTED_TOOLS = new Set(["get_account_balance", "manage_branch_admin"]);
app.all("/mcp", async (req, res) => {
// Parse the JSON-RPC body — it may be a single message or a batch
const messages = Array.isArray(req.body) ? req.body : [req.body];
// Check if any message is a tools/call for a protected tool
const needsAuth = messages.some(
(msg: any) =>
msg?.method === "tools/call" && PROTECTED_TOOLS.has(msg.params?.name),
);
// Extract and verify the Bearer token
let authInfo: AuthInfo | undefined;
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
try {
const token = authHeader.slice(7);
const { payload } = await jwtVerify(token, JWKS, {
issuer: IDP_DOMAIN,
});
authInfo = { token, sub: payload.sub as string };
} catch {
if (needsAuth) {
return res
.status(401)
.set(
"WWW-Authenticate",
`Bearer resource_metadata="${resourceMetadataUrl}"`,
)
.json({
error: "invalid_token",
error_description: "The access token is invalid",
});
}
}
} else if (needsAuth) {
return res
.status(401)
.set(
"WWW-Authenticate",
`Bearer resource_metadata="${resourceMetadataUrl}"`,
)
.json({
error: "invalid_token",
error_description: "Authorization required",
});
}
// Create a per-request MCP server with the auth context.
// authInfo is undefined for public tool calls, populated for
// authenticated requests — tool handlers use it to scope data
// to the authenticated user.
const server = createServer(authInfo);
// ... handle the request with transport
});
```
The `WWW-Authenticate` header includes the [Protected Resource Metadata](https://modelcontextprotocol.io/specification/latest/basic/authorization#authorization-server-location) URL, which tells the client where to discover the authorization server.
### Defence-in-depth in tool handlers
Even though the HTTP layer enforces authorization, protected tool handlers should also verify `authInfo` as a defence-in-depth measure. If the HTTP layer is misconfigured or bypassed, the tool handler catches unauthorized access:
```ts
registerAppTool(
server,
"get_account_balance",
{
description: "Get account balance",
inputSchema: z.object({ accountId: z.string() }),
},
async ({ accountId }) => {
if (!authInfo) {
return {
isError: true,
content: [
{
type: "text",
text: "Authorization required to access account data.",
},
],
};
}
const balance = await getBalance(authInfo.sub, accountId);
return {
content: [{ type: "text", text: `Balance: ${balance}` }],
};
},
);
```
### UI-initiated auth escalation
A powerful pattern is mixing public and protected tools in the same app. The app loads with public data (no authorization required), and the OAuth flow is triggered only when the user performs a protected action. This is a practical application of the [step-up authorization flow](https://modelcontextprotocol.io/specification/latest/basic/authorization#step-up-authorization-flow) described in the spec:
1. A public tool (e.g., `manage_branch`) loads the UI without requiring authorization
2. The user clicks a button that calls a protected tool via `app.callServerTool()`
3. The MCP host receives HTTP `401` and automatically runs the OAuth flow
4. After the user completes the OAuth flow, the host retries the tool call with the acquired token
5. The protected data appears in the UI
```tsx
function BranchItem({ branch }: { branch: Branch }) {
const [adminData, setAdminData] = useState(null);
async function handleManage() {
// This call may trigger the OAuth flow if the user
// hasn't been authorized yet — the host handles it
// transparently.
const result = await app.callServerTool({
name: "manage_branch_admin",
arguments: { branch_id: branch.id },
});
setAdminData(result.structuredContent);
}
return (
<div>
<span>{branch.name}</span>
<button onClick={handleManage}>Manage</button>
{adminData && <AdminPanel data={adminData} />}
</div>
);
}
```
This pattern keeps the initial experience fast (no login wall) while securing sensitive operations behind authorization. The host manages the entire OAuth flow — the app code simply calls the tool and handles the result.

64
docs/csp-cors.md Normal file
View file

@ -0,0 +1,64 @@
---
title: CSP and CORS
group: Security
description: Configure Content Security Policy and CORS for MCP Apps that make network requests from sandboxed iframes, including connectDomains, resourceDomains, and stable origin setup.
---
# CSP & CORS
Unlike regular web apps, MCP Apps HTML is served as an MCP resource and runs in a sandboxed iframe with no same-origin server. Any app that makes network requests must configure Content Security Policy (CSP) and possibly CORS.
**CSP** controls what the _browser_ allows. You must declare _all_ origins in {@link types!McpUiResourceMeta.csp `_meta.ui.csp`} ({@link types!McpUiResourceCsp `McpUiResourceCsp`}) — including `localhost` during development. Declare `connectDomains` for fetch/XHR/WebSocket requests and `resourceDomains` for scripts, stylesheets, images, and fonts.
**CORS** controls what the _API server_ allows. Public APIs that respond with `Access-Control-Allow-Origin: *` or use API key authentication work without CORS configuration. For APIs that allowlist specific origins, use {@link types!McpUiResourceMeta.domain `_meta.ui.domain`} to give the app a stable origin that the API server can allowlist. The format is host-specific, so check each host's documentation for its supported format.
<!-- prettier-ignore -->
```ts source="../src/server/index.examples.ts#registerAppResource_withDomain"
// Computes a stable origin from an MCP server URL for hosting in Claude.
function computeAppDomainForClaude(mcpServerUrl: string): string {
const hash = crypto
.createHash("sha256")
.update(mcpServerUrl)
.digest("hex")
.slice(0, 32);
return `${hash}.claudemcpcontent.com`;
}
const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp");
registerAppResource(
server,
"Company Dashboard",
"ui://dashboard/view.html",
{
description: "Internal dashboard with company data",
},
async () => ({
contents: [
{
uri: "ui://dashboard/view.html",
mimeType: RESOURCE_MIME_TYPE,
text: dashboardHtml,
_meta: {
ui: {
// CSP: tell browser the app is allowed to make requests
csp: {
connectDomains: ["https://api.example.com"],
},
// CORS: give app a stable origin for the API server to allowlist
//
// (Public APIs that use `Access-Control-Allow-Origin: *` or API
// key auth don't need this.)
domain: APP_DOMAIN,
},
},
},
],
}),
);
```
Note that `_meta.ui.csp` and `_meta.ui.domain` are set in the `contents[]` objects returned by the resource read callback, not in `registerAppResource()`'s config object.
> [!NOTE]
> For full examples that configures CSP, see: [`examples/sheet-music-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) (`connectDomains`) and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) (`connectDomains` and `resourceDomains`).

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Some files were not shown because too many files have changed in this diff Show more