The emitted .d.ts copy relative specifiers verbatim, and the extensionless form
(e.g. `from "./events"`) doesn't resolve under Node16/NodeNext, hiding inherited
members such as `addEventListener` (TS2339). Appending `.js` matches the existing
SDK-import style and works with both bundler-mode type-checking and Bun.build.
Types-only; runtime is unaffected.
Fixes#704
App and AppBridge extend the public Protocol from @modelcontextprotocol/client; the iframe wire protocol (ui/initialize only) is unchanged. Peers move to the split SDK 2.0 packages with client and server as optional role peers. Fixups (lockfile, peer ranges, restored handler guards, docs, wire-compat tests) follow in a separate PR.
Co-authored-by: Andrew Khadder <andrew@manufact.com>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Closes#702
Non-breaking transitive updates via `npm audit fix`, plus a `sharp` major
bump. Takes `npm audit` from 15 findings (2 critical, 7 high, 4 moderate,
2 low) to 3 moderate.
Critical / high:
- shell-quote 1.8.3 -> 1.9.0 (GHSA-w7jw-789q-3m8p, GHSA-395f-4hp3-45gv)
- @babel/core 7.28.6 -> 7.29.7 (GHSA-4x5r-pxfx-6jf8)
- sharp ^0.34.5 -> ^0.35.3 (GHSA-f88m-g3jw-g9cj; libvips CVE-2026-33327,
CVE-2026-33328, CVE-2026-35590, CVE-2026-35591)
- brace-expansion 1.1.16 / 2.1.2 (GHSA-3jxr-9vmj-r5cp)
- fast-uri 3.1.4 (GHSA-v2hh-gcrm-f6hx, GHSA-4c8g-83qw-93j6)
- hono 4.12.31 (8 advisories, incl. GHSA-88fw-hqm2-52qc, GHSA-hvrm-45r6-mjfj)
- linkify-it 5.0.2 (GHSA-22p9-wv53-3rq4, GHSA-v245-v573-v5vm)
Plus markdown-it 14.3.0, body-parser 2.3.0, type-is 2.1.0, undici 7.28.0
and vite 6.4.3.
sharp is a devDependency used only by tests/e2e/generate-grid-screenshots,
which is testIgnore'd unless GENERATE_SCREENSHOTS=1. The only breaking
change in 0.35 is the Node floor moving to >=20.9.0, and CI runs Node 22;
the API used (png / resize / toFile) is unchanged.
The 3 remaining findings are all the same advisory (GHSA-frvp-7c67-39w9,
path traversal in @hono/node-server's serve-static on Windows), reported
once each for @hono/node-server, @modelcontextprotocol/sdk and
@modelcontextprotocol/ext-apps. The fix is in @hono/node-server 2.0.5 but
sdk 1.29.0 pins ^1.19.9, so the range cannot reach it; that needs an
upstream bump. It is not reachable here either way — the SDK imports only
getRequestListener and serve from that package, never serve-static.
The Protocol-based migration needs the Protocol class exported from the
package roots, which lands in modelcontextprotocol/typescript-sdk#2501.
Pin the preview builds so the branch is installable; swap to the next
published beta once that PR is released.
Replaces the Client/Server subclassing from the v2 migration with direct
Protocol extension, now that the SDK exports Protocol from the client
package root. This restores the v1 wire contract on the iframe channel:
ui/initialize is the first and only handshake, so deployed hosts keep
working unchanged.
Deleted with the double handshake: the inner MCP initialize round-trip
and its pinned protocol version, the contentless inner capabilities and
their retro-registration plumbing, the oninitialized defineProperty
shadow, and the getCapabilities/getHostCapabilities split (v1
getCapabilities restored). Tests pin the wire order and assert no MCP
initialize is sent in either direction.
Note: package.json overrides point the SDK packages at locally built
tarballs carrying the Protocol export; replace with published versions
before any upstream PR.
The consent page shipped without a viewport meta tag, so mobile browsers
laid it out at a desktop width. Its Approve/Deny buttons were 37px tall
and nested a <button> inside an <a>, which is invalid HTML — interactive
content is not permitted inside a link.
- Add a viewport meta tag to the consent page
- Replace the <a><button> nesting with anchors styled as buttons
(role="button" plus a Space-key handler to keep button keyboard
semantics)
- Lay the actions out as a wrapping flex row with equal 56px tap targets
- Add touch-action: manipulation so a tap resolves to a click immediately
rather than waiting on the double-tap-to-zoom gesture
- Suppress long-press text selection and the iOS callout menu on the
buttons, scoped so the card's client/redirect details stay copyable
- Give buttons an :active style since removing the native tap highlight
otherwise leaves no press feedback
- Apply the same 56px minimum and touch handling to the in-app view's
buttons (the fullscreen toggle was ~33x33)
* lazy-auth-server: support mounting under a base path
createApp() previously built every advertised URL (AS metadata
endpoints, PRM resource, WWW-Authenticate resource_metadata, consent
approve link, elicitation callbacks) from the bare origin, so mounting
the app inside another Express app — app.use("/lazy-auth",
createApp()) — advertised URLs that pointed at the host's root and
collided with the host's own OAuth endpoints.
Derive the public base URL from Express's req.baseUrl instead (empty
when standalone, so standalone behavior is unchanged), and treat
PUBLIC_URL as including any mount path. Document the mounting pattern,
including the root well-known rewrite hosts must add for RFC 8414/9728
path-insertion discovery.
* lazy-auth-server: derive DIST_DIR from import.meta.url
import.meta.filename and import.meta.dirname are undefined in some
module-VM contexts (e.g. importing this package from jest), which made
createApp() throw at import time. import.meta.url is defined
everywhere; derive the dist directory from it instead.
* Require PKCE and bind redirect_uri in lazy-auth-server token exchange
Hardens the example's mock authorization server:
- /authorize now requires a PKCE S256 code_challenge (the MCP auth
spec mandates PKCE for clients; previously a code issued without a
challenge skipped verification at the token endpoint)
- /token rejects authorization-code exchanges where a provided
redirect_uri does not match the authorization request (RFC 6749
§4.1.3); OAuth 2.1 clients that omit it still work, relying on the
now-mandatory PKCE binding
* Enforce single-use authorization codes
Track redeemed code IDs (jti) in memory until the code's own 5-minute
expiry; replaying a code at the token endpoint now fails with
invalid_grant (RFC 6749 §4.1.2). Also documents why wildcard CORS is
intentional for this demo (browser-based hosts must read
WWW-Authenticate; no ambient credentials exist to protect).
No SDK API changes since 1.7.2.
Examples:
- Add lazy-auth-server example (#679)
- Publish lazy-auth-server to npm (added to publish workflows)
Also syncs example dependency ranges to ^1.7.0 (output of npm run bump).
Adds an example MCP App server demonstrating lazy (on-demand) OAuth:
the server connects and lists tools without authentication, and only
prompts for auth when a protected tool is called, by responding 401
with a WWW-Authenticate header pointing at protected-resource metadata.
- Public `show_auth_button` app calls the protected `get_secret` tool
via callServerTool; the host runs the OAuth flow on 401 and retries
- Embedded mock authorization server (authorization-code + PKCE,
short-lived HS256 tokens, refresh + session revocation) so the whole
flow runs from a single process
- TTL-scoped endpoint paths (/ttl/<seconds>/mcp) let slow or automated
clients request longer-lived tokens per connection, threaded through
OAuth via RFC 8707 resource indicators
- `elicit_url` / `elicit_by_error` tools demonstrating URL elicitation
No SDK API changes since 1.7.1.
Examples:
- pdf-server: switch pdf-lib to maintained @cantoo/pdf-lib fork (#651)
- bump example transitives to latest patches (#658)
- vite-plugin-solid 2.11.10 -> 2.11.12 (basic-server-solid)
- babel-preset-solid 1.9.10 -> 1.9.12
- babel-plugin-jsx-dom-expressions 0.40.3 -> 0.40.6
- vite-prerender-plugin 0.5.12 -> 0.5.13 (basic-server-preact)
- @modelcontextprotocol/ext-apps 1.0.0 -> 1.7.1 (lockfile had a stale
registry pin of the repo's own published package that was never bumped
alongside package.json's version field)
Tighten the basic-server-solid range to ^2.11.12 to track the patch
series with current Solid HMR fixes.
The original pdf-lib package has been unmaintained since Nov 2021.
@cantoo/pdf-lib is an actively maintained, API-compatible fork with
ongoing releases and accumulated bugfixes.
Also updates test assertions from the deprecated PDFNumber.value() to
PDFNumber.asNumber() per the fork's API guidance.
* perf(pdf-server): lazy form extraction via range transport + incremental viewer scans
Server: display_pdf now opens the document via PDFDataRangeTransport
(disableAutoFetch) and only runs the per-page form/annotation walk when
getFieldObjects() is non-empty, so form-free PDFs are probed with ~10-25%
of bytes instead of a full download. The unused viewFieldInfo Map is
removed.
Viewer: getDocument sets disableAutoFetch/disableStream; baseline
annotation scan and field-name mapping run lazily per rendered page
instead of walking every page after load, so first paint no longer
schedules a full-file pull.
E2E: new range-counting HTTPS fixture (W-9 for forms, generated
text+image PDF for no-forms) with stallAfterBytes control, and four
regression tests asserting form fields are returned, <30% served on
no-forms display_pdf, first page renders while later ranges are stalled,
and overlap stays bounded.
* fix(pdf-server): handle separated field/widget trees in extractFormSchema
pdfjs getFieldObjects() returns the full field-tree array. For PDFs with
a separated structure (pdf-lib, some authoring tools) the typed widget
sits at fields[1+] behind a typeless container at fields[0]; the previous
code only inspected fields[0] and skipped them all. Pick the first entry
with a non-empty type instead.
Makes the e2e forms.pdf fixture fully generated (no checked-in third-party
asset on the hot path); fw9.pdf stays as a unit-test fixture for the
hierarchical/XFA case.
* fix(pdf-server): address range-transport hangs and lazy-scan tombstone loss
PdfCacheRangeTransport:
- abort() is a no-op stub on PDFDataRangeTransport (it's the hook pdfjs
calls *on* the transport, not an upstream error channel). Expose a
.failed promise that rejects on the first fetch error and race every
pdfjs await against it in display_pdf, so transient network errors
surface into the existing catch instead of hanging the tool call.
- pdfjs coalesces adjacent missing chunks into one unbounded
requestDataRange; readPdfRange clamps each call to MAX_CHUNK_BYTES.
Loop and deliver in slices so every requested chunk is marked loaded.
Viewer (mcp-app.ts):
- The lazy per-page baseline scan left pdfBaselineAnnotations incomplete,
so persistAnnotations and getAnnotatedPdfBytes silently dropped
restoredRemovedIds tombstones for unvisited pages. Union those ids into
the computed diff and removedRefs.
Test fixture: release stalled handlers before resetStats/close so a
failing stalled test doesn't hang afterAll; fail fast if started with
NODE_ENV=production. NODE_TLS_REJECT_UNAUTHORIZED scope documented (full
per-process scoping needs a validateUrl localhost allow, tracked
separately).
* test(pdf-server): bridge coverage gaps and switch fixture to gated loopback HTTP
PdfCacheRangeTransport.deliver(): pdf.js's reader is keyed by the
original begin and removed after one delivery, so accumulate slices and
call onDataRange once with the full buffer (the previous multi-call
approach threw inside pdfjs). Covered by a new integration test that
drives getDocument()/getPage(1) on a >1MB PDF through a clamping
in-memory readPdfRange.
validateUrl: allow http://127.0.0.1|localhost|[::1] only when
PDF_SERVER_ALLOW_LOOPBACK_HTTP is set, so a remote deploy can't be made
to probe its own ports. Covered by env-on/off unit tests.
Fixture switched to plain HTTP (no openssl, no
NODE_TLS_REJECT_UNAUTHORIZED). Adds /error.pdf (500s after 50KB) and two
e2e tests: page 2 renders after stall release (>512KB object path), and
display_pdf returns gracefully on mid-load 500. Existing <30% test now
samples stats before the iframe loads.
New unit tests: display_pdf returns (not hangs) on mid-load fetch
failure via in-memory MCP client; computeDiff/serializeDiff contract
tests pinning the restoredRemovedIds tombstone-preservation behaviour.
* test(pdf-server): e2e for restoredRemovedIds tombstone preservation through viewer
Adds /with-native-annot.pdf fixture (2 pages, native /Text annot on
page 2) and a Playwright test that drives delete on page 2 → iframe
reload → interact add_annotations on page 1 (persist with page 2
unscanned) → assert localStorage diff still contains the removed id →
page 2 still shows it gone. Covers the mcp-app.ts glue the unit tests
can't reach.
* refactor(pdf-server): simplify per round-2 review and fix false-pass test
server.ts: extract probeFormFields() so display_pdf calls one helper
instead of inlining 40 lines of transport/orFail plumbing; make
extractFormSchema's fieldObjects param required (the optional branch
only existed for a test helper).
server.test.ts: the >1MB integration test now forces the image XObject
fetch via getOperatorList() and asserts max requestDataRange span >
MAX_CHUNK_BYTES, so it can't go vacuous. Dedupe makeRandomJpeg by
importing from the fixture.
mcp-app.ts: restore per-annotation try/catch isolation in the lazy
baseline scan (a throw was skipping AnnotationLayer.render); only
carry forward restoredRemovedIds while the baseline scan is incomplete
so a stale id can't pin dirty=true once every page is scanned.
tests: bump fixture JPEG to 1.1MB; consolidate 7 e2e tests to 4 (merge
overlap into the byte-budget test, merge stall+page-2, drop the
error-e2e duplicate of the unit integration test); delete
run-fixture.mjs; drop /error.pdf and ERROR_AFTER_BYTES; rename
tombstone test's describe and clarify page-2 covers the viewer
transport.
* fix(pdf-server): preserve partial probeFormFields results when extractFormFieldInfo throws
* test(pdf-server): fix tombstone e2e — deleted native annotations become a cleared card, not removed from DOM
* test(pdf-server): mark tombstone e2e as fixme — needs basic-host iframe-reload replay support
* test(pdf-server): link tombstone fixme to tracking issue #642
* perf(pdf-server): share cache across server instances and dedupe form parsing
In stateless HTTP deployments createServer() is called per request, so
the per-instance pdfCache and the 4 MB viewer HTML were discarded after
every call. Hoist both to module scope.
Also refactor extractFormSchema/extractFormFieldInfo to accept an
already-parsed PDFDocumentProxy so display_pdf downloads and parses the
PDF once instead of twice.
* perf(pdf-server): cap total cache bytes with LRU eviction
The module-level sharedPdfCache could grow unbounded within the 60s
lifetime window under a burst of distinct URLs. Track running total
bytes and evict least-recently-used entries on insert when it would
exceed CACHE_MAX_TOTAL_BYTES (256MB). getCacheEntry now bumps the
accessed entry to the end of insertion order so eviction targets the
LRU entry rather than the oldest insert.
createPdfCache takes an optional maxTotalBytes for testability.
When FastMCP is created with the default host=127.0.0.1, MCP SDK 1.26+
automatically enables DNS rebinding protection that only allows requests
from localhost. This breaks Docker where requests come from
host.docker.internal, causing 421 Invalid Host header errors.
By passing host=HOST (0.0.0.0 for Docker), DNS rebinding protection
is not auto-enabled, allowing requests from any host.
* test: add unit coverage for PostMessageTransport source validation
PostMessageTransport is the iframe↔host communication layer and contains
the primary security boundary (event.source validation). Previously it
had zero unit coverage — only exercised indirectly via E2E tests, where
the cross-app injection test is a no-op due to cross-origin contentDocument
restrictions.
Covers 18 test cases across:
- Source validation: trusted source delivers, untrusted/null sources
dropped silently (no onerror DoS vector), transport survives rejection
- Message format: valid requests/notifications delivered; ambient
non-JSON-RPC traffic (devtools, extensions) silently ignored;
malformed jsonrpc:'2.0' payloads surface via onerror
- send(): posts to configured target with '*' targetOrigin
- Lifecycle: start() registers listener, close() removes it and fires
onclose, post-close messages ignored, multi-transport isolation
Uses a minimal in-memory window stub rather than jsdom/happy-dom —
consistent with the existing bun test setup which has no DOM configured.
Note: the transport validates event.source (window identity), NOT
event.origin. send() posts with '*'. Origin is intentionally not part
of this transport's trust model.
* fix: resolve TS diagnostics in message-transport tests
- TS2352: cast `{ id: string }` through `unknown` before
`MessageEventSource` (lines 366, 370) — no structural overlap
- TS80007: drop ineffective `await` on bun's `.resolves` matcher
(line 355) — it returns void, not Promise<void>
* docs: clarify origin validation lives in sandbox proxy, not transport
* ci: automate release process with workflow-driven version bumps and PR flow
Adds a Release workflow that bumps all package versions, generates release
notes, and opens a PR. Merging the PR auto-tags and creates the GitHub
Release, which triggers npm-publish. Also adds `npm run bump` for local use
and a release.yml config for categorized auto-generated notes.
* ci: simplify release workflow and flatten npm-publish approval chain
- Drop auto-generated RELEASES.md prepending (notes are written manually in PR)
- Flatten npm-publish job dependencies so all publish jobs wait for approval
together instead of sequentially, reducing 3 approval clicks to 1
- Remove Release environment from publish-mcpb (only needs GH_TOKEN)
- Use --generate-notes for GitHub Release creation
* fix: address review findings in release automation
- release.yml: use RELEASE_TOKEN PAT for gh release create so the
release:published event triggers npm-publish (GITHUB_TOKEN events
don't cascade to other workflows)
- release.yml: checkout merge_commit_sha in tag job instead of ref:main
to avoid tagging later commits
- release.yml: checkout ref:main in prepare job so dispatching from a
feature branch doesn't pollute the release PR
- release.yml: constrain preid to type:choice[beta] to match
npm-publish.yml's tag detection
- release.yml: drop -f on push so re-runs fail loudly instead of
clobbering manual RELEASES.md edits; drop pr-edit fallback
- release.yml: ensure release label exists before gh pr create
- release.yml: make tag push idempotent for re-runs after partial
failure
- bump-version.mjs: update workspace dependency ranges so major bumps
don't leave examples pointing at the old major
- bump-version.mjs: run npm install --package-lock-only so npm ci
on the release PR doesn't fail on lockfile mismatch
- npm-publish.yml: add npm-tag detection to publish-examples so beta
example releases don't overwrite latest
- .github/release.yml: use [bot] suffix in author excludes so
dependabot PRs are actually filtered from release notes
- CONTRIBUTING.md: document RELEASE_TOKEN setup; fix npm run -- syntax
* style: auto-fix prettier formatting
* build: add npm run bump; parallel npm-publish approval
- scripts/bump-version.mjs (+ npm run bump): bumps root and all workspace
package versions, updates examples' ^X.Y.0 dependency range on the root
package (matters on major bumps), and refreshes package-lock so npm ci
accepts the release PR.
- npm-publish.yml: all publish jobs now depend directly on [build, test]
instead of chaining, so they enter "waiting for approval" together and a
single Review-deployments click approves the lot. Prerelease versions
(any X.Y.Z-…) publish under --tag beta so they don't take latest.
- CONTRIBUTING.md: release steps now use npm run bump.
Dropped from the original change: the workflow_dispatch release.yml
(required a long-lived PAT to cascade events), label-based
.github/release.yml auto-notes (PRs aren't labeled today), and the
release-X.Y npm-tag for non-main branches (no backport branches yet).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(react): close App on useApp unmount; relax late-handler guard for re-registration
useApp's effect cleanup now closes the App (and its transport) instead
of only flipping a `mounted` flag. Under React StrictMode dev
double-invoke, the abandoned first instance's PostMessageTransport
otherwise keeps a window `message` listener and receives every host
postMessage alongside the second instance.
The late-handler guard from #629 now tracks `_everHadListener` per
event and only flags the *first* registration of a one-shot event
post-handshake. Re-registration (React useEffect cleanup + re-add on
dep change) of an event that had a pre-connect handler is silent,
including under `strict: true`.
* feat(app-bridge): warn on second ui/initialize
If a View double-mounts (e.g. React StrictMode in dev) without closing
the previous App, AppBridge now logs a console.warn when it receives a
second ui/initialize. Behavior is unchanged — it still responds and the
latest appInfo/appCapabilities replace the previous values — this is
purely a diagnostic for host implementers.
_assertInitialized used console.error while _assertHandlerTiming and the
AppBridge override used console.warn. Align all three on console.warn for
the non-strict path; strict still throws.
Tests: hoist the console.warn spy to beforeEach/afterEach in the
pre-handshake describe to avoid bun:test spy stacking now that all three
guards target the same console method.
Mirrors the outbound-side guard from #623: registering a toolinput /
toolinputpartial / toolresult / toolcancelled handler after the
ui/initialize handshake has completed now logs a console.warn (or
throws under { strict: true }), since the host may have already fired
the notification by then. hostcontextchanged is exempt — late
listeners for repeating events are legitimate.
Covers both the on* setters and addEventListener.
Zod v4's object parser compiles a fast path with `new Function()`, which
violates CSP without `unsafe-eval`. Hosts that enforce the spec's default
CSP (e.g. VS Code) cannot load any View that parses an incoming message —
the SDK's generated schemas trip this on the very first ui/initialize
roundtrip.
The App constructor now sets `z.config({ jitless: true })` by default so
Views work out of the box under strict CSP. New `AppOptions.allowUnsafeEval`
(default false) skips this for Views that know their host permits
`unsafe-eval` and want Zod's faster JIT path.
Addresses #374. Also relevant to #199 (threejs-server has a separate
`new Function()` use in its own shader compiler that this does not fix).
New `examples/` are costly to review and maintain across SDK changes.
Add a policy section stating that example PRs from outside contributors
are not accepted, and redirect contributors to open an issue instead.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>