') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [DO NOT MERGE] Port to SDK v2 (extension() registrar / composition) by felixweinberger · Pull Request #612 · modelcontextprotocol/ext-apps · GitHub
Skip to content

[DO NOT MERGE] Port to SDK v2 (extension() registrar / composition) - #612

Closed
felixweinberger wants to merge 28 commits into
mainfrom
fweinberger/v2-extension-registrar
Closed

[DO NOT MERGE] Port to SDK v2 (extension() registrar / composition)#612
felixweinberger wants to merge 28 commits into
mainfrom
fweinberger/v2-extension-registrar

Conversation

@felixweinberger

@felixweinbergerfelixweinberger commented Apr 13, 2026

Copy link
Copy Markdown

DO NOT MERGE — proof-of-concept port to SDK v2. CI is expected to fail (package.json depends on local file:/tmp/*.tgz SDK builds that combine unpublished typescript-sdk changes).

Review guide — file map, reading order, what to skip (105 files but ~90 are mechanical)

105 files changed, but ~90 of them are mechanical. This guide maps what to read vs skim.

If you have 10 minutes

  1. BREAKING.md — the public-surface delta. This is the contract.
  2. src/app.tsclass App declaration (search for class App extends EventDispatcher). The whole composition pattern is visible in the first ~50 lines of the class body: readonly client: Client, readonly ui: ExtensionHandle<...>, then ui.setRequestHandler(...) calls in the constructor. Note: no zod import — embedded spec types validated via specTypeSchema() from the SDK.
  3. src/app-bridge.ts — same, search for class AppBridge extends EventDispatcher. Note readonly server: Server and the server.setRequestHandler('tools/call', ...) proxy wiring alongside ui.* for ui/* methods.
  4. Skim the test diff: src/app-bridge.test.ts — most of the file is unchanged. That's the point.

File map

Read carefully (~6 files, the actual design)

FileWhat changedWhy it matters
BREAKING.mdnew, 94 linesThe contract. What consumers must change.
src/app.tsrewrite, ~−1,600 lines netApp composes Client + ExtensionHandle. No assert*Capability overrides. No zod import — specTypeSchema('CallToolResult') etc. passed directly.
src/app-bridge.tsrewrite, ~−2,000 lines netAppBridge composes Server + ExtensionHandle. Standard tools/call etc. via setRequestHandler.
src/events.ts−178 linesProtocolWithEvents (subclass of SDK Protocol) → standalone EventDispatcher<EventMap>. No SDK coupling.
src/message-transport.ts−59 linesPostMessageTransport on the v2 Transport interface.
src/server/index.ts−431 linesregisterAppTool/registerAppResource on v2 McpServer.

Skim (mechanical, verify pattern then move on)

AreaFilesWhat changed
src/spec.types.ts, src/types.ts2Import paths v1→v2; interface McpUi*Capabilitiestype (for JSONObject constraint).
src/generated/{schema.ts,schema.json,schema.test.ts}3Auto-generated; uses z.custom<T>(isSpecType('T', v)) for embedded SDK types. Don't review line-by-line.
src/app-bridge.test.ts173/88 v1 tests pass unmodified. Final: 85 pass / 1 skip / 0 fail.
src/react/useApp.tsx, src/server/index.test.ts, src/*.examples.ts5Import-path changes only.
examples/**~80 files across 25 workspacesPer-example: package.json deps, main.ts import path (/stdio subpath), server.ts v2 McpServer shape. Read one (examples/basic-server-react/) to see the pattern.
docs/, scripts/generate-schemas.ts3Import paths; generator emits isSpecType() for SDK types.
package.json, build.bun.ts, tsconfig.json, lockfile4Dep swap; tsconfig exclusions for unported *.examples.ts.

Recommended reading order (by commit)

25 commits, intentionally staged. Read bottom-up in groups:

1. Setup (skim):231251a6 deps swap, b50315b1 types/schemas, a9713e3d EventDispatcher + transport.

2. The core rewrite (read):25da6658 App, a1f1f016 AppBridge, a5378733 react/server.

3. Compile fixes (skim):ea3f4af5..a97206ca.

4. v1 parity fixes (read commit messages):87fee346, f7490c6b, 47f4cdf3, be67283e — each is a behavior the test suite caught.

5. Tests + examples (skim):7e8ae100, cce8436b, 570781e0, 9998758b, 4298d34a, b845fd7c.

6. Upstream-fix follow-through (skim):953a5d81/4e26407d (#1871), 76b3ba7a/stdio import.

7. Validator integration (read):c8bc2649 eliminates sdk-compat.ts via isSpecType(); 60e9d690 passes specTypeSchema() directly to ExtensionHandle after #1846's StandardSchemaV1 widen.

Questions to evaluate

  • Does composition cover what subclassing did? Compare BREAKING.md's "Removed inherited member" table against what consumers actually use.
  • Is the ui/* ↔ standard-method split clean? In app-bridge.ts, find where ui.setRequestHandler(...) and server.setRequestHandler('tools/call', ...) coexist.
  • Is specTypeSchema() the right validator API?app.ts passes it directly to ExtensionHandle — no Zod at the consumer. generated/schema.ts uses z.custom(isSpecType()) because that file is Zod-composed.
  • Wire compat: v2 AppBridge keeps a ui/initialize handler for v1-iframe back-compat. Right choice vs. clean break?

Known gaps (intentional, documented)

  • npm run test:full includes examples/pdf-server/server.test.ts (~33 fails) — heavy example, deeper port out of scope.
  • tsconfig.json excludes src/**/*.examples.ts(x) and docs/ — companion JSDoc snippets use removed surface.
  • 18 non-basic example workspaces had import paths sed-fixed but builds untested.
  • CI is red until SDK deps publish — package.json points at file:/tmp/*.tgz.

Depends on:

Motivation and Context

App/AppBridge move from subclassing Protocol to composing Client/Server + ExtensionHandle per the SEP-1865 role assignment. See BREAKING.md for the full surface delta. 73/88 of the v1 app-bridge.test.ts tests pass unmodified against the rewrite (final: 85 pass / 1 skip / 0 fail).

How Has This Been Tested?

Locally against an SDK build with the dependencies above: npm run build, npm test (197 pass / 1 skip / 0 fail), and basic-server-react/basic-server-vanillajs/quickstart/basic-host example builds all green. npm run test:full retains the unported examples/pdf-server suite.

Breaking Changes

Yes — see BREAKING.md. Peer dep moves from @modelcontextprotocol/sdk@^1 to @modelcontextprotocol/{client,server}@^2.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

This PR is the artifact for reviewing the v2 shape. Once the SDK dependencies publish to @alpha, the file: deps will be swapped to npm versions and CI will go green.

…ssignment)
Preserves all public methods and on* handlers. Removed: 5 assert*Capability
no-op overrides, inherited Protocol surface. Wire renames: notifications/message
-> ui/log; oncalltool/onlisttools now on ui/{call-view-tool,list-view-tools}.
Standard MCP proxying via server.setRequestHandler. ui/initialize handler kept
for v1-iframe wire compat. callTool/listTools wire renamed to ui/{call-view-tool,
list-view-tools}.
…nHandle casts; ctx type params; exclude docs/; gen-schemas path
… tsc clean
npx tsc --noEmit -> 0 errors
npm run build -> see commit body
…pulls child_process)
npm run build -> see body
…express,node}; restore test tsconfig includes
…mulate, onclose shim, onupdatemodelcontext return type, getToolUiResourceUri error message
… unported pdf-server suite
The pdf-server example's server.test.ts (~77 tests) needs deeper v2 porting
beyond the SDK migration scope. SDK tests + pdf-annotations.test.ts (no SDK
coupling) all pass.
@felixweinberger
felixweinbergerforce-pushed the fweinberger/v2-extension-registrar branch from 1cfc3dd to 76b3ba7CompareApril 13, 2026 15:39
- spec.types.ts: import RequestId from @modelcontextprotocol/client (already public)
- app.ts/app-bridge.ts: replace pass-through *Schema shims with z.custom<T>(v => isSpecType('T', v))
- generate-schemas.ts: emit isSpecType-backed z.custom for external SDK types instead of importing from sdk-compat
- delete src/sdk-compat.ts
Friction surfaced: ExtensionHandle.setRequestHandler/sendRequest accept AnySchema (Zod-only),
so specTypeSchema()'s StandardSchemaV1 return cannot be passed directly. Wrapped via
z.custom(isSpecType) instead. #1868 should widen the param type to StandardSchemaV1 | AnySchema.
@felixweinberger
felixweinbergerforce-pushed the fweinberger/v2-extension-registrar branch from aa13066 to 2fe270fCompareApril 13, 2026 18:10
…pt-sdk#1846 StandardSchemaV1 widening)
generated/schema.ts keeps z.custom(isSpecType()) since it composes into Zod
chains; specTypeSchema() is used at the ExtensionHandle boundary where the
SDK now accepts StandardSchemaV1. app-bridge event-map params type widened
ZodType -> StandardSchemaV1. app.ts no longer imports zod.
@felixweinberger
felixweinbergerforce-pushed the fweinberger/v2-extension-registrar branch from 2fe270f to 60e9d69CompareApril 13, 2026 18:13
- AppBridge dual-listens on legacy notifications/message → loggingmessage event
- bridge.callTool/listTools throw descriptive error if iframe is v1 (instead of MethodNotFound)
- Unskip ping test (Server.ping() is public; skip reason was wrong)
- Document on*-setter replace-semantics with explicit test
- BREAKING.md: v1↔v2 interop section, host-first upgrade guidance
…mappings
- Remove tsconfig excludes for *.examples.ts(x) and docs/ — all now typecheck
- Port companion files to v2 API (app.ui.sendRequest, client.callTool, z.object wraps)
- typedoc externalSymbolLinkMappings for @modelcontextprotocol/{client,server}
- Fix raw-shape inputSchema and Schema.shape→Schema across 9 example servers
- 25/25 examples now build; typedoc warnings 11→0
….registerTool)
All 33 fails fixed: v2 registerTool requires StandardSchema (raw shapes lack
~standard.validate). Also extra.signal→extra.mcpReq.signal, drop stale v1
import. test:full now 277/2/0 (was 197/1/33). e2e API tests pass; browser
tests need playwright install (env, not v2 break).
felixweinberger added a commit that referenced this pull request Apr 15, 2026
- import paths sdk/* → @modelcontextprotocol/{client,server}
- events.ts: ProtocolWithEvents extends Protocol (concrete), v1-schema-shim setRequestHandler
- request/notification call-shape transforms (perl)
- drop assert*Capability overrides (Protocol no longer abstract)
- server/index.ts copied from #612 port
- tsconfig: exclude *.examples.ts, docs/ (same as #612)
NOT YET: *Schema imports → specTypeSchema(), request() visibility, ~225 tsc errors remain
@felixweinberger

Copy link
Copy Markdown
Author

Closing in favor of #614 (concrete Protocol approach via typescript-sdk#1891).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@felixweinberger