From 0266171b92567cc0a68edadb3f2a76915f464089 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 30 Mar 2026 18:48:33 +0100 Subject: [PATCH 01/97] Add addEventListener/removeEventListener with DOM-model on* semantics (#573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) --- src/app-bridge.test.ts | 176 +++++++++++++++ src/app-bridge.ts | 448 ++++++++++++++++++++++++++++--------- src/app.examples.ts | 6 +- src/app.ts | 291 ++++++++++++++++-------- src/events.ts | 279 +++++++++++++++++++++++ src/react/useHostStyles.ts | 8 +- typedoc.config.mjs | 2 +- 7 files changed, 1009 insertions(+), 201 deletions(-) create mode 100644 src/events.ts diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index c1f4612..4761d76 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1221,4 +1221,180 @@ describe("isToolVisibilityAppOnly", () => { expect(isToolVisibilityAppOnly(tool)).toBe(false); }); }); + + describe("addEventListener / removeEventListener", () => { + let app: App; + let bridge: AppBridge; + let appTransport: InMemoryTransport; + let bridgeTransport: InMemoryTransport; + + beforeEach(async () => { + [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + app = new App(testAppInfo, {}, { autoResize: false }); + bridge = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + await bridge.connect(bridgeTransport); + }); + + afterEach(async () => { + await appTransport.close(); + await bridgeTransport.close(); + }); + + it("App.addEventListener fires multiple listeners for the same event", async () => { + const a: unknown[] = []; + const b: unknown[] = []; + app.addEventListener("hostcontextchanged", (p) => a.push(p)); + app.addEventListener("hostcontextchanged", (p) => b.push(p)); + + await app.connect(appTransport); + bridge.setHostContext({ theme: "dark" }); + await flush(); + + expect(a).toEqual([{ theme: "dark" }]); + expect(b).toEqual([{ theme: "dark" }]); + }); + + it("App notification setters replace (DOM onclick model)", async () => { + const a: unknown[] = []; + const b: unknown[] = []; + const first = (p: unknown) => a.push(p); + app.ontoolinput = first; + expect(app.ontoolinput).toBe(first); + app.ontoolinput = (p) => b.push(p); + + await app.connect(appTransport); + await bridge.sendToolInput({ arguments: { x: 1 } }); + await flush(); + + // Second assignment replaced the first (like el.onclick) + expect(a).toEqual([]); + expect(b).toEqual([{ arguments: { x: 1 } }]); + }); + + it("App notification setter coexists with addEventListener", async () => { + const a: unknown[] = []; + const b: unknown[] = []; + app.ontoolinput = (p) => a.push(p); + app.addEventListener("toolinput", (p) => b.push(p)); + + await app.connect(appTransport); + await bridge.sendToolInput({ arguments: { x: 1 } }); + await flush(); + + // Both the on* handler and addEventListener listener fire + expect(a).toEqual([{ arguments: { x: 1 } }]); + expect(b).toEqual([{ arguments: { x: 1 } }]); + }); + + it("App notification getter returns the on* handler", () => { + expect(app.ontoolinput).toBeUndefined(); + const handler = () => {}; + app.ontoolinput = handler; + expect(app.ontoolinput).toBe(handler); + }); + + it("App notification setter can be cleared with undefined", async () => { + const a: unknown[] = []; + app.ontoolinput = (p) => a.push(p); + expect(app.ontoolinput).toBeDefined(); + app.ontoolinput = undefined; + + await app.connect(appTransport); + await bridge.sendToolInput({ arguments: { x: 1 } }); + await flush(); + + expect(a).toEqual([]); + expect(app.ontoolinput).toBeUndefined(); + }); + + it("App.removeEventListener stops a listener from firing", async () => { + const a: unknown[] = []; + const listener = (p: unknown) => a.push(p); + app.addEventListener("toolinput", listener); + app.removeEventListener("toolinput", listener); + + await app.connect(appTransport); + await bridge.sendToolInput({ arguments: {} }); + await flush(); + + expect(a).toEqual([]); + }); + + it("App.onEventDispatch merges hostcontext before listeners fire", async () => { + let seen: unknown; + app.addEventListener("hostcontextchanged", () => { + seen = app.getHostContext(); + }); + + await app.connect(appTransport); + bridge.setHostContext({ theme: "dark" }); + await flush(); + + expect(seen).toEqual({ theme: "dark" }); + }); + + it("AppBridge.addEventListener fires multiple listeners", async () => { + let a = 0; + let b = 0; + bridge.addEventListener("initialized", () => a++); + bridge.addEventListener("initialized", () => b++); + + await app.connect(appTransport); + + expect(a).toBe(1); + expect(b).toBe(1); + }); + + it("on* request setters have replace semantics (no throw)", () => { + app.onteardown = async () => ({}); + expect(() => { + app.onteardown = async () => ({}); + }).not.toThrow(); + }); + + it("on* request setters have getters", () => { + expect(app.onteardown).toBeUndefined(); + const handler = async () => ({}); + app.onteardown = handler; + expect(app.onteardown).toBe(handler); + }); + + it("direct setRequestHandler throws when called twice", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + bridge2.setRequestHandler( + // @ts-expect-error — exercising throw path with raw schema + { shape: { method: { value: "test/method" } } }, + () => ({}), + ); + expect(() => { + bridge2.setRequestHandler( + // @ts-expect-error — exercising throw path with raw schema + { shape: { method: { value: "test/method" } } }, + () => ({}), + ); + }).toThrow(/already registered/); + }); + + it("direct setNotificationHandler throws for event-mapped methods", () => { + const app2 = new App(testAppInfo, {}, { autoResize: false }); + app2.addEventListener("toolinput", () => {}); + expect(() => { + app2.setNotificationHandler( + // @ts-expect-error — exercising throw path with raw schema + { + shape: { method: { value: "ui/notifications/tool-input" } }, + }, + () => {}, + ); + }).toThrow(/already registered/); + }); + }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index d409fc0..7712587 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -19,6 +19,9 @@ import { ListResourceTemplatesRequestSchema, ListResourceTemplatesResult, ListResourceTemplatesResultSchema, + ListToolsRequest, + ListToolsRequestSchema, + ListToolsResultSchema, LoggingMessageNotification, LoggingMessageNotificationSchema, PingRequest, @@ -36,10 +39,10 @@ import { ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { - Protocol, ProtocolOptions, RequestOptions, } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import { ProtocolWithEvents } from "./events"; import { type AppNotification, @@ -223,6 +226,21 @@ type RequestHandlerExtra = Parameters< Parameters[1] >[1]; +/** + * Maps DOM-style event names to their notification `params` types. + * + * Used by {@link AppBridge `AppBridge`} to provide type-safe + * `addEventListener` / `removeEventListener` and singular `on*` handler + * support. + */ +export type AppBridgeEventMap = { + sizechange: McpUiSizeChangedNotification["params"]; + sandboxready: McpUiSandboxProxyReadyNotification["params"]; + initialized: McpUiInitializedNotification["params"]; + requestteardown: McpUiRequestTeardownNotification["params"]; + loggingmessage: LoggingMessageNotification["params"]; +}; + /** * Host-side bridge for communicating with a single View ({@link app!App `App`}). * @@ -280,15 +298,24 @@ type RequestHandlerExtra = Parameters< * await bridge.connect(transport); * ``` */ -export class AppBridge extends Protocol< +export class AppBridge extends ProtocolWithEvents< AppRequest, AppNotification, - AppResult + AppResult, + AppBridgeEventMap > { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; + protected readonly eventSchemas = { + sizechange: McpUiSizeChangedNotificationSchema, + sandboxready: McpUiSandboxProxyReadyNotificationSchema, + initialized: McpUiInitializedNotificationSchema, + requestteardown: McpUiRequestTeardownNotificationSchema, + loggingmessage: LoggingMessageNotificationSchema, + }; + /** * Create a new AppBridge instance. * @@ -343,10 +370,13 @@ export class AppBridge extends Protocol< // Default handler for requestDisplayMode - returns current mode from host context. // Hosts can override this by setting bridge.onrequestdisplaymode = ... - this.setRequestHandler(McpUiRequestDisplayModeRequestSchema, (request) => { - const currentMode = this._hostContext.displayMode ?? "inline"; - return { mode: currentMode }; - }); + this.replaceRequestHandler( + McpUiRequestDisplayModeRequestSchema, + (request) => { + const currentMode = this._hostContext.displayMode ?? "inline"; + return { mode: currentMode }; + }, + ); } /** @@ -442,13 +472,19 @@ export class AppBridge extends Protocol< * * @see {@link McpUiSizeChangedNotification `McpUiSizeChangedNotification`} for the notification type * @see {@link app!App.sendSizeChanged `App.sendSizeChanged`} - the View method that sends these notifications + * @deprecated Use {@link addEventListener `addEventListener("sizechange", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ + get onsizechange(): + | ((params: McpUiSizeChangedNotification["params"]) => void) + | undefined { + return this.getEventHandler("sizechange"); + } set onsizechange( - callback: (params: McpUiSizeChangedNotification["params"]) => void, + callback: + | ((params: McpUiSizeChangedNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiSizeChangedNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("sizechange", callback); } /** @@ -480,13 +516,19 @@ export class AppBridge extends Protocol< * @internal * @see {@link McpUiSandboxProxyReadyNotification `McpUiSandboxProxyReadyNotification`} for the notification type * @see {@link sendSandboxResourceReady `sendSandboxResourceReady`} for sending content to the sandbox + * @deprecated Use {@link addEventListener `addEventListener("sandboxready", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ + get onsandboxready(): + | ((params: McpUiSandboxProxyReadyNotification["params"]) => void) + | undefined { + return this.getEventHandler("sandboxready"); + } set onsandboxready( - callback: (params: McpUiSandboxProxyReadyNotification["params"]) => void, + callback: + | ((params: McpUiSandboxProxyReadyNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiSandboxProxyReadyNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("sandboxready", callback); } /** @@ -505,13 +547,19 @@ export class AppBridge extends Protocol< * * @see {@link McpUiInitializedNotification `McpUiInitializedNotification`} for the notification type * @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View + * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ + get oninitialized(): + | ((params: McpUiInitializedNotification["params"]) => void) + | undefined { + return this.getEventHandler("initialized"); + } set oninitialized( - callback: (params: McpUiInitializedNotification["params"]) => void, + callback: + | ((params: McpUiInitializedNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiInitializedNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("initialized", callback); } /** @@ -548,16 +596,28 @@ export class AppBridge extends Protocol< * @see {@link McpUiMessageRequest `McpUiMessageRequest`} for the request type * @see {@link McpUiMessageResult `McpUiMessageResult`} for the result type */ + private _onmessage?: ( + params: McpUiMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onmessage() { + return this._onmessage; + } set onmessage( - callback: ( - params: McpUiMessageRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: McpUiMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); + this._onmessage = callback; + this.replaceRequestHandler( McpUiMessageRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onmessage) throw new Error("No onmessage handler set"); + return this._onmessage(request.params, extra); }, ); } @@ -605,16 +665,28 @@ export class AppBridge extends Protocol< * @see {@link McpUiOpenLinkRequest `McpUiOpenLinkRequest`} for the request type * @see {@link McpUiOpenLinkResult `McpUiOpenLinkResult`} for the result type */ + private _onopenlink?: ( + params: McpUiOpenLinkRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onopenlink() { + return this._onopenlink; + } set onopenlink( - callback: ( - params: McpUiOpenLinkRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: McpUiOpenLinkRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); + this._onopenlink = callback; + this.replaceRequestHandler( McpUiOpenLinkRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onopenlink) throw new Error("No onopenlink handler set"); + return this._onopenlink(request.params, extra); }, ); } @@ -661,16 +733,33 @@ export class AppBridge extends Protocol< * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for the request type * @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for the result type */ + private _ondownloadfile?: ( + params: McpUiDownloadFileRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get ondownloadfile() { + return this._ondownloadfile; + } set ondownloadfile( - callback: ( - params: McpUiDownloadFileRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: McpUiDownloadFileRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced( + "ondownloadfile", + this._ondownloadfile, + callback, + ); + this._ondownloadfile = callback; + this.replaceRequestHandler( McpUiDownloadFileRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._ondownloadfile) + throw new Error("No ondownloadfile handler set"); + return this._ondownloadfile(request.params, extra); }, ); } @@ -683,9 +772,6 @@ export class AppBridge extends Protocol< * `ui/resource-teardown` (via {@link teardownResource `teardownResource`}) to allow * the view to perform gracefull termination, then unmount the iframe after the view responds. * - * @param callback - Handler that receives teardown request params - * - params - Empty object (reserved for future use) - * * @example * ```typescript * bridge.onrequestteardown = async (params) => { @@ -700,14 +786,19 @@ export class AppBridge extends Protocol< * * @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for the notification type * @see {@link teardownResource `teardownResource`} for initiating teardown + * @deprecated Use {@link addEventListener `addEventListener("requestteardown", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ + get onrequestteardown(): + | ((params: McpUiRequestTeardownNotification["params"]) => void) + | undefined { + return this.getEventHandler("requestteardown"); + } set onrequestteardown( - callback: (params: McpUiRequestTeardownNotification["params"]) => void, + callback: + | ((params: McpUiRequestTeardownNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler( - McpUiRequestTeardownNotificationSchema, - (request) => callback(request.params), - ); + this.setEventHandler("requestteardown", callback); } /** @@ -742,16 +833,33 @@ export class AppBridge extends Protocol< * @see {@link McpUiRequestDisplayModeRequest `McpUiRequestDisplayModeRequest`} for the request type * @see {@link McpUiRequestDisplayModeResult `McpUiRequestDisplayModeResult`} for the result type */ + private _onrequestdisplaymode?: ( + params: McpUiRequestDisplayModeRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onrequestdisplaymode() { + return this._onrequestdisplaymode; + } set onrequestdisplaymode( - callback: ( - params: McpUiRequestDisplayModeRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: McpUiRequestDisplayModeRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced( + "onrequestdisplaymode", + this._onrequestdisplaymode, + callback, + ); + this._onrequestdisplaymode = callback; + this.replaceRequestHandler( McpUiRequestDisplayModeRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onrequestdisplaymode) + throw new Error("No onrequestdisplaymode handler set"); + return this._onrequestdisplaymode(request.params, extra); }, ); } @@ -767,10 +875,10 @@ export class AppBridge extends Protocol< * This uses the standard MCP logging notification format, not a UI-specific * message type. * - * @param callback - Handler that receives logging params - * - `params.level` - Log level: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency" - * - `params.logger` - Optional logger name/identifier - * - `params.data` - Log message and optional structured data + * The handler receives `LoggingMessageNotification["params"]`: + * - `level` — "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency" + * - `logger` — optional logger name/identifier + * - `data` — log message and optional structured data * * @example * ```ts source="./app-bridge.examples.ts#AppBridge_onloggingmessage_handleLog" @@ -781,16 +889,19 @@ export class AppBridge extends Protocol< * ); * }; * ``` + * @deprecated Use {@link addEventListener `addEventListener("loggingmessage", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ + get onloggingmessage(): + | ((params: LoggingMessageNotification["params"]) => void) + | undefined { + return this.getEventHandler("loggingmessage"); + } set onloggingmessage( - callback: (params: LoggingMessageNotification["params"]) => void, + callback: + | ((params: LoggingMessageNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler( - LoggingMessageNotificationSchema, - async (notification) => { - callback(notification.params); - }, - ); + this.setEventHandler("loggingmessage", callback); } /** @@ -819,16 +930,33 @@ export class AppBridge extends Protocol< * * @see {@link McpUiUpdateModelContextRequest `McpUiUpdateModelContextRequest`} for the request type */ + private _onupdatemodelcontext?: ( + params: McpUiUpdateModelContextRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onupdatemodelcontext() { + return this._onupdatemodelcontext; + } set onupdatemodelcontext( - callback: ( - params: McpUiUpdateModelContextRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: McpUiUpdateModelContextRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced( + "onupdatemodelcontext", + this._onupdatemodelcontext, + callback, + ); + this._onupdatemodelcontext = callback; + this.replaceRequestHandler( McpUiUpdateModelContextRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onupdatemodelcontext) + throw new Error("No onupdatemodelcontext handler set"); + return this._onupdatemodelcontext(request.params, extra); }, ); } @@ -859,15 +987,30 @@ export class AppBridge extends Protocol< * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type */ + private _oncalltool?: ( + params: CallToolRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get oncalltool() { + return this._oncalltool; + } set oncalltool( - callback: ( - params: CallToolRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: CallToolRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler(CallToolRequestSchema, async (request, extra) => { - return callback(request.params, extra); - }); + this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); + this._oncalltool = callback; + this.replaceRequestHandler( + CallToolRequestSchema, + async (request, extra) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(request.params, extra); + }, + ); } /** @@ -922,16 +1065,33 @@ export class AppBridge extends Protocol< * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type */ + private _onlistresources?: ( + params: ListResourcesRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onlistresources() { + return this._onlistresources; + } set onlistresources( - callback: ( - params: ListResourcesRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: ListResourcesRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced( + "onlistresources", + this._onlistresources, + callback, + ); + this._onlistresources = callback; + this.replaceRequestHandler( ListResourcesRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onlistresources) + throw new Error("No onlistresources handler set"); + return this._onlistresources(request.params, extra); }, ); } @@ -962,16 +1122,33 @@ export class AppBridge extends Protocol< * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type */ + private _onlistresourcetemplates?: ( + params: ListResourceTemplatesRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onlistresourcetemplates() { + return this._onlistresourcetemplates; + } set onlistresourcetemplates( - callback: ( - params: ListResourceTemplatesRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: ListResourceTemplatesRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced( + "onlistresourcetemplates", + this._onlistresourcetemplates, + callback, + ); + this._onlistresourcetemplates = callback; + this.replaceRequestHandler( ListResourceTemplatesRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onlistresourcetemplates) + throw new Error("No onlistresourcetemplates handler set"); + return this._onlistresourcetemplates(request.params, extra); }, ); } @@ -1002,16 +1179,33 @@ export class AppBridge extends Protocol< * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type */ + private _onreadresource?: ( + params: ReadResourceRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onreadresource() { + return this._onreadresource; + } set onreadresource( - callback: ( - params: ReadResourceRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: ReadResourceRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced( + "onreadresource", + this._onreadresource, + callback, + ); + this._onreadresource = callback; + this.replaceRequestHandler( ReadResourceRequestSchema, async (request, extra) => { - return callback(request.params, extra); + if (!this._onreadresource) + throw new Error("No onreadresource handler set"); + return this._onreadresource(request.params, extra); }, ); } @@ -1070,15 +1264,35 @@ export class AppBridge extends Protocol< * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type */ + private _onlistprompts?: ( + params: ListPromptsRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onlistprompts() { + return this._onlistprompts; + } set onlistprompts( - callback: ( - params: ListPromptsRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: ListPromptsRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler(ListPromptsRequestSchema, async (request, extra) => { - return callback(request.params, extra); - }); + this.warnIfRequestHandlerReplaced( + "onlistprompts", + this._onlistprompts, + callback, + ); + this._onlistprompts = callback; + this.replaceRequestHandler( + ListPromptsRequestSchema, + async (request, extra) => { + if (!this._onlistprompts) + throw new Error("No onlistprompts handler set"); + return this._onlistprompts(request.params, extra); + }, + ); } /** @@ -1450,6 +1664,40 @@ export class AppBridge extends Protocol< /** @deprecated Use {@link teardownResource `teardownResource`} instead */ sendResourceTeardown: AppBridge["teardownResource"] = this.teardownResource; + /** + * Call a tool on the view. + * + * Sends a `tools/call` request to the view and returns the result. + * + * @param params - Tool call parameters (name and arguments) + * @param options - Request options (timeout, abort signal, etc.) + * @returns Promise resolving to the tool call result + */ + callTool(params: CallToolRequest["params"], options?: RequestOptions) { + return this.request( + { method: "tools/call", params }, + CallToolResultSchema, + options, + ); + } + + /** + * List tools available on the view. + * + * Sends a `tools/list` request to the view and returns the result. + * + * @param params - List tools parameters (may include cursor for pagination) + * @param options - Request options (timeout, abort signal, etc.) + * @returns Promise resolving to the list of tools + */ + listTools(params: ListToolsRequest["params"], options?: RequestOptions) { + return this.request( + { method: "tools/list", params }, + ListToolsResultSchema, + options, + ); + } + /** * Connect to the view via transport and optionally set up message forwarding. * diff --git a/src/app.examples.ts b/src/app.examples.ts index 1d12c5b..a0a5820 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -303,7 +303,11 @@ function App_onlisttools_returnTools(app: App) { //#region App_onlisttools_returnTools app.onlisttools = async (params, extra) => { return { - tools: ["greet", "calculate", "format"], + tools: [ + { name: "greet", inputSchema: { type: "object" as const } }, + { name: "calculate", inputSchema: { type: "object" as const } }, + { name: "format", inputSchema: { type: "object" as const } }, + ], }; }; //#endregion App_onlisttools_returnTools diff --git a/src/app.ts b/src/app.ts index fa3bf23..f5f97b3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,5 @@ import { type RequestOptions, - Protocol, ProtocolOptions, } from "@modelcontextprotocol/sdk/shared/protocol.js"; @@ -16,6 +15,7 @@ import { ListResourcesResultSchema, ListToolsRequest, ListToolsRequestSchema, + ListToolsResult, LoggingMessageNotification, PingRequestSchema, ReadResourceRequest, @@ -23,6 +23,8 @@ import { ReadResourceResultSchema, } from "@modelcontextprotocol/sdk/types.js"; import { AppNotification, AppRequest, AppResult } from "./types"; +import { ProtocolWithEvents } from "./events"; +export { ProtocolWithEvents }; import { PostMessageTransport } from "./message-transport"; import { LATEST_PROTOCOL_VERSION, @@ -159,6 +161,21 @@ type RequestHandlerExtra = Parameters< Parameters[1] >[1]; +/** + * Maps DOM-style event names to their notification `params` types. + * + * Used by {@link App `App`} (which extends {@link ProtocolWithEvents `ProtocolWithEvents`}) + * to provide type-safe `addEventListener` / `removeEventListener` and + * singular `on*` handler support. + */ +export type AppEventMap = { + toolinput: McpUiToolInputNotification["params"]; + toolinputpartial: McpUiToolInputPartialNotification["params"]; + toolresult: McpUiToolResultNotification["params"]; + toolcancelled: McpUiToolCancelledNotification["params"]; + hostcontextchanged: McpUiHostContextChangedNotification["params"]; +}; + /** * Main class for MCP Apps to communicate with their host. * @@ -182,24 +199,26 @@ type RequestHandlerExtra = Parameters< * * ## Inherited Methods * - * As a subclass of `Protocol`, `App` inherits key methods for handling communication: + * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits: * - `setRequestHandler()` - Register handlers for requests from host * - `setNotificationHandler()` - Register handlers for notifications from host + * - `addEventListener()` - Append a listener for a notification event (multi-listener) + * - `removeEventListener()` - Remove a previously added listener * - * @see `Protocol` from @modelcontextprotocol/sdk for all inherited methods + * @see {@link ProtocolWithEvents `ProtocolWithEvents`} for the DOM-model event system * - * ## Notification Setters + * ## Notification Setters (DOM-model `on*` handlers) * - * For common notifications, the `App` class provides convenient setter properties - * that simplify handler registration: + * For common notifications, the `App` class provides getter/setter properties + * that follow DOM-model replace semantics (like `el.onclick`): * - `ontoolinput` - Complete tool arguments from host * - `ontoolinputpartial` - Streaming partial tool arguments * - `ontoolresult` - Tool execution results * - `ontoolcancelled` - Tool execution was cancelled by user or host * - `onhostcontextchanged` - Host context changes (theme, locale, etc.) * - * These setters are convenience wrappers around `setNotificationHandler()`. - * Both patterns work; use whichever fits your coding style better. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * Use `addEventListener` to attach multiple listeners without replacing. * * @example Basic usage with PostMessageTransport * ```ts source="./app.examples.ts#App_basicUsage" @@ -216,11 +235,33 @@ type RequestHandlerExtra = Parameters< * await app.connect(); * ``` */ -export class App extends Protocol { +export class App extends ProtocolWithEvents< + AppRequest, + AppNotification, + AppResult, + AppEventMap +> { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; + protected readonly eventSchemas = { + toolinput: McpUiToolInputNotificationSchema, + toolinputpartial: McpUiToolInputPartialNotificationSchema, + toolresult: McpUiToolResultNotificationSchema, + toolcancelled: McpUiToolCancelledNotificationSchema, + hostcontextchanged: McpUiHostContextChangedNotificationSchema, + }; + + protected override onEventDispatch( + event: K, + params: AppEventMap[K], + ): void { + if (event === "hostcontextchanged") { + this._hostContext = { ...this._hostContext, ...params }; + } + } + /** * Create a new MCP App instance. * @@ -249,9 +290,10 @@ export class App extends Protocol { return {}; }); - // Set up default handler to update _hostContext when notifications arrive. - // Users can override this by setting onhostcontextchanged. - this.onhostcontextchanged = () => {}; + // Eagerly register the hostcontextchanged event slot so that + // onEventDispatch (which merges into _hostContext) fires even if the + // user never assigns onhostcontextchanged or calls addEventListener. + this.setEventHandler("hostcontextchanged", undefined); } /** @@ -339,13 +381,12 @@ export class App extends Protocol { * sends a tool's complete arguments. This is sent after a tool call begins * and before the tool result is available. * - * This setter is a convenience wrapper around `setNotificationHandler()` that - * automatically handles the notification schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * Use {@link addEventListener `addEventListener`} to attach multiple listeners + * without replacing. * * Register handlers before calling {@link connect `connect`} to avoid missing notifications. * - * @param callback - Function called with the tool input params ({@link McpUiToolInputNotification.params `McpUiToolInputNotification.params`}) - * * @example * ```ts source="./app.examples.ts#App_ontoolinput_setter" * // Register before connecting to ensure no notifications are missed @@ -356,15 +397,20 @@ export class App extends Protocol { * await app.connect(); * ``` * - * @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method + * @deprecated Use {@link addEventListener `addEventListener("toolinput", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. * @see {@link McpUiToolInputNotification `McpUiToolInputNotification`} for the notification structure */ + get ontoolinput(): + | ((params: McpUiToolInputNotification["params"]) => void) + | undefined { + return this.getEventHandler("toolinput"); + } set ontoolinput( - callback: (params: McpUiToolInputNotification["params"]) => void, + callback: + | ((params: McpUiToolInputNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiToolInputNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("toolinput", callback); } /** @@ -379,13 +425,12 @@ export class App extends Protocol { * (e.g., the last item in an array may be truncated). Use partial data only * for preview UI, not for critical operations. * - * This setter is a convenience wrapper around `setNotificationHandler()` that - * automatically handles the notification schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * Use {@link addEventListener `addEventListener`} to attach multiple listeners + * without replacing. * * Register handlers before calling {@link connect `connect`} to avoid missing notifications. * - * @param callback - Function called with each partial tool input update ({@link McpUiToolInputPartialNotification.params `McpUiToolInputPartialNotification.params`}) - * * @example Progressive rendering of tool arguments * ```ts source="./app.examples.ts#App_ontoolinputpartial_progressiveRendering" * const codePreview = document.querySelector("#code-preview")!; @@ -404,16 +449,21 @@ export class App extends Protocol { * }; * ``` * - * @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method + * @deprecated Use {@link addEventListener `addEventListener("toolinputpartial", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. * @see {@link McpUiToolInputPartialNotification `McpUiToolInputPartialNotification`} for the notification structure * @see {@link ontoolinput `ontoolinput`} for the complete tool input handler */ + get ontoolinputpartial(): + | ((params: McpUiToolInputPartialNotification["params"]) => void) + | undefined { + return this.getEventHandler("toolinputpartial"); + } set ontoolinputpartial( - callback: (params: McpUiToolInputPartialNotification["params"]) => void, + callback: + | ((params: McpUiToolInputPartialNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiToolInputPartialNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("toolinputpartial", callback); } /** @@ -423,13 +473,12 @@ export class App extends Protocol { * sends the result of a tool execution. This is sent after the tool completes * on the MCP server, allowing your app to display the results or update its state. * - * This setter is a convenience wrapper around `setNotificationHandler()` that - * automatically handles the notification schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * Use {@link addEventListener `addEventListener`} to attach multiple listeners + * without replacing. * * Register handlers before calling {@link connect `connect`} to avoid missing notifications. * - * @param callback - Function called with the tool result ({@link McpUiToolResultNotification.params `McpUiToolResultNotification.params`}) - * * @example Display tool execution results * ```ts source="./app.examples.ts#App_ontoolresult_displayResults" * app.ontoolresult = (params) => { @@ -441,16 +490,21 @@ export class App extends Protocol { * }; * ``` * - * @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method + * @deprecated Use {@link addEventListener `addEventListener("toolresult", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. * @see {@link McpUiToolResultNotification `McpUiToolResultNotification`} for the notification structure * @see {@link ontoolinput `ontoolinput`} for the initial tool input handler */ + get ontoolresult(): + | ((params: McpUiToolResultNotification["params"]) => void) + | undefined { + return this.getEventHandler("toolresult"); + } set ontoolresult( - callback: (params: McpUiToolResultNotification["params"]) => void, + callback: + | ((params: McpUiToolResultNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiToolResultNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("toolresult", callback); } /** @@ -461,13 +515,12 @@ export class App extends Protocol { * including user action, sampling error, classifier intervention, or other * interruptions. Apps should update their state and display appropriate feedback. * - * This setter is a convenience wrapper around `setNotificationHandler()` that - * automatically handles the notification schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * Use {@link addEventListener `addEventListener`} to attach multiple listeners + * without replacing. * * Register handlers before calling {@link connect `connect`} to avoid missing notifications. * - * @param callback - Function called when tool execution is cancelled. Receives optional cancellation reason — see {@link McpUiToolCancelledNotification.params `McpUiToolCancelledNotification.params`}. - * * @example Handle tool cancellation * ```ts source="./app.examples.ts#App_ontoolcancelled_handleCancellation" * app.ontoolcancelled = (params) => { @@ -476,16 +529,21 @@ export class App extends Protocol { * }; * ``` * - * @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method + * @deprecated Use {@link addEventListener `addEventListener("toolcancelled", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. * @see {@link McpUiToolCancelledNotification `McpUiToolCancelledNotification`} for the notification structure * @see {@link ontoolresult `ontoolresult`} for successful tool completion */ + get ontoolcancelled(): + | ((params: McpUiToolCancelledNotification["params"]) => void) + | undefined { + return this.getEventHandler("toolcancelled"); + } set ontoolcancelled( - callback: (params: McpUiToolCancelledNotification["params"]) => void, + callback: + | ((params: McpUiToolCancelledNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler(McpUiToolCancelledNotificationSchema, (n) => - callback(n.params), - ); + this.setEventHandler("toolcancelled", callback); } /** @@ -496,17 +554,17 @@ export class App extends Protocol { * other environmental updates. Apps should respond by updating their UI * accordingly. * - * This setter is a convenience wrapper around `setNotificationHandler()` that - * automatically handles the notification schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * Use {@link addEventListener `addEventListener`} to attach multiple listeners + * without replacing. * * Notification params are automatically merged into the internal host context - * before the callback is invoked. This means {@link getHostContext `getHostContext`} will - * return the updated values even before your callback runs. + * via {@link onEventDispatch `onEventDispatch`} before any handler or listener + * fires. This means {@link getHostContext `getHostContext`} will return the + * updated values even before your callback runs. * * Register handlers before calling {@link connect `connect`} to avoid missing notifications. * - * @param callback - Function called with the updated host context - * * @example Respond to theme changes * ```ts source="./app.examples.ts#App_onhostcontextchanged_respondToTheme" * app.onhostcontextchanged = (ctx) => { @@ -518,21 +576,21 @@ export class App extends Protocol { * }; * ``` * - * @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method + * @deprecated Use {@link addEventListener `addEventListener("hostcontextchanged", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. * @see {@link McpUiHostContextChangedNotification `McpUiHostContextChangedNotification`} for the notification structure * @see {@link McpUiHostContext `McpUiHostContext`} for the full context structure */ + get onhostcontextchanged(): + | ((params: McpUiHostContextChangedNotification["params"]) => void) + | undefined { + return this.getEventHandler("hostcontextchanged"); + } set onhostcontextchanged( - callback: (params: McpUiHostContextChangedNotification["params"]) => void, + callback: + | ((params: McpUiHostContextChangedNotification["params"]) => void) + | undefined, ) { - this.setNotificationHandler( - McpUiHostContextChangedNotificationSchema, - (n) => { - // Merge the partial update into the stored context - this._hostContext = { ...this._hostContext, ...n.params }; - callback(n.params); - }, - ); + this.setEventHandler("hostcontextchanged", callback); } /** @@ -545,8 +603,7 @@ export class App extends Protocol { * The handler can be sync or async. The host will wait for the returned promise * to resolve before proceeding with teardown. * - * This setter is a convenience wrapper around `setRequestHandler()` that - * automatically handles the request schema. + * Assigning replaces the previous handler; assigning `undefined` clears it. * * Register handlers before calling {@link connect `connect`} to avoid missing requests. * @@ -563,18 +620,31 @@ export class App extends Protocol { * }; * ``` * - * @see {@link setRequestHandler `setRequestHandler`} for the underlying method * @see {@link McpUiResourceTeardownRequest `McpUiResourceTeardownRequest`} for the request structure */ + private _onteardown?: ( + params: McpUiResourceTeardownRequest["params"], + extra: RequestHandlerExtra, + ) => McpUiResourceTeardownResult | Promise; + get onteardown() { + return this._onteardown; + } set onteardown( - callback: ( - params: McpUiResourceTeardownRequest["params"], - extra: RequestHandlerExtra, - ) => McpUiResourceTeardownResult | Promise, + callback: + | (( + params: McpUiResourceTeardownRequest["params"], + extra: RequestHandlerExtra, + ) => McpUiResourceTeardownResult | Promise) + | undefined, ) { - this.setRequestHandler( + this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); + this._onteardown = callback; + this.replaceRequestHandler( McpUiResourceTeardownRequestSchema, - (request, extra) => callback(request.params, extra), + (request, extra) => { + if (!this._onteardown) throw new Error("No onteardown handler set"); + return this._onteardown(request.params, extra); + }, ); } @@ -587,8 +657,7 @@ export class App extends Protocol { * * The app must declare tool capabilities in the constructor to use this handler. * - * This setter is a convenience wrapper around `setRequestHandler()` that - * automatically handles the request schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. * * Register handlers before calling {@link connect `connect`} to avoid missing requests. * @@ -606,18 +675,28 @@ export class App extends Protocol { * throw new Error(`Unknown tool: ${params.name}`); * }; * ``` - * - * @see {@link setRequestHandler `setRequestHandler`} for the underlying method */ + private _oncalltool?: ( + params: CallToolRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get oncalltool() { + return this._oncalltool; + } set oncalltool( - callback: ( - params: CallToolRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: CallToolRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler(CallToolRequestSchema, (request, extra) => - callback(request.params, extra), - ); + this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); + this._oncalltool = callback; + this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(request.params, extra); + }); } /** @@ -629,36 +708,54 @@ export class App extends Protocol { * * The app must declare tool capabilities in the constructor to use this handler. * - * This setter is a convenience wrapper around `setRequestHandler()` that - * automatically handles the request schema and extracts the params for you. + * Assigning replaces the previous handler; assigning `undefined` clears it. * * Register handlers before calling {@link connect `connect`} to avoid missing requests. * - * @param callback - Async function that returns tool names as strings (simplified - * from full `ListToolsResult` with `Tool` objects). Registration is always - * allowed; capability validation occurs when handlers are invoked. + * @param callback - Async function that returns a {@link ListToolsResult `ListToolsResult`}. + * Registration is always allowed; capability validation occurs when handlers + * are invoked. * * @example Return available tools * ```ts source="./app.examples.ts#App_onlisttools_returnTools" * app.onlisttools = async (params, extra) => { * return { - * tools: ["greet", "calculate", "format"], + * tools: [ + * { name: "greet", inputSchema: { type: "object" as const } }, + * { name: "calculate", inputSchema: { type: "object" as const } }, + * { name: "format", inputSchema: { type: "object" as const } }, + * ], * }; * }; * ``` * - * @see {@link setRequestHandler `setRequestHandler`} for the underlying method * @see {@link oncalltool `oncalltool`} for handling tool execution */ + private _onlisttools?: ( + params: ListToolsRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onlisttools() { + return this._onlisttools; + } set onlisttools( - callback: ( - params: ListToolsRequest["params"], - extra: RequestHandlerExtra, - ) => Promise<{ tools: string[] }>, + callback: + | (( + params: ListToolsRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler(ListToolsRequestSchema, (request, extra) => - callback(request.params, extra), + this.warnIfRequestHandlerReplaced( + "onlisttools", + this._onlisttools, + callback, ); + this._onlisttools = callback; + this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => { + if (!this._onlisttools) throw new Error("No onlisttools handler set"); + return this._onlisttools(request.params, extra); + }); } /** diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..4d17ca4 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,279 @@ +import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import { + Request, + Notification, + Result, +} from "@modelcontextprotocol/sdk/types.js"; +import { ZodLiteral, ZodObject } from "zod/v4"; + +type MethodSchema = ZodObject<{ method: ZodLiteral }>; + +/** + * Per-event state: a singular `on*` handler (replace semantics) plus a + * listener array (`addEventListener` semantics), mirroring the DOM model + * where `el.onclick` and `el.addEventListener("click", …)` coexist. + */ +interface EventSlot { + onHandler?: ((params: T) => void) | undefined; + listeners: ((params: T) => void)[]; +} + +/** + * Intermediate base class that adds DOM-style event support on top of the + * MCP SDK's `Protocol`. + * + * The base `Protocol` class stores one handler per method: + * `setRequestHandler()` and `setNotificationHandler()` replace any existing + * handler for the same method silently. This class introduces a two-channel + * event model inspired by the DOM: + * + * ### Singular `on*` handler (like `el.onclick`) + * + * Subclasses expose `get`/`set` pairs that delegate to + * {@link setEventHandler `setEventHandler`} / + * {@link getEventHandler `getEventHandler`}. Assigning replaces the previous + * handler; assigning `undefined` clears it. `addEventListener` listeners are + * unaffected. + * + * ### Multi-listener (`addEventListener` / `removeEventListener`) + * + * Append to a per-event listener array. Listeners fire in insertion order + * after the singular `on*` handler. + * + * ### Dispatch order + * + * When a notification arrives for a mapped event: + * 1. {@link onEventDispatch `onEventDispatch`} (subclass side-effects) + * 2. The singular `on*` handler (if set) + * 3. All `addEventListener` listeners in insertion order + * + * ### Double-set protection + * + * Direct calls to {@link setRequestHandler `setRequestHandler`} / + * {@link setNotificationHandler `setNotificationHandler`} throw if a handler + * for the same method has already been registered (through any path), so + * accidental overwrites surface as errors instead of silent bugs. + * + * @typeParam EventMap - Maps event names to the listener's `params` type. + */ +export abstract class ProtocolWithEvents< + SendRequestT extends Request, + SendNotificationT extends Notification, + SendResultT extends Result, + EventMap extends Record, +> extends Protocol { + private _registeredMethods = new Set(); + private _eventSlots = new Map(); + + /** + * Event name → notification schema. Subclasses populate this so that + * the event system can lazily register a dispatcher with the correct + * schema on first use. + */ + protected abstract readonly eventSchemas: { + [K in keyof EventMap]: MethodSchema; + }; + + /** + * Called once per incoming notification, before any handlers or listeners + * fire. Subclasses may override to perform side effects such as merging + * notification params into cached state. + */ + protected onEventDispatch( + _event: K, + _params: EventMap[K], + ): void {} + + // ── Event system (DOM model) ──────────────────────────────────────── + + /** + * Lazily create the event slot and register a single dispatcher with the + * base `Protocol`. The dispatcher fans out to the `on*` handler and all + * `addEventListener` listeners. + */ + private _ensureEventSlot( + event: K, + ): EventSlot { + let slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) { + const schema = this.eventSchemas[event]; + if (!schema) { + throw new Error(`Unknown event: ${String(event)}`); + } + slot = { listeners: [] }; + this._eventSlots.set(event, slot as EventSlot); + + // Claim this method so direct setNotificationHandler calls throw. + const method = schema.shape.method.value; + this._registeredMethods.add(method); + + const s = slot; // stable reference for the closure + super.setNotificationHandler(schema, (n) => { + const params = (n as { params: EventMap[K] }).params; + this.onEventDispatch(event, params); + // 1. Singular on* handler + s.onHandler?.(params); + // 2. addEventListener listeners — snapshot to tolerate removal during + // dispatch (e.g., a listener that calls removeEventListener on itself) + for (const l of [...s.listeners]) l(params); + }); + } + return slot; + } + + /** + * Set or clear the singular `on*` handler for an event. + * + * Replace semantics — like the DOM's `el.onclick = fn`. Assigning + * `undefined` clears the handler without affecting `addEventListener` + * listeners. + */ + protected setEventHandler( + event: K, + handler: ((params: EventMap[K]) => void) | undefined, + ): void { + const slot = this._ensureEventSlot(event); + if (slot.onHandler && handler) { + console.warn( + `[MCP Apps] on${String(event)} handler replaced. ` + + `Use addEventListener("${String(event)}", …) to add multiple listeners without replacing.`, + ); + } + slot.onHandler = handler; + } + + /** + * Get the singular `on*` handler for an event, or `undefined` if none is + * set. `addEventListener` listeners are not reflected here. + */ + protected getEventHandler( + event: K, + ): ((params: EventMap[K]) => void) | undefined { + return (this._eventSlots.get(event) as EventSlot | undefined) + ?.onHandler; + } + + /** + * Add a listener for a notification event. + * + * Unlike the singular `on*` handler, calling this multiple times appends + * listeners rather than replacing them. All registered listeners fire in + * insertion order after the `on*` handler when the notification arrives. + * + * Registration is lazy: the first call (for a given event, from either + * this method or the `on*` setter) registers a dispatcher with the base + * `Protocol`. + * + * @param event - Event name (a key of the `EventMap` type parameter). + * @param handler - Listener invoked with the notification `params`. + */ + addEventListener( + event: K, + handler: (params: EventMap[K]) => void, + ): void { + this._ensureEventSlot(event).listeners.push(handler); + } + + /** + * Remove a previously registered event listener. The dispatcher stays + * registered even if the listener array becomes empty; future + * notifications simply have no listeners to call. + */ + removeEventListener( + event: K, + handler: (params: EventMap[K]) => void, + ): void { + const slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) return; + const idx = slot.listeners.indexOf(handler); + if (idx !== -1) slot.listeners.splice(idx, 1); + } + + // ── Handler registration with double-set protection ───────────────── + + // The two overrides below are arrow-function class fields rather than + // prototype methods so that Protocol's constructor — which registers its + // own ping/cancelled/progress handlers via `this.setRequestHandler` + // before our fields initialize — hits the base implementation and skips + // tracking. Converting these to proper methods would crash with + // `_registeredMethods` undefined during super(). + + /** + * Registers a request handler. Throws if a handler for the same method + * has already been registered — use the `on*` setter (replace semantics) + * or `addEventListener` (multi-listener) for notification events. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setRequestHandler: Protocol< + SendRequestT, + SendNotificationT, + SendResultT + >["setRequestHandler"] = (schema, handler) => { + this._assertMethodNotRegistered(schema, "setRequestHandler"); + super.setRequestHandler(schema, handler); + }; + + /** + * Registers a notification handler. Throws if a handler for the same + * method has already been registered — use the `on*` setter (replace + * semantics) or `addEventListener` (multi-listener) for mapped events. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setNotificationHandler: Protocol< + SendRequestT, + SendNotificationT, + SendResultT + >["setNotificationHandler"] = (schema, handler) => { + this._assertMethodNotRegistered(schema, "setNotificationHandler"); + super.setNotificationHandler(schema, handler); + }; + + /** + * Warn if a request handler `on*` setter is replacing a previously-set + * handler. Call from each request setter before updating the backing field. + */ + protected warnIfRequestHandlerReplaced( + name: string, + previous: unknown, + next: unknown, + ): void { + if (previous && next) { + console.warn( + `[MCP Apps] ${name} handler replaced. ` + + `Previous handler will no longer be called.`, + ); + } + } + + /** + * Replace a request handler, bypassing double-set protection. Used by + * `on*` request-handler setters that need replace semantics. + */ + protected replaceRequestHandler: Protocol< + SendRequestT, + SendNotificationT, + SendResultT + >["setRequestHandler"] = (schema, handler) => { + const method = (schema as MethodSchema).shape.method.value; + this._registeredMethods.add(method); + super.setRequestHandler(schema, handler); + }; + + private _assertMethodNotRegistered(schema: unknown, via: string): void { + const method = (schema as MethodSchema).shape.method.value; + if (this._registeredMethods.has(method)) { + throw new Error( + `Handler for "${method}" already registered (via ${via}). ` + + `Use addEventListener() to attach multiple listeners, ` + + `or the on* setter for replace semantics.`, + ); + } + this._registeredMethods.add(method); + } +} diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts index 72247f7..e380efa 100644 --- a/src/react/useHostStyles.ts +++ b/src/react/useHostStyles.ts @@ -77,7 +77,7 @@ export function useHostStyleVariables( return; } - app.onhostcontextchanged = (params) => { + const listener = (params: McpUiHostContext) => { if (params.theme) { applyDocumentTheme(params.theme); } @@ -85,6 +85,8 @@ export function useHostStyleVariables( applyHostStyleVariables(params.styles.variables); } }; + app.addEventListener("hostcontextchanged", listener); + return () => app.removeEventListener("hostcontextchanged", listener); }, [app]); } @@ -145,11 +147,13 @@ export function useHostFonts( return; } - app.onhostcontextchanged = (params) => { + const listener = (params: McpUiHostContext) => { if (params.styles?.css?.fonts) { applyHostFonts(params.styles.css.fonts); } }; + app.addEventListener("hostcontextchanged", listener); + return () => app.removeEventListener("hostcontextchanged", listener); }, [app]); } diff --git a/typedoc.config.mjs b/typedoc.config.mjs index a97bb29..5775cae 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -28,7 +28,7 @@ const config = { ], excludePrivate: true, excludeInternal: false, - intentionallyNotExported: ["AppOptions"], + intentionallyNotExported: ["AppOptions", "MethodSchema"], blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false, From f987bc43b27d18230f31e0850b6333ff6480f1e4 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 18:04:00 +0100 Subject: [PATCH 02/97] =?UTF-8?q?fix(deps):=20bump=20path-to-regexp=208.3.?= =?UTF-8?q?0=20=E2=86=92=208.4.1=20to=20patch=20ReDoS=20CVEs=20(#576)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cec8c63..8487ac4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7282,9 +7282,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", + "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", "license": "MIT", "funding": { "type": "opencollective", From 38700569e747e66fe1d146614d55a92977cbedb9 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 22:28:28 +0100 Subject: [PATCH 03/97] fix(pdf-server): radio + dropdown in fill_form/save (#577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- examples/pdf-server/server.test.ts | 138 +++++++++++++++++ examples/pdf-server/server.ts | 104 ++++++++++++- examples/pdf-server/src/mcp-app.ts | 144 +++++++++++------- .../pdf-server/src/pdf-annotations.test.ts | 75 +++++++++ examples/pdf-server/src/pdf-annotations.ts | 45 ++++-- 5 files changed, 434 insertions(+), 72 deletions(-) diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index db6c56b..5207506 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -1054,4 +1054,142 @@ describe("interact tool", () => { await server.close(); }); }); + + describe("viewer liveness", () => { + // get_screenshot/get_text fail fast when the iframe never polled, instead + // of waiting 45s for a viewer that isn't there. Reproduces the case where + // the host goes idle before the iframe reaches startPolling(). + + it("get_screenshot fails fast when viewer never polled", async () => { + const { server, client } = await connect(); + const uuid = "never-polled-screenshot"; + + const started = Date.now(); + const r = await client.callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "get_screenshot", page: 1 }, + }); + const elapsed = Date.now() - started; + + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("never connected"); + expect(firstText(r)).toContain(uuid); + expect(firstText(r)).toContain("display_pdf again"); // recovery hint + // Fast-fail bound (~8s grace), well under the 45s page-data timeout. + // 15s upper bound leaves slack for CI scheduling. + expect(elapsed).toBeLessThan(15_000); + + await client.close(); + await server.close(); + }, 20_000); + + it("get_screenshot waits full timeout when viewer polled then went silent", async () => { + // Viewer polled once (proving it mounted) then hung on a heavy render. + // The grace check passes, so we fall through to the 45s page-data wait — + // verified here by racing against a 12s deadline that should NOT win. + const { server, client } = await connect(); + const uuid = "polled-then-silent"; + + // Viewer's first poll: drain whatever's there so it returns fast. + // Enqueue a trivial command first so poll returns via the batch-wait + // path (~200ms) instead of blocking on the 30s long-poll. + await client.callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "navigate", page: 1 }, + }); + await poll(client, uuid); + + // Now get_screenshot — viewer has polled, so no fast-fail. But viewer + // never calls submit_page_data → should wait beyond the grace period. + const outcome = await Promise.race([ + client + .callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "get_screenshot", page: 1 }, + }) + .then(() => "completed" as const), + new Promise<"still-waiting">((r) => + setTimeout(() => r("still-waiting"), 12_000), + ), + ]); + + expect(outcome).toBe("still-waiting"); + + await client.close(); + await server.close(); + }, 20_000); + + it("get_screenshot succeeds when viewer polls during grace window", async () => { + // Model calls interact before the viewer has polled — but the viewer + // shows up within the grace period and completes the roundtrip. + const { server, client } = await connect(); + const uuid = "late-arriving-viewer"; + + const interactPromise = client.callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "get_screenshot", page: 1 }, + }); + + // Viewer connects 500ms late — well inside the grace window. + await new Promise((r) => setTimeout(r, 500)); + const cmds = await poll(client, uuid); + const getPages = cmds.find((c) => c.type === "get_pages"); + expect(getPages).toBeDefined(); + + // Viewer responds with the page data. + await client.callTool({ + name: "submit_page_data", + arguments: { + requestId: getPages!.requestId as string, + pages: [ + { page: 1, image: Buffer.from("fake-jpeg").toString("base64") }, + ], + }, + }); + + const r = await interactPromise; + expect(r.isError).toBeFalsy(); + expect((r.content as Array<{ type: string }>)[0].type).toBe("image"); + + await client.close(); + await server.close(); + }, 15_000); + + it("batch failure puts error message first", async () => { + // When [fill_form, get_screenshot] runs and get_screenshot times out, + // content[0] must describe the failure — not the earlier success. Some + // hosts flatten isError results to content[0].text only, which previously + // showed "Queued: Filled N fields" with isError:true and dropped the + // actual timeout entirely. + const { server, client } = await connect(); + const uuid = "batch-error-ordering"; + + const r = await client.callTool({ + name: "interact", + arguments: { + viewUUID: uuid, + commands: [ + { action: "navigate", page: 3 }, // succeeds → "Queued: ..." + { action: "get_screenshot", page: 1 }, // never-polled → fast-fail + ], + }, + }); + + expect(r.isError).toBe(true); + const texts = (r.content as Array<{ type: string; text: string }>).map( + (c) => c.text, + ); + // content[0]: batch-failed summary naming the culprit + expect(texts[0]).toContain("failed"); + expect(texts[0]).toContain("2/2"); + expect(texts[0]).toContain("get_screenshot"); + // content[1]: the actual error + expect(texts[1]).toContain("never connected"); + // content[2]: the earlier success, pushed to the back + expect(texts[2]).toContain("Queued"); + + await client.close(); + await server.close(); + }, 15_000); + }); }); diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 5bc2502..b216d49 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -196,6 +196,14 @@ export type { PdfCommand }; // reject first and return a real error instead of the client cancelling us. const GET_PAGES_TIMEOUT_MS = 45_000; +/** + * Grace period for the viewer's first poll. If interact() arrives before the + * iframe has ever polled, we wait this long for it to show up (iframe mount + + * PDF load + startPolling). If no poll comes, the viewer almost certainly + * never rendered — failing fast beats a silent 45s hang. + */ +const VIEWER_FIRST_POLL_GRACE_MS = 8_000; + interface PageDataEntry { page: number; text?: string; @@ -232,6 +240,33 @@ function waitForPageData( }); } +/** + * Wait for the viewer's first poll_pdf_commands call. + * + * Called before waitForPageData() so a viewer that never mounted fails in ~8s + * with a specific message instead of a generic 45s "Timeout waiting for page + * data" that gives no hint why. + * + * Intentionally does NOT touch pollWaiters: piggybacking on that single-slot + * Map races with poll_pdf_commands' batch-wait branch (which never cancels the + * prior waiter) and with concurrent interact calls (which would overwrite each + * other). A plain check loop on viewsPolled is stateless — multiple callers + * can wait independently and all observe the same add() when it happens. + */ +async function ensureViewerIsPolling(uuid: string): Promise { + const deadline = Date.now() + VIEWER_FIRST_POLL_GRACE_MS; + while (!viewsPolled.has(uuid)) { + if (Date.now() >= deadline) { + throw new Error( + `Viewer never connected for viewUUID ${uuid} (no poll within ${VIEWER_FIRST_POLL_GRACE_MS / 1000}s). ` + + `The iframe likely failed to mount — this happens when the conversation ` + + `goes idle before the viewer finishes loading. Call display_pdf again to get a fresh viewUUID.`, + ); + } + await new Promise((r) => setTimeout(r, 100)); + } +} + interface QueueEntry { commands: PdfCommand[]; /** Timestamp of the most recent enqueue or dequeue */ @@ -243,6 +278,15 @@ const commandQueues = new Map(); /** Waiters for long-poll: resolve callback wakes up a blocked poll_pdf_commands */ const pollWaiters = new Map void>(); +/** + * viewUUIDs that have been polled at least once. A view missing from this set + * means the iframe never reached startPolling() — usually because it wasn't + * mounted yet, or ontoolresult threw before the poll loop started. Used to + * fail fast in get_screenshot/get_text instead of waiting the full 45s for + * a viewer that was never there. + */ +const viewsPolled = new Set(); + /** Valid form field names per viewer UUID (populated during display_pdf) */ const viewFieldNames = new Map>(); @@ -288,6 +332,7 @@ function pruneStaleQueues(): void { commandQueues.delete(uuid); viewFieldNames.delete(uuid); viewFieldInfo.delete(uuid); + viewsPolled.delete(uuid); stopFileWatch(uuid); } } @@ -812,6 +857,10 @@ interface FormFieldInfo { y: number; width: number; height: number; + /** Radio button export value (buttonValue) — distinguishes widgets that share a field name. */ + exportValue?: string; + /** Dropdown/listbox option values, as seen in the widget's `options` array. */ + options?: string[]; } /** @@ -864,6 +913,16 @@ async function extractFormFieldInfo( // Convert to model coords (top-left origin): modelY = pageHeight - pdfY - height const modelY = pageHeight - y2; + // Choice widgets (combo/listbox) carry `options` as + // [{exportValue, displayValue}]. Expose export values — that's + // what fill_form needs. + let options: string[] | undefined; + if (Array.isArray(ann.options) && ann.options.length > 0) { + options = ann.options + .map((o: { exportValue?: string }) => o?.exportValue) + .filter((v: unknown): v is string => typeof v === "string"); + } + fields.push({ name: fieldName, type: fieldType, @@ -873,6 +932,12 @@ async function extractFormFieldInfo( width: Math.round(width), height: Math.round(height), ...(ann.alternativeText ? { label: ann.alternativeText } : undefined), + // Radio: buttonValue is the per-widget export value — the only + // thing distinguishing three `size [Btn]` lines from each other. + ...(ann.radioButton && ann.buttonValue != null + ? { exportValue: String(ann.buttonValue) } + : undefined), + ...(options?.length ? { options } : undefined), }); } } @@ -1282,6 +1347,14 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before y: z.number(), width: z.number(), height: z.number(), + exportValue: z + .string() + .optional() + .describe("Radio button value — pass this to fill_form"), + options: z + .array(z.string()) + .optional() + .describe("Dropdown/listbox option values"), }), ) .optional() @@ -1453,8 +1526,14 @@ URL: ${normalized}`, for (const f of fields) { const label = f.label ? ` "${f.label}"` : ""; const nameStr = f.name || "(unnamed)"; + // Radio: = tells the model what value to pass. + // Dropdown: options:[...] lists valid choices. + const exportSuffix = f.exportValue ? `=${f.exportValue}` : ""; + const optsSuffix = f.options + ? ` options:[${f.options.join(", ")}]` + : ""; lines.push( - ` ${nameStr}${label} [${f.type}] at (${f.x},${f.y}) ${f.width}×${f.height}`, + ` ${nameStr}${exportSuffix}${label} [${f.type}] at (${f.x},${f.y}) ${f.width}×${f.height}${optsSuffix}`, ); } } @@ -1925,6 +2004,7 @@ URL: ${normalized}`, let pageData: PageDataEntry[]; try { + await ensureViewerIsPolling(uuid); pageData = await waitForPageData(requestId, signal); } catch (err) { return { @@ -1973,6 +2053,7 @@ URL: ${normalized}`, let pageData: PageDataEntry[]; try { + await ensureViewerIsPolling(uuid); pageData = await waitForPageData(requestId, signal); } catch (err) { return { @@ -2202,7 +2283,7 @@ Example — add a signature image and a stamp, then screenshot to verify: // Process commands sequentially, collecting all content parts const allContent: ContentPart[] = []; - let hasError = false; + let failedAt = -1; for (let i = 0; i < commandList.length; i++) { const result = await processInteractCommand( @@ -2211,15 +2292,27 @@ Example — add a signature image and a stamp, then screenshot to verify: extra.signal, ); if (result.isError) { - hasError = true; + // Error content first. Some hosts flatten isError results to + // content[0].text — if we push the error after prior successes, + // the user sees "Queued: Filled 7 fields" with isError:true and + // the actual failure is silently dropped. + allContent.unshift(...result.content); + failedAt = i; + break; } allContent.push(...result.content); - if (hasError) break; // Stop on first error + } + + if (failedAt >= 0 && commandList.length > 1) { + allContent.unshift({ + type: "text", + text: `Batch failed at step ${failedAt + 1}/${commandList.length} (${commandList[failedAt].action}):`, + }); } return { content: allContent, - ...(hasError ? { isError: true } : {}), + ...(failedAt >= 0 ? { isError: true } : {}), }; }, ); @@ -2280,6 +2373,7 @@ Example — add a signature image and a stamp, then screenshot to verify: _meta: { ui: { visibility: ["app"] } }, }, async ({ viewUUID: uuid }): Promise => { + viewsPolled.add(uuid); // If commands are already queued, wait briefly to let more accumulate if (commandQueues.has(uuid)) { await new Promise((r) => setTimeout(r, POLL_BATCH_WAIT_MS)); diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 6a59407..b20d74b 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -2708,6 +2708,60 @@ async function buildFieldNameMap( log.info(`Built field name map: ${fieldNameToIds.size} fields`); } +/** + * Set one form field's value in pdf.js's annotationStorage, in the format + * AnnotationLayer expects to READ when it re-renders. + * + * Radio buttons need per-widget booleans: pdf.js's RadioButtonWidgetAnnotation + * render() has inverted string coercion (`value !== buttonValue` → true for + * every NON-matching widget), so a string value on all widgets checks the + * first rendered one and clears the rest regardless of what you asked for. + * Match pdf.js's own change handler instead: `{value: true}` on the widget + * whose buttonValue matches, `{value: false}` on the siblings. + * + * Also patches the live DOM element for the current page so the user sees the + * change without waiting for a full re-render. + */ +function setFieldInStorage(name: string, value: string | boolean): void { + if (!pdfDocument) return; + const ids = fieldNameToIds.get(name); + if (!ids) return; + const storage = pdfDocument.annotationStorage; + + // Radio group: at least one widget ID has a buttonValue recorded. + const isRadio = ids.some((id) => radioButtonValues.has(id)); + if (isRadio) { + const want = String(value); + for (const id of ids) { + const checked = radioButtonValues.get(id) === want; + storage.setValue(id, { value: checked }); + const el = formLayerEl.querySelector( + `input[data-element-id="${id}"]`, + ) as HTMLInputElement | null; + if (el) el.checked = checked; + } + return; + } + + // Text / checkbox / select: same value on every widget (a field can have + // multiple widget annotations sharing one /V). + const storageValue = typeof value === "boolean" ? value : String(value); + for (const id of ids) { + storage.setValue(id, { value: storageValue }); + const el = formLayerEl.querySelector(`[data-element-id="${id}"]`) as + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement + | null; + if (!el) continue; + if (el instanceof HTMLInputElement && el.type === "checkbox") { + el.checked = !!value; + } else { + el.value = String(value); + } + } +} + /** Sync formFieldValues into pdfDocument.annotationStorage so AnnotationLayer renders pre-filled values. * Skips values that match the PDF's baseline — those are already in storage * in pdf.js's native format (which may differ from our string/bool repr, @@ -2715,17 +2769,9 @@ async function buildFieldNameMap( * form can break the Reset button's ability to restore defaults. */ function syncFormValuesToStorage(): void { if (!pdfDocument || fieldNameToIds.size === 0) return; - const storage = pdfDocument.annotationStorage; for (const [name, value] of formFieldValues) { if (pdfBaselineFormValues.get(name) === value) continue; - const ids = fieldNameToIds.get(name); - if (ids) { - for (const id of ids) { - storage.setValue(id, { - value: typeof value === "boolean" ? value : String(value), - }); - } - } + setFieldInStorage(name, value); } } @@ -4093,6 +4139,15 @@ app.ontoolresult = async (result: CallToolResult) => { } catch (err) { log.error("Error loading PDF:", err); showError(err instanceof Error ? err.message : String(err)); + // Poll anyway. The server's interact tool has no way to know we choked — + // without a poll it waits 45s on every get_screenshot against this + // viewUUID. handleGetPages already null-guards pdfDocument, so a failed + // load just means empty page data → server returns "No screenshot + // returned" (fast, actionable) instead of "Timeout waiting for page data + // from viewer" (slow, opaque). + if (viewUUID && interactEnabled) { + startPolling(); + } } }; @@ -4213,44 +4268,10 @@ async function processCommands(commands: PdfCommand[]): Promise { case "fill_form": for (const field of cmd.fields) { formFieldValues.set(field.name, field.value); - // Set in PDF.js annotation storage and update DOM elements directly - if (pdfDocument) { - const ids = fieldNameToIds.get(field.name); - if (ids) { - for (const id of ids) { - pdfDocument.annotationStorage.setValue(id, { - value: - typeof field.value === "boolean" - ? field.value - : String(field.value), - }); - // Update the live DOM element if it exists on the current page - const el = formLayerEl.querySelector( - `[data-element-id="${id}"]`, - ) as - | HTMLInputElement - | HTMLSelectElement - | HTMLTextAreaElement - | null; - if (el) { - if ( - el instanceof HTMLInputElement && - el.type === "checkbox" - ) { - el.checked = !!field.value; - } else if (el instanceof HTMLSelectElement) { - el.value = String(field.value); - } else { - el.value = String(field.value); - } - } - } - } else { - log.info( - `fill_form: no annotation IDs for field "${field.name}"`, - ); - } + if (!fieldNameToIds.has(field.name)) { + log.info(`fill_form: no annotation IDs for field "${field.name}"`); } + setFieldInStorage(field.name, field.value); } // Re-render to show updated form values (handles fields on other pages) renderPage(); @@ -4440,16 +4461,27 @@ app.onteardown = async () => { app.onhostcontextchanged = handleHostContextChanged; // Connect to host -app.connect().then(() => { - log.info("Connected to host"); - const ctx = app.getHostContext(); - if (ctx) { - handleHostContextChanged(ctx); - } - // Restore annotations early using toolInfo.id (available before tool result) - restoreAnnotations(); - updateAnnotationsBadge(); -}); +app + .connect() + .then(() => { + log.info("Connected to host"); + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } + // Restore annotations early using toolInfo.id (available before tool result) + restoreAnnotations(); + updateAnnotationsBadge(); + }) + .catch((err: unknown) => { + // ui/initialize failed or transport rejected. Without a catch this is an + // unhandled rejection — iframe shows blank, server times out on every + // interact call with no clue why. + log.error("Failed to connect to host:", err); + showError( + `Failed to connect to host: ${err instanceof Error ? err.message : String(err)}`, + ); + }); // Debug helper: dump all annotation state. Run in DevTools console as // `__pdfDebug()` to diagnose ghost annotations (visible on canvas but not diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index c451cc1..fe34d83 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -1031,6 +1031,81 @@ describe("buildAnnotatedPdfBytes", () => { const bytes2 = await doc.save(); expect(bytes2.length).toBeGreaterThan(0); }); + + describe("form field persistence", () => { + // One fixture with every field type we support. pdf-lib's addOptionToPage + // writes radio buttonValues as numeric index strings ("0","1","2"), which + // is the stress case — the viewer stores those, but .select() wants labels. + let formPdfBytes: Uint8Array; + beforeAll(async () => { + const doc = await PDFDocument.create(); + const page = doc.addPage([612, 792]); + const form = doc.getForm(); + form.createTextField("name").addToPage(page, { x: 10, y: 700 }); + form.createCheckBox("agree").addToPage(page, { x: 10, y: 660 }); + const dd = form.createDropdown("country"); + dd.addOptions(["USA", "UK", "Canada"]); + dd.addToPage(page, { x: 10, y: 620 }); + const rg = form.createRadioGroup("size"); + rg.addOptionToPage("small", page, { x: 10, y: 580 }); + rg.addOptionToPage("medium", page, { x: 50, y: 580 }); + rg.addOptionToPage("large", page, { x: 90, y: 580 }); + formPdfBytes = await doc.save(); + }); + + it("writes text, checkbox, dropdown, and radio (by label) in one pass", async () => { + const out = await buildAnnotatedPdfBytes( + formPdfBytes, + [], + new Map([ + ["name", "Alice"], + ["agree", true], + ["country", "Canada"], + ["size", "medium"], + ]), + ); + const form = (await PDFDocument.load(out)).getForm(); + expect(form.getTextField("name").getText()).toBe("Alice"); + expect(form.getCheckBox("agree").isChecked()).toBe(true); + expect(form.getDropdown("country").getSelected()).toEqual(["Canada"]); + expect(form.getRadioGroup("size").getSelected()).toBe("medium"); + }); + + it("maps numeric radio buttonValue to option label by index", async () => { + // The viewer stores what pdf.js reports as buttonValue ("2"), not the + // label. Save must translate or the radio is silently dropped. + const out = await buildAnnotatedPdfBytes( + formPdfBytes, + [], + new Map([["size", "2"]]), + ); + const form = (await PDFDocument.load(out)).getForm(); + expect(form.getRadioGroup("size").getSelected()).toBe("large"); + }); + + it("leaves radio unset when value is neither label nor valid index", async () => { + const out = await buildAnnotatedPdfBytes( + formPdfBytes, + [], + new Map([["size", "bogus"]]), + ); + const form = (await PDFDocument.load(out)).getForm(); + expect(form.getRadioGroup("size").getSelected()).toBeUndefined(); + }); + + it("skips unknown field names without throwing", async () => { + const out = await buildAnnotatedPdfBytes( + formPdfBytes, + [], + new Map([ + ["nonexistent", "x"], + ["name", "kept"], + ]), + ); + const form = (await PDFDocument.load(out)).getForm(); + expect(form.getTextField("name").getText()).toBe("kept"); + }); + }); }); // ============================================================================= diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts index 0628d82..25d9ebb 100644 --- a/examples/pdf-server/src/pdf-annotations.ts +++ b/examples/pdf-server/src/pdf-annotations.ts @@ -18,6 +18,10 @@ import { PDFString, PDFHexString, StandardFonts, + PDFTextField, + PDFCheckBox, + PDFDropdown, + PDFRadioGroup, } from "pdf-lib"; // ============================================================================= @@ -810,26 +814,45 @@ export async function buildAnnotatedPdfBytes( // Add proper PDF annotation objects await addAnnotationDicts(pdfDoc, annotations); - // Apply form fills + // Apply form fills. Dispatch on actual field type — getTextField(name) throws + // for dropdowns/radios, so the old try/catch silently dropped those on save. if (formFields.size > 0) { try { const form = pdfDoc.getForm(); for (const [name, value] of formFields) { - try { - if (typeof value === "boolean") { - const checkbox = form.getCheckBox(name); - if (value) checkbox.check(); - else checkbox.uncheck(); + const field = form.getFieldMaybe(name); + if (!field) continue; + + if (field instanceof PDFCheckBox) { + if (value) field.check(); + else field.uncheck(); + } else if (field instanceof PDFRadioGroup) { + // The viewer stores pdf.js's buttonValue, which for PDFs with an + // /Opt array is a numeric index ("0","1","2") rather than the + // option label pdf-lib's select() expects. Try the label first, + // then fall back to indexing into getOptions(). + const opts = field.getOptions(); + const s = String(value); + if (opts.includes(s)) { + field.select(s); } else { - const textField = form.getTextField(name); - textField.setText(value); + const idx = Number(s); + if (Number.isInteger(idx) && idx >= 0 && idx < opts.length) { + field.select(opts[idx]); + } + // else: value is neither label nor index — leave unset } - } catch { - // Field not found or wrong type — skip + } else if (field instanceof PDFDropdown) { + // select() auto-enables edit mode for values outside getOptions(), + // so this works for both enumerated and free-text combos. + field.select(String(value)); + } else if (field instanceof PDFTextField) { + field.setText(String(value)); } + // PDFButton, PDFOptionList, PDFSignature: no fill_form support yet } } catch { - // Form not available — skip + // pdfDoc.getForm() throws if the PDF has no AcroForm } } From 4b3f6252a46ee5ac4c8630f077d24696b991327b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 22:45:00 +0100 Subject: [PATCH 04/97] pdf-server: add save_as interact action (#580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- examples/pdf-server/README.md | 24 ++- examples/pdf-server/server.test.ts | 289 ++++++++++++++++++++++++++++ examples/pdf-server/server.ts | 227 +++++++++++++++++++++- examples/pdf-server/src/commands.ts | 1 + examples/pdf-server/src/mcp-app.ts | 35 +++- 5 files changed, 557 insertions(+), 19 deletions(-) diff --git a/examples/pdf-server/README.md b/examples/pdf-server/README.md index cd265f8..2bee370 100644 --- a/examples/pdf-server/README.md +++ b/examples/pdf-server/README.md @@ -188,13 +188,13 @@ When roots are ignored the server logs: ## Tools -| Tool | Visibility | Purpose | -| ---------------- | ---------- | ----------------------------------------------------- | -| `list_pdfs` | Model | List available local files and origins | -| `display_pdf` | Model + UI | Display interactive viewer | -| `interact`¹ | Model | Navigate, annotate, search, extract pages, fill forms | -| `read_pdf_bytes` | App only | Stream PDF data in chunks | -| `save_pdf` | App only | Save annotated PDF back to local file | +| Tool | Visibility | Purpose | +| ---------------- | ---------- | ------------------------------------------------------------------- | +| `list_pdfs` | Model | List available local files and origins | +| `display_pdf` | Model + UI | Display interactive viewer | +| `interact`¹ | Model | Navigate, annotate, search, extract pages, fill forms, save to file | +| `read_pdf_bytes` | App only | Stream PDF data in chunks | +| `save_pdf` | App only | Save annotated PDF back to local file | ¹ stdio only by default; in HTTP mode requires `--enable-interact` — see [Deployment](#deployment). @@ -250,6 +250,16 @@ After the model calls `display_pdf`, it receives the `viewUUID` and a descriptio > > _Model calls `interact` with action `fill_form`, fields `[{name:"Name", value:"Alice"}, {name:"Date", value:"2026-02-26"}]`_ +### Saving + +> **User:** Save the annotated version as `/docs/contract-signed.pdf`. +> +> _Model calls `interact` with action `save_as`, path `/docs/contract-signed.pdf`. Fails if the file exists; pass `overwrite: true` to replace. The target path must be under a mounted directory root._ + +> **User:** Save my changes back to the file. +> +> _Model calls `interact` with action `save_as`, `overwrite: true` (no `path`). Overwrites the original — same writability gate as the viewer's save button. Only works for local files; remote PDFs need an explicit `path`._ + ## Testing ### E2E Tests (Playwright) diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index 5207506..8080df8 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -17,6 +17,7 @@ import { cliLocalFiles, isWritablePath, writeFlags, + viewSourcePaths, CACHE_INACTIVITY_TIMEOUT_MS, CACHE_MAX_LIFETIME_MS, CACHE_MAX_PDF_SIZE_BYTES, @@ -1055,6 +1056,294 @@ describe("interact tool", () => { }); }); + describe("save_as", () => { + // Roundtrip tests need: writable scope, kick off interact WITHOUT awaiting + // (it blocks until the view replies), poll → submit → await. The poll() + // call also registers the uuid in viewsPolled, satisfying + // ensureViewerIsPolling — without it interact would hang ~8s and fail. + + let tmpDir: string; + let savedDirs: Set; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdf-saveas-")); + savedDirs = new Set(allowedLocalDirs); + allowedLocalDirs.add(tmpDir); // make tmpDir a directory root → writable + }); + + afterEach(() => { + allowedLocalDirs.clear(); + for (const x of savedDirs) allowedLocalDirs.add(x); + viewSourcePaths.clear(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("no path, no source tracked → tells model to provide a path", async () => { + // Fresh UUID never seen by display_pdf → viewSourcePaths has no entry. + // Same condition as a remote (https://) PDF or a stale viewUUID. + const { server, client } = await connect(); + const r = await client.callTool({ + name: "interact", + arguments: { viewUUID: "saveas-nosource", action: "save_as" }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("no local source file"); + expect(firstText(r)).toContain("Provide an explicit `path`"); + await client.close(); + await server.close(); + }); + + it("no path, source tracked, overwrite omitted → asks for confirmation", async () => { + const { server, client } = await connect(); + const source = path.join(tmpDir, "original.pdf"); + fs.writeFileSync(source, "%PDF-1.4\noriginal"); + viewSourcePaths.set("saveas-noconfirm", source); + + const r = await client.callTool({ + name: "interact", + arguments: { viewUUID: "saveas-noconfirm", action: "save_as" }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("overwrites the original"); + expect(firstText(r)).toContain(source); + expect(firstText(r)).toContain("overwrite: true"); + // Nothing enqueued, file untouched + expect(fs.readFileSync(source, "utf8")).toBe("%PDF-1.4\noriginal"); + await client.close(); + await server.close(); + }); + + it("no path, source not writable → same gate as save button", async () => { + const { server, client } = await connect(); + // Source outside any directory root → isWritablePath false → save button + // would be hidden in the viewer. save_as should refuse for the same reason. + const outside = path.join(os.tmpdir(), "saveas-outside.pdf"); + fs.writeFileSync(outside, "x"); + viewSourcePaths.set("saveas-buttongate", outside); + + try { + const r = await client.callTool({ + name: "interact", + arguments: { + viewUUID: "saveas-buttongate", + action: "save_as", + overwrite: true, + }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("not writable"); + expect(firstText(r)).toContain("save button is hidden"); + } finally { + fs.rmSync(outside, { force: true }); + } + await client.close(); + await server.close(); + }); + + it("no path, overwrite: true → roundtrip overwrites the original", async () => { + const { server, client } = await connect(); + const uuid = "saveas-original"; + const source = path.join(tmpDir, "report.pdf"); + fs.writeFileSync(source, "%PDF-1.4\noriginal contents"); + viewSourcePaths.set(uuid, source); + + const interactPromise = client.callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "save_as", overwrite: true }, + }); + + const cmds = await poll(client, uuid); + expect(cmds).toHaveLength(1); + expect(cmds[0].type).toBe("save_as"); + await client.callTool({ + name: "submit_save_data", + arguments: { + requestId: cmds[0].requestId as string, + data: Buffer.from("%PDF-1.4\nannotated").toString("base64"), + }, + }); + + const r = await interactPromise; + expect(r.isError).toBeFalsy(); + expect(firstText(r)).toContain(source); + expect(fs.readFileSync(source, "utf8")).toBe("%PDF-1.4\nannotated"); + + await client.close(); + await server.close(); + }); + + it("rejects non-absolute path", async () => { + const { server, client } = await connect(); + const r = await client.callTool({ + name: "interact", + arguments: { + viewUUID: "saveas-rel", + action: "save_as", + path: "relative.pdf", + }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("absolute"); + await client.close(); + await server.close(); + }); + + it("rejects non-writable path", async () => { + const { server, client } = await connect(); + // Path outside any directory root → not writable. Validation is sync, + // so nothing is enqueued and the queue stays empty. + const r = await client.callTool({ + name: "interact", + arguments: { + viewUUID: "saveas-nowrite", + action: "save_as", + path: "/somewhere/else/out.pdf", + }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("not under a mounted directory root"); + await client.close(); + await server.close(); + }); + + it("rejects existing file when overwrite is false (default)", async () => { + const { server, client } = await connect(); + const target = path.join(tmpDir, "exists.pdf"); + fs.writeFileSync(target, "old contents"); + + const r = await client.callTool({ + name: "interact", + arguments: { + viewUUID: "saveas-exists", + action: "save_as", + path: target, + }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("already exists"); + expect(firstText(r)).toContain("overwrite: true"); + // Existence check is sync — nothing enqueued, file untouched. + expect(fs.readFileSync(target, "utf8")).toBe("old contents"); + await client.close(); + await server.close(); + }); + + it("full roundtrip: enqueue → poll → submit → file written", async () => { + const { server, client } = await connect(); + const uuid = "saveas-roundtrip"; + const target = path.join(tmpDir, "out.pdf"); + const pdfBytes = "%PDF-1.4\nfake-annotated-contents\n%%EOF"; + + // interact blocks in waitForSaveData until submit_save_data resolves it + const interactPromise = client.callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "save_as", path: target }, + }); + + // Viewer polls → receives the save_as command with a requestId + const cmds = await poll(client, uuid); + expect(cmds).toHaveLength(1); + expect(cmds[0].type).toBe("save_as"); + const requestId = cmds[0].requestId as string; + expect(typeof requestId).toBe("string"); + + // Viewer submits bytes + const submit = await client.callTool({ + name: "submit_save_data", + arguments: { + requestId, + data: Buffer.from(pdfBytes).toString("base64"), + }, + }); + expect(submit.isError).toBeFalsy(); + + // interact now unblocks with success + const r = await interactPromise; + expect(r.isError).toBeFalsy(); + expect(firstText(r)).toContain("Saved"); + expect(firstText(r)).toContain(target); + expect(fs.readFileSync(target, "utf8")).toBe(pdfBytes); + + await client.close(); + await server.close(); + }); + + it("overwrite: true replaces an existing file", async () => { + const { server, client } = await connect(); + const uuid = "saveas-overwrite"; + const target = path.join(tmpDir, "replace.pdf"); + fs.writeFileSync(target, "old contents"); + + const interactPromise = client.callTool({ + name: "interact", + arguments: { + viewUUID: uuid, + action: "save_as", + path: target, + overwrite: true, + }, + }); + + const cmds = await poll(client, uuid); + const requestId = cmds[0].requestId as string; + await client.callTool({ + name: "submit_save_data", + arguments: { + requestId, + data: Buffer.from("%PDF-1.4\nnew").toString("base64"), + }, + }); + + const r = await interactPromise; + expect(r.isError).toBeFalsy(); + expect(fs.readFileSync(target, "utf8")).toBe("%PDF-1.4\nnew"); + + await client.close(); + await server.close(); + }); + + it("propagates viewer-reported errors to the model", async () => { + const { server, client } = await connect(); + const uuid = "saveas-viewerr"; + const target = path.join(tmpDir, "wontwrite.pdf"); + + const interactPromise = client.callTool({ + name: "interact", + arguments: { viewUUID: uuid, action: "save_as", path: target }, + }); + + const cmds = await poll(client, uuid); + // Viewer hit an error building bytes → reports it instead of timing out + await client.callTool({ + name: "submit_save_data", + arguments: { + requestId: cmds[0].requestId as string, + error: "pdf-lib choked on a comb field", + }, + }); + + const r = await interactPromise; + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("pdf-lib choked on a comb field"); + expect(fs.existsSync(target)).toBe(false); + + await client.close(); + await server.close(); + }); + + it("submit_save_data with unknown requestId returns isError", async () => { + const { server, client } = await connect(); + const r = await client.callTool({ + name: "submit_save_data", + arguments: { requestId: "never-created", data: "AAAA" }, + }); + expect(r.isError).toBe(true); + expect(firstText(r)).toContain("No pending request"); + await client.close(); + await server.close(); + }); + }); + describe("viewer liveness", () => { // get_screenshot/get_text fail fast when the iframe never polled, instead // of waiting 45s for a viewer that isn't there. Reproduces the case where diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index b216d49..5bd79a4 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -243,9 +243,9 @@ function waitForPageData( /** * Wait for the viewer's first poll_pdf_commands call. * - * Called before waitForPageData() so a viewer that never mounted fails in ~8s - * with a specific message instead of a generic 45s "Timeout waiting for page - * data" that gives no hint why. + * Called before waitForPageData() / waitForSaveData() so a viewer that never + * mounted fails in ~8s with a specific message instead of a generic 45s + * "Timeout waiting for ..." that gives no hint why. * * Intentionally does NOT touch pollWaiters: piggybacking on that single-slot * Map races with poll_pdf_commands' batch-wait branch (which never cancels the @@ -267,6 +267,41 @@ async function ensureViewerIsPolling(uuid: string): Promise { } } +// ============================================================================= +// Pending save_as Requests (request-response bridge via client) +// ============================================================================= +// +// Same shape as get_pages: model's interact call blocks while the viewer +// builds annotated bytes and posts them back. Reuses GET_PAGES_TIMEOUT_MS +// (45s) — generous because pdf-lib reflow on a large doc can take seconds. + +const pendingSaveRequests = new Map void>(); + +/** + * Wait for the viewer to build annotated PDF bytes and submit them as base64. + * Rejects on timeout, abort, or when the viewer reports an error. + */ +function waitForSaveData( + requestId: string, + signal?: AbortSignal, +): Promise { + return new Promise((resolve, reject) => { + const settle = (v: string | Error) => { + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + pendingSaveRequests.delete(requestId); + v instanceof Error ? reject(v) : resolve(v); + }; + const onAbort = () => settle(new Error("interact request cancelled")); + const timer = setTimeout( + () => settle(new Error("Timeout waiting for PDF bytes from viewer")), + GET_PAGES_TIMEOUT_MS, + ); + signal?.addEventListener("abort", onAbort); + pendingSaveRequests.set(requestId, settle); + }); +} + interface QueueEntry { commands: PdfCommand[]; /** Timestamp of the most recent enqueue or dequeue */ @@ -287,6 +322,15 @@ const pollWaiters = new Map void>(); */ const viewsPolled = new Set(); +/** + * Resolved local file path per viewer UUID, for save_as without an explicit + * target. Only set for local files (remote PDFs have nothing to overwrite). + * Populated during display_pdf, cleared by the heartbeat sweep. + * + * Exported for tests. + */ +export const viewSourcePaths = new Map(); + /** Valid form field names per viewer UUID (populated during display_pdf) */ const viewFieldNames = new Map>(); @@ -333,6 +377,7 @@ function pruneStaleQueues(): void { viewFieldNames.delete(uuid); viewFieldInfo.delete(uuid); viewsPolled.delete(uuid); + viewSourcePaths.delete(uuid); stopFileWatch(uuid); } } @@ -1209,7 +1254,8 @@ export function createServer(options: CreateServerOptions = {}): McpServer { "read_pdf_bytes", { title: "Read PDF Bytes", - description: "Read a range of bytes from a PDF (max 512KB per request)", + description: + "Read a range of bytes from a PDF (max 512KB per request). The model should NOT call this tool directly.", inputSchema: { url: z.string().describe("PDF URL or local file path"), offset: z.number().min(0).default(0).describe("Byte offset"), @@ -1392,6 +1438,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before : decodeURIComponent(normalized); const resolved = path.resolve(localPath); debugResolved = resolved; + if (!disableInteract) viewSourcePaths.set(uuid, resolved); if (isWritablePath(resolved)) { try { await fs.promises.access(resolved, fs.constants.W_OK); @@ -1475,7 +1522,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before ? `Displaying PDF: ${normalized}` : `PDF opened. viewUUID: ${uuid} -→ To annotate, sign, stamp, fill forms, navigate, or extract: call \`interact\` with this viewUUID. +→ To annotate, sign, stamp, fill forms, navigate, extract, or save to a file: call \`interact\` with this viewUUID. → DO NOT call display_pdf again — that spawns a separate viewer with a different viewUUID; your interact calls would target the new empty one, not the one the user is looking at. URL: ${normalized}`, @@ -1600,6 +1647,7 @@ URL: ${normalized}`, "fill_form", "get_text", "get_screenshot", + "save_as", ]) .describe("Action to perform"), page: z @@ -1654,6 +1702,16 @@ URL: ${normalized}`, .describe( "Page ranges for get_text. Each has optional start/end. [{start:1,end:5}], [{}] = all pages. Max 20 pages.", ), + path: z + .string() + .optional() + .describe( + "Target file path for save_as. Absolute path or file:// URL. Omit to overwrite the original file (requires overwrite: true).", + ), + overwrite: z + .boolean() + .optional() + .describe("Overwrite if file exists (for save_as). Default false."), }); type InteractCommand = z.infer; @@ -1793,6 +1851,8 @@ URL: ${normalized}`, content, fields, intervals, + path: savePath, + overwrite, } = cmd; let description: string; @@ -2084,6 +2144,97 @@ URL: ${normalized}`, isError: true, }; } + case "save_as": { + const saveErr = (text: string) => ({ + content: [{ type: "text" as const, text }], + isError: true as const, + }); + + let resolved: string; + if (savePath) { + // Explicit target. Same path normalisation as save_pdf — but NOT + // validateUrl(), which fails on non-existent files. + const filePath = isFileUrl(savePath) + ? fileUrlToPath(savePath) + : isLocalPath(savePath) + ? decodeURIComponent(savePath) + : null; + if (!filePath) + return saveErr( + "save_as: path must be an absolute local path or file:// URL", + ); + resolved = path.resolve(filePath); + if (!isWritablePath(resolved)) + return saveErr( + `save_as refused: ${resolved} is not under a mounted ` + + `directory root. Only paths under directory roots ` + + `(or files passed as CLI args) are writable.`, + ); + if (!overwrite && fs.existsSync(resolved)) + return saveErr( + `File already exists: ${resolved}. ` + + `Set overwrite: true to replace it, or choose a different path.`, + ); + } else { + // No target → overwrite the original. Same gate as the viewer's + // save button: isWritablePath + OS-level W_OK (so we don't try + // on read-only mounts). Remote PDFs have no source path stored. + const source = viewSourcePaths.get(uuid); + if (!source) + return saveErr( + "save_as: no `path` given and this viewer has no local source " + + "file to overwrite (it's a remote URL, or the viewUUID is " + + "stale/unknown). Provide an explicit `path`.", + ); + if (!overwrite) + return saveErr( + `save_as: omitting \`path\` overwrites the original ` + + `(${source}). Set overwrite: true to confirm.`, + ); + if (!isWritablePath(source)) + return saveErr( + `save_as refused: ${source} is not writable (the viewer's ` + + `save button is hidden for the same reason).`, + ); + try { + await fs.promises.access(source, fs.constants.W_OK); + } catch { + return saveErr( + `save_as refused: ${source} is not writable at the OS level ` + + `(read-only mount or insufficient permissions).`, + ); + } + resolved = source; + } + + const requestId = randomUUID(); + enqueueCommand(uuid, { type: "save_as", requestId }); + let data: string; + try { + await ensureViewerIsPolling(uuid); + data = await waitForSaveData(requestId, signal); + } catch (err) { + return saveErr( + `save_as failed: ${err instanceof Error ? err.message : String(err)}`, + ); + } + try { + const bytes = Buffer.from(data, "base64"); + await fs.promises.writeFile(resolved, bytes); + return { + content: [ + { + type: "text", + text: `Saved annotated PDF to ${resolved} (${bytes.length} bytes)`, + }, + ], + }; + } catch (err) { + return saveErr( + `save_as: failed to write ${resolved}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } default: return { content: [{ type: "text", text: `Unknown action: ${action}` }], @@ -2142,7 +2293,9 @@ Example — add a signature image and a stamp, then screenshot to verify: • get_text: extract text from pages. Optional \`page\` for single page, or \`intervals\` for ranges [{start?,end?}]. Max 20 pages. • get_screenshot: capture a single page as PNG image. Requires \`page\`. -**FORMS** — fill_form: fill fields with \`fields\` array of {name, value}.`, +**FORMS** — fill_form: fill fields with \`fields\` array of {name, value}. + +**SAVE** — save_as: write the annotated PDF (annotations + form values) to a file. Pass \`path\` (absolute path or file://) for a new location, or omit \`path\` to overwrite the original. Set \`overwrite: true\` to replace an existing file (always required when omitting \`path\`).`, inputSchema: { viewUUID: z .string() @@ -2164,6 +2317,7 @@ Example — add a signature image and a stamp, then screenshot to verify: "fill_form", "get_text", "get_screenshot", + "save_as", ]) .optional() .describe( @@ -2221,6 +2375,16 @@ Example — add a signature image and a stamp, then screenshot to verify: .describe( "Page ranges for get_text. Each has optional start/end. [{start:1,end:5}], [{}] = all pages. Max 20 pages.", ), + path: z + .string() + .optional() + .describe( + "Target file path for save_as. Absolute path or file:// URL. Omit to overwrite the original file (requires overwrite: true).", + ), + overwrite: z + .boolean() + .optional() + .describe("Overwrite if file exists (for save_as). Default false."), // Batch mode commands: z .array(InteractCommandSchema) @@ -2244,6 +2408,8 @@ Example — add a signature image and a stamp, then screenshot to verify: content, fields, intervals, + path: savePath, + overwrite, commands, }, extra, @@ -2265,6 +2431,8 @@ Example — add a signature image and a stamp, then screenshot to verify: content, fields, intervals, + path: savePath, + overwrite, }, ] : []; @@ -2324,7 +2492,7 @@ Example — add a signature image and a stamp, then screenshot to verify: { title: "Submit Page Data", description: - "Submit rendered page data for a get_pages request (used by viewer)", + "Submit rendered page data for a get_pages request (used by viewer). The model should NOT call this tool directly.", inputSchema: { requestId: z .string() @@ -2360,13 +2528,53 @@ Example — add a signature image and a stamp, then screenshot to verify: }, ); + // Tool: submit_save_data (app-only) - Viewer submits annotated PDF bytes + registerAppTool( + server, + "submit_save_data", + { + title: "Submit Save Data", + description: + "Submit annotated PDF bytes for a save_as request (used by viewer). The model should NOT call this tool directly.", + inputSchema: { + requestId: z + .string() + .describe("The request ID from the save_as command"), + data: z.string().optional().describe("Base64-encoded PDF bytes"), + error: z + .string() + .optional() + .describe("Error message if the viewer failed to build bytes"), + }, + _meta: { ui: { visibility: ["app"] } }, + }, + async ({ requestId, data, error }): Promise => { + const settle = pendingSaveRequests.get(requestId); + if (!settle) { + return { + content: [ + { type: "text", text: `No pending request for ${requestId}` }, + ], + isError: true, + }; + } + if (error || !data) { + settle(new Error(error || "Viewer returned no data")); + } else { + settle(data); + } + return { content: [{ type: "text", text: "Submitted" }] }; + }, + ); + // Tool: poll_pdf_commands (app-only) - Poll for pending commands registerAppTool( server, "poll_pdf_commands", { title: "Poll PDF Commands", - description: "Poll for pending commands for a PDF viewer", + description: + "Poll for pending commands for a PDF viewer. The model should NOT call this tool directly.", inputSchema: { viewUUID: z.string().describe("The viewUUID of the PDF viewer"), }, @@ -2412,7 +2620,8 @@ Example — add a signature image and a stamp, then screenshot to verify: "save_pdf", { title: "Save PDF", - description: "Save annotated PDF bytes back to a local file", + description: + "Save annotated PDF bytes back to a local file. The model should NOT call this tool directly — use interact with action: save_as instead.", inputSchema: { url: z.string().describe("Original PDF URL or local file path"), data: z.string().describe("Base64-encoded PDF bytes"), diff --git a/examples/pdf-server/src/commands.ts b/examples/pdf-server/src/commands.ts index fa657ca..c469ebd 100644 --- a/examples/pdf-server/src/commands.ts +++ b/examples/pdf-server/src/commands.ts @@ -65,4 +65,5 @@ export type PdfCommand = getText: boolean; getScreenshots: boolean; } + | { type: "save_as"; requestId: string } | { type: "file_changed"; mtimeMs: number }; diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index b20d74b..0a0eb4c 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -4298,6 +4298,30 @@ async function processCommands(commands: PdfCommand[]): Promise { .catch(() => {}); } break; + case "save_as": + // Same await-before-next-poll discipline as get_pages — submit must + // be SENT before we re-poll, or it queues behind the 30s long-poll. + try { + const pdfBytes = await getAnnotatedPdfBytes(); + const base64 = uint8ArrayToBase64(pdfBytes); + await app.callServerTool({ + name: "submit_save_data", + arguments: { requestId: cmd.requestId, data: base64 }, + }); + log.info(`save_as: submitted ${pdfBytes.length} bytes`); + } catch (err) { + log.error("save_as: failed to build bytes — submitting error:", err); + await app + .callServerTool({ + name: "submit_save_data", + arguments: { + requestId: cmd.requestId, + error: err instanceof Error ? err.message : String(err), + }, + }) + .catch(() => {}); + } + break; case "file_changed": { // Skip our own save_pdf echo: either save is still in flight, or the // event's mtime matches what save_pdf just returned. @@ -4337,10 +4361,15 @@ async function processCommands(commands: PdfCommand[]): Promise { } // Persist after processing batch — but only if anything mutated. - // get_pages / file_changed are read-only; writing localStorage and - // recomputing the diff for them is wasted work. + // get_pages / save_as / file_changed are read-only; writing localStorage + // and recomputing the diff for them is wasted work. if ( - commands.some((c) => c.type !== "get_pages" && c.type !== "file_changed") + commands.some( + (c) => + c.type !== "get_pages" && + c.type !== "save_as" && + c.type !== "file_changed", + ) ) { persistAnnotations(); } From 9d55050a009427a394772c3bb6f9671cb2447d0f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 22:45:45 +0100 Subject: [PATCH 05/97] fix(pdf-server): render page before O(numPages) annotation scans (#581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/pdf-server/src/mcp-app.ts | 32 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 0a0eb4c..5806d86 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -3896,11 +3896,16 @@ async function reloadPdf(): Promise { log.info("PDF reloaded:", totalPages, "pages,", totalBytes, "bytes"); showViewer(); + // Render immediately — annotation/form scans below are O(numPages) and + // do NOT block the canvas. See same pattern in the initial-load path. + await renderPage(); + await loadBaselineAnnotations(document); await buildFieldNameMap(document); syncFormValuesToStorage(); updateAnnotationsBadge(); renderAnnotationPanel(); + renderPage(); startPreloading(); } catch (err) { @@ -4103,9 +4108,18 @@ app.ontoolresult = async (result: CallToolResult) => { downloadBtn.style.display = app.getHostCapabilities()?.downloadFile ? "" : "none"; - // Save button visibility driven by setDirty()/updateSaveBtn(); - // restoreAnnotations() above may have already shown it via setDirty(true). - updateSaveBtn(); + + // Compute fit + render IMMEDIATELY for fast first paint. The canvas is + // unsized until renderPage() runs — anything async between showViewer() + // and here makes the empty viewer visible. The annotation/form scans + // below are O(numPages) and do NOT block the canvas (page.render only + // needs canvasContext+viewport), so they run after. + const fitScale = await computeFitToWidthScale(); + if (fitScale !== null) { + scale = fitScale; + log.info("Fit-to-width scale:", scale); + } + await renderPage(); // Import annotations from the PDF to establish baseline await loadBaselineAnnotations(document); @@ -4118,14 +4132,12 @@ app.ontoolresult = async (result: CallToolResult) => { syncFormValuesToStorage(); updateAnnotationsBadge(); + // Save button visibility driven by setDirty()/updateSaveBtn(); + // restoreAnnotations() may have just flipped it via setDirty(true). + updateSaveBtn(); - // Compute fit-to-width scale for narrow containers (e.g. mobile) - const fitScale = await computeFitToWidthScale(); - if (fitScale !== null) { - scale = fitScale; - log.info("Fit-to-width scale:", scale); - } - + // Re-render to overlay PDF-baseline annotations + restored form values. + // For PDFs with neither, the canvas is identical → no flicker. renderPage(); // Start background preloading of all pages for text extraction startPreloading(); From 800f6d08fd44a64bdd9088b3beafaaa034839126 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 00:49:29 +0100 Subject: [PATCH 06/97] fix(pdf-server): viewer liveness, 1:1 batch results, fullscreen jitter (#579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: ...", ] → 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. --- examples/pdf-server/server.test.ts | 38 ++++++----- examples/pdf-server/server.ts | 65 +++++++++++++----- examples/pdf-server/src/mcp-app.ts | 102 ++++++++++++++++++++--------- 3 files changed, 139 insertions(+), 66 deletions(-) diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index 8080df8..a2f64ee 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -1444,12 +1444,12 @@ describe("interact tool", () => { await server.close(); }, 15_000); - it("batch failure puts error message first", async () => { - // When [fill_form, get_screenshot] runs and get_screenshot times out, - // content[0] must describe the failure — not the earlier success. Some - // hosts flatten isError results to content[0].text only, which previously - // showed "Queued: Filled N fields" with isError:true and dropped the - // actual timeout entirely. + it("batch step failure: 1:1 content, no isError, ERROR-prefixed slot", async () => { + // LocalAgentMode SDK 2.1.87 collapses isError:true results to a bare + // string of content[0].text — drops images from earlier successful + // steps. So we don't set isError on a step failure: each command gets + // one content slot, the failed one starts with "ERROR", and the batch + // stops there. content.length tells the model how far it got. const { server, client } = await connect(); const uuid = "batch-error-ordering"; @@ -1460,22 +1460,24 @@ describe("interact tool", () => { commands: [ { action: "navigate", page: 3 }, // succeeds → "Queued: ..." { action: "get_screenshot", page: 1 }, // never-polled → fast-fail + { action: "navigate", page: 5 }, // never reached ], }, }); - expect(r.isError).toBe(true); - const texts = (r.content as Array<{ type: string; text: string }>).map( - (c) => c.text, - ); - // content[0]: batch-failed summary naming the culprit - expect(texts[0]).toContain("failed"); - expect(texts[0]).toContain("2/2"); - expect(texts[0]).toContain("get_screenshot"); - // content[1]: the actual error - expect(texts[1]).toContain("never connected"); - // content[2]: the earlier success, pushed to the back - expect(texts[2]).toContain("Queued"); + // Not isError — that would trigger the SDK flatten + expect(r.isError).toBeFalsy(); + const content = r.content as Array<{ type: string; text?: string }>; + // Stopped at step 2; step 3 never ran + expect(content).toHaveLength(2); + // Slot 0: step 1's success, untouched + expect(content[0].text).toContain("Queued"); + expect(content[0].text).not.toMatch(/^ERROR/); + // Slot 1: step 2's failure, ERROR-prefixed with the actual message + expect(content[1].text).toMatch(/^ERROR/); + expect(content[1].text).toContain("2/3"); + expect(content[1].text).toContain("get_screenshot"); + expect(content[1].text).toContain("never connected"); await client.close(); await server.close(); diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 5bd79a4..3c6168b 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -2254,7 +2254,7 @@ URL: ${normalized}`, description: `Interact with a PDF viewer: annotate, navigate, search, extract text/screenshots, fill forms. IMPORTANT: viewUUID must be the exact UUID returned by display_pdf (e.g. "a1b2c3d4-..."). Do NOT use arbitrary strings. -**BATCHING**: Send multiple commands in one call via \`commands\` array. Commands run sequentially. TIP: End with \`get_screenshot\` to verify your changes. +**BATCHING**: Send multiple commands in one call via \`commands\` array. Commands run sequentially; results are returned in the same order, one content item per command. If a command fails, the batch stops there and that command's slot contains text starting with \`ERROR\` — content.length tells you how far it got. TIP: End with \`get_screenshot\` to verify your changes. **ANNOTATION** — add_annotations with array of annotation objects. Each needs: id (unique string), type, page (1-indexed). @@ -2449,9 +2449,21 @@ Example — add a signature image and a stamp, then screenshot to verify: }; } - // Process commands sequentially, collecting all content parts + // 1:1 content array — content[i] is the result of commands[i]. + // + // For multi-step batches we do NOT set isError on a step failure: + // LocalAgentMode SDK 2.1.87 collapses isError:true results to a + // bare string of content[0].text, which would drop any images + // from earlier successful steps. Instead, the failed step's slot + // is text starting with "ERROR", and the batch stops there — the + // model reads content.length to see how far it got. + // + // Single-command calls have no prior results to lose, so they + // keep isError:true. The SDK's flatten-to-content[0].text is + // exactly the ERROR text we want it to see. const allContent: ContentPart[] = []; let failedAt = -1; + const t0 = Date.now(); for (let i = 0; i < commandList.length; i++) { const result = await processInteractCommand( @@ -2460,27 +2472,48 @@ Example — add a signature image and a stamp, then screenshot to verify: extra.signal, ); if (result.isError) { - // Error content first. Some hosts flatten isError results to - // content[0].text — if we push the error after prior successes, - // the user sees "Queued: Filled 7 fields" with isError:true and - // the actual failure is silently dropped. - allContent.unshift(...result.content); + const errText = result.content + .map((c) => (c.type === "text" ? c.text : null)) + .filter((t) => t != null) + .join(" — "); + // Normalize the prefix — processInteractCommand's catch blocks + // are inconsistent ("Error: ...", "add_annotations: ...", etc.) + const stripped = errText.replace(/^error:\s*/i, ""); + allContent.push({ + type: "text", + text: + commandList.length > 1 + ? `ERROR at step ${i + 1}/${commandList.length} (${commandList[i].action}): ${stripped}` + : `ERROR: ${stripped}`, + }); failedAt = i; + console.error( + `[interact] uuid=${uuid} step ${i + 1}/${commandList.length} ` + + `(${commandList[i].action}) failed after ${Date.now() - t0}ms: ${stripped}`, + ); break; } - allContent.push(...result.content); - } - - if (failedAt >= 0 && commandList.length > 1) { - allContent.unshift({ - type: "text", - text: `Batch failed at step ${failedAt + 1}/${commandList.length} (${commandList[failedAt].action}):`, - }); + // Squash multi-part successes (e.g. get_text returning multiple + // text blocks) into one slot so 1:1 indexing holds. Preserve a + // lone image as-is — that's the screenshot the model wants. + if (result.content.length === 1) { + allContent.push(result.content[0]); + } else { + const texts = result.content + .map((c) => (c.type === "text" ? c.text : null)) + .filter((t) => t != null); + allContent.push({ type: "text", text: texts.join("\n") }); + } } return { content: allContent, - ...(failedAt >= 0 ? { isError: true } : {}), + // isError flattens to a string in some SDKs, losing the array + // shape. Only set it when there was never going to be an array + // — single command, no positional contract to break. + ...(failedAt >= 0 && commandList.length === 1 + ? { isError: true } + : {}), }; }, ); diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 5806d86..66f7d04 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -287,8 +287,14 @@ async function computeFitToWidthScale(): Promise { * Only applies in inline mode - fullscreen mode uses scrolling. */ function requestFitToContent() { - if (currentDisplayMode === "fullscreen") { - return; // Fullscreen uses scrolling + // Read the host's current state, not our cached currentDisplayMode. + // currentDisplayMode defaults to "inline" and handleHostContextChanged + // only updates it `if (ctx.displayMode)` — if the host omits the field + // or the update lands one tick late, the cached value lies. We've seen + // this measure a near-empty pageWrapper (~85px = toolbar + padding) and + // shrink a fullscreen iframe to a sliver. + if (app.getHostContext()?.displayMode === "fullscreen") { + return; } const canvasHeight = canvasEl.height; @@ -324,6 +330,16 @@ function requestFitToContent() { // In inline mode (this function early-returns for fullscreen) the side panel is hidden const totalWidth = pageWrapperEl.offsetWidth + BUFFER; + // pageWrapper measuring ≈ 0 means the canvas hasn't laid out yet (early + // render, hidden ancestor, etc). Sending toolbar-height-only would shrink + // the iframe to a sliver. The next renderPage() will measure correctly. + if (pageWrapperHeight < toolbarHeight) { + log.info( + `requestFitToContent: pageWrapper ${pageWrapperHeight}px < toolbar ${toolbarHeight}px — skipping`, + ); + return; + } + app.sendSizeChanged({ width: totalWidth, height: totalHeight }); } @@ -4229,42 +4245,64 @@ async function processCommands(commands: PdfCommand[]): Promise { } break; case "add_annotations": + // Per-def isolation. If convertFromModelCoords or addAnnotation throws + // for one def (bad shape, NaN coords), the rest still apply — and + // critically, a get_pages later in this batch still runs. Without + // this, a single bad annotation makes the whole batch throw out of + // processCommands, the iframe never reaches submit_page_data, and the + // server's interact() waits the full 45s for a reply that never comes. for (const def of cmd.annotations) { - const pageHeight = await getPageHeight(def.page); - addAnnotation(convertFromModelCoords(def, pageHeight)); + try { + const pageHeight = await getPageHeight(def.page); + addAnnotation(convertFromModelCoords(def, pageHeight)); + } catch (err) { + log.error(`add_annotations: failed for id=${def.id}:`, err); + } } break; case "update_annotations": for (const update of cmd.annotations) { const existing = annotationMap.get(update.id); - if (!existing) continue; - // The model sends model coords (y-down, y = top-left). existing.def - // is internal coords (y-up, y = bottom-left). For rect/circle/image - // the converted internal y = pageHeight - modelY - height — a function - // of BOTH y AND height. If the model patches only {height}, we must - // still rewrite internal y to keep the top fixed; otherwise the - // bottom stays fixed and the top shifts. Same coupling applies to - // {page} changes across differently-sized pages. - // - // Fix: round-trip through model space. Convert existing to model - // coords, spread the patch on top (all-model now), convert back. - // convertToModelCoords is self-inverse (pdf-annotations.ts:192) so - // unchanged fields pass through unmolested. - const srcPageH = await getPageHeight(existing.def.page); - const existingModel = convertToModelCoords(existing.def, srcPageH); - const mergedModel = { - ...existingModel, - ...update, - } as PdfAnnotationDef; - const dstPageH = - update.page != null && update.page !== existing.def.page - ? await getPageHeight(update.page) - : srcPageH; - const mergedInternal = convertFromModelCoords(mergedModel, dstPageH); - // Pass the FULL merged def. updateAnnotation() already merges over - // the tracked def, so passing everything is correct and avoids the - // "only copy back Object.keys(update)" loop that caused the bug. - updateAnnotation(mergedInternal); + if (!existing) { + log.error( + `update_annotations: id=${update.id} not found — skipping`, + ); + continue; + } + try { + // The model sends model coords (y-down, y = top-left). existing.def + // is internal coords (y-up, y = bottom-left). For rect/circle/image + // the converted internal y = pageHeight - modelY - height — a function + // of BOTH y AND height. If the model patches only {height}, we must + // still rewrite internal y to keep the top fixed; otherwise the + // bottom stays fixed and the top shifts. Same coupling applies to + // {page} changes across differently-sized pages. + // + // Fix: round-trip through model space. Convert existing to model + // coords, spread the patch on top (all-model now), convert back. + // convertToModelCoords is self-inverse (pdf-annotations.ts:192) so + // unchanged fields pass through unmolested. + const srcPageH = await getPageHeight(existing.def.page); + const existingModel = convertToModelCoords(existing.def, srcPageH); + const mergedModel = { + ...existingModel, + ...update, + } as PdfAnnotationDef; + const dstPageH = + update.page != null && update.page !== existing.def.page + ? await getPageHeight(update.page) + : srcPageH; + const mergedInternal = convertFromModelCoords( + mergedModel, + dstPageH, + ); + // Pass the FULL merged def. updateAnnotation() already merges over + // the tracked def, so passing everything is correct and avoids the + // "only copy back Object.keys(update)" loop that caused the bug. + updateAnnotation(mergedInternal); + } catch (err) { + log.error(`update_annotations: failed for id=${update.id}:`, err); + } } break; case "remove_annotations": From 68c58b60b791d9256707eea9a5ca2fd3e3e54912 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 03:43:47 +0100 Subject: [PATCH 07/97] feat(pdf-server): fit-to-page on fullscreen + pinch-to-zoom (#583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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). --- examples/basic-host/src/implementation.ts | 22 ++ examples/pdf-server/src/mcp-app.css | 11 + examples/pdf-server/src/mcp-app.ts | 279 +++++++++++++++++++--- tests/e2e/pdf-viewer-zoom.spec.ts | 168 +++++++++++++ 4 files changed, 445 insertions(+), 35 deletions(-) create mode 100644 tests/e2e/pdf-viewer-zoom.spec.ts diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 36ecee0..31e3698 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -306,6 +306,28 @@ export function newAppBridge( appBridge.sendHostContextChange({ theme: newTheme }); }); + // Per spec, the host SHOULD notify the view when container dimensions + // change. A ResizeObserver on the iframe covers window resize, layout + // shifts, and the inline↔fullscreen panel toggle (which React applies + // a tick after onrequestdisplaymode returns — sending containerDimensions + // alongside displayMode there would race the layout). Height stays + // flexible (maxHeight) so the view can keep driving it via sendSizeChanged. + const iframeResizeObserver = new ResizeObserver(([entry]) => { + const width = Math.round(entry.contentRect.width); + if (width > 0) { + appBridge.sendHostContextChange({ + containerDimensions: { width, maxHeight: 6000 }, + }); + } + }); + iframeResizeObserver.observe(iframe); + // AppBridge inherits Protocol's onclose hook — chain disposal there. + const prevOnclose = appBridge.onclose; + appBridge.onclose = () => { + iframeResizeObserver.disconnect(); + prevOnclose?.(); + }; + // Register all handlers before calling connect(). The view can start // sending requests immediately after the initialization handshake, so any // handlers registered after connect() might miss early requests. diff --git a/examples/pdf-server/src/mcp-app.css b/examples/pdf-server/src/mcp-app.css index b307594..b6bbb43 100644 --- a/examples/pdf-server/src/mcp-app.css +++ b/examples/pdf-server/src/mcp-app.css @@ -326,6 +326,17 @@ body { .main.fullscreen .canvas-container { min-height: 0; /* Allow flex item to shrink below content size */ overflow: auto; /* Scroll within the document area only */ + /* Let JS own pinch-zoom (we re-render the PDF, not the viewport). + * pan-x pan-y keeps native scrolling/panning working. iOS Safari only + * partially honours this — preventDefault on touchmove backs it up. */ + touch-action: pan-x pan-y; + /* Keep the page clear of host overlays (chat input, home indicator). + * computeFitScale subtracts padding, so fit-to-page lands above these. + * The container background still extends edge-to-edge (.main.fullscreen + * has padding:0). Top inset is on the toolbar; the rest land here. */ + padding-bottom: calc(1rem + var(--safe-bottom, 0px)); + padding-left: calc(1rem + var(--safe-left, 0px)); + padding-right: calc(1rem + var(--safe-right, 0px)); } /* Search Button */ diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 66f7d04..7e53959 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -128,6 +128,8 @@ let pdfDocument: pdfjsLib.PDFDocumentProxy | null = null; let currentPage = 1; let totalPages = 0; let scale = 1.0; +const ZOOM_MIN = 0.5; +const ZOOM_MAX = 3.0; let pdfUrl = ""; let pdfTitle: string | undefined; let viewUUID: string | undefined; @@ -209,6 +211,7 @@ const searchCloseBtn = document.getElementById( ) as HTMLButtonElement; const highlightLayerEl = document.getElementById("highlight-layer")!; const annotationLayerEl = document.getElementById("annotation-layer")!; +const pageWrapperEl = document.querySelector(".page-wrapper") as HTMLElement; // formLayerEl → imported from ./viewer-state.js const saveBtn = document.getElementById("save-btn") as HTMLButtonElement; const downloadBtn = document.getElementById( @@ -256,32 +259,99 @@ let currentDisplayMode: "inline" | "fullscreen" = "inline"; let userHasZoomed = false; /** - * Compute a scale that fits the PDF page width to the available container width. - * Returns null if the container isn't visible or the page width is unavailable. + * Compute the scale that best fits the PDF page to the container. + * Returns null only when the container hasn't laid out yet. + * + * Inline mode: fit-to-WIDTH capped at 1.0. We shrink to fit a narrow chat + * column but don't blow up past natural size — the iframe sizes itself to + * the page via sendSizeChanged, so growing past 1.0 would just make the + * iframe huge. + * + * Fullscreen mode: fit-to-PAGE capped at ZOOM_MAX. The whole page is visible + * without scrolling (min of width-fit and height-fit). On a wide screen this + * typically lands well above 1.0; on a phone in portrait, width is the + * tighter constraint and it degrades to fit-to-width. */ -async function computeFitToWidthScale(): Promise { +async function computeFitScale(): Promise { if (!pdfDocument) return null; try { const page = await pdfDocument.getPage(currentPage); const naturalViewport = page.getViewport({ scale: 1.0 }); const pageWidth = naturalViewport.width; + const pageHeight = naturalViewport.height; const container = canvasContainerEl as HTMLElement; const containerStyle = getComputedStyle(container); - const paddingLeft = parseFloat(containerStyle.paddingLeft); - const paddingRight = parseFloat(containerStyle.paddingRight); - const availableWidth = container.clientWidth - paddingLeft - paddingRight; + const padX = + parseFloat(containerStyle.paddingLeft) + + parseFloat(containerStyle.paddingRight); + const padY = + parseFloat(containerStyle.paddingTop) + + parseFloat(containerStyle.paddingBottom); + const availableWidth = container.clientWidth - padX; + const availableHeight = container.clientHeight - padY; if (availableWidth <= 0 || pageWidth <= 0) return null; - if (availableWidth >= pageWidth) return null; // Already fits - return availableWidth / pageWidth; + const widthFit = availableWidth / pageWidth; + if (currentDisplayMode !== "fullscreen") { + return Math.min(1.0, widthFit); + } + // Fullscreen: fit the WHOLE page. If height isn't measurable yet + // (early layout) fall back to width-fit. + const heightFit = + availableHeight > 0 && pageHeight > 0 + ? availableHeight / pageHeight + : widthFit; + return Math.min(ZOOM_MAX, widthFit, heightFit); } catch { return null; } } +/** + * Re-apply the auto-fit scale if the user hasn't taken over zoom. Runs on + * container resize (ResizeObserver) and display-mode transitions. + * + * The ResizeObserver path is the load-bearing one. Hosts disagree on + * what they send and when: + * - basic-host sends containerDimensions only at init, never on resize + * - Claude Desktop resizes the iframe and sends displayMode, but the + * iframe element may not have its new size yet when the message lands + * The observer fires after the iframe has actually laid out at the new + * size, so clientWidth is fresh. The hostContextChanged hooks are kept + * as a fast path / belt-and-suspenders. + */ +async function refitScale(): Promise { + if (!pdfDocument || userHasZoomed) return; + const fitScale = await computeFitScale(); + if (fitScale !== null && Math.abs(fitScale - scale) > 0.01) { + scale = fitScale; + log.info("Refit scale:", scale); + renderPage(); + } +} + +// Refit when the container actually changes size. In INLINE mode this is +// gated to width-GROWTH only — renderPage() → requestFitToContent() → host +// resizes iframe to the (smaller) page width would otherwise re-trigger the +// observer and walk the scale down to ZOOM_MIN. In FULLSCREEN +// requestFitToContent early-returns so there's no loop, and fit-to-page +// needs height changes too (rotation, browser chrome on mobile). +let lastContainerW = 0; +let lastContainerH = 0; +const containerResizeObserver = new ResizeObserver(([entry]) => { + const { width: w, height: h } = entry.contentRect; + const grewW = w > lastContainerW + 1; + const changed = + Math.abs(w - lastContainerW) > 1 || Math.abs(h - lastContainerH) > 1; + lastContainerW = w; + lastContainerH = h; + if (currentDisplayMode === "fullscreen" ? changed : grewW) refitScale(); +}); +containerResizeObserver.observe(canvasContainerEl as HTMLElement); + /** * Request the host to resize the app to fit the current PDF page. * Only applies in inline mode - fullscreen mode uses scrolling. @@ -303,13 +373,9 @@ function requestFitToContent() { } // Get actual element dimensions - const canvasContainerEl = document.querySelector( - ".canvas-container", - ) as HTMLElement; - const pageWrapperEl = document.querySelector(".page-wrapper") as HTMLElement; const toolbarEl = document.querySelector(".toolbar") as HTMLElement; - if (!canvasContainerEl || !toolbarEl || !pageWrapperEl) { + if (!toolbarEl) { return; } @@ -2982,6 +3048,9 @@ async function renderPage() { // Set display size in CSS pixels canvasEl.style.width = `${viewport.width}px`; canvasEl.style.height = `${viewport.height}px`; + // Drop any pinch preview transform in the same frame as the canvas + // resize so the size handoff is atomic. + pageWrapperEl.style.transform = ""; // Scale context for retina ctx.scale(dpr, dpr); @@ -3249,20 +3318,24 @@ function scrollSelectionIntoView(): void { function zoomIn() { userHasZoomed = true; - scale = Math.min(scale + 0.25, 3.0); + scale = Math.min(scale + 0.25, ZOOM_MAX); renderPage().then(scrollSelectionIntoView); } function zoomOut() { userHasZoomed = true; - scale = Math.max(scale - 0.25, 0.5); + scale = Math.max(scale - 0.25, ZOOM_MIN); renderPage().then(scrollSelectionIntoView); } function resetZoom() { userHasZoomed = false; - scale = 1.0; - renderPage().then(scrollSelectionIntoView); + // Re-fit rather than blindly snapping to 1.0 — in a narrow inline iframe + // 1.0 overflows, and in fullscreen 1.0 leaves the page floating in space. + computeFitScale().then((fitScale) => { + scale = fitScale ?? 1.0; + renderPage().then(scrollSelectionIntoView); + }); } async function toggleFullscreen() { @@ -3666,6 +3739,59 @@ document.addEventListener("selectionchange", () => { }, 300); }); +// --- Pinch zoom (fullscreen only) --- +// +// Covers two input paths: +// 1. wheel + ctrlKey → trackpad pinch on macOS Safari/Chrome/FF and +// Windows precision touchpads. The browser synthesizes +// these on pinch; deltaY < 0 is zoom-in. +// 2. two-finger touch → mobile Safari / Chrome Android. We track the +// distance between the two touches and scale by the +// ratio against the initial distance. +// +// Both paths apply a CSS transform to .page-wrapper for live feedback (GPU, +// no canvas re-render per frame), then commit to a real renderPage() once +// the gesture settles. renderPage() is heavy (PDF.js page.render → canvas, +// TextLayer, AnnotationLayer all rebuilt) — way too slow for touchmove. + +/** Scale at gesture start. The CSS transform is relative to the rendered + * canvas, so previewScale / pinchStartScale is what we paint. */ +let pinchStartScale = 1.0; +/** What we'd commit to if the gesture ended right now. */ +let previewScale = 1.0; +/** Debounce timer — wheel events have no end event, so we wait for quiet. */ +let pinchSettleTimer: ReturnType | null = null; + +function beginPinch() { + pinchStartScale = scale; + previewScale = scale; + // transform-origin matches the flex layout's anchor (justify-content: + // center, align-items: flex-start) so the preview and the committed + // canvas grow from the same point — otherwise the page jumps on release. + pageWrapperEl.style.transformOrigin = "50% 0"; +} + +function updatePinch(nextScale: number) { + previewScale = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, nextScale)); + // Transform is RELATIVE to the rendered canvas (which sits at + // pinchStartScale), so a previewScale equal to pinchStartScale → ratio 1. + pageWrapperEl.style.transform = `scale(${previewScale / pinchStartScale})`; + zoomLevelEl.textContent = `${Math.round(previewScale * 100)}%`; +} + +function commitPinch() { + if (Math.abs(previewScale - scale) < 0.01) { + // Dead-zone — no re-render. Clear here since renderPage won't run. + pageWrapperEl.style.transform = ""; + return; + } + userHasZoomed = true; + scale = previewScale; + // renderPage clears the transform in the same frame as the canvas + // resize (after its first await) so there's no snap-back. + renderPage().then(scrollSelectionIntoView); +} + // Horizontal scroll/swipe to change pages (disabled when zoomed) let horizontalScrollAccumulator = 0; const SCROLL_THRESHOLD = 50; @@ -3675,6 +3801,28 @@ canvasContainerEl.addEventListener( (event) => { const e = event as WheelEvent; + // Trackpad pinch arrives as wheel with ctrlKey set (Chrome/FF/Edge on + // macOS+Windows, Safari on macOS). MUST check before the deltaX/deltaY + // comparison below — pinch deltas come through on deltaY. + if (e.ctrlKey && currentDisplayMode === "fullscreen") { + e.preventDefault(); + if (pinchSettleTimer === null) beginPinch(); + // exp(-deltaY * k) makes equal-magnitude in/out deltas inverse — + // pinch out then back lands where you started. Clamp per event so a + // physical mouse wheel (deltaY ≈ ±100/notch) doesn't slam to the + // limit; trackpad pinch deltas are ~±1-10 so the clamp is a no-op. + const d = Math.max(-25, Math.min(25, e.deltaY)); + updatePinch(previewScale * Math.exp(-d * 0.01)); + if (pinchSettleTimer) clearTimeout(pinchSettleTimer); + // 200ms — slow trackpad pinches can leave >150ms gaps between wheel + // events, which would commit-then-restart and feel steppy. + pinchSettleTimer = setTimeout(() => { + pinchSettleTimer = null; + commitPinch(); + }, 200); + return; + } + // Only intercept horizontal scroll, let vertical scroll through if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return; @@ -3695,6 +3843,58 @@ canvasContainerEl.addEventListener( { passive: false }, ); +// Two-finger touch pinch. We listen on the container (not page-wrapper) +// because the wrapper transforms during the gesture and would shift the +// touch target out from under the fingers. +let touchStartDist = 0; + +function touchDist(t: TouchList): number { + const dx = t[0].clientX - t[1].clientX; + const dy = t[0].clientY - t[1].clientY; + return Math.hypot(dx, dy); +} + +canvasContainerEl.addEventListener( + "touchstart", + (event) => { + const e = event as TouchEvent; + if (e.touches.length !== 2 || currentDisplayMode !== "fullscreen") return; + // No preventDefault here — keep iOS Safari happy. We block native + // pinch-zoom via touch-action CSS + preventDefault on touchmove. + touchStartDist = touchDist(e.touches); + if (touchStartDist > 0) beginPinch(); + }, + { passive: true }, +); + +canvasContainerEl.addEventListener( + "touchmove", + (event) => { + const e = event as TouchEvent; + if (e.touches.length !== 2 || touchStartDist === 0) return; + e.preventDefault(); // stop the browser zooming the whole viewport + updatePinch(pinchStartScale * (touchDist(e.touches) / touchStartDist)); + }, + { passive: false }, +); + +canvasContainerEl.addEventListener("touchend", (event) => { + const e = event as TouchEvent; + // Gesture ends when we drop below two fingers. e.touches is the + // REMAINING set — lifting one of two leaves length 1. + if (touchStartDist === 0 || e.touches.length >= 2) return; + touchStartDist = 0; + commitPinch(); +}); + +canvasContainerEl.addEventListener("touchcancel", () => { + if (touchStartDist === 0) return; + touchStartDist = 0; + // Cancelled (call, app-switch) → revert, don't commit a half-gesture. + pageWrapperEl.style.transform = ""; + zoomLevelEl.textContent = `${Math.round(scale * 100)}%`; +}); + // Parse tool result function parseToolResult(result: CallToolResult): { url: string; @@ -4130,10 +4330,10 @@ app.ontoolresult = async (result: CallToolResult) => { // and here makes the empty viewer visible. The annotation/form scans // below are O(numPages) and do NOT block the canvas (page.render only // needs canvasContext+viewport), so they run after. - const fitScale = await computeFitToWidthScale(); + const fitScale = await computeFitScale(); if (fitScale !== null) { scale = fitScale; - log.info("Fit-to-width scale:", scale); + log.info("Initial fit scale:", scale); } await renderPage(); @@ -4493,19 +4693,10 @@ function handleHostContextChanged(ctx: McpUiHostContext) { mainEl.style.paddingLeft = `${left}px`; } - // Recompute fit-to-width when container dimensions change - if (ctx.containerDimensions && pdfDocument && !userHasZoomed) { - log.info("Container dimensions changed:", ctx.containerDimensions); - computeFitToWidthScale().then((fitScale) => { - if (fitScale !== null && Math.abs(fitScale - scale) > 0.01) { - scale = fitScale; - log.info("Recomputed fit-to-width scale:", scale); - renderPage(); - } - }); - } - - // Handle display mode changes + // Display-mode handling MUST run before the fit-to-width recompute below. + // Toggling .fullscreen flips `padding: 0 !important` on mainEl, which + // changes how much width the canvas-container actually gets. Measuring + // before the class lands sees the wrong padding. if (ctx.displayMode) { const wasFullscreen = currentDisplayMode === "fullscreen"; currentDisplayMode = ctx.displayMode as "inline" | "fullscreen"; @@ -4516,12 +4707,25 @@ function handleHostContextChanged(ctx: McpUiHostContext) { if (panelState.open) { setAnnotationPanelOpen(true); } - // When exiting fullscreen, request resize to fit content - if (wasFullscreen && !isFullscreen && pdfDocument) { - requestFitToContent(); + if (wasFullscreen !== isFullscreen) { + // Mode changed → refit. computeFitScale reads displayMode, so + // this scales UP to fill on enter and back DOWN to ≤1.0 on exit. + // refitScale → renderPage → requestFitToContent handles the + // host-resize on exit. If userHasZoomed, refit no-ops; on exit fall + // back to requestFitToContent so the iframe still shrinks to whatever + // scale the user left it at. + void refitScale().then(() => { + if (!isFullscreen && userHasZoomed) requestFitToContent(); + }); } updateFullscreenButton(); } + + // ResizeObserver on canvasContainerEl drives refit on actual size change; + // ctx.containerDimensions is logged for debugging but isn't load-bearing. + if (ctx.containerDimensions) { + log.info("Container dimensions changed:", ctx.containerDimensions); + } } app.onteardown = async () => { @@ -4534,6 +4738,11 @@ app.onteardown = async () => { clearTimeout(searchDebounceTimer); searchDebounceTimer = null; } + if (pinchSettleTimer) { + clearTimeout(pinchSettleTimer); + pinchSettleTimer = null; + } + containerResizeObserver.disconnect(); return {}; }; diff --git a/tests/e2e/pdf-viewer-zoom.spec.ts b/tests/e2e/pdf-viewer-zoom.spec.ts new file mode 100644 index 0000000..28e5d36 --- /dev/null +++ b/tests/e2e/pdf-viewer-zoom.spec.ts @@ -0,0 +1,168 @@ +import { test, expect, type Page } from "@playwright/test"; + +/** + * PDF Viewer zoom + fullscreen-fit tests. + * + * Covers: + * - Inline → fullscreen refits the page (the cramped inline scale is dropped) + * - Trackpad pinch (wheel + ctrlKey) zooms the page in fullscreen + * - Pinch zoom is ignored in inline mode + */ + +test.setTimeout(120000); + +function getAppFrame(page: Page) { + return page.frameLocator("iframe").first().frameLocator("iframe").first(); +} + +async function loadPdfServer(page: Page) { + await page.goto("/?theme=hide"); + await expect(page.locator("select").first()).toBeEnabled({ timeout: 30000 }); + await page.locator("select").first().selectOption({ label: "PDF Server" }); + await page.click('button:has-text("Call Tool")'); + // Wait for nested app iframe to mount + const outer = page.frameLocator("iframe").first(); + await expect(outer.locator("iframe")).toBeVisible({ timeout: 30000 }); +} + +async function waitForPdfRender(page: Page) { + const app = getAppFrame(page); + // Canvas reports a non-zero CSS width once renderPage() has sized it. + // toBeVisible alone isn't enough — the canvas exists at 0×0 before + // first paint, so a fast test would race the render. + await expect + .poll( + async () => { + const w = await app + .locator("#pdf-canvas") + .evaluate((el: HTMLCanvasElement) => parseFloat(el.style.width)); + return w > 0 ? w : 0; + }, + { timeout: 30000 }, + ) + .toBeGreaterThan(0); +} + +/** Read the current zoom level (e.g. "65%") and return the integer percent. */ +async function readZoomPercent(page: Page): Promise { + const text = await getAppFrame(page).locator("#zoom-level").textContent(); + const m = text?.match(/(\d+)%/); + if (!m) throw new Error(`Unexpected zoom-level text: ${text}`); + return parseInt(m[1], 10); +} + +test.describe("PDF Viewer — fullscreen fit + pinch zoom", () => { + test("entering fullscreen drops the inline shrink-to-fit scale", async ({ + page, + }) => { + // Start narrow so the initial fit-to-width lands below 100%. + // The default arxiv PDF is ~612pt wide; a 500px iframe forces a + // fit scale around 60-70%. + await page.setViewportSize({ width: 500, height: 800 }); + await loadPdfServer(page); + await waitForPdfRender(page); + const app = getAppFrame(page); + + const inlineZoom = await readZoomPercent(page); + expect(inlineZoom).toBeLessThan(100); + + // Widen + give plenty of height, then enter fullscreen. Fullscreen uses + // fit-to-PAGE (whole page visible), so the resulting zoom is whichever + // axis is tighter. We just assert it's higher than the cramped inline + // value AND that the page actually fits without scrolling. + await page.setViewportSize({ width: 1400, height: 1000 }); + await app.locator("#fullscreen-btn").click(); + await expect(app.locator(".main.fullscreen")).toBeVisible({ + timeout: 5000, + }); + + await expect + .poll(() => readZoomPercent(page), { timeout: 5000 }) + .toBeGreaterThan(inlineZoom); + + // Whole page visible → no scroll inside the canvas-container. + const overflows = await app + .locator(".canvas-container") + .evaluate( + (el: HTMLElement) => + el.scrollHeight > el.clientHeight + 2 || + el.scrollWidth > el.clientWidth + 2, + ); + expect(overflows).toBe(false); + }); + + test("trackpad pinch (wheel + ctrlKey) zooms in fullscreen", async ({ + page, + }) => { + await page.setViewportSize({ width: 1400, height: 800 }); + await loadPdfServer(page); + await waitForPdfRender(page); + const app = getAppFrame(page); + + // Go fullscreen first — pinch is fullscreen-only. + await app.locator("#fullscreen-btn").click(); + await expect(app.locator(".main.fullscreen")).toBeVisible({ + timeout: 5000, + }); + // Let the entering-fullscreen fit-to-page refit settle. + await page.waitForTimeout(500); + const before = await readZoomPercent(page); + + // Dispatch a synthetic trackpad pinch-OUT on the canvas-container. + // ctrlKey:true is what Chrome/FF/Edge/Safari emit for trackpad pinch; + // deltaY > 0 = zoom out. We pinch out so we don't hit ZOOM_MAX. + // Can't use page.mouse.wheel — it doesn't expose ctrlKey. + await app.locator(".canvas-container").evaluate((el) => { + el.dispatchEvent( + new WheelEvent("wheel", { + deltaY: 50, + ctrlKey: true, + bubbles: true, + cancelable: true, + }), + ); + }); + + // The viewer applies a CSS transform live, then commits to a real + // renderPage() after a 150ms settle timer. exp(-50/100) ≈ 0.607. + await expect + .poll(() => readZoomPercent(page), { timeout: 5000 }) + .toBeLessThan(before * 0.8); + + // The CSS transform should be cleared once committed (so the canvas + // isn't double-scaled — the new render IS the new scale). + await expect + .poll( + () => + app + .locator(".page-wrapper") + .evaluate((el: HTMLElement) => el.style.transform), + { timeout: 5000 }, + ) + .toBe(""); + }); + + test("trackpad pinch is ignored outside fullscreen", async ({ page }) => { + await page.setViewportSize({ width: 1400, height: 800 }); + await loadPdfServer(page); + await waitForPdfRender(page); + const app = getAppFrame(page); + + const before = await readZoomPercent(page); + + await app.locator(".canvas-container").evaluate((el) => { + el.dispatchEvent( + new WheelEvent("wheel", { + deltaY: -50, + ctrlKey: true, + bubbles: true, + cancelable: true, + }), + ); + }); + + // No settle timer should have started — zoom stays put. + await page.waitForTimeout(300); + expect(await readZoomPercent(page)).toBe(before); + }); +}); From 04fed524a9f7eed61ad7b25668823c26c334a27e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 04:16:33 +0100 Subject: [PATCH 08/97] fix(pdf-server): npx DOMMatrix crash + broken MCPB bundle (#584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) --- .github/workflows/ci.yml | 10 ++++- .github/workflows/npm-publish.yml | 8 +--- .gitignore | 4 +- examples/pdf-server/build-mcpb.mjs | 56 +++++++++++++++++++++++++++ examples/pdf-server/manifest.json | 6 +-- examples/pdf-server/package.json | 5 ++- examples/pdf-server/pdfjs-polyfill.ts | 26 +++++++++++++ examples/pdf-server/server.ts | 5 ++- 8 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 examples/pdf-server/build-mcpb.mjs create mode 100644 examples/pdf-server/pdfjs-polyfill.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c227080..fb41938 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,15 @@ jobs: - name: Build MCPB bundle (pdf-server) if: runner.os == 'Linux' && matrix.name == 'Linux x64' - run: npx -y @anthropic-ai/mcpb pack + 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 e2e: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d969585..ece35d3 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -159,12 +159,8 @@ jobs: cache: npm - run: npm ci - - name: Build pdf-server - run: npm run build --workspace examples/pdf-server - - - name: Pack MCPB bundle - run: npx -y @anthropic-ai/mcpb pack - working-directory: examples/pdf-server + - 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 diff --git a/.gitignore b/.gitignore index abb7660..5b5486b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ __pycache__/ *.pyc # MCPB bundles (built artifacts) -*.mcpb \ No newline at end of file +*.mcpb +.mcpb-stage/ +.mcpb-smoke/ \ No newline at end of file diff --git a/examples/pdf-server/build-mcpb.mjs b/examples/pdf-server/build-mcpb.mjs new file mode 100644 index 0000000..c1c3469 --- /dev/null +++ b/examples/pdf-server/build-mcpb.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/** + * Build a self-contained .mcpb bundle for pdf-server. + * + * `mcpb pack` zips whatever is on disk; in this monorepo all runtime deps + * are hoisted to the root node_modules, so packing in-place produces a + * bundle with no pdfjs-dist/ajv/etc. that crashes in Claude Desktop. + * + * This script stages dist/ + manifest into a clean temp dir, runs a fresh + * non-workspace `npm install --omit=dev --omit=optional` (the polyfill in + * dist/pdfjs-polyfill.js makes @napi-rs/canvas's ~130MB of native binaries + * unnecessary), syncs the manifest version to package.json, then packs. + */ + +import { + cpSync, + rmSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const stage = path.join(here, ".mcpb-stage"); +const out = path.join(here, "pdf-server.mcpb"); + +const pkg = JSON.parse(readFileSync(path.join(here, "package.json"), "utf8")); +const manifest = JSON.parse( + readFileSync(path.join(here, "manifest.json"), "utf8"), +); +manifest.version = pkg.version; + +rmSync(stage, { recursive: true, force: true }); +mkdirSync(stage); + +for (const f of ["dist", "icon.png", "README.md", ".mcpbignore"]) { + cpSync(path.join(here, f), path.join(stage, f), { recursive: true }); +} +writeFileSync( + path.join(stage, "manifest.json"), + JSON.stringify(manifest, null, 2), +); +writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2)); + +const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" }); +run( + "npm install --omit=dev --omit=optional --no-audit --no-fund --no-package-lock " + + "--registry=https://registry.npmjs.org/", +); +run(`npx -y @anthropic-ai/mcpb pack . ${JSON.stringify(out)}`); + +rmSync(stage, { recursive: true, force: true }); +console.log(`\n✅ ${path.relative(process.cwd(), out)}`); diff --git a/examples/pdf-server/manifest.json b/examples/pdf-server/manifest.json index 47c6bbc..f4b897e 100644 --- a/examples/pdf-server/manifest.json +++ b/examples/pdf-server/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": "0.3", "name": "@modelcontextprotocol/server-pdf", - "display_name": "PDF (By Anthropic)", + "display_name": "pdf-viewer", "version": "1.0.1", "description": "Read and interact with PDF files using an interactive viewer with search, navigation, and text extraction", "author": { @@ -34,8 +34,8 @@ "description": "Display an interactive PDF viewer with search and navigation" }, { - "name": "read_pdf_bytes", - "description": "Read a range of bytes from a PDF file (max 512KB per request)" + "name": "interact", + "description": "Interact with a PDF viewer: annotate, navigate, search, extract text/screenshots, fill forms" } ], "keywords": ["pdf", "documents", "viewer", "reading"], diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 520e234..1660ecf 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -14,14 +14,15 @@ "dist" ], "scripts": { - "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node --external pdfjs-dist && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --external pdfjs-dist --banner \"#!/usr/bin/env node\"", + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build pdfjs-polyfill.ts --outdir dist --target node && bun build server.ts --outdir dist --target node --external pdfjs-dist --external \"./pdfjs-polyfill.js\" && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --external pdfjs-dist --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts --enable-interact", "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "build:mcpb": "npm run build && node build-mcpb.mjs" }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/examples/pdf-server/pdfjs-polyfill.ts b/examples/pdf-server/pdfjs-polyfill.ts new file mode 100644 index 0000000..80e1705 --- /dev/null +++ b/examples/pdf-server/pdfjs-polyfill.ts @@ -0,0 +1,26 @@ +/** + * Stub polyfills for pdfjs-dist's legacy build under Node.js. + * + * pdfjs-dist/legacy/build/pdf.mjs eagerly does `new DOMMatrix()` at module + * scope. Its own polyfill loads `@napi-rs/canvas` (an optionalDependency), + * but `npx` / fresh installs frequently miss the platform-native binary + * (npm optional-deps bug, see https://github.com/npm/cli/issues/4828), + * so the import crashes before our server code runs. + * + * The server only uses pdfjs for text/metadata/form-field extraction — + * never canvas rendering — so no-op stubs are sufficient and avoid + * shipping ~130MB of native binaries we don't need. + * + * MUST be a separate ESM module imported before pdfjs-dist (and kept + * `--external` in the bun bundle) so it executes before pdfjs's + * module-level initializer. Inlined body code would run too late + * because static imports are hoisted. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const g = globalThis as any; + +// Only stub if missing — let a real @napi-rs/canvas (or jsdom) win. +g.DOMMatrix ??= class DOMMatrix {}; +g.ImageData ??= class ImageData {}; +g.Path2D ??= class Path2D {}; diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 3c6168b..ba16431 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -25,7 +25,10 @@ import { type CallToolResult, type ReadResourceResult, } from "@modelcontextprotocol/sdk/types.js"; -// Use the legacy build to avoid DOMMatrix dependency in Node.js +// Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy +// build instantiates DOMMatrix at module scope and the @napi-rs/canvas +// polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details. +import "./pdfjs-polyfill.js"; import { getDocument, VerbosityLevel, From 8e7206cb1540c6498d582be3e4a1576c85cd2bb6 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 04:25:00 +0100 Subject: [PATCH 09/97] =?UTF-8?q?chore(deps):=20npm=20audit=20fix=20?= =?UTF-8?q?=E2=80=94=20sdk=201.29.0,=20systeminformation=205.31.5,=20+13?= =?UTF-8?q?=20transitives=20(#585)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 3 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 3 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 4 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 526 ++++++++++-------- package.json | 7 +- src/generated/schema.json | 12 + 28 files changed, 327 insertions(+), 272 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index b9ad39d..1976ebc 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 72cdb61..bbb7f71 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 3064e66..24a15d8 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index ee53e77..cfb415b 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 4d6d721..e75a4ce 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index c0a0c75..4402622 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index b241304..51b6a44 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 200e664..cb94708 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 391767f..dc09267 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 61a353a..74bc9f4 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 5e1e1c7..2772bd8 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 8fbabe0..e24d0c8 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 8eb073f..2a6225f 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 1660ecf..33e39dc 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "pdf-lib": "^1.17.1", diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index 768799a..026e9f5 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -8,6 +8,7 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0" + "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.29.0" } } diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index ac981d5..27dfe37 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0" }, diff --git a/examples/say-server/package.json b/examples/say-server/package.json index c0191d2..52c750d 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -15,6 +15,7 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0" + "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.29.0" } } diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index d7dd803..602a4e0 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 08b7448..3a64d14 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index f5eb706..345e7b3 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index a9b2476..8e26004 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,11 +27,11 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "systeminformation": "^5.27.11", + "systeminformation": "^5.31.5", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index ffcb4fa..0750c12 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index bcee120..8586ac3 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index c4dbd34..0af85df 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index a51161e..5116342 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/package-lock.json b/package-lock.json index 8487ac4..a992ea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,12 @@ "workspaces": [ "examples/*" ], + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0" + }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "1.25.2", + "@modelcontextprotocol/sdk": "^1.29.0", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -47,7 +50,7 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" @@ -66,7 +69,7 @@ "version": "1.3.2", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" @@ -110,7 +113,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", @@ -154,7 +157,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -201,7 +204,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", @@ -245,7 +248,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", @@ -289,7 +292,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -331,7 +334,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", @@ -375,7 +378,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -418,7 +421,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -465,7 +468,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -508,7 +511,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.1.13" }, "bin": { @@ -548,7 +551,7 @@ "version": "1.3.2", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -594,7 +597,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -636,7 +639,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "pdf-lib": "^1.17.1", @@ -678,7 +681,8 @@ "name": "@modelcontextprotocol/server-qr", "version": "1.3.2", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0" + "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.29.0" } }, "examples/quickstart": { @@ -687,7 +691,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0" }, @@ -718,7 +722,8 @@ "version": "1.3.2", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0" + "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.29.0" } }, "examples/scenario-modeler-server": { @@ -727,7 +732,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -775,7 +780,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -817,7 +822,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", @@ -860,11 +865,11 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "systeminformation": "^5.27.11", + "systeminformation": "^5.31.5", "zod": "^4.1.13" }, "bin": { @@ -891,6 +896,32 @@ "undici-types": "~6.20.0" } }, + "examples/system-monitor-server/node_modules/systeminformation": { + "version": "5.31.5", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.5.tgz", + "integrity": "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "examples/system-monitor-server/node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -904,7 +935,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -953,7 +984,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -996,7 +1027,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1038,7 +1069,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -2567,12 +2598,12 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -2580,14 +2611,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -2974,9 +3006,9 @@ } }, "node_modules/@oclif/core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2984,13 +3016,13 @@ } }, "node_modules/@oclif/core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3275,9 +3307,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", - "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -3289,9 +3321,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", - "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -3303,9 +3335,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", - "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -3316,9 +3348,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", - "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -3329,9 +3361,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", - "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -3343,9 +3375,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", - "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -3357,9 +3389,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", - "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], @@ -3371,9 +3403,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", - "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], @@ -3385,9 +3417,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", - "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], @@ -3398,9 +3430,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", - "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], @@ -3412,9 +3444,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", - "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], @@ -3426,9 +3458,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", - "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], @@ -3440,9 +3472,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", - "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "cpu": [ "ppc64" ], @@ -3454,9 +3486,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", - "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], @@ -3468,9 +3500,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", - "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], @@ -3482,9 +3514,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", - "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], @@ -3496,9 +3528,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", - "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], @@ -3510,9 +3542,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", - "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], @@ -3523,9 +3555,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", - "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], @@ -3537,9 +3569,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", - "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], @@ -3551,9 +3583,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", - "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -3565,9 +3597,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", - "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -3578,9 +3610,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", - "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -3592,9 +3624,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", - "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "cpu": [ "x64" ], @@ -3606,9 +3638,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", - "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -3970,6 +4002,12 @@ "meshoptimizer": "~0.22.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3984,6 +4022,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript/vfs": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz", @@ -4322,9 +4373,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4431,9 +4482,9 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -4612,9 +4663,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5535,9 +5586,9 @@ } }, "node_modules/devalue": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", - "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", "license": "MIT" }, "node_modules/dom-serializer": { @@ -5807,12 +5858,13 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz", - "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" } }, "node_modules/estree-walker": { @@ -5912,10 +5964,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, "engines": { "node": ">= 16" }, @@ -5966,9 +6021,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -5976,9 +6031,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -6269,11 +6324,10 @@ } }, "node_modules/hono": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.6.tgz", - "integrity": "sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -6411,6 +6465,15 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6751,9 +6814,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true, "license": "MIT" }, @@ -6883,9 +6946,9 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -7013,9 +7076,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7345,9 +7408,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -7498,9 +7561,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7650,9 +7713,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", - "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", "dependencies": { @@ -7666,31 +7729,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.56.0", - "@rollup/rollup-android-arm64": "4.56.0", - "@rollup/rollup-darwin-arm64": "4.56.0", - "@rollup/rollup-darwin-x64": "4.56.0", - "@rollup/rollup-freebsd-arm64": "4.56.0", - "@rollup/rollup-freebsd-x64": "4.56.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", - "@rollup/rollup-linux-arm-musleabihf": "4.56.0", - "@rollup/rollup-linux-arm64-gnu": "4.56.0", - "@rollup/rollup-linux-arm64-musl": "4.56.0", - "@rollup/rollup-linux-loong64-gnu": "4.56.0", - "@rollup/rollup-linux-loong64-musl": "4.56.0", - "@rollup/rollup-linux-ppc64-gnu": "4.56.0", - "@rollup/rollup-linux-ppc64-musl": "4.56.0", - "@rollup/rollup-linux-riscv64-gnu": "4.56.0", - "@rollup/rollup-linux-riscv64-musl": "4.56.0", - "@rollup/rollup-linux-s390x-gnu": "4.56.0", - "@rollup/rollup-linux-x64-gnu": "4.56.0", - "@rollup/rollup-linux-x64-musl": "4.56.0", - "@rollup/rollup-openbsd-x64": "4.56.0", - "@rollup/rollup-openharmony-arm64": "4.56.0", - "@rollup/rollup-win32-arm64-msvc": "4.56.0", - "@rollup/rollup-win32-ia32-msvc": "4.56.0", - "@rollup/rollup-win32-x64-gnu": "4.56.0", - "@rollup/rollup-win32-x64-msvc": "4.56.0", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -8240,22 +8303,23 @@ } }, "node_modules/svelte": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.48.2.tgz", - "integrity": "sha512-VPWD+UyoSFZ7Nxix5K/F8yWiKWOiROkLlWYXOZReE0TUycw+58YWB3D6lAKT+57xmN99wRX4H3oZmw0NPy7y3Q==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz", + "integrity": "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.2", + "devalue": "^5.6.4", "esm-env": "^1.2.1", - "esrap": "^2.2.1", + "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -8265,32 +8329,6 @@ "node": ">=18" } }, - "node_modules/systeminformation": { - "version": "5.30.6", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.6.tgz", - "integrity": "sha512-LEIyK1aEv5P3BhAPW3swdlIyCihxwEq/Gki+kcONieU4PIeRCSLDuGkk0Va/56PSBgjVgEksOM88dmY6YqOyfQ==", - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, "node_modules/text-camel-case": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/text-camel-case/-/text-camel-case-1.2.9.tgz", @@ -8566,9 +8604,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -9302,9 +9340,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -9312,13 +9350,13 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9356,9 +9394,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", + "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -9597,9 +9635,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -9703,9 +9741,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -9875,9 +9913,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 66c3548..8b6fb6f 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "1.25.2", + "@modelcontextprotocol/sdk": "^1.29.0", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -107,7 +107,7 @@ "zod": "^4.1.13" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" @@ -125,5 +125,8 @@ "seroval-plugins": "1.4.2", "solid-js": "1.9.10", "@types/node": "20.19.27" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0" } } diff --git a/src/generated/schema.json b/src/generated/schema.json index eb2a71f..d66e557 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -231,6 +231,9 @@ "mimeType": { "type": "string" }, + "size": { + "type": "number" + }, "annotations": { "type": "object", "properties": { @@ -3829,6 +3832,9 @@ "mimeType": { "type": "string" }, + "size": { + "type": "number" + }, "annotations": { "type": "object", "properties": { @@ -5498,6 +5504,9 @@ "mimeType": { "type": "string" }, + "size": { + "type": "number" + }, "annotations": { "type": "object", "properties": { @@ -5859,6 +5868,9 @@ "mimeType": { "type": "string" }, + "size": { + "type": "number" + }, "annotations": { "type": "object", "properties": { From 9757f35fd19b19fea9171b4622c3538101f59fb2 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 04:27:49 +0100 Subject: [PATCH 10/97] chore: drop spurious root dependencies block from #585 (#586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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. --- package-lock.json | 3 --- package.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a992ea9..871c7ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "workspaces": [ "examples/*" ], - "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" - }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/package.json b/package.json index 8b6fb6f..99bbf93 100644 --- a/package.json +++ b/package.json @@ -125,8 +125,5 @@ "seroval-plugins": "1.4.2", "solid-js": "1.9.10", "@types/node": "20.19.27" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" } } From 9da4a75ff60bcd127c494bbca98f7b22e87f95e7 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 04:33:13 +0100 Subject: [PATCH 11/97] chore: bump ext-apps to 1.4.0 (#582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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] --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 54 +++++++++--------- package.json | 2 +- .../debug-server.png | Bin 53549 -> 54133 bytes 28 files changed, 53 insertions(+), 53 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 1976ebc..b29f04a 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index bbb7f71..a6c0343 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 24a15d8..f75d662 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index cfb415b..f193fa1 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index e75a4ce..90dd6c6 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 4402622..e97eb68 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 51b6a44..ad8c119 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index cb94708..71b7ace 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index dc09267..d753706 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 74bc9f4..ade9dae 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 2772bd8..15dc903 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index e24d0c8..5686a02 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.3.2", + "version": "1.4.0", "private": true, "type": "module", "scripts": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 2a6225f..a8d727b 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 33e39dc..cdcda33 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index 026e9f5..3ecd3d5 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.3.2", + "version": "1.4.0", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 27dfe37..e94baa2 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 52c750d..bcb5a95 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.3.2", + "version": "1.4.0", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 602a4e0..1e0a31a 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 3a64d14..6af2dd7 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 345e7b3..7e59e5b 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 8e26004..c0d1dcf 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 0750c12..d148669 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 8586ac3..462c4a1 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 0af85df..16566ac 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 5116342..1bfc981 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.3.2", + "version": "1.4.0", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index 871c7ab..54d7d69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.3.2", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "workspaces": [ "examples/*" @@ -63,7 +63,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.3.2", + "version": "1.4.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -106,7 +106,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -150,7 +150,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -197,7 +197,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -241,7 +241,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -285,7 +285,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -327,7 +327,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -371,7 +371,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -414,7 +414,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -461,7 +461,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -504,7 +504,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -545,7 +545,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.3.2", + "version": "1.4.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -590,7 +590,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -632,7 +632,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -676,7 +676,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.3.2", + "version": "1.4.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -684,7 +684,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -716,7 +716,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -725,7 +725,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -773,7 +773,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -815,7 +815,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -858,7 +858,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -928,7 +928,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -977,7 +977,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1020,7 +1020,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1062,7 +1062,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/package.json b/package.json index 99bbf93..7b92e3c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", diff --git a/tests/e2e/servers.spec.ts-snapshots/debug-server.png b/tests/e2e/servers.spec.ts-snapshots/debug-server.png index c1fc4ada51ea73e4249fe9334c1616fc3b790988..2b902e3d642c198e02d7f07d09ed8d0730c31c73 100644 GIT binary patch literal 54133 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 Date: Thu, 2 Apr 2026 06:44:56 +0100 Subject: [PATCH 12/97] fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout (#587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout Highlights rendered opaque: inline `background: ` 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). --- examples/pdf-server/src/mcp-app.css | 21 +++- examples/pdf-server/src/mcp-app.ts | 158 +++++++++++++++++++++++----- tests/e2e/pdf-annotations.spec.ts | 6 ++ tests/e2e/pdf-viewer-zoom.spec.ts | 31 +++++- 4 files changed, 186 insertions(+), 30 deletions(-) diff --git a/examples/pdf-server/src/mcp-app.css b/examples/pdf-server/src/mcp-app.css index b6bbb43..4f03520 100644 --- a/examples/pdf-server/src/mcp-app.css +++ b/examples/pdf-server/src/mcp-app.css @@ -227,6 +227,10 @@ body { align-items: flex-start; padding: 1rem; background: var(--bg200); + /* JS owns pinch (inline pinch-in → fullscreen). pan-x/pan-y keeps native + * scrolling; the fullscreen rule below repeats this with a comment on the + * iOS-Safari preventDefault backstop. */ + touch-action: pan-x pan-y; } .page-wrapper { @@ -310,9 +314,20 @@ body { } .main.fullscreen .toolbar { - padding-top: calc(0.5rem + var(--safe-top, 0px)); - padding-left: calc(0.5rem + var(--safe-left, 0px)); - padding-right: calc(0.5rem + var(--safe-right, 0px)); + /* Hosts wrap fullscreen in their own header (title + close), which already + * clears the top safe-area — so no --safe-top here, only sides. */ + padding: 0.25rem calc(0.5rem + var(--safe-right, 0px)) 0.25rem + calc(0.5rem + var(--safe-left, 0px)); + min-height: 40px; + /* Inline can wrap (narrow chat bubble); fullscreen has the width, and + * wrapping would double the bar height + desync the search-bar offset. */ + flex-wrap: nowrap; +} + +.main.fullscreen .search-bar { + /* -1px overlaps the toolbar border so the dropdown looks attached. */ + top: 39px; + right: calc(var(--safe-right, 0px) - 1px); } .main.fullscreen .viewer { diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 7e53959..7ea0bf4 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -27,6 +27,7 @@ import { type ImageAnnotation, type NoteAnnotation, type FreetextAnnotation, + cssColorToRgb, serializeDiff, deserializeDiff, mergeAnnotations, @@ -341,6 +342,11 @@ async function refitScale(): Promise { // needs height changes too (rotation, browser chrome on mobile). let lastContainerW = 0; let lastContainerH = 0; +/** One-shot: refit on the next resize even if it's a shrink in inline mode. + * Set on fullscreen→inline so the page snaps to the new (smaller) width + * once the host has actually resized the iframe — the inline `grewW` gate + * would otherwise swallow that shrink. */ +let forceNextResizeRefit = false; const containerResizeObserver = new ResizeObserver(([entry]) => { const { width: w, height: h } = entry.contentRect; const grewW = w > lastContainerW + 1; @@ -348,7 +354,12 @@ const containerResizeObserver = new ResizeObserver(([entry]) => { Math.abs(w - lastContainerW) > 1 || Math.abs(h - lastContainerH) > 1; lastContainerW = w; lastContainerH = h; - if (currentDisplayMode === "fullscreen" ? changed : grewW) refitScale(); + if (forceNextResizeRefit && changed) { + forceNextResizeRefit = false; + refitScale(); + } else if (currentDisplayMode === "fullscreen" ? changed : grewW) { + refitScale(); + } }); containerResizeObserver.observe(canvasContainerEl as HTMLElement); @@ -1933,13 +1944,20 @@ function renderAnnotation( viewport: { width: number; height: number; scale: number }, ): HTMLElement[] { switch (def.type) { - case "highlight": + case "highlight": { + // Force translucency: def.color is an opaque hex (e.g. "#ffff00"), which + // would override the rgba()/mix-blend-mode in CSS and hide the text. + const rgb = def.color ? cssColorToRgb(def.color) : null; + const bg = rgb + ? `rgba(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)}, 0.35)` + : undefined; return renderRectsAnnotation( def.rects, "annotation-highlight", viewport, - def.color ? { background: def.color } : {}, + bg ? { background: bg } : {}, ); + } case "underline": return renderRectsAnnotation( def.rects, @@ -3322,9 +3340,13 @@ function zoomIn() { renderPage().then(scrollSelectionIntoView); } -function zoomOut() { +async function zoomOut() { userHasZoomed = true; - scale = Math.max(scale - 0.25, ZOOM_MIN); + // Fullscreen floor is fit-to-page (anything smaller is dead margin). + const fit = + currentDisplayMode === "fullscreen" ? await computeFitScale() : null; + const floor = fit !== null ? Math.max(ZOOM_MIN, fit) : ZOOM_MIN; + scale = Math.max(scale - 0.25, floor); renderPage().then(scrollSelectionIntoView); } @@ -3761,18 +3783,39 @@ let pinchStartScale = 1.0; let previewScale = 1.0; /** Debounce timer — wheel events have no end event, so we wait for quiet. */ let pinchSettleTimer: ReturnType | null = null; +/** computeFitScale() snapshot at gesture start (async — may be null briefly). */ +let fitScaleAtPinchStart: number | null = null; +/** Guards against firing toggleFullscreen() once per wheel event during a + * single inline pinch-in gesture. */ +let modeTransitionInFlight = false; function beginPinch() { pinchStartScale = scale; previewScale = scale; + // Seed synchronously when we can (at fit ⇔ !userHasZoomed) so the very + // first updatePinch already has the right floor — avoids a one-frame + // jitter when the async computeFitScale resolves mid-gesture. + fitScaleAtPinchStart = userHasZoomed ? null : scale; + void computeFitScale().then((s) => (fitScaleAtPinchStart = s)); // transform-origin matches the flex layout's anchor (justify-content: // center, align-items: flex-start) so the preview and the committed // canvas grow from the same point — otherwise the page jumps on release. pageWrapperEl.style.transformOrigin = "50% 0"; } +/** Fit-to-page floor for fullscreen (committed scale never goes below this). + * The preview is allowed to overshoot down to 0.75×fit for rubber-band + * feedback; release below 0.9×fit exits to inline, otherwise snaps to fit. */ +function pinchFitFloor(): number | null { + return currentDisplayMode === "fullscreen" ? fitScaleAtPinchStart : null; +} + function updatePinch(nextScale: number) { - previewScale = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, nextScale)); + const fit = pinchFitFloor(); + // Rubber-band: preview may dip to 0.75×fit so the user sees the page pull + // away as they pinch out. Committed scale is clamped to fit in commitPinch. + const previewFloor = fit !== null ? fit * 0.75 : ZOOM_MIN; + previewScale = Math.min(ZOOM_MAX, Math.max(previewFloor, nextScale)); // Transform is RELATIVE to the rendered canvas (which sits at // pinchStartScale), so a previewScale equal to pinchStartScale → ratio 1. pageWrapperEl.style.transform = `scale(${previewScale / pinchStartScale})`; @@ -3780,13 +3823,37 @@ function updatePinch(nextScale: number) { } function commitPinch() { - if (Math.abs(previewScale - scale) < 0.01) { - // Dead-zone — no re-render. Clear here since renderPage won't run. + const fit = pinchFitFloor(); + // Pinched out past fit (page visibly pulled away) → exit fullscreen. + // Only when the gesture *started* near fit, so a single big pinch-out + // from deep zoom lands at fit instead of ejecting unexpectedly. + if ( + fit !== null && + pinchStartScale <= fit * 1.05 && + previewScale < fit * 0.9 + ) { pageWrapperEl.style.transform = ""; + userHasZoomed = false; // let refitScale() size the inline view + forceNextResizeRefit = true; // ResizeObserver inline path ignores shrinks + modeTransitionInFlight = true; + void toggleFullscreen().finally(() => { + setTimeout(() => (modeTransitionInFlight = false), 250); + }); + return; + } + // Committed scale never below fit in fullscreen — overshoot snaps back. + const target = + fit !== null + ? Math.max(fit, previewScale) + : Math.max(ZOOM_MIN, previewScale); + if (Math.abs(target - scale) < 0.01) { + // Snap-back / dead-zone — no re-render needed. + pageWrapperEl.style.transform = ""; + zoomLevelEl.textContent = `${Math.round(scale * 100)}%`; return; } userHasZoomed = true; - scale = previewScale; + scale = target; // renderPage clears the transform in the same frame as the canvas // resize (after its first await) so there's no snap-back. renderPage().then(scrollSelectionIntoView); @@ -3804,8 +3871,23 @@ canvasContainerEl.addEventListener( // Trackpad pinch arrives as wheel with ctrlKey set (Chrome/FF/Edge on // macOS+Windows, Safari on macOS). MUST check before the deltaX/deltaY // comparison below — pinch deltas come through on deltaY. - if (e.ctrlKey && currentDisplayMode === "fullscreen") { + if (e.ctrlKey) { e.preventDefault(); + if (currentDisplayMode !== "fullscreen") { + // Inline: pinch-in (deltaY<0) is a request to go fullscreen. + // Pinch-out is ignored — nothing smaller than inline. + if (e.deltaY < 0 && !modeTransitionInFlight) { + modeTransitionInFlight = true; + void toggleFullscreen().finally(() => { + // Hold the latch through the settle window so the tail of the + // gesture doesn't immediately start zooming the new fullscreen + // view (or, worse, re-toggle). + setTimeout(() => (modeTransitionInFlight = false), 250); + }); + } + return; + } + if (modeTransitionInFlight) return; // swallow gesture tail post-toggle if (pinchSettleTimer === null) beginPinch(); // exp(-deltaY * k) makes equal-magnitude in/out deltas inverse — // pinch out then back lands where you started. Clamp per event so a @@ -3826,10 +3908,15 @@ canvasContainerEl.addEventListener( // Only intercept horizontal scroll, let vertical scroll through if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return; - // When zoomed, let natural panning happen (no page changes) - if (scale > 1.0) return; + // If the page overflows horizontally, let native panning handle it + // (no page changes). Checking actual overflow rather than `scale > 1.0` + // because fullscreen fit-scale is often >100% with the page still fully + // visible — we want swipe-to-page there. +1 absorbs sub-pixel rounding. + if (canvasContainerEl.scrollWidth > canvasContainerEl.clientWidth + 1) { + return; + } - // At 100% zoom, handle page navigation + // No horizontal overflow → swipe changes pages. e.preventDefault(); horizontalScrollAccumulator += e.deltaX; if (horizontalScrollAccumulator > SCROLL_THRESHOLD) { @@ -3858,7 +3945,7 @@ canvasContainerEl.addEventListener( "touchstart", (event) => { const e = event as TouchEvent; - if (e.touches.length !== 2 || currentDisplayMode !== "fullscreen") return; + if (e.touches.length !== 2) return; // No preventDefault here — keep iOS Safari happy. We block native // pinch-zoom via touch-action CSS + preventDefault on touchmove. touchStartDist = touchDist(e.touches); @@ -3873,7 +3960,21 @@ canvasContainerEl.addEventListener( const e = event as TouchEvent; if (e.touches.length !== 2 || touchStartDist === 0) return; e.preventDefault(); // stop the browser zooming the whole viewport - updatePinch(pinchStartScale * (touchDist(e.touches) / touchStartDist)); + const ratio = touchDist(e.touches) / touchStartDist; + if (currentDisplayMode !== "fullscreen") { + // Inline: a clear pinch-in means "go fullscreen". 1.15× threshold + // avoids triggering on jittery two-finger taps/scrolls. + if (ratio > 1.15 && !modeTransitionInFlight) { + modeTransitionInFlight = true; + touchStartDist = 0; // end this gesture; fullscreen will refit + pageWrapperEl.style.transform = ""; + void toggleFullscreen().finally(() => { + setTimeout(() => (modeTransitionInFlight = false), 250); + }); + } + return; + } + updatePinch(pinchStartScale * ratio); }, { passive: false }, ); @@ -3884,6 +3985,11 @@ canvasContainerEl.addEventListener("touchend", (event) => { // REMAINING set — lifting one of two leaves length 1. if (touchStartDist === 0 || e.touches.length >= 2) return; touchStartDist = 0; + if (currentDisplayMode !== "fullscreen") { + // Inline pinch that didn't cross the threshold — discard preview. + pageWrapperEl.style.transform = ""; + return; + } commitPinch(); }); @@ -4707,16 +4813,20 @@ function handleHostContextChanged(ctx: McpUiHostContext) { if (panelState.open) { setAnnotationPanelOpen(true); } + if (!isFullscreen) { + // Fullscreen zoom level is meaningless inline — always refit on exit, + // however it was triggered (pinch, button, host Escape/×). + userHasZoomed = false; + // The iframe shrink lands after this handler; let the ResizeObserver + // do one refit on that shrink (its inline branch normally ignores + // shrinks to avoid a requestFitToContent feedback loop). + forceNextResizeRefit = true; + } if (wasFullscreen !== isFullscreen) { - // Mode changed → refit. computeFitScale reads displayMode, so - // this scales UP to fill on enter and back DOWN to ≤1.0 on exit. - // refitScale → renderPage → requestFitToContent handles the - // host-resize on exit. If userHasZoomed, refit no-ops; on exit fall - // back to requestFitToContent so the iframe still shrinks to whatever - // scale the user left it at. - void refitScale().then(() => { - if (!isFullscreen && userHasZoomed) requestFitToContent(); - }); + // Fast-path refit (computeFitScale reads displayMode). The iframe may + // not have its final size yet — the ResizeObserver one-shot above + // covers the inline-shrink case once it does. + void refitScale(); } updateFullscreenButton(); } diff --git a/tests/e2e/pdf-annotations.spec.ts b/tests/e2e/pdf-annotations.spec.ts index 9d4ab95..1b8077b 100644 --- a/tests/e2e/pdf-annotations.spec.ts +++ b/tests/e2e/pdf-annotations.spec.ts @@ -151,6 +151,12 @@ test.describe("PDF Server - Annotations", () => { // Check that a highlight annotation element was rendered const highlightEl = appFrame.locator(".annotation-highlight"); await expect(highlightEl.first()).toBeVisible({ timeout: 5000 }); + // Regression: highlight must be translucent (not opaque hex), so text + // underneath remains readable. + await expect(highlightEl.first()).toHaveCSS( + "background-color", + /rgba\(255, 255, 0, 0\.35\)/, + ); }); test("add_annotations renders multiple annotation types", async ({ diff --git a/tests/e2e/pdf-viewer-zoom.spec.ts b/tests/e2e/pdf-viewer-zoom.spec.ts index 28e5d36..6258faa 100644 --- a/tests/e2e/pdf-viewer-zoom.spec.ts +++ b/tests/e2e/pdf-viewer-zoom.spec.ts @@ -142,13 +142,13 @@ test.describe("PDF Viewer — fullscreen fit + pinch zoom", () => { .toBe(""); }); - test("trackpad pinch is ignored outside fullscreen", async ({ page }) => { + test("trackpad pinch-in while inline enters fullscreen", async ({ page }) => { await page.setViewportSize({ width: 1400, height: 800 }); await loadPdfServer(page); await waitForPdfRender(page); const app = getAppFrame(page); - const before = await readZoomPercent(page); + await expect(app.locator(".main.fullscreen")).toHaveCount(0); await app.locator(".canvas-container").evaluate((el) => { el.dispatchEvent( @@ -161,8 +161,33 @@ test.describe("PDF Viewer — fullscreen fit + pinch zoom", () => { ); }); - // No settle timer should have started — zoom stays put. + // Pinch-in should request fullscreen, not zoom the inline view. + await expect(app.locator(".main.fullscreen")).toHaveCount(1, { + timeout: 5000, + }); + }); + + test("trackpad pinch-out while inline is a no-op", async ({ page }) => { + await page.setViewportSize({ width: 1400, height: 800 }); + await loadPdfServer(page); + await waitForPdfRender(page); + const app = getAppFrame(page); + + const before = await readZoomPercent(page); + + await app.locator(".canvas-container").evaluate((el) => { + el.dispatchEvent( + new WheelEvent("wheel", { + deltaY: 50, // pinch-out + ctrlKey: true, + bubbles: true, + cancelable: true, + }), + ); + }); + await page.waitForTimeout(300); expect(await readZoomPercent(page)).toBe(before); + await expect(app.locator(".main.fullscreen")).toHaveCount(0); }); }); From 2e55c842ba0d5163bf1046b6290049169e08b89b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 08:09:24 +0100 Subject: [PATCH 13/97] 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. --- examples/pdf-server/src/mcp-app.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 7ea0bf4..deec2e7 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -3340,13 +3340,13 @@ function zoomIn() { renderPage().then(scrollSelectionIntoView); } -async function zoomOut() { +function zoomOut() { userHasZoomed = true; - // Fullscreen floor is fit-to-page (anything smaller is dead margin). - const fit = - currentDisplayMode === "fullscreen" ? await computeFitScale() : null; - const floor = fit !== null ? Math.max(ZOOM_MIN, fit) : ZOOM_MIN; - scale = Math.max(scale - 0.25, floor); + // Intentionally NOT floored at fit-to-page (unlike pinch). Hosts may + // overlay UI on the iframe without reporting it in safeAreaInsets, so + // "fit" can leave the page bottom hidden; the button is the escape hatch + // to shrink past it. Pinch still rubber-bands at fit (see commitPinch). + scale = Math.max(scale - 0.25, ZOOM_MIN); renderPage().then(scrollSelectionIntoView); } From 4fc95131bf3f4f3a566ee6cb4d769c8a3c75356c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 08:47:48 +0100 Subject: [PATCH 14/97] 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 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. --- examples/pdf-server/server.ts | 95 ++++++++++++++++++++++++++++- examples/pdf-server/src/commands.ts | 1 + examples/pdf-server/src/mcp-app.ts | 95 +++++++++++++++++++++++++++++ playwright.config.ts | 1 + tests/e2e/pdf-annotations.spec.ts | 90 +++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 1 deletion(-) diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index ba16431..d2a0b7d 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -305,6 +305,33 @@ function waitForSaveData( }); } +const pendingStateRequests = new Map void>(); + +/** + * Wait for the viewer to report its current state (page, zoom, selection, …) + * as a JSON string. Same timeout/abort semantics as waitForSaveData. + */ +function waitForViewerState( + requestId: string, + signal?: AbortSignal, +): Promise { + return new Promise((resolve, reject) => { + const settle = (v: string | Error) => { + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + pendingStateRequests.delete(requestId); + v instanceof Error ? reject(v) : resolve(v); + }; + const onAbort = () => settle(new Error("interact request cancelled")); + const timer = setTimeout( + () => settle(new Error("Timeout waiting for viewer state")), + GET_PAGES_TIMEOUT_MS, + ); + signal?.addEventListener("abort", onAbort); + pendingStateRequests.set(requestId, settle); + }); +} + interface QueueEntry { commands: PdfCommand[]; /** Timestamp of the most recent enqueue or dequeue */ @@ -1350,7 +1377,8 @@ Returns a viewUUID in structuredContent. Pass it to \`interact\`: - add_annotations, update_annotations, remove_annotations, highlight_text - fill_form (fill PDF form fields) - navigate, search, find, search_navigate, zoom -- get_text, get_screenshot (extract content) +- get_text, get_screenshot, get_viewer_state (extract content / read selection & current page) +- save_as (write annotated PDF to disk) Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL. Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before display.`, @@ -1650,6 +1678,7 @@ URL: ${normalized}`, "fill_form", "get_text", "get_screenshot", + "get_viewer_state", "save_as", ]) .describe("Action to perform"), @@ -2238,6 +2267,26 @@ URL: ${normalized}`, ); } } + case "get_viewer_state": { + const requestId = randomUUID(); + enqueueCommand(uuid, { type: "get_viewer_state", requestId }); + let state: string; + try { + await ensureViewerIsPolling(uuid); + state = await waitForViewerState(requestId, signal); + } catch (err) { + return { + content: [ + { + type: "text", + text: `Error: ${err instanceof Error ? err.message : String(err)}`, + }, + ], + isError: true, + }; + } + return { content: [{ type: "text", text: state }] }; + } default: return { content: [{ type: "text", text: `Unknown action: ${action}` }], @@ -2295,6 +2344,7 @@ Example — add a signature image and a stamp, then screenshot to verify: **TEXT/SCREENSHOTS**: • get_text: extract text from pages. Optional \`page\` for single page, or \`intervals\` for ranges [{start?,end?}]. Max 20 pages. • get_screenshot: capture a single page as PNG image. Requires \`page\`. +• get_viewer_state: snapshot of the live viewer — JSON {currentPage, pageCount, zoom, displayMode, selectedAnnotationIds, selection:{text,contextBefore,contextAfter,boundingRect}|null}. Use this to read what the user has selected or which page they're on. **FORMS** — fill_form: fill fields with \`fields\` array of {name, value}. @@ -2320,6 +2370,7 @@ Example — add a signature image and a stamp, then screenshot to verify: "fill_form", "get_text", "get_screenshot", + "get_viewer_state", "save_as", ]) .optional() @@ -2603,6 +2654,48 @@ Example — add a signature image and a stamp, then screenshot to verify: }, ); + // Tool: submit_viewer_state (app-only) - Viewer reports its live state + registerAppTool( + server, + "submit_viewer_state", + { + title: "Submit Viewer State", + description: + "Submit a viewer-state snapshot for a get_viewer_state request (used by viewer). The model should NOT call this tool directly.", + inputSchema: { + requestId: z + .string() + .describe("The request ID from the get_viewer_state command"), + state: z + .string() + .optional() + .describe("JSON-encoded viewer state snapshot"), + error: z + .string() + .optional() + .describe("Error message if the viewer failed to read state"), + }, + _meta: { ui: { visibility: ["app"] } }, + }, + async ({ requestId, state, error }): Promise => { + const settle = pendingStateRequests.get(requestId); + if (!settle) { + return { + content: [ + { type: "text", text: `No pending request for ${requestId}` }, + ], + isError: true, + }; + } + if (error || !state) { + settle(new Error(error || "Viewer returned no state")); + } else { + settle(state); + } + return { content: [{ type: "text", text: "Submitted" }] }; + }, + ); + // Tool: poll_pdf_commands (app-only) - Poll for pending commands registerAppTool( server, diff --git a/examples/pdf-server/src/commands.ts b/examples/pdf-server/src/commands.ts index c469ebd..6d27edf 100644 --- a/examples/pdf-server/src/commands.ts +++ b/examples/pdf-server/src/commands.ts @@ -66,4 +66,5 @@ export type PdfCommand = getScreenshots: boolean; } | { type: "save_as"; requestId: string } + | { type: "get_viewer_state"; requestId: string } | { type: "file_changed"; mtimeMs: number }; diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index deec2e7..fe30b14 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -2428,6 +2428,84 @@ async function renderPageOffscreen(pageNum: number): Promise { return dataUrl.split(",")[1]; } +/** + * Snapshot the live viewer for `interact({action:"get_viewer_state"})`. + * + * Selection is read from `window.getSelection()` at call time — no caching; + * if the user navigated away or nothing is selected, `selection` is `null`. + * `boundingRect` is in model coords (PDF points, origin top-left, y-down) so + * it can be fed straight back into `add_annotations`. + */ +async function handleGetViewerState(requestId: string): Promise { + const CONTEXT_CHARS = 200; + + let selection: { + text: string; + contextBefore: string; + contextAfter: string; + boundingRect: { x: number; y: number; width: number; height: number }; + } | null = null; + + const sel = window.getSelection(); + const selectedText = sel?.toString().replace(/\s+/g, " ").trim(); + if (sel && selectedText && sel.rangeCount > 0) { + // Only treat it as a PDF selection if it lives inside the text layer of + // the rendered page (not the toolbar, search box, etc.). + const range = sel.getRangeAt(0); + const anchor = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? (range.commonAncestorContainer as Element) + : range.commonAncestorContainer.parentElement; + if (anchor && textLayerEl.contains(anchor)) { + // Context: locate selection in the page's extracted text and slice + // ±CONTEXT_CHARS around it. Falls back to empty strings if fuzzy + // match fails (still return text + rect — they're the load-bearing + // bits). + const pageText = pageTextCache.get(currentPage) ?? ""; + const loc = findSelectionInText(pageText, selectedText); + const contextBefore = loc + ? pageText.slice(Math.max(0, loc.start - CONTEXT_CHARS), loc.start) + : ""; + const contextAfter = loc + ? pageText.slice(loc.end, loc.end + CONTEXT_CHARS) + : ""; + + // Single bounding box, page-relative model coords. getBoundingClientRect + // is viewport-relative; subtract the page-wrapper origin then divide by + // scale → PDF points (top-left origin, y-down — matches the coord + // system documented in the interact tool description). + const r = range.getBoundingClientRect(); + const origin = pageWrapperEl.getBoundingClientRect(); + const round = (n: number) => Math.round(n * 100) / 100; + selection = { + text: selectedText, + contextBefore, + contextAfter, + boundingRect: { + x: round((r.left - origin.left) / scale), + y: round((r.top - origin.top) / scale), + width: round(r.width / scale), + height: round(r.height / scale), + }, + }; + } + } + + const state = { + currentPage, + pageCount: totalPages, + zoom: Math.round(scale * 100), + displayMode: currentDisplayMode, + selectedAnnotationIds: [...selectedAnnotationIds], + selection, + }; + + await app.callServerTool({ + name: "submit_viewer_state", + arguments: { requestId, state: JSON.stringify(state, null, 2) }, + }); +} + async function handleGetPages(cmd: { requestId: string; intervals: Array<{ start?: number; end?: number }>; @@ -4678,6 +4756,23 @@ async function processCommands(commands: PdfCommand[]): Promise { .catch(() => {}); } break; + case "get_viewer_state": + // Same await-before-next-poll discipline as get_pages/save_as. + try { + await handleGetViewerState(cmd.requestId); + } catch (err) { + log.error("get_viewer_state failed — submitting error:", err); + await app + .callServerTool({ + name: "submit_viewer_state", + arguments: { + requestId: cmd.requestId, + error: err instanceof Error ? err.message : String(err), + }, + }) + .catch(() => {}); + } + break; case "file_changed": { // Skip our own save_pdf echo: either save is still in flight, or the // event's mtime matches what save_pdf just returned. diff --git a/playwright.config.ts b/playwright.config.ts index 8485b00..6f6cb65 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,6 +30,7 @@ export default defineConfig({ ...devices["Desktop Chrome"], // Use default Chromium everywhere for consistent screenshot rendering // Run `npm run test:e2e:docker` locally for CI-identical results + ...(process.env.PW_CHANNEL ? { channel: process.env.PW_CHANNEL } : {}), }, }, ], diff --git a/tests/e2e/pdf-annotations.spec.ts b/tests/e2e/pdf-annotations.spec.ts index 1b8077b..af67bb7 100644 --- a/tests/e2e/pdf-annotations.spec.ts +++ b/tests/e2e/pdf-annotations.spec.ts @@ -314,3 +314,93 @@ test.describe("PDF Server - Annotations", () => { ); }); }); + +/** + * Read the most recent interact result text from the basic-host UI. + * Waits for the result-panel count to reach `expectedCount` first — + * `callInteract` doesn't block, so `.last()` would otherwise race to the + * previous (display_pdf) panel. + */ +async function readLastToolResult( + page: Page, + expectedCount: number, +): Promise { + const panels = page.locator('text="📤 Tool Result"'); + await expect(panels).toHaveCount(expectedCount, { timeout: 30000 }); + await panels.last().click(); + const pre = page.locator("pre").last(); + await expect(pre).toBeVisible({ timeout: 5000 }); + return (await pre.textContent()) ?? ""; +} + +/** Unwrap basic-host's `CallToolResult` JSON to the first text block. */ +function unwrapTextResult(raw: string): string { + const parsed = JSON.parse(raw) as { + content?: { type: string; text?: string }[]; + }; + const block = parsed.content?.find((c) => c.type === "text"); + if (!block?.text) throw new Error(`No text block in: ${raw.slice(0, 200)}`); + return block.text; +} + +test.describe("PDF Server - get_viewer_state", () => { + test("returns page/zoom/mode and selection:null when nothing is selected", async ({ + page, + }) => { + await loadPdfServer(page); + await waitForPdfCanvas(page); + + const viewUUID = await extractViewUUID(page); + + await callInteract(page, { viewUUID, action: "get_viewer_state" }); + const raw = await readLastToolResult(page, 2); + const state = JSON.parse(unwrapTextResult(raw)); + + expect(state.currentPage).toBe(1); + expect(state.pageCount).toBeGreaterThan(1); + expect(typeof state.zoom).toBe("number"); + expect(state.displayMode).toBe("inline"); + expect(state.selection).toBeNull(); + expect(Array.isArray(state.selectedAnnotationIds)).toBe(true); + }); + + test("returns selected text and bounding rect when text-layer text is selected", async ({ + page, + }) => { + await loadPdfServer(page); + await waitForPdfCanvas(page); + + const viewUUID = await extractViewUUID(page); + const app = getAppFrame(page); + + // Programmatically select the contents of the first text-layer span. + const selectedText = await app + .locator("#text-layer span") + .first() + .evaluate((span) => { + const range = span.ownerDocument.createRange(); + range.selectNodeContents(span); + const sel = span.ownerDocument.defaultView!.getSelection()!; + sel.removeAllRanges(); + sel.addRange(range); + return sel.toString().replace(/\s+/g, " ").trim(); + }); + expect(selectedText.length).toBeGreaterThan(0); + + await callInteract(page, { viewUUID, action: "get_viewer_state" }); + const raw = await readLastToolResult(page, 2); + const state = JSON.parse(unwrapTextResult(raw)); + + expect(state.currentPage).toBe(1); + expect(state.selection).not.toBeNull(); + expect(state.selection.text).toContain(selectedText); + expect(state.selection.boundingRect).toEqual( + expect.objectContaining({ + x: expect.any(Number), + y: expect.any(Number), + width: expect.any(Number), + height: expect.any(Number), + }), + ); + }); +}); From f3bf1065db694848a4c3a24b45d95b0ed87a6bd2 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 10:00:53 +0100 Subject: [PATCH 15/97] 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. --- .../pdf-server/src/pdf-annotations.test.ts | 53 +++++++++++++++++-- examples/pdf-server/src/pdf-annotations.ts | 47 ++++++++-------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index fe34d83..d0fabcb 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -460,7 +460,8 @@ describe("importPdfjsAnnotation", () => { annotationType: 9, ref: { num: 5, gen: 0 }, rect: [72, 700, 272, 712], - quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]], + // pdf.js emits a FLAT Float32Array, not nested arrays. + quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]), color: new Uint8ClampedArray([255, 255, 0]), contentsObj: { str: "Important" }, }; @@ -474,12 +475,57 @@ describe("importPdfjsAnnotation", () => { expect((result as any).color).toBe("#ffff00"); }); + it("imports a multi-line highlight (multiple quads → multiple rects)", () => { + // Regression: the parser iterated quadPoints as if nested; pdf.js's + // flat array yielded numbers, so rects stayed empty and import bailed. + const ann = { + annotationType: 9, + ref: { num: 8, gen: 0 }, + quadPoints: new Float32Array([ + // line 1 + 72, 712, 272, 712, 72, 700, 272, 700, + // line 2 + 72, 698, 200, 698, 72, 686, 200, 686, + ]), + color: new Uint8ClampedArray([255, 255, 0]), + }; + const result = importPdfjsAnnotation(ann, 1, 0); + expect(result).not.toBeNull(); + expect(result!.type).toBe("highlight"); + expect((result as any).rects).toHaveLength(2); + expect((result as any).rects[0]).toEqual({ + x: 72, + y: 700, + width: 200, + height: 12, + }); + expect((result as any).rects[1]).toEqual({ + x: 72, + y: 686, + width: 128, + height: 12, + }); + }); + + it("falls back to ann.rect when quadPoints is absent", () => { + const ann = { + annotationType: 9, + ref: { num: 9, gen: 0 }, + rect: [72, 700, 272, 712], + color: new Uint8ClampedArray([255, 255, 0]), + }; + const result = importPdfjsAnnotation(ann, 1, 0); + expect(result).not.toBeNull(); + expect((result as any).rects).toHaveLength(1); + }); + it("imports an underline annotation", () => { const ann = { annotationType: 10, ref: { num: 6, gen: 0 }, rect: [72, 700, 272, 712], - quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]], + // pdf.js emits a FLAT Float32Array, not nested arrays. + quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]), color: new Uint8ClampedArray([255, 0, 0]), }; const result = importPdfjsAnnotation(ann, 1, 0); @@ -493,7 +539,8 @@ describe("importPdfjsAnnotation", () => { annotationType: 12, ref: { num: 7, gen: 0 }, rect: [72, 700, 272, 712], - quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]], + // pdf.js emits a FLAT Float32Array, not nested arrays. + quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]), color: new Uint8ClampedArray([255, 0, 0]), }; const result = importPdfjsAnnotation(ann, 2, 0); diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts index 25d9ebb..6a7e268 100644 --- a/examples/pdf-server/src/pdf-annotations.ts +++ b/examples/pdf-server/src/pdf-annotations.ts @@ -948,33 +948,28 @@ export function importPdfjsAnnotation( case "highlight": case "underline": case "strikethrough": { - // PDF.js provides quadPoints as array of arrays [[x1,y1,x2,y2,...], ...] - // or rect as [x1,y1,x2,y2] - let rects: Rect[]; - if (ann.quadPoints && ann.quadPoints.length > 0) { - rects = []; - for (const qp of ann.quadPoints) { - // Each quadPoint is [x1,y1,x2,y2,x3,y3,x4,y4] - // We need the bounding box - if (qp.length >= 8) { - const xs = [qp[0], qp[2], qp[4], qp[6]]; - const ys = [qp[1], qp[3], qp[5], qp[7]]; - const minX = Math.min(...xs); - const minY = Math.min(...ys); - const maxX = Math.max(...xs); - const maxY = Math.max(...ys); - rects.push({ - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY, - }); - } + // pdf.js emits quadPoints as a FLAT Float32Array [x1,y1,...,x4,y4, …] + // (8 numbers per quad), NOT as nested arrays. Iterating it yields + // numbers, so the old `for (const qp of …) if (qp.length>=8)` never + // matched and every quad-based annotation was dropped (#506). + const rects: Rect[] = []; + const qp = ann.quadPoints as ArrayLike | undefined; + if (qp && qp.length >= 8) { + for (let i = 0; i + 8 <= qp.length; i += 8) { + const xs = [qp[i], qp[i + 2], qp[i + 4], qp[i + 6]]; + const ys = [qp[i + 1], qp[i + 3], qp[i + 5], qp[i + 7]]; + const minX = Math.min(...xs); + const minY = Math.min(...ys); + rects.push({ + x: minX, + y: minY, + width: Math.max(...xs) - minX, + height: Math.max(...ys) - minY, + }); } - } else if (ann.rect) { - rects = [pdfjsRectToRect(ann.rect)]; - } else { - return null; + } + if (rects.length === 0 && ann.rect) { + rects.push(pdfjsRectToRect(ann.rect)); } if (rects.length === 0) return null; From 3af019144f254fec937517a25a00dd4e50ff04ac Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 10:01:23 +0100 Subject: [PATCH 16/97] 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/ 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 is only the first option; join them all + // so save can select() the full set on a PDFOptionList. + value = Array.from(target.selectedOptions, (o) => o.value).join(","); } else { value = target.value; } diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index d0fabcb..6ceb346 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -1152,6 +1152,76 @@ describe("buildAnnotatedPdfBytes", () => { const form = (await PDFDocument.load(out)).getForm(); expect(form.getTextField("name").getText()).toBe("kept"); }); + + it("a field that throws on write does not abort subsequent fields", async () => { + // Regression for #577: the per-field try/catch was dropped, so the + // first throwing field bubbled to the outer catch and silently dropped + // every field after it. setText() throws when value exceeds maxLength. + const doc = await PDFDocument.create(); + const page = doc.addPage([612, 792]); + const form = doc.getForm(); + const limited = form.createTextField("limited"); + limited.setMaxLength(2); + limited.addToPage(page, { x: 10, y: 700 }); + form.createTextField("after").addToPage(page, { x: 10, y: 660 }); + const fixture = await doc.save(); + + const out = await buildAnnotatedPdfBytes( + fixture, + [], + new Map([ + ["limited", "way too long"], // throws + ["after", "kept"], + ]), + ); + + const saved = (await PDFDocument.load(out)).getForm(); + expect(saved.getTextField("after").getText()).toBe("kept"); + // The throwing field is left at whatever pdf-lib could do with it — + // we only assert it didn't poison "after". + }); + + it("radio misclassified as PDFCheckBox: string value selects the matching widget", async () => { + // Some PDFs (e.g. IRS/third-party forms) omit the /Ff Radio bit, so + // pdf-lib hands us a PDFCheckBox. The viewer stored pdf.js's + // buttonValue ("0"/"1"), not a boolean — check() would always pick + // the first widget. setButtonGroupValue writes /V + per-widget /AS + // directly so the chosen widget sticks. + const doc = await PDFDocument.create(); + const page = doc.addPage([612, 792]); + const form = doc.getForm(); + const rg = form.createRadioGroup("Gender"); + rg.addOptionToPage("Male", page, { x: 10, y: 700 }); + rg.addOptionToPage("Female", page, { x: 60, y: 700 }); + // pdf-lib's addOptionToPage writes widget on-values "0","1". Clear the + // Radio flag (bit 16) so the reloaded form classifies it as checkbox. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (rg.acroField as any).clearFlag(1 << 15); + const fixture = await doc.save(); + + // Sanity: reload sees it as PDFCheckBox now. + const reForm = (await PDFDocument.load(fixture)).getForm(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const reField = reForm.getFieldMaybe("Gender") as any; + expect(reField?.constructor?.name).toBe("PDFCheckBox"); + + const out = await buildAnnotatedPdfBytes( + fixture, + [], + new Map([["Gender", "1"]]), + ); + + const saved = await PDFDocument.load(out); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const acro = (saved.getForm().getFieldMaybe("Gender") as any).acroField; + const v = acro.dict.get(PDFName.of("V")); + expect(v).toBeInstanceOf(PDFName); + expect((v as PDFName).decodeText()).toBe("1"); + // Second widget /AS is the on-state, first is /Off. + const widgets = acro.getWidgets(); + expect(widgets[0].getAppearanceState()?.decodeText()).toBe("Off"); + expect(widgets[1].getAppearanceState()?.decodeText()).toBe("1"); + }); }); }); diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts index 6a7e268..8541706 100644 --- a/examples/pdf-server/src/pdf-annotations.ts +++ b/examples/pdf-server/src/pdf-annotations.ts @@ -21,7 +21,9 @@ import { PDFTextField, PDFCheckBox, PDFDropdown, + PDFOptionList, PDFRadioGroup, + type PDFForm, } from "pdf-lib"; // ============================================================================= @@ -800,6 +802,40 @@ export async function addAnnotationDicts( } } +/** + * Select a radio-style button group by widget on-value, bypassing pdf-lib's + * type-level guards. Used when pdf-lib classifies a radio as `PDFCheckBox` + * (PDF lacks the /Ff Radio bit) — `check()` would always pick the first + * widget. Mirrors `PDFAcroRadioButton.setValue` minus its `onValues` throw. + */ +function setButtonGroupValue( + field: PDFCheckBox | PDFRadioGroup, + onValue: string, +): void { + const acro = field.acroField; + const off = PDFName.of("Off"); + const widgets = acro.getWidgets(); + // Match by PDFName identity (pdf-lib interns names) — the viewer stored + // pdf.js's buttonValue, which IS the widget's /AP /N on-state name. + let target = onValue && onValue !== "Off" ? PDFName.of(onValue) : off; + if ( + target !== off && + !widgets.some((w: { getOnValue(): PDFName | undefined }) => { + return w.getOnValue() === target; + }) + ) { + // No widget has this on-state — leave as-is rather than corrupt /V. + return; + } + acro.dict.set(PDFName.of("V"), target); + for (const w of widgets) { + const on = (w as { getOnValue(): PDFName | undefined }).getOnValue(); + (w as { setAppearanceState(s: PDFName): void }).setAppearanceState( + on === target ? target : off, + ); + } +} + /** * Build annotated PDF bytes from the original document. * Applies user annotations and form fills, returns Uint8Array of the new PDF. @@ -815,44 +851,96 @@ export async function buildAnnotatedPdfBytes( await addAnnotationDicts(pdfDoc, annotations); // Apply form fills. Dispatch on actual field type — getTextField(name) throws - // for dropdowns/radios, so the old try/catch silently dropped those on save. + // for dropdowns/radios, so we look up the generic field and instanceof it. + // Each field is wrapped in its own try/catch: pdf-lib can throw on + // length-constrained text, radios whose buttonValue maps to neither label + // nor index, checkboxes missing a /Yes appearance, etc. One bad field must + // not abort the rest of the loop (regressed in #577 when the inner catch + // was dropped along with the type-specific getters). if (formFields.size > 0) { + let form: PDFForm | undefined; try { - const form = pdfDoc.getForm(); - for (const [name, value] of formFields) { - const field = form.getFieldMaybe(name); - if (!field) continue; - - if (field instanceof PDFCheckBox) { - if (value) field.check(); - else field.uncheck(); - } else if (field instanceof PDFRadioGroup) { - // The viewer stores pdf.js's buttonValue, which for PDFs with an - // /Opt array is a numeric index ("0","1","2") rather than the - // option label pdf-lib's select() expects. Try the label first, - // then fall back to indexing into getOptions(). - const opts = field.getOptions(); - const s = String(value); - if (opts.includes(s)) { - field.select(s); - } else { - const idx = Number(s); - if (Number.isInteger(idx) && idx >= 0 && idx < opts.length) { - field.select(opts[idx]); - } - // else: value is neither label nor index — leave unset - } - } else if (field instanceof PDFDropdown) { - // select() auto-enables edit mode for values outside getOptions(), - // so this works for both enumerated and free-text combos. - field.select(String(value)); - } else if (field instanceof PDFTextField) { - field.setText(String(value)); - } - // PDFButton, PDFOptionList, PDFSignature: no fill_form support yet - } + form = pdfDoc.getForm(); } catch { - // pdfDoc.getForm() throws if the PDF has no AcroForm + // No AcroForm in this PDF + } + if (form) { + for (const [name, value] of formFields) { + try { + const field = form.getFieldMaybe(name); + if (!field) continue; + + if (field instanceof PDFCheckBox) { + const widgets = field.acroField.getWidgets(); + if (typeof value === "string" && widgets.length > 1) { + // Multi-widget "checkbox" with a string value = pdf-lib + // misclassified a radio group (PDF lacks the /Ff Radio flag). + // The viewer stored pdf.js's buttonValue (the widget's on-state + // name, e.g. "0"/"1"); check()/uncheck() would hit the FIRST + // widget regardless. Write /V and per-widget /AS directly. + setButtonGroupValue(field, value); + } else { + // Single-widget (real) checkbox. Viewer normally stores boolean, + // but be liberal: any truthy non-"Off" string counts as checked. + const on = + value === true || + (typeof value === "string" && value !== "" && value !== "Off"); + if (on) field.check(); + else field.uncheck(); + } + } else if (field instanceof PDFRadioGroup) { + // The viewer stores pdf.js's buttonValue, which for PDFs with an + // /Opt array is a numeric index ("0","1","2") rather than the + // option label pdf-lib's select() expects. Try the label first, + // then fall back to indexing into getOptions(). + const opts = field.getOptions(); + const s = String(value); + if (opts.includes(s)) { + field.select(s); + } else { + const idx = Number(s); + if (Number.isInteger(idx) && idx >= 0 && idx < opts.length) { + field.select(opts[idx]); + } + // else: value is neither label nor index — leave unset + } + } else if (field instanceof PDFDropdown) { + // select() auto-enables edit mode for values outside getOptions(), + // so this works for both enumerated and free-text combos. + field.select(String(value)); + } else if (field instanceof PDFOptionList) { + // Viewer stores multiselect listboxes as a comma-joined string + // (pdf.js's annotationStorage value for /Ch is string|string[]; + // our Map flattens arrays). Only select + // values that are actually in the option list — pdf-lib throws + // on unknowns and there's no edit-mode fallback like Dropdown. + const opts = field.getOptions(); + const wanted = String(value) + .split(",") + .map((s) => s.trim()) + .filter((s) => opts.includes(s)); + if (wanted.length > 0) field.select(wanted); + else field.clear(); + } else if (field instanceof PDFTextField) { + field.setText(String(value)); + } + // PDFButton, PDFSignature: no fill_form support yet + } catch (err) { + // Skip this field; carry on with the rest. Surfacing per-field + // failures is the caller's job (see fill_form result), not save's. + console.warn(`buildAnnotatedPdfBytes: skipped field "${name}":`, err); + } + } + // Some PDFs ship widgets with no on-state appearance stream (no + // /AP/N/). pdf-lib's check()/select()/setText() set /V and /AS + // but don't synthesize the missing appearance, so other readers + // (Preview, Acrobat) show the field as if unchanged. This generates a + // default appearance for any field marked dirty above. + try { + form.updateFieldAppearances(); + } catch (err) { + console.warn("buildAnnotatedPdfBytes: updateFieldAppearances:", err); + } } } From 61fe39b4dbe3ed92e0769b378741d4dc1748e8b5 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 10:03:35 +0100 Subject: [PATCH 17/97] 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 ' (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-- and pdf-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. --- examples/pdf-server/src/annotation-panel.ts | 89 +++++++++- examples/pdf-server/src/mcp-app.css | 15 ++ examples/pdf-server/src/mcp-app.ts | 168 +++++++++++++++--- .../pdf-server/src/pdf-annotations.test.ts | 141 +++++++++++++++ examples/pdf-server/src/pdf-annotations.ts | 123 ++++++++++++- 5 files changed, 505 insertions(+), 31 deletions(-) diff --git a/examples/pdf-server/src/annotation-panel.ts b/examples/pdf-server/src/annotation-panel.ts index 081d5c8..9fd4457 100644 --- a/examples/pdf-server/src/annotation-panel.ts +++ b/examples/pdf-server/src/annotation-panel.ts @@ -289,10 +289,23 @@ function panelFieldNames(): Set { return new Set([...formFieldValues.keys(), ...pdfBaselineFormValues.keys()]); } +/** Baseline annotations the user has deleted. Shown crossed-out in the panel + * (mirroring cleared form fields) so they can be reverted, and so save knows + * to strip their refs from /Annots. */ +function removedBaselineAnnotations(): PdfAnnotationDef[] { + return deps + .state() + .pdfBaselineAnnotations.filter((a) => !annotationMap.has(a.id)); +} + /** Total count of annotations + form fields for the sidebar badge. * Uses the union so cleared baseline items still contribute. */ function sidebarItemCount(): number { - return annotationMap.size + panelFieldNames().size; + return ( + annotationMap.size + + removedBaselineAnnotations().length + + panelFieldNames().size + ); } export function updateAnnotationsBadge(): void { @@ -338,6 +351,8 @@ export function getAnnotationLabel(def: PdfAnnotationDef): string { return "Line"; case "image": return "Image"; + case "imported": + return `${def.subtype} (from PDF)`; } } @@ -381,6 +396,8 @@ export function getAnnotationColor(def: PdfAnnotationDef): string { return "#333"; case "image": return "#999"; + case "imported": + return "#666"; } } @@ -425,6 +442,14 @@ export function renderAnnotationPanel(): void { byPage.get(page)!.push(tracked); } + // Removed baseline annotations: still listed (crossed-out) so they can be + // reverted before save strips them from the file. + const removedByPage = new Map(); + for (const def of removedBaselineAnnotations()) { + if (!removedByPage.has(def.page)) removedByPage.set(def.page, []); + removedByPage.get(def.page)!.push(def); + } + // Group form fields by page — iterate the UNION so cleared baseline // fields remain visible (crossed out) with a per-item revert button. const fieldsByPage = new Map(); @@ -441,7 +466,11 @@ export function renderAnnotationPanel(): void { } // Collect all pages that have annotations or form fields - const allPages = new Set([...byPage.keys(), ...fieldsByPage.keys()]); + const allPages = new Set([ + ...byPage.keys(), + ...removedByPage.keys(), + ...fieldsByPage.keys(), + ]); const sortedPages = [...allPages].sort((a, b) => a - b); // Sort annotations within each page by Y position (descending = top-first in PDF coords) @@ -466,8 +495,9 @@ export function renderAnnotationPanel(): void { const sectionKey = `page-${pageNum}`; const isOpen = panelState.openAccordionSection === sectionKey; const annotations = byPage.get(pageNum) ?? []; + const removed = removedByPage.get(pageNum) ?? []; const fields = fieldsByPage.get(pageNum) ?? []; - const itemCount = annotations.length + fields.length; + const itemCount = annotations.length + removed.length + fields.length; appendAccordionSection( `Page ${pageNum} (${itemCount})`, @@ -483,6 +513,10 @@ export function renderAnnotationPanel(): void { for (const tracked of annotations) { body.appendChild(createAnnotationCard(tracked)); } + // Then removed baseline annotations (crossed-out, revertable) + for (const def of removed) { + body.appendChild(createRemovedAnnotationCard(def)); + } }, ); } @@ -648,6 +682,55 @@ function createAnnotationCard(tracked: TrackedAnnotation): HTMLElement { return card; } +/** + * Card for a baseline annotation the user deleted: crossed-out, no select/ + * navigate (it has no DOM on the page anymore), revert button puts it back + * into `annotationMap` so it renders again and save leaves it in the file. + */ +function createRemovedAnnotationCard(def: PdfAnnotationDef): HTMLElement { + const card = document.createElement("div"); + card.className = "annotation-card annotation-card-cleared"; + card.dataset.annotationId = def.id; + + const row = document.createElement("div"); + row.className = "annotation-card-row"; + + const swatch = document.createElement("div"); + swatch.className = "annotation-card-swatch annotation-card-swatch-cleared"; + swatch.innerHTML = ``; + row.appendChild(swatch); + + const typeLabel = document.createElement("span"); + typeLabel.className = "annotation-card-type"; + typeLabel.textContent = getAnnotationLabel(def); + row.appendChild(typeLabel); + + const preview = getAnnotationPreview(def); + if (preview) { + const previewEl = document.createElement("span"); + previewEl.className = "annotation-card-preview"; + previewEl.textContent = preview; + row.appendChild(previewEl); + } + + const revertBtn = document.createElement("button"); + revertBtn.className = "annotation-card-delete"; + revertBtn.title = "Restore annotation from file"; + revertBtn.innerHTML = REVERT_SVG; + revertBtn.addEventListener("click", (e) => { + e.stopPropagation(); + annotationMap.set(def.id, { def: { ...def }, elements: [] }); + updateAnnotationsBadge(); + renderAnnotationPanel(); + deps.renderPage(); + deps.persistAnnotations(); + }); + row.appendChild(revertBtn); + + card.appendChild(row); + return card; +} + /** Revert one field to its PDF-stored baseline value. */ function revertFieldToBaseline(name: string): void { const base = pdfBaselineFormValues.get(name); diff --git a/examples/pdf-server/src/mcp-app.css b/examples/pdf-server/src/mcp-app.css index 4f03520..a87dbcd 100644 --- a/examples/pdf-server/src/mcp-app.css +++ b/examples/pdf-server/src/mcp-app.css @@ -590,6 +590,21 @@ body { user-select: none; } +/* Annotation imported verbatim from the PDF (Stamp/Ink/etc. via + * annotationCanvasMap). The body is the rasterized appearance canvas; + * if pdf.js didn't divert it (no hasOwnCanvas), the box stays transparent + * over the main-canvas pixel and just captures clicks. */ +.annotation-imported { + position: absolute; + pointer-events: auto; + cursor: grab; + user-select: none; +} +.annotation-imported:hover { + outline: 1px dashed var(--accent, #2563eb); + outline-offset: 1px; +} + /* Selection visuals */ .annotation-selected { outline: 2px solid var(--accent, #2563eb); diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 678721d..f4a1790 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -25,6 +25,7 @@ import { type LineAnnotation, type StampAnnotation, type ImageAnnotation, + type ImportedAnnotation, type NoteAnnotation, type FreetextAnnotation, cssColorToRgb, @@ -34,6 +35,7 @@ import { computeDiff, isDiffEmpty, buildAnnotatedPdfBytes, + parseAnnotationRef, importPdfjsAnnotation, uint8ArrayToBase64, convertFromModelCoords, @@ -1383,6 +1385,10 @@ const DRAGGABLE_TYPES = new Set([ "stamp", "note", "image", + // "imported" is draggable in the UI but the move does NOT persist to the + // PDF on save (addAnnotationDicts skips it). Resize/rotate stay disabled + // — the appearance bitmap would just stretch. + "imported", ]); function setupAnnotationInteraction( @@ -1890,6 +1896,25 @@ function paintAnnotationsOnCanvas( } break; } + + case "imported": { + const s = pdfRectToScreen( + { x: def.x, y: def.y, width: def.width, height: def.height }, + viewport, + ); + const bmp = annotationCanvasMap.get(def.pdfjsId); + ctx.save(); + if (bmp) { + ctx.drawImage(bmp, s.left, s.top, s.width, s.height); + } else { + ctx.strokeStyle = "#666"; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.strokeRect(s.left, s.top, s.width, s.height); + } + ctx.restore(); + break; + } } } } @@ -1987,6 +2012,8 @@ function renderAnnotation( return [renderLineAnnotation(def, viewport)]; case "image": return [renderImageAnnotation(def, viewport)]; + case "imported": + return [renderImportedAnnotation(def, viewport)]; } } @@ -2175,6 +2202,45 @@ function renderImageAnnotation( return el; } +/** + * Per-annotation appearance bitmaps from page.render(). Keyed by pdf.js + * annotation id (e.g. "118R"). Populated for the current page only — + * cleared at the start of each renderPage(). + */ +const annotationCanvasMap = new Map(); + +function renderImportedAnnotation( + def: ImportedAnnotation, + viewport: { width: number; height: number; scale: number }, +): HTMLElement { + const screen = pdfRectToScreen( + { x: def.x, y: def.y, width: def.width, height: def.height }, + viewport, + ); + const el = document.createElement("div"); + el.className = "annotation-imported"; + el.style.left = `${screen.left}px`; + el.style.top = `${screen.top}px`; + el.style.width = `${screen.width}px`; + el.style.height = `${screen.height}px`; + el.title = `${def.subtype} (from PDF)`; + + // page.render() may or may not have produced a separate canvas for this + // annotation (hasOwnCanvas depends on the PDF's flags). When it did, use + // it as a pixel-faithful body; when it didn't, the appearance is on the + // main canvas already, so leave the box transparent — it still captures + // clicks for select/delete. + const canvas = annotationCanvasMap.get(def.pdfjsId); + if (canvas) { + canvas.style.width = "100%"; + canvas.style.height = "100%"; + canvas.style.display = "block"; + canvas.style.pointerEvents = "none"; + el.appendChild(canvas); + } + return el; +} + // ============================================================================= // Annotation CRUD // ============================================================================= @@ -2761,7 +2827,8 @@ function normaliseFieldValue( */ async function buildFieldNameMap( doc: pdfjsLib.PDFDocumentProxy, -): Promise { +): Promise { + let pushedToStorage = false; fieldNameToIds.clear(); radioButtonValues.clear(); fieldNameToPage.clear(); @@ -2857,12 +2924,16 @@ async function buildFieldNameMap( if (!widgetIds) continue; // no widget → not rendered anyway // Type comes from getFieldObjects (widget annot data doesn't have it). - // Value comes from the widget annotation (fall back to field-dict if - // the widget didn't expose one). + // Value: prefer the AcroForm field-tree value over the widget's + // fieldValue. pdf-lib's save() can leave a page widget pointing at a + // stale /V while the field tree has the new one (seen with comb text + // fields), and getAnnotations() reads the widget. If the two disagree + // we push the field-tree value into annotationStorage so the rendered + // input matches what's actually in /AcroForm. const type = fieldArr.find((f) => f.type)?.type; - const raw = widgetFieldValues.has(name) - ? widgetFieldValues.get(name) - : fieldArr.find((f) => f.value != null)?.value; + const fieldTreeRaw = fieldArr.find((f) => f.value != null)?.value; + const widgetRaw = widgetFieldValues.get(name); + const raw = fieldTreeRaw ?? widgetRaw; const v = normaliseFieldValue(type, raw); if (v !== null) { pdfBaselineFormValues.set(name, v); @@ -2870,6 +2941,13 @@ async function buildFieldNameMap( // restored localStorage diff (applied in restoreAnnotations) will // overwrite specific fields the user changed. if (!formFieldValues.has(name)) formFieldValues.set(name, v); + // Widget out of sync with field tree → force storage so + // AnnotationLayer renders the field-tree value, not the stale + // widget. (syncFormValuesToStorage skips baseline==current.) + if (fieldTreeRaw != null && fieldTreeRaw !== widgetRaw) { + setFieldInStorage(name, v); + pushedToStorage = true; + } } // Skip parent entries with no concrete id (radio groups: the /T tree @@ -2884,6 +2962,7 @@ async function buildFieldNameMap( } log.info(`Built field name map: ${fieldNameToIds.size} fields`); + return pushedToStorage; } /** @@ -2971,6 +3050,14 @@ async function getAnnotatedPdfBytes(): Promise { } } + // Baseline annotations the user deleted: strip their refs from /Annots so + // they don't reappear on reload. Ids without a recoverable ref (page-index + // fallback) can't be removed by-ref and are skipped. + const removedRefs = pdfBaselineAnnotations + .filter((a) => !annotationMap.has(a.id)) + .map((a) => parseAnnotationRef(a.id)) + .filter((r): r is NonNullable => r !== null); + // Only write fields that actually changed vs. what's already in the PDF. // Unchanged fields are no-ops at best, and at worst trip pdf-lib edge // cases (max-length text, missing /Yes appearance, …) on fields the user @@ -2995,6 +3082,7 @@ async function getAnnotatedPdfBytes(): Promise { fullBytes as Uint8Array, annotations, formFieldsOut, + removedRefs, ); } @@ -3036,15 +3124,6 @@ async function savePdf(): Promise { const sc = result.structuredContent as { mtimeMs?: number } | undefined; lastSavedMtime = sc?.mtimeMs ?? null; - // Rebase: the file on disk now contains our annotations + form values. - // Update the baseline so future diffs are relative to what was saved. - pdfBaselineAnnotations = [...annotationMap.values()].map((t) => ({ - ...t.def, - })); - pdfBaselineFormValues.clear(); - for (const [k, v] of formFieldValues) pdfBaselineFormValues.set(k, v); - - setDirty(false); // → updateSaveBtn() disables button const key = annotationStorageKey(); if (key) { try { @@ -3053,6 +3132,13 @@ async function savePdf(): Promise { /* ignore */ } } + // Reload from the bytes we just wrote. The previous approach (rebase + // baselines but keep the old pdfDocument) drifts: subsequent renders + // still rasterize stripped annotations from the old bytes, and the + // field/widget split that pdf-lib's save can create isn't reflected + // until reload anyway. Reload makes "what you see = what's on disk" + // an invariant. (file_changed echo is suppressed by lastSavedMtime.) + await reloadPdf(); } } catch (err) { log.error("Save failed:", err); @@ -3156,8 +3242,15 @@ async function renderPage() { // resize so the size handoff is atomic. pageWrapperEl.style.transform = ""; - // Scale context for retina - ctx.scale(dpr, dpr); + // Retina: pass dpr via page.render's `transform` (NOT ctx.scale). + // pdf.js sizes per-annotation canvases as + // width = rectW * outputScaleX * viewport.scale + // and outputScaleX is read from transform[0] (defaults to 1). A bare + // ctx.scale(dpr,dpr) leaves outputScaleX at 1, so the + // annotationCanvasMap canvases get a half-sized backing store on + // retina while their internal setTransform IS dpr-aware → the + // appearance renders 2× too big into a 1× buffer → cropped/shifted. + const dprTransform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined; // Clear and setup text layer textLayerEl.innerHTML = ""; @@ -3166,11 +3259,29 @@ async function renderPage() { // Set --scale-factor so CSS font-size/transform rules work correctly. textLayerEl.style.setProperty("--scale-factor", `${scale}`); - // Render canvas - track the task so we can cancel it + // Render canvas - track the task so we can cancel it. + // + // annotationCanvasMap: pdf.js diverts annotations whose appearance needs + // its own bitmap (Stamp/Ink/FreeText/etc. with hasOwnCanvas) into + // per-id canvases instead of compositing onto the main canvas. + // renderImportedAnnotation() pulls from this map so those annotations + // become movable DOM elements with pixel-faithful visuals — instead of + // unselectable canvas pixels (the old "ghost annotation" problem) or + // our lossy text-label re-render. + annotationCanvasMap.clear(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const renderTask = (page.render as any)({ canvasContext: ctx, viewport, + transform: dprTransform, + annotationCanvasMap, + // isEditing forces hasOwnCanvas=true for stamps regardless of /F + // NoRotate (StampAnnotation.mustBeViewedWhenEditing in pdf.worker). + // Without this, stamps without NoRotate composite onto the main canvas + // and deleting the "imported" overlay leaves an unclickable pixel + // behind. Other markup types still gate on noRotate; for those the + // overlay stays a transparent click-box (delete is UI-only until save). + isEditing: true, }); currentRenderTask = renderTask; @@ -3251,8 +3362,15 @@ async function renderPage() { commentManager: null, } as any); // eslint-disable-next-line @typescript-eslint/no-explicit-any + // Only feed Widgets (form fields) here. Markup annotations are + // owned by #annotation-layer; letting AnnotationLayer create + //
elements for them in #form-layer adds invisible + // pointer-events:auto boxes that steal clicks from our overlays. + const widgetAnns = annotations.filter( + (a: { subtype?: string }) => a.subtype === "Widget", + ); await annotationLayer.render({ - annotations, + annotations: widgetAnns, div: formLayerEl, page, viewport, @@ -4313,8 +4431,9 @@ async function reloadPdf(): Promise { await renderPage(); await loadBaselineAnnotations(document); - await buildFieldNameMap(document); + const seeded = await buildFieldNameMap(document); syncFormValuesToStorage(); + if (seeded) await renderPage(); updateAnnotationsBadge(); renderAnnotationPanel(); @@ -4539,9 +4658,16 @@ app.ontoolresult = async (result: CallToolResult) => { restoreAnnotations(); // Build field name → annotation ID mapping for form filling - await buildFieldNameMap(document); + const seeded = await buildFieldNameMap(document); // Pre-populate annotationStorage from restored formFieldValues syncFormValuesToStorage(); + // buildFieldNameMap may have pushed AcroForm-tree values into storage + // (when the page widget's /V is stale vs the field dict — pdf-lib's save + // can leave them split). The first renderPage above ran BEFORE that, so + // the form layer shows the stale widget value. Re-render so it picks up + // storage. Only when something was actually seeded — most PDFs don't hit + // this and the extra render would be pure waste. + if (seeded) await renderPage(); updateAnnotationsBadge(); // Save button visibility driven by setDirty()/updateSaveBtn(); diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index 6ceb346..5b0905d 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -10,6 +10,7 @@ import { defaultColor, importPdfjsAnnotation, buildAnnotatedPdfBytes, + parseAnnotationRef, base64ToUint8Array, uint8ArrayToBase64, convertFromModelCoords, @@ -549,6 +550,76 @@ describe("importPdfjsAnnotation", () => { expect(result!.page).toBe(2); }); + it("imports an unsupported subtype as 'imported' (placement only)", () => { + // annotationType 15 = Ink, not in PDFJS_TYPE_MAP. We keep it as a + // placement-only "imported" record so it's listed in the panel and + // rendered from annotationCanvasMap instead of being dropped. + const ann = { + annotationType: 15, + subtype: "Ink", + id: "200R", + rect: [100, 200, 180, 260], + }; + const result = importPdfjsAnnotation(ann, 3, 0); + expect(result).not.toBeNull(); + expect(result!.type).toBe("imported"); + expect(result!.page).toBe(3); + expect((result as any).pdfjsId).toBe("200R"); + expect((result as any).subtype).toBe("Ink"); + expect((result as any).width).toBeCloseTo(80); + expect((result as any).height).toBeCloseTo(60); + }); + + it("imports an appearance-stream stamp as 'imported' (not text-label)", () => { + // A Stamp with hasAppearance carries a custom visual (e.g. an image + // signature) that our text-label StampAnnotation can't reproduce. + const ann = { + annotationType: 13, + subtype: "Stamp", + id: "118R", + rect: [420, 760, 514, 792], + hasAppearance: true, + contentsObj: { str: "DRAFT" }, + }; + const result = importPdfjsAnnotation(ann, 1, 0); + expect(result!.type).toBe("imported"); + expect((result as any).pdfjsId).toBe("118R"); + expect((result as any).subtype).toBe("Stamp"); + }); + + it("computeDiff: 'imported' present in both baseline and current → no diff", () => { + const imp: PdfAnnotationDef = { + type: "imported", + id: "pdf-118R", + page: 1, + x: 420, + y: 760, + width: 94, + height: 32, + pdfjsId: "118R", + subtype: "Stamp", + }; + const diff = computeDiff([imp], [imp], new Map()); + expect(diff.added).toHaveLength(0); + expect(diff.removed).toHaveLength(0); + }); + + it("computeDiff: deleting an 'imported' annotation lists it in removed", () => { + const imp: PdfAnnotationDef = { + type: "imported", + id: "pdf-118R", + page: 1, + x: 420, + y: 760, + width: 94, + height: 32, + pdfjsId: "118R", + subtype: "Stamp", + }; + const diff = computeDiff([imp], [], new Map()); + expect(diff.removed).toEqual(["pdf-118R"]); + }); + it("imports a note (Text) annotation", () => { const ann = { annotationType: 1, @@ -718,6 +789,29 @@ describe("base64 helpers", () => { // PDF Annotation Dict Creation (integration test with pdf-lib) // ============================================================================= +describe("parseAnnotationRef", () => { + it("parses pdf-- ids", () => { + expect(parseAnnotationRef("pdf-118-0")).toEqual({ + objectNumber: 118, + generationNumber: 0, + }); + expect(parseAnnotationRef("pdf-5-2")).toEqual({ + objectNumber: 5, + generationNumber: 2, + }); + }); + it("parses pdf-R ids (pdf.js string id, gen=0)", () => { + expect(parseAnnotationRef("pdf-118R")).toEqual({ + objectNumber: 118, + generationNumber: 0, + }); + }); + it("returns null for page-index fallback ids", () => { + expect(parseAnnotationRef("pdf-1-idx-3")).toBeNull(); + expect(parseAnnotationRef("user-abc")).toBeNull(); + }); +}); + describe("buildAnnotatedPdfBytes", () => { let blankPdfBytes: Uint8Array; @@ -738,6 +832,53 @@ describe("buildAnnotatedPdfBytes", () => { expect(header).toBe("%PDF-"); }); + it("strips removedRefs entries from each page's /Annots array", async () => { + // Seed: add two highlights, save, capture their object refs. + const seeded = await buildAnnotatedPdfBytes( + blankPdfBytes, + [ + { + type: "highlight", + id: "h1", + page: 1, + rects: [{ x: 72, y: 700, width: 100, height: 12 }], + color: "#ffff00", + }, + { + type: "highlight", + id: "h2", + page: 1, + rects: [{ x: 72, y: 680, width: 100, height: 12 }], + color: "#ffff00", + }, + ], + new Map(), + ); + const seededDoc = await PDFDocument.load(seeded); + const annots = seededDoc.getPage(0).node.Annots()!; + expect(annots.size()).toBe(2); + const ref0 = annots.get(0) as unknown as { + objectNumber: number; + generationNumber: number; + }; + + // Now remove the first one by ref. + const stripped = await buildAnnotatedPdfBytes(seeded, [], new Map(), [ + { objectNumber: ref0.objectNumber, generationNumber: ref0.generationNumber }, + ]); + const strippedDoc = await PDFDocument.load(stripped); + const remaining = strippedDoc.getPage(0).node.Annots(); + expect(remaining?.size() ?? 0).toBe(1); + }); + + it("removedRefs ignores refs not present in /Annots", async () => { + const out = await buildAnnotatedPdfBytes(blankPdfBytes, [], new Map(), [ + { objectNumber: 9999, generationNumber: 0 }, + ]); + const doc = await PDFDocument.load(out); + expect(doc.getPage(0).node.Annots()?.size() ?? 0).toBe(0); + }); + it("adds highlight annotation to PDF", async () => { const annotations: PdfAnnotationDef[] = [ { diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts index 8541706..79e9901 100644 --- a/examples/pdf-server/src/pdf-annotations.ts +++ b/examples/pdf-server/src/pdf-annotations.ts @@ -130,6 +130,32 @@ export interface ImageAnnotation extends AnnotationBase { aspect?: "preserve" | "ignore"; } +/** + * An annotation that already exists in the loaded PDF and that we render + * verbatim from its appearance stream (via pdf.js's annotationCanvasMap) + * rather than re-modeling it with one of our shape types. + * + * Covers two cases: + * - subtypes we don't model (Ink, Polygon, Caret, FileAttachment, …) + * - subtypes we *could* model but whose appearance carries information + * our model would drop (e.g. Stamp with an image signature) + * + * The rasterized canvas is supplied at render time by mcp-app.ts; this + * struct only carries placement and identity. Coords are PDF user-space + * (origin bottom-left), matching the other rect-shaped types. + */ +export interface ImportedAnnotation extends AnnotationBase { + type: "imported"; + x: number; + y: number; + width: number; + height: number; + /** pdf.js getAnnotations() id (e.g. "118R") — key into annotationCanvasMap. */ + pdfjsId: string; + /** Original PDF /Subtype (e.g. "Stamp", "Ink") for the panel label. */ + subtype: string; +} + export type PdfAnnotationDef = | HighlightAnnotation | UnderlineAnnotation @@ -140,7 +166,8 @@ export type PdfAnnotationDef = | LineAnnotation | FreetextAnnotation | StampAnnotation - | ImageAnnotation; + | ImageAnnotation + | ImportedAnnotation; // ============================================================================= // Coordinate Conversion (model ↔ internal PDF coords) @@ -174,6 +201,7 @@ export function convertFromModelCoords( case "rectangle": case "circle": case "image": + case "imported": return { ...def, y: pageHeight - def.y - def.height }; case "line": return { @@ -374,6 +402,7 @@ export function defaultColor(type: PdfAnnotationDef["type"]): string { case "stamp": return "#cc0000"; case "image": + case "imported": return "#00000000"; } } @@ -495,6 +524,11 @@ export async function addAnnotationDicts( const pages = pdfDoc.getPages(); for (const def of annotations) { + // "imported" annotations are already in the source PDF — never + // re-serialize them. Deletion is handled by buildAnnotatedPdfBytes' + // removedRefs strip; moving an imported annotation is still UI-only. + if (def.type === "imported") continue; + const pageIdx = def.page - 1; if (pageIdx < 0 || pageIdx >= pages.length) continue; const page = pages[pageIdx]; @@ -836,17 +870,62 @@ function setButtonGroupValue( } } +/** + * Recover the PDF object reference from an annotation id assigned by + * `makeAnnotationId`. Handles both `pdf--` (from `ann.ref`) and + * `pdf-R` (from pdf.js's string `ann.id`, gen always 0). Returns null for + * page-index fallback ids — those have no stable ref to remove by. + */ +export function parseAnnotationRef( + id: string, +): { objectNumber: number; generationNumber: number } | null { + let m = /^pdf-(\d+)-(\d+)$/.exec(id); + if (m) return { objectNumber: +m[1], generationNumber: +m[2] }; + m = /^pdf-(\d+)R$/.exec(id); + if (m) return { objectNumber: +m[1], generationNumber: 0 }; + return null; +} + /** * Build annotated PDF bytes from the original document. - * Applies user annotations and form fills, returns Uint8Array of the new PDF. + * Applies user annotations and form fills, removes baseline annotations the + * user deleted, returns Uint8Array of the new PDF. */ export async function buildAnnotatedPdfBytes( pdfBytes: Uint8Array, annotations: PdfAnnotationDef[], formFields: Map, + removedRefs: { objectNumber: number; generationNumber: number }[] = [], ): Promise { const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true }); + // Strip baseline annotations the user deleted. We match on object number + + // generation against each page's /Annots array entries (PDFRef). We don't + // know which page they were on, so scan every page — /Annots arrays are + // small and this only runs at save time. + if (removedRefs.length > 0) { + const wanted = new Set( + removedRefs.map((r) => `${r.objectNumber} ${r.generationNumber}`), + ); + for (const page of pdfDoc.getPages()) { + const annots = page.node.Annots(); + if (!annots) continue; + // Walk backwards so .remove(idx) doesn't shift unprocessed entries. + for (let i = annots.size() - 1; i >= 0; i--) { + const ref = annots.get(i) as { + objectNumber?: number; + generationNumber?: number; + }; + if ( + ref?.objectNumber !== undefined && + wanted.has(`${ref.objectNumber} ${ref.generationNumber ?? 0}`) + ) { + annots.remove(i); + } + } + } + } + // Add proper PDF annotation objects await addAnnotationDicts(pdfDoc, annotations); @@ -1023,14 +1102,44 @@ export function importPdfjsAnnotation( pageNum: number, index: number, ): PdfAnnotationDef | null { - const ourType = PDFJS_TYPE_MAP[ann.annotationType]; - if (!ourType) return null; - - // Skip form widgets (they're handled separately by AnnotationLayer) - if (ann.annotationType === 20) return null; + // Skip form widgets (they're handled separately by AnnotationLayer) and + // auxiliary types that aren't user-visible markup: + // 2 = Link (navigational, AnnotationLayer handles the click target) + // 16 = Popup (the speech-bubble UI for a parent annotation, not content) + if ( + ann.annotationType === 20 || + ann.annotationType === 2 || + ann.annotationType === 16 + ) { + return null; + } const id = makeAnnotationId(ann, pageNum, index); const color = pdfjsColorToHex(ann.color); + const ourType = PDFJS_TYPE_MAP[ann.annotationType]; + + // Anything we don't model — and stamps whose visual is an appearance + // stream we can't reproduce as a text label — are kept as "imported": + // a placement-only record that the renderer fills with the rasterized + // appearance from pdf.js's annotationCanvasMap. This keeps Ink, Polygon, + // image-signature stamps, etc. visible AND selectable in our layer. + const importAsBitmap = + !ourType || (ourType === "stamp" && ann.hasAppearance); + if (importAsBitmap) { + if (!ann.rect) return null; + const r = pdfjsRectToRect(ann.rect); + return { + type: "imported", + id, + page: pageNum, + x: r.x, + y: r.y, + width: r.width, + height: r.height, + pdfjsId: String(ann.id ?? ""), + subtype: String(ann.subtype ?? `type${ann.annotationType}`), + }; + } switch (ourType) { case "highlight": From 621a70a7242bf9c76dcb89a61d785ab9bb75cd87 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 2 Apr 2026 10:09:49 +0100 Subject: [PATCH 18/97] 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] --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- .../pdf-server/src/pdf-annotations.test.ts | 5 +- examples/pdf-server/src/pdf-annotations.ts | 3 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 54 +++++++++---------- package.json | 2 +- 29 files changed, 58 insertions(+), 56 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index b29f04a..c2fc987 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index a6c0343..12e1365 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index f75d662..a7153e1 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index f193fa1..0606408 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 90dd6c6..3b54309 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index e97eb68..dc7c333 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index ad8c119..c479adf 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 71b7ace..bd8865f 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index d753706..c92105a 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index ade9dae..b753250 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 15dc903..9f2ba83 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 5686a02..807ce2d 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.4.0", + "version": "1.5.0", "private": true, "type": "module", "scripts": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index a8d727b..52c6d80 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index cdcda33..fc08d5e 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index 5b0905d..38ef27b 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -864,7 +864,10 @@ describe("buildAnnotatedPdfBytes", () => { // Now remove the first one by ref. const stripped = await buildAnnotatedPdfBytes(seeded, [], new Map(), [ - { objectNumber: ref0.objectNumber, generationNumber: ref0.generationNumber }, + { + objectNumber: ref0.objectNumber, + generationNumber: ref0.generationNumber, + }, ]); const strippedDoc = await PDFDocument.load(stripped); const remaining = strippedDoc.getPage(0).node.Annots(); diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts index 79e9901..73b9b8d 100644 --- a/examples/pdf-server/src/pdf-annotations.ts +++ b/examples/pdf-server/src/pdf-annotations.ts @@ -1123,8 +1123,7 @@ export function importPdfjsAnnotation( // a placement-only record that the renderer fills with the rasterized // appearance from pdf.js's annotationCanvasMap. This keeps Ink, Polygon, // image-signature stamps, etc. visible AND selectable in our layer. - const importAsBitmap = - !ourType || (ourType === "stamp" && ann.hasAppearance); + const importAsBitmap = !ourType || (ourType === "stamp" && ann.hasAppearance); if (importAsBitmap) { if (!ann.rect) return null; const r = pdfjsRectToRect(ann.rect); diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index 3ecd3d5..a7813e3 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.4.0", + "version": "1.5.0", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index e94baa2..8451e20 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index bcb5a95..2e90491 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.4.0", + "version": "1.5.0", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 1e0a31a..e00afc0 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 6af2dd7..03bcb96 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 7e59e5b..3706c25 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index c0d1dcf..c80b9ad 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index d148669..d4b8192 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 462c4a1..3417801 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 16566ac..fb01e31 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 1bfc981..605c39a 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.4.0", + "version": "1.5.0", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index 54d7d69..b63a8e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "workspaces": [ "examples/*" @@ -63,7 +63,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.4.0", + "version": "1.5.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -106,7 +106,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -150,7 +150,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -197,7 +197,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -241,7 +241,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -285,7 +285,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -327,7 +327,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -371,7 +371,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -414,7 +414,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -461,7 +461,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -504,7 +504,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -545,7 +545,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.4.0", + "version": "1.5.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -590,7 +590,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -632,7 +632,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -676,7 +676,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.4.0", + "version": "1.5.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -684,7 +684,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -716,7 +716,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -725,7 +725,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -773,7 +773,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -815,7 +815,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -858,7 +858,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -928,7 +928,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -977,7 +977,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1020,7 +1020,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1062,7 +1062,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/package.json b/package.json index 7b92e3c..3e040d1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From 4f4dd8b6f03d53041a598c23f5f5705d8530791f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 9 Apr 2026 13:51:40 +0100 Subject: [PATCH 19/97] 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. --- tests/e2e/pdf-viewer-zoom.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/e2e/pdf-viewer-zoom.spec.ts b/tests/e2e/pdf-viewer-zoom.spec.ts index 6258faa..574809b 100644 --- a/tests/e2e/pdf-viewer-zoom.spec.ts +++ b/tests/e2e/pdf-viewer-zoom.spec.ts @@ -63,8 +63,13 @@ test.describe("PDF Viewer — fullscreen fit + pinch zoom", () => { await waitForPdfRender(page); const app = getAppFrame(page); + // Initial fit-to-width measures container.clientWidth immediately after + // showViewer() flips display:flex; under CI load the reflow can lag, so + // poll until the shrink-to-fit scale has actually applied. + await expect + .poll(() => readZoomPercent(page), { timeout: 5000 }) + .toBeLessThan(100); const inlineZoom = await readZoomPercent(page); - expect(inlineZoom).toBeLessThan(100); // Widen + give plenty of height, then enter fullscreen. Fullscreen uses // fit-to-PAGE (whole page visible), so the resulting zoom is whichever From c59c6060cddd48a8a3b04672767a9c583aa52141 Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 14 Apr 2026 05:06:22 -0700 Subject: [PATCH 20/97] 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. --- src/app.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index f5f97b3..4f429e8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -860,7 +860,14 @@ export class App extends ProtocolWithEvents< return await this.request( { method: "tools/call", params }, CallToolResultSchema, - options, + { + // Hosts may interpose long-running or user-interactive steps before the + // tool result arrives. Opting in here lets a host heartbeat keep the + // request alive past the default timeout; callers can still override. + onprogress: () => {}, + resetTimeoutOnProgress: true, + ...options, + }, ); } From 01d826aea000a2f774d1d3e9b57fa352632c1c50 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Apr 2026 13:58:12 +0100 Subject: [PATCH 21/97] chore: bump ext-apps to 1.6.0 (#613) --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 54 +++++++++---------- package.json | 2 +- 27 files changed, 53 insertions(+), 53 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index c2fc987..0153242 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 12e1365..9b79e20 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index a7153e1..b53c2bd 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 0606408..efb64d4 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 3b54309..238efe8 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index dc7c333..f89331c 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index c479adf..8423293 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index bd8865f..d2b1221 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index c92105a..08c7048 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index b753250..16d3322 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 9f2ba83..6c3f52a 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 807ce2d..fb8cc99 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.5.0", + "version": "1.6.0", "private": true, "type": "module", "scripts": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 52c6d80..e6c2124 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index fc08d5e..0180f7b 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index a7813e3..a71f39f 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.5.0", + "version": "1.6.0", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 8451e20..31c4d6f 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 2e90491..c9c268d 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.5.0", + "version": "1.6.0", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index e00afc0..b7fbcef 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 03bcb96..6e5899a 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 3706c25..756ed33 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index c80b9ad..5f9d333 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index d4b8192..50ff990 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 3417801..0660aa4 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index fb01e31..88f3b97 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 605c39a..a2aea7a 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index b63a8e5..1e689bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "workspaces": [ "examples/*" @@ -63,7 +63,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -106,7 +106,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -150,7 +150,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -197,7 +197,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -241,7 +241,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -285,7 +285,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -327,7 +327,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -371,7 +371,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -414,7 +414,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -461,7 +461,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -504,7 +504,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -545,7 +545,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -590,7 +590,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -632,7 +632,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -676,7 +676,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -684,7 +684,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -716,7 +716,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -725,7 +725,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -773,7 +773,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -815,7 +815,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -858,7 +858,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -928,7 +928,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -977,7 +977,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1020,7 +1020,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1062,7 +1062,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/package.json b/package.json index 3e040d1..34b2c6a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From 739c0e049d7b644ba96e12888598c1e8c70da7ea Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 11:28:46 +0100 Subject: [PATCH 22/97] 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. --- .husky/pre-commit | 5 +++-- scripts/link-self.mjs | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index b0d5ff5..ae5b3c7 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -15,8 +15,9 @@ if grep -E '"resolved": "https?://' package-lock.json | grep -v registry.npmjs.o fi # Capture staged files so we only re-stage what the user intended to commit -# (avoids sweeping unrelated WIP into the commit) -STAGED=$(git diff --name-only --cached) +# (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 diff --git a/scripts/link-self.mjs b/scripts/link-self.mjs index 41051bf..c1f79cc 100644 --- a/scripts/link-self.mjs +++ b/scripts/link-self.mjs @@ -5,10 +5,12 @@ * so examples always type-check against the latest local types. * See: https://github.com/npm/feedback/discussions/774 */ -import { cpSync, existsSync } from "fs"; +import { cpSync, existsSync, lstatSync } from "fs"; const target = "node_modules/@modelcontextprotocol/ext-apps"; if (!existsSync(target)) process.exit(0); +// If target is a symlink (e.g. to the repo root), dist is already current. +if (lstatSync(target).isSymbolicLink()) process.exit(0); cpSync("dist", `${target}/dist`, { recursive: true }); cpSync("package.json", `${target}/package.json`); From 94c94216d48737982c0ef560020896e31336db23 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 11:45:14 +0100 Subject: [PATCH 23/97] feat(app,app-bridge): guard requests sent before handshake completes (#620) (#623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/app-bridge.test.ts | 98 +++++++++++++++++++++++++++++++++++++++++- src/app-bridge.ts | 37 ++++++++++++++++ src/app.ts | 51 +++++++++++++++++++++- src/react/useApp.tsx | 21 ++++++--- typedoc.config.mjs | 2 +- 5 files changed, 200 insertions(+), 9 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 4761d76..e2e471b 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js"; @@ -780,6 +780,102 @@ describe("App <-> AppBridge integration", () => { ); }); + describe("pre-handshake guard (claude-ai-mcp#149)", () => { + const guardMsg = + /called before connect\(\) completed the ui\/initialize handshake/; + + it("callServerTool warns when called before connect() completes", async () => { + const errSpy = spyOn(console, "error").mockImplementation(() => {}); + bridge.oncalltool = async () => ({ content: [] }); + await bridge.connect(bridgeTransport); + + const connecting = app.connect(appTransport); + // Handshake is in flight; _initializedSent is still false. + await app.callServerTool({ name: "t", arguments: {} }).catch(() => {}); + + expect(errSpy).toHaveBeenCalledTimes(1); + expect(errSpy.mock.calls[0][0]).toMatch(guardMsg); + expect(errSpy.mock.calls[0][0]).toContain("App.callServerTool()"); + + await connecting; + errSpy.mockRestore(); + }); + + it("callServerTool does not warn after connect() resolves", async () => { + const errSpy = spyOn(console, "error").mockImplementation(() => {}); + bridge.oncalltool = async () => ({ content: [] }); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + await app.callServerTool({ name: "t", arguments: {} }); + + expect(errSpy).not.toHaveBeenCalled(); + errSpy.mockRestore(); + }); + + it("sendMessage and readServerResource also warn before handshake", async () => { + const errSpy = spyOn(console, "error").mockImplementation(() => {}); + + await app.sendMessage({ role: "user", content: [] }).catch(() => {}); + await app.readServerResource({ uri: "test://r" }).catch(() => {}); + + expect(errSpy).toHaveBeenCalledTimes(2); + expect(errSpy.mock.calls[0][0]).toContain("App.sendMessage()"); + expect(errSpy.mock.calls[1][0]).toContain("App.readServerResource()"); + errSpy.mockRestore(); + }); + + it("throws instead of warning when strict: true", async () => { + const errSpy = spyOn(console, "error").mockImplementation(() => {}); + const strictApp = new App( + testAppInfo, + {}, + { autoResize: false, strict: true }, + ); + + await expect( + strictApp.callServerTool({ name: "t", arguments: {} }), + ).rejects.toThrow(guardMsg); + expect(errSpy).not.toHaveBeenCalled(); + + errSpy.mockRestore(); + }); + + it("AppBridge warns on tools/call from a View that skipped the handshake", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + bridge.oncalltool = async () => ({ content: [] }); + await bridge.connect(bridgeTransport); + + // Simulate a hand-rolled View (no SDK, no handshake) sending tools/call. + await appTransport.start(); + appTransport.send({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "t", arguments: {} }, + }); + await flush(); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toContain( + "received 'tools/call' before ui/notifications/initialized", + ); + warnSpy.mockRestore(); + }); + + it("AppBridge does not warn after initialized is received", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + bridge.oncalltool = async () => ({ content: [] }); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + await app.callServerTool({ name: "t", arguments: {} }); + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + }); + it("onlistresources setter registers handler for resources/list requests", async () => { const requestParams = {}; const resources = [{ uri: "test://resource", name: "Test" }]; diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 7712587..3c61618 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -39,6 +39,7 @@ import { ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { + Protocol, ProtocolOptions, RequestOptions, } from "@modelcontextprotocol/sdk/shared/protocol.js"; @@ -307,6 +308,37 @@ export class AppBridge extends ProtocolWithEvents< private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; + private _initializedReceived = false; + + /** + * Wrap every handler registered via `replaceRequestHandler` with a check + * that the View has sent `ui/notifications/initialized`. Warns (never + * throws) so lenient hosts keep working while still surfacing the + * misordering that leaves strict hosts with a permanently hidden iframe. + * `ui/initialize` and `ping` use `setRequestHandler` directly and are + * intentionally exempt. + * + * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} + */ + private _baseReplaceRequestHandler = this.replaceRequestHandler; + protected override replaceRequestHandler: Protocol< + AppRequest, + AppNotification, + AppResult + >["setRequestHandler"] = (schema, handler) => { + this._baseReplaceRequestHandler(schema, (request, extra) => { + if (!this._initializedReceived) { + console.warn( + `[ext-apps] AppBridge received '${request.method}' before ` + + `ui/notifications/initialized. The View is calling host ` + + `methods before completing the handshake; it should await ` + + `app.connect() first. See ` + + `https://github.com/anthropics/claude-ai-mcp/issues/149`, + ); + } + return handler(request, extra); + }); + }; protected readonly eventSchemas = { sizechange: McpUiSizeChangedNotificationSchema, @@ -357,6 +389,10 @@ export class AppBridge extends ProtocolWithEvents< ) { super(options); + this.addEventListener("initialized", () => { + this._initializedReceived = true; + }); + this._hostContext = options?.hostContext || {}; this.setRequestHandler(McpUiInitializeRequestSchema, (request) => @@ -1758,6 +1794,7 @@ export class AppBridge extends ProtocolWithEvents< "AppBridge is already connected. Call close() before connecting again.", ); } + this._initializedReceived = false; if (this._client) { // When a client was passed to the constructor, automatically forward // MCP requests/notifications between the view and the server diff --git a/src/app.ts b/src/app.ts index 4f429e8..fa48440 100644 --- a/src/app.ts +++ b/src/app.ts @@ -144,7 +144,7 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; * * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options */ -type AppOptions = ProtocolOptions & { +export type AppOptions = ProtocolOptions & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -155,6 +155,19 @@ type AppOptions = ProtocolOptions & { * @default true */ autoResize?: boolean; + /** + * Throw on detected misuse instead of logging a console error. + * + * Currently this affects calling host-bound methods (e.g. + * {@link App.callServerTool `callServerTool`}, {@link App.sendMessage `sendMessage`}) + * before {@link App.connect `connect`} has completed the `ui/initialize` + * handshake. With `strict: false` (default) a `console.error` is emitted; + * with `strict: true` an `Error` is thrown. + * + * @remarks Throwing will become the default in a future release. + * @default false + */ + strict?: boolean; }; type RequestHandlerExtra = Parameters< @@ -244,6 +257,32 @@ export class App extends ProtocolWithEvents< private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; + private _initializedSent = false; + + /** + * Warn if a host-bound method is called before {@link connect `connect`} has + * completed the `ui/initialize` → `ui/notifications/initialized` handshake. + * + * Calling these methods early can race the handshake on strict hosts and + * leave the iframe permanently hidden. See + * {@link https://github.com/anthropics/claude-ai-mcp/issues/61 claude-ai-mcp#61} / + * {@link https://github.com/anthropics/claude-ai-mcp/issues/149 #149}. + * + * @remarks This will become a thrown `Error` in a future minor release. + */ + private _assertInitialized(method: string): void { + if (this._initializedSent) return; + const msg = + `[ext-apps] App.${method}() called before connect() completed the ` + + `ui/initialize handshake. Await app.connect() before calling this ` + + `method, or move data loading to an ontoolresult handler. ` + + `See https://github.com/anthropics/claude-ai-mcp/issues/149`; + if (this.options?.strict) { + throw new Error(msg); + } + // TODO(next-minor): make `strict: true` the default. + console.error(`${msg}. This will throw in a future release.`); + } protected readonly eventSchemas = { toolinput: McpUiToolInputNotificationSchema, @@ -851,6 +890,7 @@ export class App extends ProtocolWithEvents< params: CallToolRequest["params"], options?: RequestOptions, ): Promise { + this._assertInitialized("callServerTool"); if (typeof params === "string") { throw new Error( `callServerTool() expects an object as its first argument, but received a string ("${params}"). ` + @@ -915,6 +955,7 @@ export class App extends ProtocolWithEvents< params: ReadResourceRequest["params"], options?: RequestOptions, ): Promise { + this._assertInitialized("readServerResource"); return await this.request( { method: "resources/read", params }, ReadResourceResultSchema, @@ -962,6 +1003,7 @@ export class App extends ProtocolWithEvents< params?: ListResourcesRequest["params"], options?: RequestOptions, ): Promise { + this._assertInitialized("listServerResources"); return await this.request( { method: "resources/list", params }, ListResourcesResultSchema, @@ -1020,6 +1062,7 @@ export class App extends ProtocolWithEvents< * @see {@link McpUiMessageRequest `McpUiMessageRequest`} for request structure */ sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) { + this._assertInitialized("sendMessage"); return this.request( { method: "ui/message", @@ -1113,6 +1156,7 @@ export class App extends ProtocolWithEvents< params: McpUiUpdateModelContextRequest["params"], options?: RequestOptions, ) { + this._assertInitialized("updateModelContext"); return this.request( { method: "ui/update-model-context", @@ -1149,6 +1193,7 @@ export class App extends ProtocolWithEvents< * @see {@link McpUiOpenLinkResult `McpUiOpenLinkResult`} for result structure */ openLink(params: McpUiOpenLinkRequest["params"], options?: RequestOptions) { + this._assertInitialized("openLink"); return this.request( { method: "ui/open-link", @@ -1229,6 +1274,7 @@ export class App extends ProtocolWithEvents< params: McpUiDownloadFileRequest["params"], options?: RequestOptions, ) { + this._assertInitialized("downloadFile"); return this.request( { method: "ui/download-file", @@ -1313,6 +1359,7 @@ export class App extends ProtocolWithEvents< params: McpUiRequestDisplayModeRequest["params"], options?: RequestOptions, ) { + this._assertInitialized("requestDisplayMode"); return this.request( { method: "ui/request-display-mode", @@ -1475,6 +1522,7 @@ export class App extends ProtocolWithEvents< "App is already connected. Call close() before connecting again.", ); } + this._initializedSent = false; await super.connect(transport); try { @@ -1502,6 +1550,7 @@ export class App extends ProtocolWithEvents< await this.notification({ method: "ui/notifications/initialized", }); + this._initializedSent = true; if (this.options?.autoResize) { this.setupSizeChangedNotifications(); diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index d6dcfbb..75b3e15 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -1,20 +1,25 @@ import { useEffect, useState } from "react"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; import { Client } from "@modelcontextprotocol/sdk/client"; -import { App, McpUiAppCapabilities, PostMessageTransport } from "../app"; +import { + App, + AppOptions, + McpUiAppCapabilities, + PostMessageTransport, +} from "../app"; export * from "../app"; /** * Options for configuring the {@link useApp `useApp`} hook. * - * Note: This interface does NOT expose {@link App `App`} options like `autoResize`. - * The hook creates the `App` with default options (`autoResize: true`). If you - * need custom `App` options, create the `App` manually instead of using this hook. + * The `strict` option is forwarded to the underlying {@link App `App`} + * instance. For other {@link AppOptions `AppOptions`}, create the `App` + * manually instead of using this hook. * * @see {@link useApp `useApp`} for the hook that uses these options * @see {@link useAutoResize `useAutoResize`} for manual auto-resize control with custom `App` options */ -export interface UseAppOptions { +export interface UseAppOptions extends Pick { /** App identification (name and version) */ appInfo: Implementation; /** @@ -121,6 +126,7 @@ export function useApp({ appInfo, capabilities, onAppCreated, + strict, }: UseAppOptions): AppState { const [app, setApp] = useState(null); const [isConnected, setIsConnected] = useState(false); @@ -135,7 +141,10 @@ export function useApp({ window.parent, window.parent, ); - const app = new App(appInfo, capabilities); + const app = new App(appInfo, capabilities, { + autoResize: true, + strict, + }); // Register handlers BEFORE connecting onAppCreated?.(app); diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 5775cae..793737c 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -28,7 +28,7 @@ const config = { ], excludePrivate: true, excludeInternal: false, - intentionallyNotExported: ["AppOptions", "MethodSchema"], + intentionallyNotExported: ["MethodSchema"], blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false, From 4f89a6f8ea18bc807c6a24819c76f15397cc6b0a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 11:51:04 +0100 Subject: [PATCH 24/97] 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. --- src/react/useApp.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 75b3e15..518d6ca 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -12,14 +12,17 @@ export * from "../app"; /** * Options for configuring the {@link useApp `useApp`} hook. * - * The `strict` option is forwarded to the underlying {@link App `App`} - * instance. For other {@link AppOptions `AppOptions`}, create the `App` - * manually instead of using this hook. + * The `autoResize` and `strict` options are forwarded to the underlying + * {@link App `App`} instance. For other {@link AppOptions `AppOptions`}, + * create the `App` manually instead of using this hook. * * @see {@link useApp `useApp`} for the hook that uses these options * @see {@link useAutoResize `useAutoResize`} for manual auto-resize control with custom `App` options */ -export interface UseAppOptions extends Pick { +export interface UseAppOptions extends Pick< + AppOptions, + "autoResize" | "strict" +> { /** App identification (name and version) */ appInfo: Implementation; /** @@ -126,6 +129,7 @@ export function useApp({ appInfo, capabilities, onAppCreated, + autoResize = true, strict, }: UseAppOptions): AppState { const [app, setApp] = useState(null); @@ -141,10 +145,7 @@ export function useApp({ window.parent, window.parent, ); - const app = new App(appInfo, capabilities, { - autoResize: true, - strict, - }); + const app = new App(appInfo, capabilities, { autoResize, strict }); // Register handlers BEFORE connecting onAppCreated?.(app); From 926dcb4fe9e1526ba4910affc137d1560e25bd4b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 13:29:09 +0100 Subject: [PATCH 25/97] feat: MCP app sampling support via stock SDK types (alt to #477) (#530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- specification/draft/apps.mdx | 12 +++++ src/app-bridge.examples.ts | 22 ++++++++ src/app-bridge.test.ts | 37 ++++++++++++++ src/app-bridge.ts | 47 +++++++++++++++++ src/app.examples.ts | 48 +++++++++++++++++ src/app.ts | 99 +++++++++++++++++++++++++++++++++++- src/generated/schema.json | 26 ++++++++++ src/generated/schema.ts | 18 +++++++ src/spec.types.ts | 8 +++ src/types.ts | 7 +++ 10 files changed, 323 insertions(+), 1 deletion(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 6c810d6..aa364bc 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -516,6 +516,10 @@ UI iframes can use the following subset of standard MCP protocol messages: - `resources/read` - Read resource content +**Sampling:** + +- `sampling/createMessage` - Request an LLM completion from the host (uses the standard MCP [`CreateMessageRequest`](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) / `CreateMessageResult` types, including SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks). The host has full discretion over model selection and SHOULD apply rate limiting, cost controls, and user approval (human-in-the-loop). Apps MUST check `hostCapabilities.sampling` before sending this request, and `hostCapabilities.sampling.tools` before including `tools` in the request params. + **Notifications:** - `notifications/message` - Log messages to host @@ -662,6 +666,14 @@ interface HostCapabilities { }; /** Host accepts log messages. */ logging?: {}; + /** + * Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors MCP ClientCapabilities.sampling so hosts can pass it through. + */ + sampling?: { + /** Host supports tool use via `tools` and `toolChoice` params (SEP-1577). */ + tools?: {}; + }; /** Sandbox configuration applied by the host. */ sandbox?: { /** Permissions granted by the host (camera, microphone, geolocation, clipboard-write). */ diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index b613451..d5dc5bc 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -12,6 +12,8 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { CallToolResult, CallToolResultSchema, + CreateMessageRequest, + CreateMessageResult, ListResourcesResultSchema, ReadResourceResultSchema, ListPromptsResultSchema, @@ -228,6 +230,26 @@ function AppBridge_oncalltool_forwardToServer( //#endregion AppBridge_oncalltool_forwardToServer } +/** + * Example: Forward sampling requests to your LLM provider. + */ +function AppBridge_oncreatesamplingmessage_forwardToLlm( + bridge: AppBridge, + myLlmProvider: { + complete: ( + params: CreateMessageRequest["params"], + opts: { signal: AbortSignal }, + ) => Promise; + }, +) { + //#region AppBridge_oncreatesamplingmessage_forwardToLlm + bridge.oncreatesamplingmessage = async (params, extra) => { + // Apply rate limiting, user approval, cost controls here + return await myLlmProvider.complete(params, { signal: extra.signal }); + }; + //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm +} + /** * Example: Forward list resources requests to the MCP server. */ diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index e2e471b..287e0de 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -737,6 +737,43 @@ describe("App <-> AppBridge integration", () => { expect(result.content).toEqual(resultContent); }); + it("oncreatesamplingmessage setter registers handler for sampling/createMessage requests", async () => { + // Re-create bridge with sampling capability so App's capability check passes + bridge = new AppBridge(null, testHostInfo, { + ...testHostCapabilities, + sampling: { tools: {} }, + }); + + const receivedParams: unknown[] = []; + bridge.oncreatesamplingmessage = async (params) => { + receivedParams.push(params); + return { + role: "assistant", + content: { type: "text", text: "Hello from the model" }, + model: "test-model", + stopReason: "endTurn", + }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + expect(app.getHostCapabilities()?.sampling?.tools).toEqual({}); + + const result = await app.createSamplingMessage({ + messages: [{ role: "user", content: { type: "text", text: "Hi" } }], + maxTokens: 50, + }); + + expect(receivedParams).toHaveLength(1); + expect(receivedParams[0]).toMatchObject({ maxTokens: 50 }); + expect(result.model).toEqual("test-model"); + expect(result.content).toEqual({ + type: "text", + text: "Hello from the model", + }); + }); + it("ondownloadfile setter registers handler for ui/download-file requests", async () => { const downloadParams = { contents: [ diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 3c61618..4c8be2e 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -5,6 +5,10 @@ import { CallToolRequestSchema, CallToolResult, CallToolResultSchema, + CreateMessageRequest, + CreateMessageRequestSchema, + CreateMessageResult, + CreateMessageResultWithTools, EmptyResult, Implementation, ListPromptsRequest, @@ -1049,6 +1053,49 @@ export class AppBridge extends ProtocolWithEvents< ); } + /** + * Register a handler for LLM sampling requests from the view. + * + * The view sends standard MCP `sampling/createMessage` requests to obtain + * LLM completions via the host's model connection. The host has full + * discretion over which model to use and SHOULD apply rate limiting, + * cost controls, and user approval (human-in-the-loop) before sampling. + * + * Hosts that register this handler SHOULD advertise `sampling` (and + * `sampling.tools` if tool-calling is supported) in + * {@link McpUiHostCapabilities `McpUiHostCapabilities`}. + * + * @param callback - Handler that receives `CreateMessageRequest` params and + * returns a `CreateMessageResult` (or `CreateMessageResultWithTools` when + * `params.tools` was provided) + * - `params` - Standard MCP sampling params (messages, maxTokens, tools, etc.) + * - `extra` - Request metadata (abort signal, session info) + * + * @example Forward to your LLM provider + * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm" + * bridge.oncreatesamplingmessage = async (params, extra) => { + * // Apply rate limiting, user approval, cost controls here + * return await myLlmProvider.complete(params, { signal: extra.signal }); + * }; + * ``` + * + * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + */ + set oncreatesamplingmessage( + callback: ( + params: CreateMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + return callback(request.params, extra); + }, + ); + } + /** * Notify the view that the MCP server's tool list has changed. * diff --git a/src/app.examples.ts b/src/app.examples.ts index a0a5820..f6e8b56 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -334,6 +334,54 @@ async function App_callServerTool_fetchWeather(app: App) { //#endregion App_callServerTool_fetchWeather } +/** + * Example: Simple LLM completion via host sampling. + */ +async function App_createSamplingMessage_simple(app: App) { + //#region App_createSamplingMessage_simple + const result = await app.createSamplingMessage({ + messages: [ + { + role: "user", + content: { type: "text", text: "Summarize this in one line." }, + }, + ], + maxTokens: 100, + }); + console.log(result.content); + //#endregion App_createSamplingMessage_simple +} + +/** + * Example: Agentic loop with tools (requires host sampling.tools capability). + */ +async function App_createSamplingMessage_withTools( + app: App, + messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[], +) { + //#region App_createSamplingMessage_withTools + if (!app.getHostCapabilities()?.sampling?.tools) return; + + const result = await app.createSamplingMessage({ + messages, + maxTokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get the current weather", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + }, + }, + ], + }); + if (result.stopReason === "toolUse") { + // result.content may be an array containing tool_use blocks + } + //#endregion App_createSamplingMessage_withTools +} + /** * Example: Read a video resource and play it. */ diff --git a/src/app.ts b/src/app.ts index fa48440..473d345 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,6 +8,11 @@ import { CallToolRequestSchema, CallToolResult, CallToolResultSchema, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultSchema, + CreateMessageResultWithTools, + CreateMessageResultWithToolsSchema, EmptyResultSchema, Implementation, ListResourcesRequest, @@ -802,7 +807,15 @@ export class App extends ProtocolWithEvents< * @internal */ assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO + switch (method) { + case "sampling/createMessage": + if (!this._hostCapabilities?.sampling) { + throw new Error( + `Host does not support sampling (required for ${method})`, + ); + } + break; + } } /** @@ -1011,6 +1024,90 @@ export class App extends ProtocolWithEvents< ); } + /** + * Request an LLM completion from the host (standard MCP `sampling/createMessage`). + * + * Enables the app to use the host's model connection for completions. The host + * has full discretion over which model to select and MAY modify or reject the + * request (human-in-the-loop). Check {@link getHostCapabilities `getHostCapabilities`}`()?.sampling` + * before calling — hosts without this capability will reject the request. + * + * This method reuses the stock MCP `CreateMessageRequest` shape. When `params.tools` + * is provided, the result is parsed with the extended schema that permits + * `stopReason: "toolUse"` and array content containing `tool_use` blocks. + * + * @param params - Standard MCP `CreateMessageRequest` params (messages, maxTokens, + * systemPrompt, temperature, modelPreferences, tools, toolChoice, etc.) + * @param options - Request options (timeout, abort signal) + * @returns `CreateMessageResult` (single content block) or `CreateMessageResultWithTools` + * (array content, may include `tool_use` blocks) depending on whether `tools` was set + * + * @throws {Error} If the host rejects the request or does not support sampling + * @throws {Error} If the request times out or the connection is lost + * + * @example Simple completion + * ```ts source="./app.examples.ts#App_createSamplingMessage_simple" + * const result = await app.createSamplingMessage({ + * messages: [ + * { + * role: "user", + * content: { type: "text", text: "Summarize this in one line." }, + * }, + * ], + * maxTokens: 100, + * }); + * console.log(result.content); + * ``` + * + * @example Agentic loop with tools + * ```ts source="./app.examples.ts#App_createSamplingMessage_withTools" + * if (!app.getHostCapabilities()?.sampling?.tools) return; + * + * const result = await app.createSamplingMessage({ + * messages, + * maxTokens: 1024, + * tools: [ + * { + * name: "get_weather", + * description: "Get the current weather", + * inputSchema: { + * type: "object", + * properties: { city: { type: "string" } }, + * }, + * }, + * ], + * }); + * if (result.stopReason === "toolUse") { + * // result.content may be an array containing tool_use blocks + * } + * ``` + * + * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + */ + async createSamplingMessage( + params: CreateMessageRequest["params"] & { tools?: undefined }, + options?: RequestOptions, + ): Promise; + async createSamplingMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, + ): Promise; + async createSamplingMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, + ): Promise { + this._assertInitialized("createSamplingMessage"); + const resultSchema = params.tools + ? CreateMessageResultWithToolsSchema + : CreateMessageResultSchema; + return await this.request( + { method: "sampling/createMessage", params }, + resultSchema, + options, + ); + } + /** * Send a message to the host's chat interface. * diff --git a/src/generated/schema.json b/src/generated/schema.json index d66e557..a72ec2d 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -501,6 +501,19 @@ } }, "additionalProperties": false + }, + "sampling": { + "description": "Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.", + "type": "object", + "properties": { + "tools": { + "description": "Host supports tool use via `tools` and `toolChoice` parameters.", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2841,6 +2854,19 @@ } }, "additionalProperties": false + }, + "sampling": { + "description": "Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.", + "type": "object", + "properties": { + "tools": { + "description": "Host supports tool use via `tools` and `toolChoice` parameters.", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/src/generated/schema.ts b/src/generated/schema.ts index aed1f96..707e185 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -558,6 +558,24 @@ export const McpUiHostCapabilitiesSchema = z.object({ message: McpUiSupportedContentBlockModalitiesSchema.optional().describe( "Host supports receiving content messages (ui/message) from the view.", ), + /** + * @description Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through. + */ + sampling: z + .object({ + /** @description Host supports tool use via `tools` and `toolChoice` parameters. */ + tools: z + .object({}) + .optional() + .describe( + "Host supports tool use via `tools` and `toolChoice` parameters.", + ), + }) + .optional() + .describe( + "Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.", + ), }); /** diff --git a/src/spec.types.ts b/src/spec.types.ts index 9f83fa9..3678fb5 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -521,6 +521,14 @@ export interface McpUiHostCapabilities { updateModelContext?: McpUiSupportedContentBlockModalities; /** @description Host supports receiving content messages (ui/message) from the view. */ message?: McpUiSupportedContentBlockModalities; + /** + * @description Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through. + */ + sampling?: { + /** @description Host supports tool use via `tools` and `toolChoice` parameters. */ + tools?: {}; + }; } /** diff --git a/src/types.ts b/src/types.ts index 24bffec..7fc6b71 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,6 +140,9 @@ export { import { CallToolRequest, CallToolResult, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultWithTools, EmptyResult, ListPromptsRequest, ListPromptsResult, @@ -165,6 +168,7 @@ import { * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode) * - MCP server requests forwarded from the app (tools/call, tools/list, resources/list, * resources/templates/list, resources/read, prompts/list) + * - MCP client requests forwarded to the host (sampling/createMessage) * - Protocol requests (ping) */ export type AppRequest = @@ -181,6 +185,7 @@ export type AppRequest = | ListResourceTemplatesRequest | ReadResourceRequest | ListPromptsRequest + | CreateMessageRequest | PingRequest; /** @@ -230,4 +235,6 @@ export type AppResult = | ListResourceTemplatesResult | ReadResourceResult | ListPromptsResult + | CreateMessageResult + | CreateMessageResultWithTools | EmptyResult; From 3fb521b71f4065749b745af82d7321cda02e74fd Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 13:46:13 +0100 Subject: [PATCH 26/97] spec: Add tool registration for Apps, to be called by Host (WebMCP-style!) (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 * 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 - 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 --- .prettierignore | 1 + examples/budget-allocator-server/server.ts | 2 +- .../budget-allocator-server/src/mcp-app.ts | 290 +++++ examples/map-server/server.ts | 2 +- examples/map-server/src/mcp-app.ts | 220 +++- examples/pdf-server/server.ts | 2 +- examples/pdf-server/src/mcp-app.ts | 439 ++++++- examples/shadertoy-server/server.ts | 4 +- examples/shadertoy-server/src/mcp-app.ts | 196 ++- .../threejs-server/src/mcp-app-wrapper.tsx | 197 ++- examples/threejs-server/src/threejs-app.tsx | 17 +- examples/wiki-explorer-server/server.ts | 2 +- examples/wiki-explorer-server/src/mcp-app.ts | 341 +++++ package-lock.json | 9 + package.json | 3 + specification/draft/apps.mdx | 748 ++++++++++- src/app-bridge.test.ts | 1097 ++++++++++++++++- src/app.examples.ts | 18 +- src/app.ts | 266 +++- src/server/index.ts | 29 +- src/standard-schema.ts | 93 ++ typedoc.config.mjs | 2 +- 22 files changed, 3861 insertions(+), 117 deletions(-) create mode 100644 src/standard-schema.ts diff --git a/.prettierignore b/.prettierignore index 6d5fc6f..c51df7b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,5 @@ examples/basic-server-*/**/*.tsx examples/quickstart/**/*.ts **/vendor/** **/.build/** +**/.claude/** SKILL.md diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index e522918..04fba11 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -269,7 +269,7 @@ export function createServer(): McpServer { { title: "Get Budget Data", description: - "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", + "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage. The widget is interactive and exposes tools for reading/modifying allocations, adjusting budgets, and comparing against industry benchmarks.", inputSchema: {}, outputSchema: BudgetDataResponseSchema, _meta: { ui: { resourceUri } }, diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 4e8c62d..a9bae12 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -3,6 +3,7 @@ */ import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { Chart, registerables } from "chart.js"; +import { z } from "zod"; import "./global.css"; import "./mcp-app.css"; @@ -626,6 +627,295 @@ function handleHostContextChanged(ctx: McpUiHostContext) { app.onhostcontextchanged = handleHostContextChanged; +// Register tools for model interaction +app.registerTool( + "get-allocations", + { + title: "Get Budget Allocations", + description: + "Get the current budget allocations including total budget, percentages, and amounts per category", + }, + async () => { + if (!state.config) { + return { + content: [ + { type: "text" as const, text: "Error: Configuration not loaded" }, + ], + isError: true, + }; + } + + const allocations: Record = {}; + for (const category of state.config.categories) { + const percent = state.allocations.get(category.id) ?? 0; + allocations[category.id] = { + percent, + amount: (percent / 100) * state.totalBudget, + }; + } + + const result = { + totalBudget: state.totalBudget, + currency: state.config.currency, + currencySymbol: state.config.currencySymbol, + selectedStage: state.selectedStage, + allocations, + categories: state.config.categories.map((c) => ({ + id: c.id, + name: c.name, + color: c.color, + })), + }; + + return { + content: [ + { type: "text" as const, text: JSON.stringify(result, null, 2) }, + ], + structuredContent: result, + }; + }, +); + +app.registerTool( + "set-allocation", + { + title: "Set Category Allocation", + description: "Set the allocation percentage for a specific budget category", + inputSchema: z.object({ + categoryId: z + .string() + .describe( + "Category ID (e.g., 'marketing', 'engineering', 'operations', 'sales', 'rd')", + ), + percent: z + .number() + .min(0) + .max(100) + .describe("Allocation percentage (0-100)"), + }), + }, + async (args) => { + if (!state.config) { + return { + content: [ + { type: "text" as const, text: "Error: Configuration not loaded" }, + ], + isError: true, + }; + } + + const category = state.config.categories.find( + (c) => c.id === args.categoryId, + ); + if (!category) { + return { + content: [ + { + type: "text" as const, + text: `Error: Category "${args.categoryId}" not found. Available: ${state.config.categories.map((c) => c.id).join(", ")}`, + }, + ], + isError: true, + }; + } + + handleSliderChange(args.categoryId, args.percent); + + // Also update the slider UI + const slider = document.querySelector( + `.slider-row[data-category-id="${args.categoryId}"] .slider`, + ) as HTMLInputElement | null; + if (slider) { + slider.value = String(args.percent); + } + + const amount = (args.percent / 100) * state.totalBudget; + return { + content: [ + { + type: "text" as const, + text: `Set ${category.name} allocation to ${args.percent.toFixed(1)}% (${state.config.currencySymbol}${amount.toLocaleString()})`, + }, + ], + }; + }, +); + +app.registerTool( + "set-total-budget", + { + title: "Set Total Budget", + description: "Set the total budget amount", + inputSchema: z.object({ + amount: z.number().positive().describe("Total budget amount"), + }), + }, + async (args) => { + if (!state.config) { + return { + content: [ + { type: "text" as const, text: "Error: Configuration not loaded" }, + ], + isError: true, + }; + } + + state.totalBudget = args.amount; + + // Update the budget selector if this amount is a preset + const budgetSelector = document.getElementById( + "budget-selector", + ) as HTMLSelectElement | null; + if (budgetSelector) { + const option = Array.from(budgetSelector.options).find( + (opt) => parseInt(opt.value) === args.amount, + ); + if (option) { + budgetSelector.value = String(args.amount); + } + } + + updateAllSliderAmounts(); + updateStatusBar(); + updateComparisonSummary(); + + return { + content: [ + { + type: "text" as const, + text: `Total budget set to ${state.config.currencySymbol}${args.amount.toLocaleString()}`, + }, + ], + }; + }, +); + +app.registerTool( + "set-company-stage", + { + title: "Set Company Stage", + description: + "Set the company stage for benchmark comparison (e.g., 'Seed', 'Series A', 'Series B', 'Growth')", + inputSchema: z.object({ + stage: z.string().describe("Company stage ID"), + }), + }, + async (args) => { + if (!state.analytics) { + return { + content: [ + { type: "text" as const, text: "Error: Analytics not loaded" }, + ], + isError: true, + }; + } + + if (!state.analytics.stages.includes(args.stage)) { + return { + content: [ + { + type: "text" as const, + text: `Error: Stage "${args.stage}" not found. Available: ${state.analytics.stages.join(", ")}`, + }, + ], + isError: true, + }; + } + + state.selectedStage = args.stage; + + // Update the stage selector UI + const stageSelector = document.getElementById( + "stage-selector", + ) as HTMLSelectElement | null; + if (stageSelector) { + stageSelector.value = args.stage; + } + + // Update all badges and summary + if (state.config) { + for (const category of state.config.categories) { + updatePercentileBadge(category.id); + } + updateComparisonSummary(); + } + + return { + content: [ + { + type: "text" as const, + text: `Company stage set to "${args.stage}"`, + }, + ], + }; + }, +); + +app.registerTool( + "get-benchmark-comparison", + { + title: "Get Benchmark Comparison", + description: + "Compare current allocations against industry benchmarks for the selected stage", + }, + async () => { + if (!state.config || !state.analytics) { + return { + content: [{ type: "text" as const, text: "Error: Data not loaded" }], + isError: true, + }; + } + + const benchmark = state.analytics.benchmarks.find( + (b) => b.stage === state.selectedStage, + ); + if (!benchmark) { + return { + content: [ + { + type: "text" as const, + text: `Error: No benchmark data for stage "${state.selectedStage}"`, + }, + ], + isError: true, + }; + } + + const comparison: Record< + string, + { current: number; p25: number; p50: number; p75: number; status: string } + > = {}; + + for (const category of state.config.categories) { + const current = state.allocations.get(category.id) ?? 0; + const benchmarkData = benchmark.categoryBenchmarks[category.id]; + let status = "within range"; + if (current < benchmarkData.p25) status = "below p25"; + else if (current > benchmarkData.p75) status = "above p75"; + + comparison[category.id] = { + current, + p25: benchmarkData.p25, + p50: benchmarkData.p50, + p75: benchmarkData.p75, + status, + }; + } + + const result = { + stage: state.selectedStage, + comparison, + }; + + return { + content: [ + { type: "text" as const, text: JSON.stringify(result, null, 2) }, + ], + structuredContent: result, + }; + }, +); + // Handle theme changes window .matchMedia("(prefers-color-scheme: dark)") diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts index 540d35c..10125a6 100644 --- a/examples/map-server/server.ts +++ b/examples/map-server/server.ts @@ -147,7 +147,7 @@ export function createServer(): McpServer { { title: "Show Map", description: - "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location.", + "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location. The widget is interactive and exposes tools for navigation (fly to locations) and querying the current view.", inputSchema: { west: z .number() diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts index 4fda132..39d7c2a 100644 --- a/examples/map-server/src/mcp-app.ts +++ b/examples/map-server/src/mcp-app.ts @@ -7,6 +7,7 @@ */ import { App } from "@modelcontextprotocol/ext-apps"; import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js"; +import { z } from "zod"; // TypeScript declaration for Cesium loaded from CDN // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -559,6 +560,76 @@ function setViewToBoundingBox(cesiumViewer: any, bbox: BoundingBox): void { ); } +/** + * Fly camera to view a bounding box with animation + */ +function flyToBoundingBox( + cesiumViewer: any, + bbox: BoundingBox, + duration = 2, +): Promise { + return new Promise((resolve) => { + const { destination, centerLon, centerLat, height } = + calculateDestination(bbox); + + log.info("flyTo destination:", centerLon, centerLat, "height:", height); + + cesiumViewer.camera.flyTo({ + destination, + orientation: { + heading: 0, + pitch: Cesium.Math.toRadians(-90), // Look straight down + roll: 0, + }, + duration, + complete: () => { + log.info( + "flyTo complete, camera height:", + cesiumViewer.camera.positionCartographic.height, + ); + resolve(); + }, + cancel: () => { + log.warn("flyTo cancelled"); + resolve(); + }, + }); + }); +} + +// Label element for displaying location info +let labelElement: HTMLDivElement | null = null; + +/** + * Set or clear the label displayed on the map + */ +function setLabel(text?: string): void { + if (!labelElement) { + labelElement = document.createElement("div"); + labelElement.style.cssText = ` + position: absolute; + top: 10px; + left: 10px; + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 8px 12px; + border-radius: 4px; + font-family: sans-serif; + font-size: 14px; + z-index: 100; + pointer-events: none; + `; + document.body.appendChild(labelElement); + } + + if (text) { + labelElement.textContent = text; + labelElement.style.display = "block"; + } else { + labelElement.style.display = "none"; + } +} + /** * Wait for globe tiles to finish loading */ @@ -801,57 +872,110 @@ app.ontoolinput = async (params) => { } }; -/* - Register tools for the model to interact w/ this component - Needs https://github.com/modelcontextprotocol/ext-apps/pull/72 -*/ -// app.registerTool( -// "navigate-to", -// { -// title: "Navigate To", -// description: "Navigate the globe to a new bounding box location", -// inputSchema: z.object({ -// west: z.number().describe("Western longitude (-180 to 180)"), -// south: z.number().describe("Southern latitude (-90 to 90)"), -// east: z.number().describe("Eastern longitude (-180 to 180)"), -// north: z.number().describe("Northern latitude (-90 to 90)"), -// duration: z -// .number() -// .optional() -// .describe("Animation duration in seconds (default: 2)"), -// label: z.string().optional().describe("Optional label to display"), -// }), -// }, -// async (args) => { -// if (!viewer) { -// return { -// content: [ -// { type: "text" as const, text: "Error: Viewer not initialized" }, -// ], -// isError: true, -// }; -// } +// Register tools for the model to interact with this component +app.registerTool( + "navigate-to", + { + title: "Navigate To", + description: "Navigate the globe to a new bounding box location", + inputSchema: z.object({ + west: z.number().describe("Western longitude (-180 to 180)"), + south: z.number().describe("Southern latitude (-90 to 90)"), + east: z.number().describe("Eastern longitude (-180 to 180)"), + north: z.number().describe("Northern latitude (-90 to 90)"), + duration: z + .number() + .optional() + .describe("Animation duration in seconds (default: 2)"), + label: z.string().optional().describe("Optional label to display"), + }), + }, + async (args) => { + if (!viewer) { + return { + content: [ + { type: "text" as const, text: "Error: Viewer not initialized" }, + ], + isError: true, + }; + } -// const bbox: BoundingBox = { -// west: args.west, -// south: args.south, -// east: args.east, -// north: args.north, -// }; + const bbox: BoundingBox = { + west: args.west, + south: args.south, + east: args.east, + north: args.north, + }; -// await flyToBoundingBox(viewer, bbox, args.duration ?? 2); -// setLabel(args.label); + await flyToBoundingBox(viewer, bbox, args.duration ?? 2); + setLabel(args.label); -// return { -// content: [ -// { -// type: "text" as const, -// text: `Navigated to: W:${bbox.west.toFixed(4)}, S:${bbox.south.toFixed(4)}, E:${bbox.east.toFixed(4)}, N:${bbox.north.toFixed(4)}${args.label ? ` (${args.label})` : ""}`, -// }, -// ], -// }; -// }, -// ); + return { + content: [ + { + type: "text" as const, + text: `Navigated to: W:${bbox.west.toFixed(4)}, S:${bbox.south.toFixed(4)}, E:${bbox.east.toFixed(4)}, N:${bbox.north.toFixed(4)}${args.label ? ` (${args.label})` : ""}`, + }, + ], + }; + }, +); + +app.registerTool( + "get-current-view", + { + title: "Get Current View", + description: + "Get the current camera position and bounding box visible on the globe", + }, + async () => { + if (!viewer) { + return { + content: [ + { type: "text" as const, text: "Error: Viewer not initialized" }, + ], + isError: true, + }; + } + + const camera = viewer.camera; + const positionCartographic = camera.positionCartographic; + const latitude = Cesium.Math.toDegrees(positionCartographic.latitude); + const longitude = Cesium.Math.toDegrees(positionCartographic.longitude); + const height = positionCartographic.height; + + // Get the visible bounding box + const rectangle = viewer.camera.computeViewRectangle(); + let bbox = null; + if (rectangle) { + bbox = { + west: Cesium.Math.toDegrees(rectangle.west), + south: Cesium.Math.toDegrees(rectangle.south), + east: Cesium.Math.toDegrees(rectangle.east), + north: Cesium.Math.toDegrees(rectangle.north), + }; + } + + const viewData = { + camera: { + latitude, + longitude, + height, + }, + bbox, + }; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(viewData, null, 2), + }, + ], + structuredContent: viewData, + }; + }, +); // Handle tool result - extract viewUUID and restore persisted view if available app.ontoolresult = async (result) => { diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index d2a0b7d..80a5499 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -1366,7 +1366,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { description: disableInteract ? `Show and render a PDF in a read-only viewer. -Use this tool when the user wants to view or read a PDF. The renderer displays the document for viewing. +Use this tool when the user wants to view or read a PDF. The renderer displays the document for viewing. The widget exposes app-registered tools for page navigation, text extraction, searching, and zoom control. Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL.` : `Open a PDF in an interactive viewer. Call this ONCE per PDF. diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index f4a1790..500879a 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -14,6 +14,7 @@ import { } from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js"; +import { z } from "zod"; import * as pdfjsLib from "pdfjs-dist"; import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; @@ -2572,17 +2573,21 @@ async function handleGetViewerState(requestId: string): Promise { }); } -async function handleGetPages(cmd: { - requestId: string; - intervals: Array<{ start?: number; end?: number }>; - getText: boolean; - getScreenshots: boolean; -}): Promise { - const allPages = expandIntervals(cmd.intervals); +/** + * Collect text and/or screenshots for a set of page intervals. + * Shared by the server-driven `get_pages` command (via handleGetPages) + * and the app-registered `get_text` / `get_screenshot` tools. + */ +async function collectPageData( + intervals: Array<{ start?: number; end?: number }>, + getText: boolean, + getScreenshots: boolean, +): Promise> { + const allPages = expandIntervals(intervals); const pages = allPages.slice(0, MAX_GET_PAGES); log.info( - `get_pages: ${pages.length} pages (${pages[0]}..${pages[pages.length - 1]}), text=${cmd.getText}, screenshots=${cmd.getScreenshots}`, + `collectPageData: ${pages.length} pages (${pages[0]}..${pages[pages.length - 1]}), text=${getText}, screenshots=${getScreenshots}`, ); const results: Array<{ @@ -2596,7 +2601,7 @@ async function handleGetPages(cmd: { page: pageNum, }; - if (cmd.getText) { + if (getText) { // Use cached text if available, otherwise extract on the fly let text = pageTextCache.get(pageNum); if (text == null && pdfDocument) { @@ -2609,7 +2614,7 @@ async function handleGetPages(cmd: { pageTextCache.set(pageNum, text); } catch (err) { log.error( - `get_pages: text extraction failed for page ${pageNum}:`, + `collectPageData: text extraction failed for page ${pageNum}:`, err, ); text = ""; @@ -2618,17 +2623,35 @@ async function handleGetPages(cmd: { entry.text = text ?? ""; } - if (cmd.getScreenshots) { + if (getScreenshots) { try { entry.image = await renderPageOffscreen(pageNum); } catch (err) { - log.error(`get_pages: screenshot failed for page ${pageNum}:`, err); + log.error( + `collectPageData: screenshot failed for page ${pageNum}:`, + err, + ); } } results.push(entry); } + return results; +} + +async function handleGetPages(cmd: { + requestId: string; + intervals: Array<{ start?: number; end?: number }>; + getText: boolean; + getScreenshots: boolean; +}): Promise { + const results = await collectPageData( + cmd.intervals, + cmd.getText, + cmd.getScreenshots, + ); + // Submit results back to server try { await app.callServerTool({ @@ -5091,6 +5114,398 @@ app.onteardown = async () => { app.onhostcontextchanged = handleHostContextChanged; +// ============================================================================= +// App-registered tools — 1:1 with the server's `interact` commands. +// +// Each tool constructs the matching PdfCommand and dispatches through +// processCommands(), so the command-handling logic lives in exactly one +// place. The server-side `interact` tool remains for hosts that don't +// support app-registered tools. +// ============================================================================= + +/** Shared zod shapes mirroring server.ts interact schema. */ +const FormFieldSchema = z.object({ + name: z.string(), + value: z.union([z.string(), z.boolean()]), +}); +const PageIntervalSchema = z.object({ + start: z.number().min(1).optional(), + end: z.number().min(1).optional(), +}); + +/** Dispatch a command via processCommands and return a text result. */ +async function runCommand( + cmd: PdfCommand, + okText: string | (() => string), +): Promise { + if (!pdfDocument) { + return { + content: [{ type: "text" as const, text: "Error: No document loaded" }], + isError: true, + }; + } + await processCommands([cmd]); + const text = typeof okText === "function" ? okText() : okText; + return { content: [{ type: "text" as const, text }] }; +} + +app.registerTool( + "get-document-info", + { + title: "Get Document Info", + description: + "Get information about the current PDF document including title, current page, total pages, and zoom level", + }, + async () => { + if (!pdfDocument) { + return { + content: [{ type: "text" as const, text: "Error: No document loaded" }], + isError: true, + }; + } + const info = { + title: pdfTitle || "Untitled", + url: pdfUrl, + currentPage, + totalPages, + scale, + displayMode: currentDisplayMode, + }; + return { + content: [{ type: "text" as const, text: JSON.stringify(info, null, 2) }], + structuredContent: info, + }; + }, +); + +app.registerTool( + "navigate", + { + title: "Navigate", + description: "Jump to a specific page in the document", + inputSchema: z.object({ + page: z.number().int().min(1).describe("Page number (1-indexed)"), + }), + }, + async ({ page }) => { + if (pdfDocument && (page < 1 || page > totalPages)) { + return { + content: [ + { + type: "text" as const, + text: `Error: Page ${page} out of range (1-${totalPages})`, + }, + ], + isError: true, + }; + } + return runCommand( + { type: "navigate", page }, + `Navigated to page ${page}/${totalPages}`, + ); + }, +); + +app.registerTool( + "search", + { + title: "Search", + description: + "Search for text and highlight matches in the viewer UI. Opens the search bar and jumps to the first match.", + inputSchema: z.object({ + query: z.string().describe("Text to search for"), + }), + }, + async ({ query }) => + runCommand( + { type: "search", query }, + () => `Searched for "${query}": ${allMatches.length} match(es)`, + ), +); + +app.registerTool( + "find", + { + title: "Find", + description: + "Silent search — locate matches without opening the search UI. Use before search_navigate.", + inputSchema: z.object({ + query: z.string().describe("Text to search for"), + }), + }, + async ({ query }) => + runCommand( + { type: "find", query }, + () => `Found ${allMatches.length} match(es) for "${query}"`, + ), +); + +app.registerTool( + "search_navigate", + { + title: "Search Navigate", + description: + "Jump to the Nth search match (0-indexed). Call search or find first.", + inputSchema: z.object({ + matchIndex: z.number().int().min(0).describe("Match index (0-indexed)"), + }), + }, + async ({ matchIndex }) => { + if (allMatches.length === 0) { + return { + content: [ + { + type: "text" as const, + text: "Error: No search results. Call search or find first.", + }, + ], + isError: true, + }; + } + if (matchIndex >= allMatches.length) { + return { + content: [ + { + type: "text" as const, + text: `Error: matchIndex ${matchIndex} out of range (0-${allMatches.length - 1})`, + }, + ], + isError: true, + }; + } + return runCommand( + { type: "search_navigate", matchIndex }, + `Jumped to match ${matchIndex + 1}/${allMatches.length} on page ${allMatches[matchIndex].pageNum}`, + ); + }, +); + +app.registerTool( + "zoom", + { + title: "Zoom", + description: "Set the zoom scale for the document", + inputSchema: z.object({ + scale: z + .number() + .min(0.5) + .max(3.0) + .describe("Zoom scale, 1.0 = 100% (range: 0.5-3.0)"), + }), + }, + async ({ scale }) => + runCommand( + { type: "zoom", scale }, + `Zoom set to ${Math.round(scale * 100)}%`, + ), +); + +app.registerTool( + "add_annotations", + { + title: "Add Annotations", + description: + "Add one or more annotations (highlight, note, rectangle, circle, line, stamp, image, freetext). Each needs id, type, page, and type-specific geometry.", + inputSchema: z.object({ + annotations: z + .array(z.record(z.string(), z.any())) + .min(1) + .describe( + "Annotation objects. Each needs: id, type, page, plus type-specific fields (x, y, width, height, rects, color, content, etc.)", + ), + }), + }, + async ({ annotations }) => + runCommand( + { + type: "add_annotations", + annotations: annotations as PdfAnnotationDef[], + }, + `Added ${annotations.length} annotation(s)`, + ), +); + +app.registerTool( + "update_annotations", + { + title: "Update Annotations", + description: + "Patch existing annotations by id. Only id and type are required; other fields are merged.", + inputSchema: z.object({ + annotations: z + .array(z.record(z.string(), z.any())) + .min(1) + .describe("Partial annotation objects. Each needs: id, type."), + }), + }, + async ({ annotations }) => + runCommand( + { + type: "update_annotations", + annotations: annotations as Extract< + PdfCommand, + { type: "update_annotations" } + >["annotations"], + }, + `Updated ${annotations.length} annotation(s)`, + ), +); + +app.registerTool( + "remove_annotations", + { + title: "Remove Annotations", + description: "Delete annotations by id", + inputSchema: z.object({ + ids: z.array(z.string()).min(1).describe("Annotation IDs to remove"), + }), + }, + async ({ ids }) => + runCommand( + { type: "remove_annotations", ids }, + `Removed ${ids.length} annotation(s)`, + ), +); + +app.registerTool( + "highlight_text", + { + title: "Highlight Text", + description: + "Auto-locate text and add a highlight annotation. Searches the document (or a specific page) and highlights the first match.", + inputSchema: z.object({ + query: z.string().describe("Text to locate and highlight"), + page: z + .number() + .int() + .min(1) + .optional() + .describe("Restrict search to this page"), + color: z.string().optional().describe("Highlight color (CSS color)"), + content: z.string().optional().describe("Tooltip/note content"), + }), + }, + async ({ query, page, color, content }) => { + const id = `ht_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + return runCommand( + { type: "highlight_text", id, query, page, color, content }, + `Highlighted "${query}"${page ? ` on page ${page}` : ""} (id: ${id})`, + ); + }, +); + +app.registerTool( + "fill_form", + { + title: "Fill Form", + description: "Fill PDF form fields by name", + inputSchema: z.object({ + fields: z + .array(FormFieldSchema) + .min(1) + .describe( + "Form fields: { name, value } where value is string or boolean", + ), + }), + }, + async ({ fields }) => + runCommand( + { type: "fill_form", fields }, + `Filled ${fields.length} field(s): ${fields.map((f) => f.name).join(", ")}`, + ), +); + +app.registerTool( + "get_text", + { + title: "Get Text", + description: + "Extract text from one or more pages. Returns one text block per page.", + inputSchema: z.object({ + page: z + .number() + .int() + .min(1) + .optional() + .describe("Single page (shorthand for intervals: [{start:N, end:N}])"), + intervals: z + .array(PageIntervalSchema) + .optional() + .describe( + "Page ranges. Each has optional start/end. [{start:1,end:5}], [{}] = all pages. Max 20 pages.", + ), + }), + }, + async ({ page, intervals }) => { + if (!pdfDocument) { + return { + content: [{ type: "text" as const, text: "Error: No document loaded" }], + isError: true, + }; + } + const resolved = intervals ?? (page ? [{ start: page, end: page }] : [{}]); + const data = await collectPageData(resolved, true, false); + const parts = data + .filter((e) => e.text != null) + .map((e) => ({ + type: "text" as const, + text: `--- Page ${e.page} ---\n${e.text}`, + })); + return { + content: + parts.length > 0 + ? parts + : [{ type: "text" as const, text: "No text content returned" }], + structuredContent: { pages: data }, + }; + }, +); + +app.registerTool( + "get_screenshot", + { + title: "Get Screenshot", + description: "Render a page to a JPEG image for visual analysis", + inputSchema: z.object({ + page: z.number().int().min(1).describe("Page number to render"), + }), + }, + async ({ page }) => { + if (!pdfDocument) { + return { + content: [{ type: "text" as const, text: "Error: No document loaded" }], + isError: true, + }; + } + const data = await collectPageData( + [{ start: page, end: page }], + false, + true, + ); + const entry = data[0]; + if (entry?.image) { + return { + content: [ + { + type: "image" as const, + data: entry.image, + mimeType: "image/jpeg", + }, + ], + }; + } + return { + content: [ + { + type: "text" as const, + text: `Error: screenshot failed for page ${page}`, + }, + ], + isError: true, + }; + }, +); + // Connect to host app .connect() diff --git a/examples/shadertoy-server/server.ts b/examples/shadertoy-server/server.ts index a4bfe9e..5238c40 100644 --- a/examples/shadertoy-server/server.ts +++ b/examples/shadertoy-server/server.ts @@ -63,7 +63,9 @@ LIMITATIONS - Do NOT use: - VR features (mainVR not available) For procedural noise: - float hash(vec2 p) { return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }`; + float hash(vec2 p) { return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); } + +The widget is interactive and exposes tools for updating shader source code and querying compilation status. Compilation errors are sent to model context automatically.`; const DEFAULT_FRAGMENT_SHADER = `void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; diff --git a/examples/shadertoy-server/src/mcp-app.ts b/examples/shadertoy-server/src/mcp-app.ts index 546de5a..7cbb849 100644 --- a/examples/shadertoy-server/src/mcp-app.ts +++ b/examples/shadertoy-server/src/mcp-app.ts @@ -7,6 +7,7 @@ import { applyHostStyleVariables, applyDocumentTheme, } from "@modelcontextprotocol/ext-apps"; +import { z } from "zod"; import "./global.css"; import "./mcp-app.css"; import ShaderToyLite, { @@ -39,7 +40,8 @@ const log = { // Get element references const mainEl = document.querySelector(".main") as HTMLElement; const canvas = document.getElementById("canvas") as HTMLCanvasElement; -const codePreview = document.getElementById("code-preview") as HTMLPreElement; +// TODO: code-preview streaming toggle was removed when ontoolinputpartial was +// dropped during app-tool refactor — element still exists in HTML, restore if needed const fullscreenBtn = document.getElementById( "fullscreen-btn", ) as HTMLButtonElement; @@ -100,6 +102,39 @@ fullscreenBtn.addEventListener("click", toggleFullscreen); // ShaderToyLite instance let shaderToy: ShaderToyLiteInstance | null = null; +// Track current shader sources +let currentShaderSources: ShaderInput = { + fragmentShader: "", +}; + +// Track compilation status +interface CompilationStatus { + success: boolean; + errors: string[]; + timestamp: number; +} +let lastCompilationStatus: CompilationStatus = { + success: true, + errors: [], + timestamp: Date.now(), +}; + +// Intercept console.error to capture shader compilation errors +const originalConsoleError = console.error.bind(console); +const capturedErrors: string[] = []; +console.error = (...args: unknown[]) => { + originalConsoleError(...args); + // Capture shader compilation errors + const message = args.map((arg) => String(arg)).join(" "); + if ( + message.includes("Shader compilation failed") || + message.includes("Program initialization failed") || + message.includes("Failed to compile") + ) { + capturedErrors.push(message); + } +}; + // Create app instance const app = new App({ name: "ShaderToy Renderer", version: "1.0.0" }); @@ -111,35 +146,18 @@ app.onteardown = async () => { return {}; }; -app.ontoolinputpartial = (params) => { - // Show code preview, hide canvas - codePreview.classList.add("visible"); - canvas.classList.add("hidden"); - const code = params.arguments?.fragmentShader; - codePreview.textContent = typeof code === "string" ? code : ""; - codePreview.scrollTop = codePreview.scrollHeight; -}; - -app.ontoolinput = (params) => { - log.info("Received shader input"); - - // Hide code preview, show canvas - codePreview.classList.remove("visible"); - canvas.classList.remove("hidden"); - - if (!isShaderInput(params.arguments)) { - log.error("Invalid tool input"); - return; - } - - const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } = - params.arguments; +// Helper function to compile shader and update status +function compileAndUpdateStatus(input: ShaderInput): void { + // Clear captured errors before compilation + capturedErrors.length = 0; // Initialize ShaderToyLite if needed if (!shaderToy) { shaderToy = new ShaderToyLite("canvas"); } + const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } = input; + // Set common code (shared across all shaders) shaderToy.setCommon(common || ""); @@ -167,6 +185,45 @@ app.ontoolinput = (params) => { }); shaderToy.play(); + + // Update compilation status + const hasErrors = capturedErrors.length > 0; + lastCompilationStatus = { + success: !hasErrors, + errors: [...capturedErrors], + timestamp: Date.now(), + }; + + // Store current sources + currentShaderSources = { ...input }; + + // Send compilation status to model context if there are errors + if (hasErrors) { + app + .updateModelContext({ + content: [ + { + type: "text", + text: `Shader compilation failed:\n${capturedErrors.join("\n")}`, + }, + ], + structuredContent: { + compilationStatus: lastCompilationStatus, + }, + }) + .catch((err) => log.error("Failed to update model context:", err)); + } +} + +app.ontoolinput = (params) => { + log.info("Received shader input"); + + if (!isShaderInput(params.arguments)) { + log.error("Invalid tool input"); + return; + } + + compileAndUpdateStatus(params.arguments); log.info("Setup complete"); }; @@ -174,6 +231,97 @@ app.onerror = log.error; app.onhostcontextchanged = handleHostContextChanged; +// Register tool: set-shader-source +app.registerTool( + "set-shader-source", + { + title: "Set Shader Source", + description: + "Update the shader source code. Compiles and runs the new shader immediately.", + inputSchema: z.object({ + fragmentShader: z + .string() + .describe("The main fragment shader source code (mainImage function)"), + common: z + .string() + .optional() + .describe("Common code shared across all shaders"), + bufferA: z + .string() + .optional() + .describe("Buffer A shader source (for multi-pass rendering)"), + bufferB: z + .string() + .optional() + .describe("Buffer B shader source (for multi-pass rendering)"), + bufferC: z + .string() + .optional() + .describe("Buffer C shader source (for multi-pass rendering)"), + bufferD: z + .string() + .optional() + .describe("Buffer D shader source (for multi-pass rendering)"), + }), + }, + async (args) => { + log.info("set-shader-source tool called"); + + compileAndUpdateStatus(args); + + const result = lastCompilationStatus.success + ? "Shader compiled and running successfully." + : `Shader compilation failed:\n${lastCompilationStatus.errors.join("\n")}`; + + return { + content: [{ type: "text" as const, text: result }], + structuredContent: { + success: lastCompilationStatus.success, + errors: lastCompilationStatus.errors, + timestamp: lastCompilationStatus.timestamp, + }, + }; + }, +); + +// Register tool: get-shader-info +app.registerTool( + "get-shader-info", + { + title: "Get Shader Info", + description: "Get the current shader source code and compilation status.", + }, + async () => { + log.info("get-shader-info tool called"); + + const hasShader = currentShaderSources.fragmentShader.length > 0; + const isPlaying = shaderToy?.isPlaying() ?? false; + + let statusText = ""; + if (!hasShader) { + statusText = "No shader loaded."; + } else if (lastCompilationStatus.success) { + statusText = `Shader is ${isPlaying ? "running" : "paused"}.`; + } else { + statusText = `Shader has compilation errors:\n${lastCompilationStatus.errors.join("\n")}`; + } + + return { + content: [ + { + type: "text" as const, + text: `${statusText}\n\nCurrent fragment shader:\n${currentShaderSources.fragmentShader || "(none)"}`, + }, + ], + structuredContent: { + sources: currentShaderSources, + compilationStatus: lastCompilationStatus, + isPlaying, + }, + }; + }, +); + // Pause/resume shader based on visibility const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 9ef24d2..34194b1 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -7,8 +7,9 @@ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { StrictMode, useState, useEffect } from "react"; +import { StrictMode, useState, useCallback, useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; +import { z } from "zod"; import ThreeJSApp from "./threejs-app.tsx"; import "./global.css"; @@ -16,6 +17,20 @@ import "./global.css"; // Types // ============================================================================= +/** + * Scene state tracked for view interaction tools. + */ +export interface SceneState { + /** Current Three.js code */ + code: string | null; + /** Canvas height */ + height: number; + /** Last error message if any */ + error: string | null; + /** Whether the scene is currently rendering */ + isRendering: boolean; +} + /** * Props passed to the view component. * This interface can be reused for other views. @@ -31,6 +46,105 @@ export interface ViewProps> { toolResult: CallToolResult | null; /** Host context (theme, dimensions, locale, etc.) */ hostContext: McpUiHostContext | null; + /** Call a tool on the MCP server */ + callServerTool: App["callServerTool"]; + /** Send a message to the host's chat */ + sendMessage: App["sendMessage"]; + /** Request the host to open a URL */ + openLink: App["openLink"]; + /** Send log messages to the host */ + sendLog: App["sendLog"]; + /** Callback to report scene errors */ + onSceneError: (error: string | null) => void; + /** Callback to report scene is rendering */ + onSceneRendering: (isRendering: boolean) => void; +} + +// ============================================================================= +// Widget Interaction Tools +// ============================================================================= + +/** + * Registers widget interaction tools on the App instance. + * These tools allow the model to interact with the Three.js scene. + */ +function registerWidgetTools( + app: App, + sceneStateRef: React.RefObject, + setToolInputs: (args: Record) => void, +): void { + // Tool: set-scene-source - Update the scene source/configuration + app.registerTool( + "set-scene-source", + { + title: "Set Scene Source", + description: + "Update the Three.js scene source code. The code will be executed in a sandboxed environment with access to THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, and height.", + inputSchema: z.object({ + code: z.string().describe("JavaScript code to render the 3D scene"), + height: z + .number() + .int() + .positive() + .optional() + .describe("Height in pixels (optional, defaults to current)"), + }), + outputSchema: z.object({ + success: z.boolean(), + code: z.string(), + height: z.number(), + }), + }, + async (args) => { + sceneStateRef.current.code = args.code; + if (args.height !== undefined) { + sceneStateRef.current.height = args.height; + } + sceneStateRef.current.error = null; + setToolInputs({ code: args.code, height: sceneStateRef.current.height }); + + const result = { + success: true, + code: args.code, + height: sceneStateRef.current.height, + }; + + return { + content: [{ type: "text" as const, text: JSON.stringify(result) }], + structuredContent: result, + }; + }, + ); + + // Tool: get-scene-info - Get current scene state and any errors + app.registerTool( + "get-scene-info", + { + title: "Get Scene Info", + description: + "Get the current Three.js scene state including source code, dimensions, rendering status, and any errors.", + outputSchema: z.object({ + code: z.string().nullable(), + height: z.number(), + error: z.string().nullable(), + isRendering: z.boolean(), + }), + }, + async () => { + const state = sceneStateRef.current; + const result = { + code: state.code, + height: state.height, + error: state.error, + isRendering: state.isRendering, + }; + + return { + content: [{ type: "text" as const, text: JSON.stringify(result) }], + structuredContent: result, + }; + }, + ); } // ============================================================================= @@ -48,14 +162,38 @@ function McpAppWrapper() { const [toolResult, setToolResult] = useState(null); const [hostContext, setHostContext] = useState(null); + // Scene state for widget interaction tools + const sceneStateRef = useRef({ + code: null, + height: 400, + error: null, + isRendering: false, + }); + + // Reference to app for tools to access updateModelContext + const appRef = useRef(null); + const { app, error } = useApp({ appInfo: { name: "Three.js View", version: "1.0.0" }, - capabilities: {}, + capabilities: { tools: {} }, onAppCreated: (app) => { + appRef.current = app; + + // Register widget interaction tools before connect() + registerWidgetTools(app, sceneStateRef, setToolInputs); + // Complete tool input (streaming finished) app.ontoolinput = (params) => { - setToolInputs(params.arguments as Record); + const args = params.arguments as Record; + setToolInputs(args); setToolInputsPartial(null); + // Update scene state from tool input + if (typeof args.code === "string") { + sceneStateRef.current.code = args.code; + } + if (typeof args.height === "number") { + sceneStateRef.current.height = args.height; + } }; // Partial tool input (streaming in progress) app.ontoolinputpartial = (params) => { @@ -110,6 +248,53 @@ function McpAppWrapper() { } }, [app]); + // Memoized callbacks that forward to app methods + const callServerTool = useCallback( + (params, options) => app!.callServerTool(params, options), + [app], + ); + const sendMessage = useCallback( + (params, options) => app!.sendMessage(params, options), + [app], + ); + const openLink = useCallback( + (params, options) => app!.openLink(params, options), + [app], + ); + const sendLog = useCallback( + (params) => app!.sendLog(params), + [app], + ); + + // Callback for scene to report errors + const onSceneError = useCallback((sceneError: string | null) => { + sceneStateRef.current.error = sceneError; + + // Send errors to model context for awareness + if (sceneError && appRef.current) { + void appRef.current + .updateModelContext({ + content: [ + { + type: "text" as const, + text: `Three.js Scene Error: ${sceneError}`, + }, + ], + structuredContent: { + type: "scene_error", + error: sceneError, + timestamp: new Date().toISOString(), + }, + }) + .catch(() => {}); + } + }, []); + + // Callback for scene to report rendering state + const onSceneRendering = useCallback((isRendering: boolean) => { + sceneStateRef.current.isRendering = isRendering; + }, []); + if (error) { return
Error: {error.message}
; } @@ -125,6 +310,12 @@ function McpAppWrapper() { toolInputsPartial={toolInputsPartial} toolResult={toolResult} hostContext={hostContext} + callServerTool={callServerTool} + sendMessage={sendMessage} + openLink={openLink} + sendLog={sendLog} + onSceneError={onSceneError} + onSceneRendering={onSceneRendering} /> ); } diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 1de585f..966bd64 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -200,6 +200,12 @@ export default function ThreeJSApp({ toolInputs, toolInputsPartial, hostContext, + callServerTool: _callServerTool, + sendMessage: _sendMessage, + openLink: _openLink, + sendLog: _sendLog, + onSceneError, + onSceneRendering, }: ThreeJSAppProps) { const [error, setError] = useState(null); const [currentDisplayMode, setCurrentDisplayMode] = useState< @@ -304,6 +310,9 @@ export default function ThreeJSApp({ animControllerRef.current = createAnimationController(); setError(null); + onSceneError(null); + onSceneRendering(true); + const w = containerWidth || containerRef.current.offsetWidth || 800; const h = isFullscreen && hostHeight > 0 ? hostHeight : height; executeThreeCode( @@ -312,12 +321,18 @@ export default function ThreeJSApp({ w, h, animControllerRef.current.visibilityAwareRAF, - ).catch((e) => setError(e instanceof Error ? e.message : "Unknown error")); + ).catch((e) => { + const msg = e instanceof Error ? e.message : "Unknown error"; + setError(msg); + onSceneError(msg); + onSceneRendering(false); + }); return () => { canvas.removeEventListener("touchstart", preventDefault); canvas.removeEventListener("touchmove", preventDefault); animControllerRef.current?.cleanup(); + onSceneRendering(false); }; }, [code, height, containerWidth, isFullscreen, hostHeight]); diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index fe2e89b..59d5e18 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -86,7 +86,7 @@ export function createServer(): McpServer { { title: "Get First-Degree Links", description: - "Returns all Wikipedia pages that the given page links to directly.", + "Returns all Wikipedia pages that the given page links to directly. The widget is interactive and exposes tools for exploring the graph (expanding nodes to see their links), searching for articles, and querying visible nodes.", inputSchema: z.object({ url: z .string() diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 69bec05..291292d 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -3,6 +3,7 @@ */ import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; import { forceCenter, forceCollide, @@ -397,6 +398,346 @@ function handleHostContextChanged(ctx: McpUiHostContext) { app.onhostcontextchanged = handleHostContextChanged; +// ============================================================================= +// Widget Interaction Tools +// ============================================================================= + +// Tool: Search for a Wikipedia article and navigate to it +app.registerTool( + "search-article", + { + title: "Search Article", + description: + "Search for a Wikipedia article and add it to the graph as the new starting point", + inputSchema: z.object({ + query: z.string().describe("Search query for Wikipedia article"), + }), + }, + async (args) => { + const { query } = args as { query: string }; + + // Construct Wikipedia search URL that redirects to the article + const searchUrl = `https://en.wikipedia.org/wiki/Special:Search?go=Go&search=${encodeURIComponent(query)}`; + + // Use the server tool to fetch the article + const result = await app.callServerTool({ + name: "get-first-degree-links", + arguments: { url: searchUrl }, + }); + + const response = result.structuredContent as unknown as ToolResponse; + if (response && response.page) { + // Clear existing graph and start fresh with this article + graphData.nodes = []; + graphData.links = []; + initialUrl = response.page.url; + addNode(response.page.url, response.page.title, "default", { + x: 0, + y: 0, + }); + graph.warmupTicks(100); + handleToolResultData(result); + graph.centerAt(0, 0, 500); + + return { + content: [ + { + type: "text" as const, + text: `Navigated to article: ${response.page.title}`, + }, + ], + structuredContent: { + success: true, + article: response.page, + linksFound: response.links?.length ?? 0, + }, + }; + } + + return { + content: [ + { type: "text" as const, text: `Could not find article for: ${query}` }, + ], + structuredContent: { + success: false, + error: "Article not found", + }, + }; + }, +); + +// Tool: Get information about the currently displayed article +app.registerTool( + "get-current-article", + { + title: "Get Current Article", + description: + "Get information about the currently selected or initial article in the graph", + }, + async () => { + const currentUrl = selectedNodeUrl || initialUrl; + + if (!currentUrl) { + return { + content: [ + { type: "text" as const, text: "No article is currently selected" }, + ], + structuredContent: { + hasSelection: false, + article: null, + }, + }; + } + + const node = graphData.nodes.find((n) => n.url === currentUrl); + + if (!node) { + return { + content: [ + { + type: "text" as const, + text: "Selected article not found in graph", + }, + ], + structuredContent: { + hasSelection: false, + article: null, + }, + }; + } + + return { + content: [ + { + type: "text" as const, + text: `Current article: ${node.title}\nURL: ${node.url}\nState: ${node.state}`, + }, + ], + structuredContent: { + hasSelection: true, + article: { + url: node.url, + title: node.title, + state: node.state, + isExpanded: node.state === "expanded", + hasError: node.state === "error", + errorMessage: node.errorMessage, + }, + }, + }; + }, +); + +// Tool: Highlight a specific node in the graph +app.registerTool( + "highlight-node", + { + title: "Highlight Node", + description: + "Highlight and center on a specific node in the graph by title or URL", + inputSchema: z.object({ + identifier: z + .string() + .describe("The title or URL of the node to highlight"), + }), + }, + async (args) => { + const { identifier } = args as { identifier: string }; + const lowerIdentifier = identifier.toLowerCase(); + + // Find node by title (case-insensitive partial match) or exact URL + const node = graphData.nodes.find( + (n) => + n.url === identifier || n.title.toLowerCase().includes(lowerIdentifier), + ); + + if (!node) { + return { + content: [ + { type: "text" as const, text: `Node not found: ${identifier}` }, + ], + structuredContent: { + success: false, + error: "Node not found in graph", + availableNodes: graphData.nodes.map((n) => n.title), + }, + }; + } + + // Center on the node and select it + selectedNodeUrl = node.url; + if (node.x !== undefined && node.y !== undefined) { + graph.centerAt(node.x, node.y, 500); + graph.zoom(2, 500); + } + + return { + content: [ + { type: "text" as const, text: `Highlighted node: ${node.title}` }, + ], + structuredContent: { + success: true, + node: { + url: node.url, + title: node.title, + state: node.state, + }, + }, + }; + }, +); + +// Tool: Expand a node to show its linked pages +app.registerTool( + "expand-node", + { + title: "Expand Node", + description: + "Expand a node to fetch and display all Wikipedia pages it links to. This is the core way to explore the graph.", + inputSchema: z.object({ + identifier: z.string().describe("The title or URL of the node to expand"), + }), + }, + async (args) => { + const { identifier } = args as { identifier: string }; + const lowerIdentifier = identifier.toLowerCase(); + + // Find node by title (case-insensitive partial match) or exact URL + const node = graphData.nodes.find( + (n) => + n.url === identifier || n.title.toLowerCase().includes(lowerIdentifier), + ); + + if (!node) { + return { + content: [ + { type: "text" as const, text: `Node not found: ${identifier}` }, + ], + structuredContent: { + success: false, + error: "Node not found in graph", + availableNodes: graphData.nodes.map((n) => n.title), + }, + }; + } + + if (node.state === "expanded") { + return { + content: [ + { + type: "text" as const, + text: `Node "${node.title}" is already expanded`, + }, + ], + structuredContent: { + success: true, + alreadyExpanded: true, + node: { url: node.url, title: node.title }, + }, + }; + } + + if (node.state === "error") { + return { + content: [ + { + type: "text" as const, + text: `Node "${node.title}" has an error: ${node.errorMessage}`, + }, + ], + structuredContent: { + success: false, + error: node.errorMessage, + }, + }; + } + + try { + // Fetch the linked pages using the server tool + const result = await app.callServerTool({ + name: "get-first-degree-links", + arguments: { url: node.url }, + }); + + graph.warmupTicks(0); + handleToolResultData(result); + + const response = result.structuredContent as unknown as ToolResponse; + const linksAdded = response?.links?.length ?? 0; + + // Center on the expanded node + if (node.x !== undefined && node.y !== undefined) { + graph.centerAt(node.x, node.y, 500); + } + + return { + content: [ + { + type: "text" as const, + text: `Expanded "${node.title}" - found ${linksAdded} linked articles`, + }, + ], + structuredContent: { + success: true, + node: { url: node.url, title: node.title }, + linksAdded, + }, + }; + } catch (e) { + setNodeState(node.url, "error", "Request failed"); + updateGraph(); + return { + content: [ + { + type: "text" as const, + text: `Failed to expand "${node.title}": ${e instanceof Error ? e.message : String(e)}`, + }, + ], + structuredContent: { + success: false, + error: String(e), + }, + }; + } + }, +); + +// Tool: Get list of currently visible nodes in the graph +app.registerTool( + "get-visible-nodes", + { + title: "Get Visible Nodes", + description: "Get a list of all nodes currently visible in the graph", + }, + async () => { + const nodes = graphData.nodes.map((n) => ({ + url: n.url, + title: n.title, + state: n.state, + isExpanded: n.state === "expanded", + hasError: n.state === "error", + })); + + const expandedCount = nodes.filter((n) => n.isExpanded).length; + const errorCount = nodes.filter((n) => n.hasError).length; + + return { + content: [ + { + type: "text" as const, + text: `Graph contains ${nodes.length} nodes:\n${nodes.map((n) => `- ${n.title} (${n.state})`).join("\n")}`, + }, + ], + structuredContent: { + totalNodes: nodes.length, + expandedNodes: expandedCount, + errorNodes: errorCount, + nodes, + }, + }; + }, +); + // Connect to host app.connect().then(() => { const ctx = app.getHostContext(); diff --git a/package-lock.json b/package-lock.json index 1e689bc..15d88de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "workspaces": [ "examples/*" ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -3696,6 +3699,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", diff --git a/package.json b/package.json index 34b2c6a..d1fa9df 100644 --- a/package.json +++ b/package.json @@ -125,5 +125,8 @@ "seroval-plugins": "1.4.2", "solid-js": "1.9.10", "@types/node": "20.19.27" + }, + "dependencies": { + "@standard-schema/spec": "^1.1.0" } } diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index aa364bc..1a14d3f 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -506,11 +506,23 @@ If the Host is a web page, it MUST wrap the View and communicate with it through ### Standard MCP Messages -UI iframes can use the following subset of standard MCP protocol messages: +UI iframes can use the following subset of standard MCP protocol messages. + +Note that `tools/call` and `tools/list` flow **bidirectionally**: +- **App → Host → Server**: Apps call server tools (requires host `serverTools` capability) +- **Host → App**: Host calls app-registered tools (requires app `tools` capability) **Tools:** -- `tools/call` - Execute a tool on the MCP server +- `tools/call` - Execute a tool (bidirectional) + - **App → Host**: Call server tool via host proxy + - **Host → App**: Call app-registered tool +- `tools/list` - List available tools (bidirectional) + - **App → Host**: List server tools + - **Host → App**: List app-registered tools +- `notifications/tools/list_changed` - Notify when tool list changes (bidirectional) + - **Server → Host → App**: Server tools changed + - **App → Host**: App-registered tools changed **Resources:** @@ -1200,6 +1212,95 @@ Host behavior: - If multiple updates are received before the next user message, Host SHOULD only send the last update to the model - MAY display context updates to the user +#### Requests (Host → App) + +When Apps declare the `tools` capability, the Host can send standard MCP tool requests to the App: + +`tools/call` - Execute an App-registered tool + +```typescript +// Request (Host → App) +{ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: string, // Name of app-registered tool to execute + arguments?: object // Tool arguments (validated against inputSchema) + } +} + +// Success Response (App → Host) +{ + jsonrpc: "2.0", + id: 1, + result: { + content: Array, // Result for model context + structuredContent?: object, // Optional structured data for UI + isError?: boolean, // True if tool execution failed + _meta?: object // Optional metadata + } +} + +// Error Response +{ + jsonrpc: "2.0", + id: 1, + error: { + code: number, + message: string + } +} +``` + +**App Behavior:** +- Apps MUST implement `oncalltool` handler if they declare `tools` capability +- Apps SHOULD validate tool names and arguments +- Apps MAY use `app.registerTool()` SDK helper for automatic validation +- Apps SHOULD return `isError: true` for tool execution failures + +`tools/list` - List App-registered tools + +```typescript +// Request (Host → App) +{ + jsonrpc: "2.0", + id: 2, + method: "tools/list", + params: { + cursor?: string // Optional pagination cursor + } +} + +// Response (App → Host) +{ + jsonrpc: "2.0", + id: 2, + result: { + tools: Array, // List of available tools + nextCursor?: string // Pagination cursor if more tools exist + } +} +``` + +**Tool Structure** (identical to [core MCP `Tool`](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)): +```typescript +interface Tool { + name: string; // Unique tool identifier + title?: string; // Display name + description?: string; // Human-readable description + inputSchema: object; // JSON Schema for arguments + outputSchema?: object; // JSON Schema for structuredContent + annotations?: ToolAnnotations; // MCP tool annotations (e.g., readOnlyHint) + _meta?: object; +} +``` + +**App Behavior:** +- Apps MUST implement `onlisttools` handler if they declare `tools` capability +- Apps SHOULD return complete tool metadata including schemas +- Apps MAY filter tools based on context or permissions + #### Notifications (Host → View) `ui/notifications/tool-input` - Host MUST send this notification with the complete tool arguments after the View's initialize request completes. @@ -1611,6 +1712,446 @@ This pattern enables interactive, self-updating views. Note: Tools with `visibility: ["app"]` are hidden from the agent but remain callable by apps via `tools/call`. This enables UI-only interactions (refresh buttons, form submissions) without exposing implementation details to the model. See the Visibility section under Resource Discovery for details. +### App-Provided Tools + +Apps can register their own tools that hosts and agents can call, making apps **introspectable and accessible** to the model. This complements the existing capability where apps call server tools (via host proxy). + +#### Motivation: Semantic Introspection + +Without tool registration, apps are opaque to the model: +- The host renders the app in a sandboxed iframe and has no guaranteed access to its internals — apps may embed further cross-origin iframes (via `frameDomains`), and even where same-origin, DOM access is not part of the host↔app contract +- The model therefore cannot query app state or discover what operations the app supports + +With tool registration, apps expose a semantic interface: +- The model discovers available operations via `tools/list` +- The model queries app state via tools (e.g., `get_board_state`) +- The model executes actions via tools (e.g., `make_move`) +- Apps return structured data rather than relying on the host to interpret rendered output + +This is *pull*-based and complements the *push*-based [`ui/update-model-context`](#uiupdate-model-context) request already defined in this spec (and analogous mechanisms elsewhere, e.g. OAI Apps SDK's [`setWidgetState()`](https://developers.openai.com/apps-sdk/reference/sdk#setwidgetstate)). Push lets an app proactively keep the model's context current; pull lets the agent query and command the app on demand. Apps may use either or both. + +#### App Tool Registration + +Apps register tools using the SDK's `registerTool()` method: + +```typescript +import { App } from '@modelcontextprotocol/ext-apps'; +import { z } from 'zod'; + +const app = new App( + { name: "TicTacToe", version: "1.0.0" }, + { tools: { listChanged: true } } // Declare tool capability +); + +// Register a tool with schema validation +const moveTool = app.registerTool( + "tictactoe_move", + { + description: "Make a move in the tic-tac-toe game", + inputSchema: z.object({ + position: z.number().int().min(0).max(8), + player: z.enum(['X', 'O']) + }), + outputSchema: z.object({ + board: z.array(z.string()).length(9), + winner: z.enum(['X', 'O', 'draw', null]).nullable() + }), + annotations: { + readOnlyHint: false // This tool has side effects + } + }, + async (params) => { + // Validate and execute move + const newBoard = makeMove(params.position, params.player); + const winner = checkWinner(newBoard); + + return { + content: [{ + type: "text", + text: `Move made at position ${params.position}` + }], + structuredContent: { + board: newBoard, + winner + } + }; + } +); + +await app.connect(new PostMessageTransport(window.parent)); +``` + +**Registration Options:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Unique tool identifier | +| `description` | string | No | Human-readable description for agent | +| `inputSchema` | [Standard Schema](https://standardschema.dev/) | No | Validates arguments; serialized to JSON Schema for `tools/list` | +| `outputSchema` | [Standard Schema](https://standardschema.dev/) | No | Validates `structuredContent` | +| `annotations` | ToolAnnotations | No | MCP tool hints (e.g., `readOnlyHint`) | +| `_meta` | object | No | Custom metadata | + +Apps can also implement tool handling manually without the SDK: + +```javascript +app.oncalltool = async (params, extra) => { + if (params.name === "tictactoe_move") { + // Manual validation + if (typeof params.arguments?.position !== 'number') { + throw new Error("Invalid position"); + } + + // Execute tool + const newBoard = makeMove(params.arguments.position, params.arguments.player); + + return { + content: [{ type: "text", text: "Move made" }], + structuredContent: { board: newBoard } + }; + } + + throw new Error(`Unknown tool: ${params.name}`); +}; + +app.onlisttools = async () => { + return { + tools: [ + { + name: "tictactoe_move", + description: "Make a move in the game", + inputSchema: { + type: "object", + properties: { + position: { type: "number", minimum: 0, maximum: 8 }, + player: { type: "string", enum: ["X", "O"] } + }, + required: ["position", "player"] + } + } + ] + }; +}; +``` + +#### Tool Lifecycle + +Registered tools support dynamic lifecycle management: + +**Enable/Disable:** + +```typescript +const tool = app.registerTool("my_tool", config, callback); + +// Disable tool (hide from tools/list) +tool.disable(); + +// Re-enable tool +tool.enable(); +``` + +When a tool is disabled/enabled, the app automatically sends `notifications/tools/list_changed` (if the app declared `listChanged: true` capability). + +**Update:** + +```typescript +// Update tool description or schema +tool.update({ + description: "New description", + inputSchema: newSchema +}); +``` + +Updates also trigger `notifications/tools/list_changed`. + +**Remove:** + +```typescript +// Permanently remove tool +tool.remove(); +``` + +#### Long-Running App Tools + +App tools are bounded by the app's render lifecycle. For work that may outlive a single render, app `tools/call` MAY be task-augmented per [core MCP Tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks); hosts proxy the `tasks/*` methods to the app exactly as they proxy `tools/call`. Because the iframe can be torn down at any time, apps SHOULD delegate long-lived work to the server (via `callServerTool()`) and return the server's task handle, so the host can continue polling the server after teardown. + +#### Schema Validation + +The SDK validates input and output against any [Standard Schema](https://standardschema.dev/)–compatible library (Zod, ArkType, Valibot, …). Examples below use Zod: + +**Input Validation:** + +```typescript +app.registerTool( + "search", + { + inputSchema: z.object({ + query: z.string().min(1).max(100), + limit: z.number().int().positive().default(10) + }) + }, + async (params) => { + // params.query is guaranteed to be a string (1-100 chars) + // params.limit is guaranteed to be a positive integer (default 10) + return performSearch(params.query, params.limit); + } +); +``` + +If the host sends invalid arguments, the tool automatically returns an error before the callback is invoked. + +**Output Validation:** + +```typescript +app.registerTool( + "get_status", + { + outputSchema: z.object({ + status: z.enum(['ready', 'busy', 'error']), + timestamp: z.string().datetime() + }) + }, + async () => { + return { + content: [{ type: "text", text: "Status retrieved" }], + structuredContent: { + status: 'ready', + timestamp: new Date().toISOString() + } + }; + } +); +``` + +If the callback returns data that doesn't match `outputSchema`, the tool returns an error. + +#### Complete Example: Introspectable Tic-Tac-Toe + +This example demonstrates how apps expose semantic interfaces through tools: + +```typescript +import { App } from '@modelcontextprotocol/ext-apps'; +import { z } from 'zod'; + +// Game state +let board: Array<'X' | 'O' | null> = Array(9).fill(null); +let currentPlayer: 'X' | 'O' = 'X'; +let moveHistory: number[] = []; + +const app = new App( + { name: "TicTacToe", version: "1.0.0" }, + { tools: { listChanged: true } } +); + +// Agent can query semantic state directly +app.registerTool( + "get_board_state", + { + description: "Get current game state including board, current player, and winner", + outputSchema: z.object({ + board: z.array(z.enum(['X', 'O', null])).length(9), + currentPlayer: z.enum(['X', 'O']), + winner: z.enum(['X', 'O', 'draw', null]).nullable(), + moveHistory: z.array(z.number()) + }) + }, + async () => { + return { + content: [{ + type: "text", + text: `Board: ${board.map(c => c || '-').join('')}, Player: ${currentPlayer}` + }], + structuredContent: { + board, + currentPlayer, + winner: checkWinner(board), + moveHistory + } + }; + } +); + +// Agent can execute moves +app.registerTool( + "make_move", + { + description: "Place a piece at the specified position", + inputSchema: z.object({ + position: z.number().int().min(0).max(8) + }), + annotations: { readOnlyHint: false } + }, + async ({ position }) => { + if (board[position] !== null) { + return { + content: [{ type: "text", text: "Position already taken" }], + isError: true + }; + } + + board[position] = currentPlayer; + moveHistory.push(position); + const winner = checkWinner(board); + currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; + + return { + content: [{ + type: "text", + text: `Player ${board[position]} moved to position ${position}` + + (winner ? `. ${winner} wins!` : '') + }], + structuredContent: { + board, + currentPlayer, + winner, + moveHistory + } + }; + } +); + +// Agent can reset game +app.registerTool( + "reset_game", + { + description: "Reset the game board to initial state", + annotations: { readOnlyHint: false } + }, + async () => { + board = Array(9).fill(null); + currentPlayer = 'X'; + moveHistory = []; + + return { + content: [{ type: "text", text: "Game reset" }], + structuredContent: { board, currentPlayer, moveHistory } + }; + } +); + +await app.connect(new PostMessageTransport(window.parent)); + +function checkWinner(board: Array<'X' | 'O' | null>): 'X' | 'O' | 'draw' | null { + const lines = [ + [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows + [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns + [0, 4, 8], [2, 4, 6] // diagonals + ]; + + for (const [a, b, c] of lines) { + if (board[a] && board[a] === board[b] && board[a] === board[c]) { + return board[a]; + } + } + + return board.every(cell => cell !== null) ? 'draw' : null; +} +``` + +**Agent Interaction:** + +```typescript +// 1. Discover available operations +const { tools } = await bridge.sendListTools({}); +// → ["get_board_state", "make_move", "reset_game"] + +// 2. Query semantic state (not visual/DOM) +const state = await bridge.sendCallTool({ + name: "get_board_state", + arguments: {} +}); +// → { board: [null, null, null, ...], currentPlayer: 'X', winner: null } + +// 3. Execute actions based on semantic understanding +if (state.structuredContent.board[4] === null) { + await bridge.sendCallTool({ + name: "make_move", + arguments: { position: 4 } + }); +} + +// 4. Query updated state +const newState = await bridge.sendCallTool({ + name: "get_board_state", + arguments: {} +}); +// → { board: [null, null, null, null, 'X', null, ...], currentPlayer: 'O', ... } +``` + +The agent interacts with the app through semantic operations rather than visual interpretation. + +#### Tool Flow Directions + +**Existing Flow (unchanged): App → Host → Server** + +Apps call server tools (proxied by host): + +```typescript +// App calls server tool +const result = await app.callServerTool("get_weather", { location: "NYC" }); +``` + +Requires host `serverTools` capability. + +**New Flow: Host/Agent → App** + +Host/Agent calls app-registered tools: + +```typescript +// Host calls app tool +const result = await bridge.sendCallTool({ + name: "tictactoe_move", + arguments: { position: 4 } +}); +``` + +Requires app `tools` capability. + +**Key Distinction:** + +| Aspect | Server Tools | App Tools | +|--------|-------------|-----------| +| **Lifetime** | Persistent (server process) | Ephemeral (while app loaded) | +| **Source** | MCP Server | App JavaScript | +| **Trust** | Trusted | Sandboxed (untrusted) | +| **Discovery** | Server `tools/list` | App `tools/list` (when app declares capability) | +| **When Available** | Always | Only while app is loaded | + +#### Use Cases + +**Introspection:** Agent queries app state semantically — the app's DOM is not exposed to the host or model + +**Voice mode:** Agent drives app interactions programmatically based on voice commands + +**Accessibility:** Structured state and operations more accessible than visual rendering + +**Complex workflows:** Agent discovers available operations and coordinates multi-step interactions + +**Stateful apps:** Apps expose operations (move, reset, query) rather than pushing state updates via messages + +#### Security Implications + +App tools run in **sandboxed iframes** (untrusted). See Security Implications section for detailed mitigations. + +Key considerations: +- App tools could provide misleading descriptions +- Tool namespacing needed to avoid conflicts with server tools +- Resource limits (max tools, execution timeouts) +- Audit trail for app tool invocations +- User confirmation for tools with side effects + +#### Relation to WebMCP + +This feature is inspired by [WebMCP](https://github.com/webmachinelearning/webmcp) (W3C incubation), which proposes allowing web pages to register JavaScript functions as tools via `navigator.modelContext.registerTool()`. + +Key differences: +- **WebMCP**: General web pages, browser API, manifest-based discovery +- **This spec**: MCP Apps, standard MCP messages, capability-based negotiation + +Similar to WebMCP but without turning the App (embedded page) into an MCP server - apps register tools within the App/Host architecture. + +See [ext-apps#35](https://github.com/modelcontextprotocol/ext-apps/issues/35) for discussion. + ### Client\<\>Server Capability Negotiation Clients and servers negotiate MCP Apps support through the standard MCP extensions capability mechanism (defined in SEP-1724). @@ -1681,15 +2222,104 @@ if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) { - Tools MUST return meaningful content array even when UI is available - Servers MAY register different tool variants based on host capabilities +#### App (Guest UI) Capabilities + +Apps advertise their capabilities in the `ui/initialize` request to the host. When an app supports tool registration, it includes the `tools` capability: + +```json +{ + "method": "ui/initialize", + "params": { + "appInfo": { + "name": "TicTacToe", + "version": "1.0.0" + }, + "appCapabilities": { + "tools": { + "listChanged": true + } + } + } +} +``` + +The host responds with its own capabilities, including support for proxying server tools: + +```json +{ + "result": { + "hostInfo": { + "name": "claude-desktop", + "version": "1.0.0" + }, + "hostCapabilities": { + "serverTools": { + "listChanged": true + }, + "openLinks": {}, + "logging": {} + } + } +} +``` + +**App Capability: `tools`** + +When present, the app can register tools that the host and agent can call. + +- `listChanged` (boolean, optional): If `true`, the app will send `notifications/tools/list_changed` when tools are added, removed, or modified + +**Host Capability: `serverTools`** + +When present, the host can proxy calls from the app to MCP server tools. + +- `listChanged` (boolean, optional): If `true`, the host will send `notifications/tools/list_changed` when server tools change + +These capabilities are independent - an app can have one, both, or neither. + +**TypeScript Types:** + +```typescript +interface McpUiAppCapabilities { + tools?: { + listChanged?: boolean; + }; +} + +interface McpUiHostCapabilities { + serverTools?: { + listChanged?: boolean; + }; + openLinks?: {}; + logging?: {}; +} +``` + ### Extensibility -This specification defines the Minimum Viable Product (MVP) for MCP Apps. Future extensions may include: +This specification defines the Minimum Viable Product (MVP) for MCP Apps. + +**Included in MVP:** + +- **App-Provided Tools:** Apps can register tools via `app.registerTool()` that agents can call + - Bidirectional tool flow (Apps consume server tools AND provide app tools) + - Full lifecycle management (enable/disable/update/remove) + - Schema validation via [Standard Schema](https://standardschema.dev/) (Zod, ArkType, Valibot, …) + - Tool list change notifications **Content Types (deferred from MVP):** - `externalUrl`: Embed external web applications (e.g., `text/uri-list`) -**Advanced Features (see Future Considerations):** +**Advanced Tool Features (future extensions):** + +- Tool namespacing standards and conventions +- Standardized permission model specifications +- Tool categories/tags for organization +- Cross-app tool composition +- Tool marketplace/discovery mechanisms + +**Other Advanced Features (see Future Considerations):** - Support multiple UI resources in a tool response - State persistence and restoration @@ -1727,7 +2357,7 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im - Reuses existing MCP infrastructure (type definitions, error handling, timeouts) - UI developers can use standard MCP SDK (`@modelcontextprotocol/sdk`) or alternatives -- Automatic compatibility with future MCP features (long-running tools, sampling, etc.) +- Automatic compatibility with core MCP features (Tasks for long-running calls, sampling, etc.) - Better auditability through structured JSON-RPC messages - Reduces maintenance burden (no parallel protocol to evolve) @@ -1789,6 +2419,37 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im - **Boolean `private` flag:** Simpler but less flexible; doesn't express model-only tools. - **Flat `ui/visibility` key:** Rejected in favor of nested structure for consistency with future `_meta.ui` fields. +#### 6. App Tool Registration Support + +**Decision:** Enable Apps to register tools using standard MCP `tools/call` and `tools/list` messages, making tools flow bidirectionally between Apps and Hosts. + +**Rationale:** + +- **Semantic introspection:** Apps expose state and operations in a structured, machine-readable form. Hosts have no guaranteed DOM access into an app (which may itself nest cross-origin frames via `frameDomains`), so a tool surface is the only reliable introspection path +- **Protocol reuse:** Reuses existing MCP tool infrastructure (`tools/call`, `tools/list`, `notifications/tools/list_changed`) instead of inventing new message types +- **WebMCP alignment:** Brings WebMCP's vision of "JavaScript functions as tools" to MCP Apps while staying MCP-native +- **Agent-driven interaction:** Agents can pull app state and issue commands on demand, complementing push-style [`ui/update-model-context`](#uiupdate-model-context) +- **Bidirectional symmetry:** Apps act as both MCP clients (calling server tools) and MCP servers (providing app tools), creating clean architectural symmetry +- **Use case coverage:** Enables interactive games, stateful forms, complex workflows, and reusable widgets + +**Alternatives considered:** + +- **Custom app-action API:** Rejected because it would duplicate MCP's existing tool infrastructure and create parallel protocol semantics. Using standard `tools/call` means automatic compatibility with future MCP features and better ecosystem integration. +- **Server-side proxy tools:** Apps could expose operations by having the server register proxy tools that communicate back to the app. Rejected because it doesn't leverage the app's JavaScript execution environment, adds unnecessary round-trips, and couples app functionality to server implementation. +- **Resources instead of tools:** Apps could expose state via `resources/read` rather than tools. Rejected because resources have wrong semantics (passive data retrieval vs. active operations), don't support parameters well, and don't convey operational intent. + +**Security implications:** + +Apps are forward-deployed emanations of server tools, running in the client context. Hosts should consider how to handle tool call approval: + +- Per-app-instance approval (confirm each time a specific app instance calls a tool) +- Per-server approval (approve all apps from a trusted server) +- Per-tool approval (approve based on tool semantics and annotations) +- Clear attribution showing which app instance is calling a tool +- Audit trails for app tool calls + +See [Security Implications: App-Provided Tools Security](#5-app-provided-tools-security) for detailed considerations. + ### Backward Compatibility The proposal builds on the existing core protocol. There are no incompatibilities. @@ -1881,6 +2542,83 @@ const allowAttribute = allowList.join(' '); - Host SHOULD warn users when UI requires external domain access - Host MAY implement global domain allowlists/blocklists +#### 5. App-Provided Tools Security + +Apps can register their own tools that agents can call. Apps are forward-deployed emanations of server tools, running in the client context. Hosts need to decide how to handle approval for app tool calls. + +**Approval Considerations:** + +App-provided tools introduce additional approval considerations: + +- **Tool description accuracy:** Apps may describe tools in ways that don't fully capture side effects +- **Namespace conflicts:** Apps could register tools with names conflicting with server tools +- **Resource consumption:** Apps could register many tools or implement slow callbacks +- **Data validation:** Tool results should match declared schemas +- **Semantic clarity:** Tool operations should be clear from their descriptions + +**Approval Granularity:** + +Hosts have discretion in how they handle app tool call approval: + +1. **Per-app-instance approval:** Confirm each time a specific app instance's tool is called +2. **Per-server approval:** Trust all apps from servers the user has approved +3. **Per-tool approval:** Approve based on tool annotations (e.g., `readOnlyHint`) +4. **Hybrid approaches:** Combine strategies (e.g., auto-approve read-only tools from trusted servers) + +**Host Protections:** + +Hosts SHOULD implement the following protections for app-provided tools: + +1. **Clear Attribution:** + - Display tool source in agent's tool list (e.g., "Tool from TicTacToe App") + - Visually distinguish app tools from server tools in UI + - Show app name and version in tool call confirmations + +2. **User Confirmation:** + - Require explicit user approval for tools with `readOnlyHint: false` + - Consider auto-approving tools with `readOnlyHint: true` after review + - Implement per-app permission settings (always allow, always deny, ask) + +3. **Namespace Management:** + - Recommend or enforce tool name prefixes (e.g., `app:move`, `tictactoe:move`) + - Prevent apps from registering tool names that conflict with server tools + - Document namespace conventions for app developers + +4. **Resource Limits:** + - Limit maximum number of tools per app (recommended: 50) + - Enforce execution timeouts for tool callbacks (recommended: 30 seconds) + - Limit tool result sizes (recommended: 10 MB) + - Throttle `tools/list_changed` notifications to prevent spam + +5. **Audit Trail:** + - Log all app tool registrations with timestamps + - Log all app tool calls with arguments and results + - Provide audit interface for users to review app tool activity + +6. **Result Validation:** + - Validate tool results match declared schemas + - Sanitize result content before displaying to user or agent + - Reject results that appear malicious (e.g., phishing content) + +**Permission Model:** + +Hosts MAY implement different permission levels based on tool annotations: + +| Annotation | Recommended Permission | Example | +|---------------------|------------------------|-------------------| +| `readOnlyHint: true`| Auto-approve (with caution) | `get_board_state()` | +| `readOnlyHint: false` | User confirmation required | `make_move()` | +| No annotation | User confirmation required (safe default) | Any tool | + +**App Tool Lifecycle:** + +App tools MUST be tied to the app's lifecycle: + +- Tools become available once the app advertises the `tools` capability in `ui/initialize` and the host issues `tools/list`; subsequent changes are signaled via `notifications/tools/list_changed` +- Tools automatically disappear when app iframe is torn down +- Hosts MUST NOT persist app tool registrations across sessions +- Calling a tool from a closed app MUST return an error + ### Other risks - **Social engineering:** UI can still display misleading content. Hosts should clearly indicate sandboxed UI boundaries. diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 287e0de..e7b32b3 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -12,6 +12,7 @@ import { ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod/v4"; import { App } from "./app"; import { @@ -514,7 +515,7 @@ describe("App <-> AppBridge integration", () => { }; await app.connect(appTransport); - await expect( + expect( app.updateModelContext({ content: [{ type: "text", text: "Test" }], }), @@ -647,7 +648,7 @@ describe("App <-> AppBridge integration", () => { // Attempting to connect again with a different transport should throw const [, secondBridgeTransport] = InMemoryTransport.createLinkedPair(); - await expect(bridge.connect(secondBridgeTransport)).rejects.toThrow( + expect(bridge.connect(secondBridgeTransport)).rejects.toThrow( "AppBridge is already connected", ); }); @@ -658,7 +659,7 @@ describe("App <-> AppBridge integration", () => { // Attempting to connect again should throw const [secondAppTransport] = InMemoryTransport.createLinkedPair(); - await expect(app.connect(secondAppTransport)).rejects.toThrow( + expect(app.connect(secondAppTransport)).rejects.toThrow( "App is already connected", ); }); @@ -668,7 +669,7 @@ describe("App <-> AppBridge integration", () => { await app.connect(appTransport); // Should throw regardless of whether it's the same or a different transport - await expect(bridge.connect(bridgeTransport)).rejects.toThrow( + expect(bridge.connect(bridgeTransport)).rejects.toThrow( "AppBridge is already connected", ); }); @@ -689,6 +690,1092 @@ describe("App <-> AppBridge integration", () => { }); }); + describe("App tool registration", () => { + beforeEach(async () => { + app = new App( + testAppInfo, + { tools: { listChanged: true } }, + { autoResize: false }, + ); + await bridge.connect(bridgeTransport); + }); + + it("registerTool creates a registered tool", async () => { + const InputSchema = z.object({ name: z.string() }); + const OutputSchema = z.object({ greeting: z.string() }); + + const tool = app.registerTool( + "greet", + { + title: "Greet User", + description: "Greets a user by name", + inputSchema: InputSchema, + outputSchema: OutputSchema, + }, + async (args: any) => ({ + content: [{ type: "text" as const, text: `Hello, ${args.name}!` }], + structuredContent: { greeting: `Hello, ${args.name}!` }, + }), + ); + + expect(tool.title).toBe("Greet User"); + expect(tool.description).toBe("Greets a user by name"); + expect(tool.enabled).toBe(true); + }); + + it("registered tool can be enabled and disabled", async () => { + await app.connect(appTransport); + + const tool = app.registerTool( + "test-tool", + { + description: "Test tool", + }, + async (_extra: any) => ({ content: [] }), + ); + + expect(tool.enabled).toBe(true); + + tool.disable(); + expect(tool.enabled).toBe(false); + + tool.enable(); + expect(tool.enabled).toBe(true); + }); + + it("registered tool can be updated", async () => { + await app.connect(appTransport); + + const tool = app.registerTool( + "test-tool", + { + description: "Original description", + }, + async (_extra: any) => ({ content: [] }), + ); + + expect(tool.description).toBe("Original description"); + + tool.update({ description: "Updated description" }); + expect(tool.description).toBe("Updated description"); + }); + + it("registered tool can be removed", async () => { + await app.connect(appTransport); + + const tool = app.registerTool( + "test-tool", + { + description: "Test tool", + }, + async (_extra: any) => ({ content: [] }), + ); + + tool.remove(); + // Tool should no longer be registered (internal check) + }); + + it("registerTool throws on duplicate name", () => { + app.registerTool("dup", {}, async () => ({ content: [] })); + expect(() => + app.registerTool("dup", {}, async () => ({ content: [] })), + ).toThrow(/already registered/); + }); + + it("enable/disable/update/remove pre-connect do not throw", () => { + const tool = app.registerTool("t", {}, async () => ({ content: [] })); + expect(() => tool.disable()).not.toThrow(); + expect(() => tool.enable()).not.toThrow(); + expect(() => tool.update({ description: "x" })).not.toThrow(); + expect(() => tool.remove()).not.toThrow(); + }); + + it("callback without inputSchema receives extra as first arg", async () => { + await app.connect(appTransport); + let receivedExtra: any; + app.registerTool("noargs", {}, async (extra: any) => { + receivedExtra = extra; + return { content: [] }; + }); + await bridge.callTool({ name: "noargs", arguments: {} }); + expect(receivedExtra).toBeDefined(); + expect(receivedExtra.signal).toBeInstanceOf(AbortSignal); + }); + + it("isError result skips output schema validation", async () => { + await app.connect(appTransport); + app.registerTool( + "errs", + { outputSchema: z.object({ ok: z.boolean() }) }, + async () => ({ + content: [{ type: "text" as const, text: "boom" }], + isError: true, + }), + ); + const res = await bridge.callTool({ name: "errs", arguments: {} }); + expect(res.isError).toBe(true); + expect(res.structuredContent).toBeUndefined(); + }); + + it("stale handle remove() does not delete a re-registered tool", async () => { + const t1 = app.registerTool("phoenix", {}, async () => ({ content: [] })); + t1.remove(); + app.registerTool("phoenix", {}, async () => ({ content: [] })); + t1.remove(); + await app.connect(appTransport); + const list = await bridge.listTools({}); + expect(list.tools.map((t) => t.name)).toContain("phoenix"); + }); + + it("host omitting arguments defaults to empty object", async () => { + await app.connect(appTransport); + let received: unknown; + app.registerTool( + "noargs2", + { inputSchema: z.object({}) }, + async (args) => { + received = args; + return { content: [] }; + }, + ); + await bridge.callTool({ name: "noargs2" }); + expect(received).toEqual({}); + }); + + it("update({inputSchema}) is honored by handler validation", async () => { + await app.connect(appTransport); + const tool = app.registerTool( + "evolving", + { inputSchema: z.object({ a: z.string() }) }, + async (args: any) => ({ + content: [{ type: "text" as const, text: JSON.stringify(args) }], + }), + ); + expect( + bridge.callTool({ name: "evolving", arguments: { a: 123 } }), + ).rejects.toThrow(/Invalid input/); + tool.update({ inputSchema: z.object({ a: z.number() }) }); + const result = await bridge.callTool({ + name: "evolving", + arguments: { a: 123 }, + }); + expect(result.content[0]).toEqual({ type: "text", text: '{"a":123}' }); + }); + + it("tool throws error when disabled and called", async () => { + await app.connect(appTransport); + + const tool = app.registerTool( + "test-tool", + { + description: "Test tool", + }, + async (_extra: any) => ({ content: [] }), + ); + + tool.disable(); + + const mockExtra = { + signal: new AbortController().signal, + requestId: "test", + sendNotification: async () => {}, + sendRequest: async () => ({}), + } as any; + + expect((tool.handler as any)(mockExtra)).rejects.toThrow( + "Tool test-tool is disabled", + ); + }); + + it("tool validates input schema", async () => { + const InputSchema = z.object({ name: z.string() }); + + const tool = app.registerTool( + "greet", + { + inputSchema: InputSchema, + }, + async (args: any) => ({ + content: [{ type: "text" as const, text: `Hello, ${args.name}!` }], + }), + ); + + // Create a mock RequestHandlerExtra + const mockExtra = { + signal: new AbortController().signal, + requestId: "test", + sendNotification: async () => {}, + sendRequest: async () => ({}), + } as any; + + // Valid input should work + expect( + (tool.handler as any)({ name: "Alice" }, mockExtra), + ).resolves.toBeDefined(); + + // Invalid input should fail + expect( + (tool.handler as any)({ invalid: "field" }, mockExtra), + ).rejects.toThrow("Invalid input for tool greet"); + }); + + it("tool validates output schema", async () => { + const OutputSchema = z.object({ greeting: z.string() }); + + const tool = app.registerTool( + "greet", + { + outputSchema: OutputSchema, + }, + async (_extra: any) => ({ + content: [{ type: "text" as const, text: "Hello!" }], + structuredContent: { greeting: "Hello!" }, + }), + ); + + // Create a mock RequestHandlerExtra + const mockExtra = { + signal: new AbortController().signal, + requestId: "test", + sendNotification: async () => {}, + sendRequest: async () => ({}), + } as any; + + // Valid output should work + expect((tool.handler as any)(mockExtra)).resolves.toBeDefined(); + }); + + it("tool enable/disable/update/remove trigger sendToolListChanged", async () => { + await app.connect(appTransport); + + const tool = app.registerTool( + "test-tool", + { + description: "Test tool", + }, + async (_extra: any) => ({ content: [] }), + ); + + // The methods should not throw when connected + expect(() => tool.disable()).not.toThrow(); + expect(() => tool.enable()).not.toThrow(); + expect(() => tool.update({ description: "Updated" })).not.toThrow(); + expect(() => tool.remove()).not.toThrow(); + }); + }); + + describe("AppBridge -> App tool requests", () => { + beforeEach(async () => { + await bridge.connect(bridgeTransport); + }); + + it("bridge.callTool calls app.oncalltool handler", async () => { + // App needs tool capabilities to handle tool calls + const appCapabilities = { tools: {} }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const receivedCalls: unknown[] = []; + + app.oncalltool = async (params) => { + receivedCalls.push(params); + return { + content: [{ type: "text", text: `Executed: ${params.name}` }], + }; + }; + + await app.connect(appTransport); + + const result = await bridge.callTool({ + name: "test-tool", + arguments: { foo: "bar" }, + }); + + expect(receivedCalls).toHaveLength(1); + expect(receivedCalls[0]).toMatchObject({ + name: "test-tool", + arguments: { foo: "bar" }, + }); + expect(result.content).toEqual([ + { type: "text", text: "Executed: test-tool" }, + ]); + }); + + it("bridge.listTools calls app.onlisttools handler", async () => { + // App needs tool capabilities to handle tool list requests + const appCapabilities = { tools: {} }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const receivedCalls: unknown[] = []; + + app.onlisttools = async (params, _extra) => { + receivedCalls.push(params); + return { + tools: [ + { + name: "tool1", + description: "First tool", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "tool2", + description: "Second tool", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "tool3", + description: "Third tool", + inputSchema: { type: "object", properties: {} }, + }, + ], + }; + }; + + await app.connect(appTransport); + + const result = await bridge.listTools({}); + + expect(receivedCalls).toHaveLength(1); + expect(result.tools).toHaveLength(3); + expect(result.tools[0].name).toBe("tool1"); + expect(result.tools[1].name).toBe("tool2"); + expect(result.tools[2].name).toBe("tool3"); + }); + }); + + describe("App tool capabilities", () => { + it("App with tool capabilities can handle tool calls", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const receivedCalls: unknown[] = []; + app.oncalltool = async (params) => { + receivedCalls.push(params); + return { + content: [{ type: "text", text: "Success" }], + }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + await bridge.callTool({ + name: "test-tool", + arguments: {}, + }); + + expect(receivedCalls).toHaveLength(1); + }); + + it("registered tool is invoked via oncalltool", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const tool = app.registerTool( + "greet", + { + description: "Greets user", + inputSchema: z.object({ name: z.string() }), + }, + async (args: any) => ({ + content: [{ type: "text" as const, text: `Hello, ${args.name}!` }], + }), + ); + + app.oncalltool = async (params, extra) => { + if (params.name === "greet") { + return await (tool.handler as any)(params.arguments || {}, extra); + } + throw new Error(`Unknown tool: ${params.name}`); + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + const result = await bridge.callTool({ + name: "greet", + arguments: { name: "Alice" }, + }); + + expect(result.content).toEqual([{ type: "text", text: "Hello, Alice!" }]); + }); + }); + + describe("Automatic request handlers", () => { + beforeEach(async () => { + await bridge.connect(bridgeTransport); + }); + + describe("oncalltool automatic handler", () => { + it("automatically calls registered tool without manual oncalltool setup", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + // Register a tool + app.registerTool( + "greet", + { + description: "Greets user", + inputSchema: z.object({ name: z.string() }), + }, + async (args: any) => ({ + content: [{ type: "text" as const, text: `Hello, ${args.name}!` }], + }), + ); + + await app.connect(appTransport); + + // Call the tool through bridge - should work automatically + const result = await bridge.callTool({ + name: "greet", + arguments: { name: "Bob" }, + }); + + expect(result.content).toEqual([{ type: "text", text: "Hello, Bob!" }]); + }); + + it("throws error when calling non-existent tool", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + // Register a tool to initialize handlers + app.registerTool("existing-tool", {}, async (_args: any) => ({ + content: [], + })); + + await app.connect(appTransport); + + // Try to call a tool that doesn't exist + expect( + bridge.callTool({ + name: "nonexistent", + arguments: {}, + }), + ).rejects.toThrow("Tool nonexistent not found"); + }); + + it("handles multiple registered tools correctly", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + // Register multiple tools + app.registerTool( + "add", + { + description: "Add two numbers", + inputSchema: z.object({ a: z.number(), b: z.number() }), + }, + async (args: any) => ({ + content: [ + { + type: "text" as const, + text: `Result: ${args.a + args.b}`, + }, + ], + structuredContent: { result: args.a + args.b }, + }), + ); + + app.registerTool( + "multiply", + { + description: "Multiply two numbers", + inputSchema: z.object({ a: z.number(), b: z.number() }), + }, + async (args: any) => ({ + content: [ + { + type: "text" as const, + text: `Result: ${args.a * args.b}`, + }, + ], + structuredContent: { result: args.a * args.b }, + }), + ); + + await app.connect(appTransport); + + // Call first tool + const addResult = await bridge.callTool({ + name: "add", + arguments: { a: 5, b: 3 }, + }); + expect(addResult.content).toEqual([ + { type: "text", text: "Result: 8" }, + ]); + + // Call second tool + const multiplyResult = await bridge.callTool({ + name: "multiply", + arguments: { a: 5, b: 3 }, + }); + expect(multiplyResult.content).toEqual([ + { type: "text", text: "Result: 15" }, + ]); + }); + + it("respects tool enable/disable state", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const tool = app.registerTool( + "test-tool", + { + description: "Test tool", + }, + async (_args: any) => ({ + content: [{ type: "text" as const, text: "Success" }], + }), + ); + + await app.connect(appTransport); + + // Should work when enabled + expect( + bridge.callTool({ name: "test-tool", arguments: {} }), + ).resolves.toBeDefined(); + + // Disable tool + tool.disable(); + + // Should throw when disabled + expect( + bridge.callTool({ name: "test-tool", arguments: {} }), + ).rejects.toThrow("Tool test-tool is disabled"); + }); + + it("validates input schema through automatic handler", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + app.registerTool( + "strict-tool", + { + description: "Requires specific input", + inputSchema: z.object({ + required: z.string(), + optional: z.number().optional(), + }) as any, + }, + async (args: any) => ({ + content: [{ type: "text" as const, text: `Got: ${args.required}` }], + }), + ); + + await app.connect(appTransport); + + // Valid input should work + expect( + bridge.callTool({ + name: "strict-tool", + arguments: { required: "hello" }, + }), + ).resolves.toBeDefined(); + + // Invalid input should fail + expect( + bridge.callTool({ + name: "strict-tool", + arguments: { wrong: "field" }, + }), + ).rejects.toThrow("Invalid input for tool strict-tool"); + }); + + it("validates output schema through automatic handler", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + app.registerTool( + "validated-output", + { + description: "Has output validation", + outputSchema: z.object({ + status: z.enum(["success", "error"]), + }) as any, + }, + async (_args: any) => ({ + content: [{ type: "text" as const, text: "Done" }], + structuredContent: { status: "success" }, + }), + ); + + await app.connect(appTransport); + + // Valid output should work + const result = await bridge.callTool({ + name: "validated-output", + arguments: {}, + }); + expect(result).toBeDefined(); + }); + + it("works after tool is removed and re-registered", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const tool = app.registerTool( + "dynamic-tool", + {}, + async (_args: any) => ({ + content: [{ type: "text" as const, text: "Version 1" }], + }), + ); + + await app.connect(appTransport); + + // First version + let result = await bridge.callTool({ + name: "dynamic-tool", + arguments: {}, + }); + expect(result.content).toEqual([{ type: "text", text: "Version 1" }]); + + // Remove tool + tool.remove(); + + // Should fail after removal + expect( + bridge.callTool({ name: "dynamic-tool", arguments: {} }), + ).rejects.toThrow("Tool dynamic-tool not found"); + + // Re-register with different behavior + app.registerTool("dynamic-tool", {}, async (_args: any) => ({ + content: [{ type: "text" as const, text: "Version 2" }], + })); + + // Should work with new version + result = await bridge.callTool({ + name: "dynamic-tool", + arguments: {}, + }); + expect(result.content).toEqual([{ type: "text", text: "Version 2" }]); + }); + }); + + describe("onlisttools automatic handler", () => { + it("automatically returns list of registered tool names", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + // Register some tools + app.registerTool("tool1", {}, async (_args: any) => ({ + content: [], + })); + app.registerTool("tool2", {}, async (_args: any) => ({ + content: [], + })); + app.registerTool("tool3", {}, async (_args: any) => ({ + content: [], + })); + + await app.connect(appTransport); + + const result = await bridge.listTools({}); + + expect(result.tools).toHaveLength(3); + expect(result.tools.map((t) => t.name)).toContain("tool1"); + expect(result.tools.map((t) => t.name)).toContain("tool2"); + expect(result.tools.map((t) => t.name)).toContain("tool3"); + }); + + it("emits core MCP Tool fields (title, outputSchema only when provided)", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + app.registerTool( + "with-output", + { + title: "With Output", + description: "has structured output", + outputSchema: z.object({ ok: z.boolean() }), + }, + async () => ({ + content: [], + structuredContent: { ok: true }, + }), + ); + app.registerTool( + "no-output", + { description: "no structured output" }, + async () => ({ content: [] }), + ); + + await app.connect(appTransport); + const result = await bridge.listTools({}); + const byName = Object.fromEntries(result.tools.map((t) => [t.name, t])); + + expect(byName["with-output"].title).toBe("With Output"); + expect(byName["with-output"].inputSchema).toBeDefined(); + expect(byName["with-output"].outputSchema).toBeDefined(); + // outputSchema is optional in core MCP — omitted when not declared + expect(byName["no-output"]).not.toHaveProperty("outputSchema"); + expect(byName["no-output"].inputSchema).toBeDefined(); + }); + + it("accepts any Standard Schema implementation, not only zod", async () => { + // Hand-rolled StandardSchemaWithJSON — proves registerTool has no + // zod-specific runtime path. Any library implementing the spec + // (ArkType, Valibot, …) works the same way. + type Point = { x: number; y: number }; + const PointSchema = { + "~standard": { + version: 1 as const, + vendor: "test", + types: undefined as + | undefined + | { readonly input: Point; readonly output: Point }, + validate: (v: unknown) => + typeof v === "object" && + v !== null && + typeof (v as any).x === "number" && + typeof (v as any).y === "number" + ? { value: v as { x: number; y: number } } + : { issues: [{ message: "expected {x:number,y:number}" }] }, + jsonSchema: { + input: () => ({ + type: "object", + properties: { x: { type: "number" }, y: { type: "number" } }, + required: ["x", "y"], + }), + output: () => ({ + type: "object", + properties: { x: { type: "number" }, y: { type: "number" } }, + }), + }, + }, + }; + + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + app.registerTool( + "translate", + { inputSchema: PointSchema, outputSchema: PointSchema }, + async ({ x, y }) => ({ + content: [], + structuredContent: { x: x + 1, y: y + 1 }, + }), + ); + await app.connect(appTransport); + + const list = await bridge.listTools({}); + expect(list.tools[0].inputSchema).toEqual({ + type: "object", + properties: { x: { type: "number" }, y: { type: "number" } }, + required: ["x", "y"], + }); + expect(list.tools[0].outputSchema).toBeDefined(); + + const ok = await bridge.callTool({ + name: "translate", + arguments: { x: 1, y: 2 }, + }); + expect(ok.structuredContent).toEqual({ x: 2, y: 3 }); + + expect( + bridge.callTool({ name: "translate", arguments: { x: "bad" } }), + ).rejects.toThrow(/Invalid input for tool translate/); + }); + + it("falls back to z.toJSONSchema for zod schemas lacking ~standard.jsonSchema (zod v3.25.x)", async () => { + // zod v3.25 implements ~standard.validate but not ~standard.jsonSchema. + // Simulate by stripping jsonSchema from a real zod schema. + const v4Schema = z.object({ q: z.string() }); + const zod3LikeSchema = Object.assign(Object.create(v4Schema), { + "~standard": { + version: 1 as const, + vendor: "zod", + validate: v4Schema["~standard"].validate, + types: undefined as + | undefined + | { + readonly input: { q: string }; + readonly output: { q: string }; + }, + // no jsonSchema + }, + }); + + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + app.registerTool( + "search", + { inputSchema: zod3LikeSchema }, + async ({ q }: { q: string }) => ({ + content: [{ type: "text" as const, text: q }], + }), + ); + await app.connect(appTransport); + + const list = await bridge.listTools({}); + expect(list.tools[0].inputSchema.properties).toHaveProperty("q"); + + // Non-zod schema without jsonSchema → listTools rejects with guidance. + app.registerTool( + "broken", + { + inputSchema: { + "~standard": { + version: 1 as const, + vendor: "mystery", + validate: () => ({ value: {} }), + }, + }, + }, + async () => ({ content: [] }), + ); + expect(bridge.listTools({})).rejects.toThrow( + /does not implement Standard JSON Schema/, + ); + }); + + it("returns empty list when no tools registered", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + // Register a tool to ensure handlers are initialized + const dummyTool = app.registerTool("dummy", {}, async () => ({ + content: [], + })); + + await app.connect(appTransport); + + // Remove the tool after connecting + dummyTool.remove(); + + const result = await bridge.listTools({}); + + expect(result.tools).toEqual([]); + }); + + it("updates list when tools are added", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + await app.connect(appTransport); + + // Register then remove a tool to initialize handlers + const dummy = app.registerTool("init", {}, async () => ({ + content: [], + })); + dummy.remove(); + + // Initially no tools + let result = await bridge.listTools({}); + expect(result.tools).toEqual([]); + + // Add a tool + app.registerTool("new-tool", {}, async (_args: any) => ({ + content: [], + })); + + // Should now include the new tool + result = await bridge.listTools({}); + expect(result.tools.map((t) => t.name)).toEqual(["new-tool"]); + + // Add another tool + app.registerTool("another-tool", {}, async (_args: any) => ({ + content: [], + })); + + // Should now include both tools + result = await bridge.listTools({}); + expect(result.tools).toHaveLength(2); + expect(result.tools.map((t) => t.name)).toContain("new-tool"); + expect(result.tools.map((t) => t.name)).toContain("another-tool"); + }); + + it("updates list when tools are removed", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + const tool1 = app.registerTool("tool1", {}, async (_args: any) => ({ + content: [], + })); + const tool2 = app.registerTool("tool2", {}, async (_args: any) => ({ + content: [], + })); + app.registerTool("tool3", {}, async (_args: any) => ({ + content: [], + })); + + await app.connect(appTransport); + + // Initially all three tools + let result = await bridge.listTools({}); + expect(result.tools).toHaveLength(3); + + // Remove one tool + tool2.remove(); + + // Should now have two tools + result = await bridge.listTools({}); + expect(result.tools).toHaveLength(2); + expect(result.tools.map((t) => t.name)).toContain("tool1"); + expect(result.tools.map((t) => t.name)).toContain("tool3"); + expect(result.tools.map((t) => t.name)).not.toContain("tool2"); + + // Remove another tool + tool1.remove(); + + // Should now have one tool + result = await bridge.listTools({}); + expect(result.tools.map((t) => t.name)).toEqual(["tool3"]); + }); + + it("only includes enabled tools in list", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + app.registerTool("enabled-tool", {}, async (_args: any) => ({ + content: [], + })); + const tool2 = app.registerTool( + "disabled-tool", + {}, + async (_args: any) => ({ + content: [], + }), + ); + + await app.connect(appTransport); + + // Disable one tool after connecting + tool2.disable(); + + const result = await bridge.listTools({}); + + // Only enabled tool should be in the list + expect(result.tools).toHaveLength(1); + expect(result.tools.map((t) => t.name)).toContain("enabled-tool"); + expect(result.tools.map((t) => t.name)).not.toContain("disabled-tool"); + }); + }); + + describe("Integration: automatic handlers with tool lifecycle", () => { + it("handles complete tool lifecycle: register -> call -> update -> call -> remove", async () => { + const appCapabilities = { tools: { listChanged: true } }; + app = new App(testAppInfo, appCapabilities, { autoResize: false }); + + await app.connect(appTransport); + + // Register tool + const tool = app.registerTool( + "counter", + { + description: "A counter tool", + }, + async (_args: any) => ({ + content: [{ type: "text" as const, text: "Count: 1" }], + structuredContent: { count: 1 }, + }), + ); + + // List should include the tool + let listResult = await bridge.listTools({}); + expect(listResult.tools.map((t) => t.name)).toContain("counter"); + + // Call the tool + let callResult = await bridge.callTool({ + name: "counter", + arguments: {}, + }); + expect(callResult.content).toEqual([ + { type: "text", text: "Count: 1" }, + ]); + + // Update tool description + tool.update({ description: "An updated counter tool" }); + + // Should still be callable + callResult = await bridge.callTool({ + name: "counter", + arguments: {}, + }); + expect(callResult).toBeDefined(); + + // Remove tool + tool.remove(); + + // Should no longer be in list + listResult = await bridge.listTools({}); + expect(listResult.tools.map((t) => t.name)).not.toContain("counter"); + + // Should no longer be callable + expect( + bridge.callTool({ name: "counter", arguments: {} }), + ).rejects.toThrow("Tool counter not found"); + }); + + it("multiple apps can have separate tool registries", async () => { + const appCapabilities = { tools: { listChanged: true } }; + + // Create two separate apps + const app1 = new App( + { name: "App1", version: "1.0.0" }, + appCapabilities, + { autoResize: false }, + ); + const app2 = new App( + { name: "App2", version: "1.0.0" }, + appCapabilities, + { autoResize: false }, + ); + + // Create separate transports for each app + const [app1Transport, bridge1Transport] = + InMemoryTransport.createLinkedPair(); + const [app2Transport, bridge2Transport] = + InMemoryTransport.createLinkedPair(); + + const bridge1 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + + // Register different tools in each app + app1.registerTool("app1-tool", {}, async (_args: any) => ({ + content: [{ type: "text" as const, text: "From App1" }], + })); + + app2.registerTool("app2-tool", {}, async (_args: any) => ({ + content: [{ type: "text" as const, text: "From App2" }], + })); + + await bridge1.connect(bridge1Transport); + await bridge2.connect(bridge2Transport); + await app1.connect(app1Transport); + await app2.connect(app2Transport); + + // Each app should only see its own tools + const list1 = await bridge1.listTools({}); + expect(list1.tools.map((t) => t.name)).toEqual(["app1-tool"]); + + const list2 = await bridge2.listTools({}); + expect(list2.tools.map((t) => t.name)).toEqual(["app2-tool"]); + + // Each app should only be able to call its own tools + expect( + bridge1.callTool({ name: "app1-tool", arguments: {} }), + ).resolves.toBeDefined(); + + expect( + bridge1.callTool({ name: "app2-tool", arguments: {} }), + ).rejects.toThrow("Tool app2-tool not found"); + + // Clean up + await app1Transport.close(); + await bridge1Transport.close(); + await app2Transport.close(); + await bridge2Transport.close(); + }); + }); + }); + describe("AppBridge without MCP client (manual handlers)", () => { let app: App; let bridge: AppBridge; @@ -808,7 +1895,7 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - await expect( + expect( // @ts-expect-error intentionally testing wrong usage app.callServerTool("my_tool"), ).rejects.toThrow( diff --git a/src/app.examples.ts b/src/app.examples.ts index f6e8b56..81bdd44 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -304,9 +304,21 @@ function App_onlisttools_returnTools(app: App) { app.onlisttools = async (params, extra) => { return { tools: [ - { name: "greet", inputSchema: { type: "object" as const } }, - { name: "calculate", inputSchema: { type: "object" as const } }, - { name: "format", inputSchema: { type: "object" as const } }, + { + name: "greet", + description: "Greet the user", + inputSchema: { type: "object" as const }, + }, + { + name: "calculate", + description: "Perform a calculation", + inputSchema: { type: "object" as const }, + }, + { + name: "format", + description: "Format text", + inputSchema: { type: "object" as const }, + }, ], }; }; diff --git a/src/app.ts b/src/app.ts index 473d345..09efadf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,6 @@ import { type RequestOptions, + mergeCapabilities, ProtocolOptions, } from "@modelcontextprotocol/sdk/shared/protocol.js"; @@ -26,6 +27,9 @@ import { ReadResourceRequest, ReadResourceResult, ReadResourceResultSchema, + Tool, + ToolAnnotations, + ToolListChangedNotification, } from "@modelcontextprotocol/sdk/types.js"; import { AppNotification, AppRequest, AppResult } from "./types"; import { ProtocolWithEvents } from "./events"; @@ -65,6 +69,16 @@ import { McpUiRequestDisplayModeResultSchema, } from "./types"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { + StandardSchemaV1, + standardSchemaToJsonSchema, + validateStandardSchema, +} from "./standard-schema"; + +export type { + StandardSchemaV1, + StandardSchemaWithJSON, +} from "./standard-schema"; export { PostMessageTransport } from "./message-transport"; export * from "./types"; @@ -179,6 +193,66 @@ type RequestHandlerExtra = Parameters< Parameters[1] >[1]; +/** + * Result of an app-registered tool callback. When `Out` is provided, + * `structuredContent` is required and typed (unless `isError: true`). + */ +export type AppToolResult< + Out extends StandardSchemaV1 | undefined = undefined, +> = Out extends StandardSchemaV1 + ? + | (CallToolResult & { + structuredContent: StandardSchemaV1.InferOutput; + isError?: false; + }) + | (CallToolResult & { isError: true }) + : CallToolResult; + +/** + * Callback for an app-registered tool. When `In` is provided, `args` is the + * validated/parsed input; when `In` is `undefined`, the callback receives only + * `extra`. When `Out` is provided, the return's `structuredContent` is typed. + * + * Mirrors `ToolCallback` from `@modelcontextprotocol/sdk/server/mcp.js` but is + * parameterized over {@link StandardSchemaV1} instead of zod, so any + * Standard-Schema-compatible library (Zod, ArkType, Valibot, …) can be used. + */ +export type AppToolCallback< + In extends StandardSchemaV1 | undefined = undefined, + Out extends StandardSchemaV1 | undefined = undefined, +> = In extends StandardSchemaV1 + ? ( + args: StandardSchemaV1.InferOutput, + extra: RequestHandlerExtra, + ) => AppToolResult | Promise> + : ( + extra: RequestHandlerExtra, + ) => AppToolResult | Promise>; + +/** + * Handle returned by {@link App.registerTool}. Mirrors `RegisteredTool` from + * `@modelcontextprotocol/sdk/server/mcp.js` but stores + * {@link StandardSchemaV1} schemas. + */ +export type RegisteredAppTool = { + title?: string; + description?: string; + inputSchema?: StandardSchemaV1; + outputSchema?: StandardSchemaV1; + annotations?: ToolAnnotations; + _meta?: Record; + enabled: boolean; + enable(): void; + disable(): void; + remove(): void; + update(updates: Partial>): void; + /** @internal */ + handler: ( + args: unknown, + extra: RequestHandlerExtra, + ) => Promise; +}; + /** * Maps DOM-style event names to their notification `params` types. * @@ -262,6 +336,7 @@ export class App extends ProtocolWithEvents< private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; + private _registeredTools: { [name: string]: RegisteredAppTool } = {}; private _initializedSent = false; /** @@ -340,6 +415,177 @@ export class App extends ProtocolWithEvents< this.setEventHandler("hostcontextchanged", undefined); } + private registerCapabilities(capabilities: McpUiAppCapabilities): void { + if (this.transport) { + throw new Error( + "Cannot register capabilities after transport is established", + ); + } + this._capabilities = mergeCapabilities(this._capabilities, capabilities); + } + + registerTool< + OutputArgs extends undefined | StandardSchemaV1 = undefined, + InputArgs extends undefined | StandardSchemaV1 = undefined, + >( + name: string, + config: { + title?: string; + description?: string; + inputSchema?: InputArgs; + outputSchema?: OutputArgs; + annotations?: ToolAnnotations; + _meta?: Record; + }, + cb: AppToolCallback, + ): RegisteredAppTool { + if (this._registeredTools[name]) { + throw new Error(`Tool ${name} is already registered`); + } + const app = this; + const notify = () => { + if (app._initializedSent && app._capabilities.tools?.listChanged) { + void app.sendToolListChanged(); + } + }; + // Arity is fixed at registration time even if inputSchema is later + // update()d, since cb's signature can't change. + const cbTakesArgs = config.inputSchema !== undefined; + const registeredTool: RegisteredAppTool = { + title: config.title, + description: config.description, + inputSchema: config.inputSchema, + outputSchema: config.outputSchema, + annotations: config.annotations, + _meta: config._meta, + enabled: true, + enable(): void { + this.enabled = true; + notify(); + }, + disable(): void { + this.enabled = false; + notify(); + }, + update(updates) { + Object.assign(this, updates); + notify(); + }, + remove() { + if (app._registeredTools[name] !== registeredTool) return; + delete app._registeredTools[name]; + notify(); + }, + handler: async (rawArgs, extra) => { + if (!registeredTool.enabled) { + throw new Error(`Tool ${name} is disabled`); + } + let result: CallToolResult; + if (cbTakesArgs) { + const schema = registeredTool.inputSchema; + const parsedArgs = schema + ? await validateStandardSchema( + schema, + rawArgs ?? {}, + `Invalid input for tool ${name}: `, + ) + : (rawArgs ?? {}); + result = await ( + cb as AppToolCallback + )(parsedArgs, extra); + } else { + result = await (cb as AppToolCallback)( + extra, + ); + } + if (registeredTool.outputSchema && !result.isError) { + result.structuredContent = (await validateStandardSchema( + registeredTool.outputSchema, + result.structuredContent, + `Invalid output for tool ${name}: `, + )) as CallToolResult["structuredContent"]; + } + return result; + }, + }; + + this._registeredTools[name] = registeredTool; + + // Auto-register tools capability so setRequestHandler's capability check + // passes. Mirrors McpServer.registerTool behavior — callers shouldn't need + // to declare { tools: {} } in the constructor just to use registerTool. + // Only do this pre-connect; post-connect the capability was already + // advertised (or wasn't) and can't change. + if (!this._capabilities.tools && !this.transport) { + this.registerCapabilities({ tools: { listChanged: true } }); + } + + this.ensureToolHandlersInitialized(); + notify(); + return registeredTool; + } + + private _toolHandlersInitialized = false; + private ensureToolHandlersInitialized(): void { + if (this._toolHandlersInitialized) { + return; + } + this._toolHandlersInitialized = true; + + this.oncalltool = async (params, extra) => { + const tool = this._registeredTools[params.name]; + if (!tool) { + throw new Error(`Tool ${params.name} not found`); + } + return tool.handler(params.arguments, extra); + }; + this.onlisttools = async (_params, _extra) => { + const tools: Tool[] = await Promise.all( + Object.entries(this._registeredTools) + .filter(([_, tool]) => tool.enabled) + .map(async ([name, tool]) => { + const result: Tool = { + name, + title: tool.title, + description: tool.description, + inputSchema: (tool.inputSchema + ? await standardSchemaToJsonSchema(tool.inputSchema, "input") + : { + type: "object" as const, + properties: {}, + }) as Tool["inputSchema"], + }; + // outputSchema is optional in core MCP — only emit when the app + // provided one, otherwise hosts would assume structuredContent. + if (tool.outputSchema) { + result.outputSchema = (await standardSchemaToJsonSchema( + tool.outputSchema, + "output", + )) as Tool["outputSchema"]; + } + if (tool.annotations) { + result.annotations = tool.annotations; + } + if (tool._meta) { + result._meta = tool._meta; + } + return result; + }), + ); + return { tools }; + }; + } + + async sendToolListChanged( + params: ToolListChangedNotification["params"] = {}, + ): Promise { + this._assertInitialized("sendToolListChanged"); + await this.notification({ + method: "notifications/tools/list_changed", + params, + }); + } + /** * Get the host's capabilities discovered during initialization. * @@ -765,9 +1011,21 @@ export class App extends ProtocolWithEvents< * app.onlisttools = async (params, extra) => { * return { * tools: [ - * { name: "greet", inputSchema: { type: "object" as const } }, - * { name: "calculate", inputSchema: { type: "object" as const } }, - * { name: "format", inputSchema: { type: "object" as const } }, + * { + * name: "greet", + * description: "Greet the user", + * inputSchema: { type: "object" as const }, + * }, + * { + * name: "calculate", + * description: "Perform a calculation", + * inputSchema: { type: "object" as const }, + * }, + * { + * name: "format", + * description: "Format text", + * inputSchema: { type: "object" as const }, + * }, * ], * }; * }; @@ -844,7 +1102,7 @@ export class App extends ProtocolWithEvents< * Verify that the app supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(method: AppNotification["method"]): void { + assertNotificationCapability(_method: AppNotification["method"]): void { // TODO } diff --git a/src/server/index.ts b/src/server/index.ts index 87209ef..c90514a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -52,6 +52,7 @@ import type { AnySchema, ZodRawShapeCompat, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; +import type { StandardSchemaWithJSON } from "../standard-schema"; import type { ClientCapabilities, ReadResourceResult, @@ -69,8 +70,8 @@ export type { ResourceMetadata, ToolCallback }; export interface ToolConfig { title?: string; description?: string; - inputSchema?: ZodRawShapeCompat | AnySchema; - outputSchema?: ZodRawShapeCompat | AnySchema; + inputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON; + outputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON; annotations?: ToolAnnotations; _meta?: Record; } @@ -214,8 +215,9 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool */ export function registerAppTool< - OutputArgs extends ZodRawShapeCompat | AnySchema, - InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, + OutputArgs extends ZodRawShapeCompat | StandardSchemaWithJSON, + InputArgs extends undefined | ZodRawShapeCompat | StandardSchemaWithJSON = + undefined, >( server: Pick, name: string, @@ -223,7 +225,15 @@ export function registerAppTool< inputSchema?: InputArgs; outputSchema?: OutputArgs; }, - cb: ToolCallback, + // The widened constraint signals the v2 API shape, but NOTE: McpServer in + // sdk@1.x still calls zod internals at runtime, so non-zod schemas will fail + // here until we depend on sdk v2. Zod (which all current callers use) is + // unaffected. The cast below bridges the 1.x type signature. + cb: ToolCallback< + InputArgs extends undefined | ZodRawShapeCompat | AnySchema + ? InputArgs + : AnySchema + >, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key @@ -241,7 +251,14 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); + // Cast bridges the widened StandardSchemaWithJSON constraint to the + // sdk@1.x zod-typed signature. Drops once we depend on sdk v2. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return server.registerTool( + name, + { ...config, _meta: normalizedMeta } as any, + cb as any, + ); } export type McpUiReadResourceResult = ReadResourceResult & { diff --git a/src/standard-schema.ts b/src/standard-schema.ts new file mode 100644 index 0000000..4bb85de --- /dev/null +++ b/src/standard-schema.ts @@ -0,0 +1,93 @@ +import type { + StandardJSONSchemaV1, + StandardSchemaV1, + StandardTypedV1, +} from "@standard-schema/spec"; + +export type { StandardJSONSchemaV1, StandardSchemaV1, StandardTypedV1 }; + +// TODO(sdk-v2): once @modelcontextprotocol/core v2 is stable, import +// StandardSchemaWithJSON / standardSchemaToJsonSchema / validateStandardSchema +// from there and delete this file. At that point decide whether to tighten +// App.registerTool to StandardSchemaWithJSON (drops zod 3 from the peer range +// and the lazy z.toJSONSchema fallback below). + +/** + * A schema that implements both Standard Schema (validation) and Standard JSON + * Schema (serialization). Zod v4, ArkType, and Valibot (via + * `@valibot/to-json-schema`) all satisfy this. + * + * Mirrors the type of the same name in `@modelcontextprotocol/core` v2 so that + * bumping to that package later is a drop-in import swap. + * + * @see https://standardschema.dev/ + * @see https://github.com/modelcontextprotocol/typescript-sdk/pull/1689 + */ +export interface StandardSchemaWithJSON { + readonly "~standard": StandardSchemaV1.Props & + StandardJSONSchemaV1.Props; +} + +export namespace StandardSchemaWithJSON { + export type InferInput = + StandardTypedV1.InferInput; + export type InferOutput = + StandardTypedV1.InferOutput; +} + +/** JSON-Schema target draft used for tool input/output schemas (matches core MCP). */ +const TARGET = { target: "draft-2020-12" } as const; + +/** + * Serialize a Standard Schema to JSON Schema for the given direction. + * + * Uses `~standard.jsonSchema` when present (zod v4, ArkType, Valibot, …). + * Falls back to a lazy `zod/v4` import for zod v3.25.x — which implements + * `~standard.validate` but not yet `~standard.jsonSchema` — so the existing + * `^3.25.0 || ^4.0.0` peer range keeps working. Non-zod schemas without + * `jsonSchema` throw. + */ +export async function standardSchemaToJsonSchema( + schema: StandardSchemaV1, + io: "input" | "output", +): Promise> { + const std = schema["~standard"] as Partial< + StandardSchemaWithJSON["~standard"] + >; + if (std.jsonSchema) { + return std.jsonSchema[io](TARGET); + } + if (std.vendor === "zod") { + const { z } = await import("zod/v4"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridging StandardSchemaV1 → zod's $ZodType for the v3.25 fallback + return z.toJSONSchema(schema as any, { io }); + } + throw new Error( + `Schema (vendor: ${std.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). ` + + `Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`, + ); +} + +/** + * Validate a value against a Standard Schema. Returns the parsed value on + * success or throws with a formatted issue list (optionally prefixed). + */ +export async function validateStandardSchema( + schema: S, + value: unknown, + errorPrefix = "", +): Promise> { + const result = await schema["~standard"].validate(value); + if (result.issues) { + const msg = result.issues + .map((i) => { + const path = i.path + ?.map((p) => (typeof p === "object" ? p.key : p)) + .join("."); + return path ? `${path}: ${i.message}` : i.message; + }) + .join("; "); + throw new Error(errorPrefix + msg); + } + return result.value; +} diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 793737c..4c5baa3 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -28,7 +28,7 @@ const config = { ], excludePrivate: true, excludeInternal: false, - intentionallyNotExported: ["MethodSchema"], + intentionallyNotExported: ["MethodSchema", "RequestHandlerExtra"], blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false, From 4995746759e01e10481e61cc33ebb227b7043450 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 13:49:07 +0100 Subject: [PATCH 27/97] 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 --- src/generated/schema.json | 6 ++++++ src/generated/schema.ts | 11 +++++++++++ src/spec.types.ts | 11 +++++++++++ 3 files changed, 28 insertions(+) diff --git a/src/generated/schema.json b/src/generated/schema.json index a72ec2d..80b4ac6 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -5300,6 +5300,12 @@ ], "description": "Tool visibility scope - who can access the tool." } + }, + "csp": { + "not": {} + }, + "permissions": { + "not": {} } }, "additionalProperties": false diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 707e185..0e0a390 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -730,6 +730,17 @@ export const McpUiToolMetaSchema = z.object({ .describe( 'Who can access this tool. Default: ["model", "app"]\n- "model": Tool visible to and callable by the agent\n- "app": Tool callable by the app from this server only', ), + /** + * `csp` belongs on the UI **resource** (see {@link McpUiResourceMeta}), + * not the tool. Hosts read it from the `resources/read` content item + * (with `resources/list` entry as fallback) and ignore it here. + */ + csp: z.never().optional(), + /** + * `permissions` belongs on the UI **resource** (see {@link McpUiResourceMeta}), + * not the tool. Hosts ignore it here. + */ + permissions: z.never().optional(), }); /** diff --git a/src/spec.types.ts b/src/spec.types.ts index 3678fb5..ad3d4d2 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -783,6 +783,17 @@ export interface McpUiToolMeta { * - "app": Tool callable by the app from this server only */ visibility?: McpUiToolVisibility[]; + /** + * `csp` belongs on the UI **resource** (see {@link McpUiResourceMeta}), + * not the tool. Hosts read it from the `resources/read` content item + * (with `resources/list` entry as fallback) and ignore it here. + */ + csp?: never; + /** + * `permissions` belongs on the UI **resource** (see {@link McpUiResourceMeta}), + * not the tool. Hosts ignore it here. + */ + permissions?: never; } /** From 9a75b891b2219cc988f08959966eccae6a57b5ef Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 13:50:10 +0100 Subject: [PATCH 28/97] 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. --- src/app-bridge.ts | 3 +-- src/app.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 4c8be2e..5186112 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -336,8 +336,7 @@ export class AppBridge extends ProtocolWithEvents< `[ext-apps] AppBridge received '${request.method}' before ` + `ui/notifications/initialized. The View is calling host ` + `methods before completing the handshake; it should await ` + - `app.connect() first. See ` + - `https://github.com/anthropics/claude-ai-mcp/issues/149`, + `app.connect() first.`, ); } return handler(request, extra); diff --git a/src/app.ts b/src/app.ts index 09efadf..d8571ee 100644 --- a/src/app.ts +++ b/src/app.ts @@ -355,8 +355,7 @@ export class App extends ProtocolWithEvents< const msg = `[ext-apps] App.${method}() called before connect() completed the ` + `ui/initialize handshake. Await app.connect() before calling this ` + - `method, or move data loading to an ontoolresult handler. ` + - `See https://github.com/anthropics/claude-ai-mcp/issues/149`; + `method, or move data loading to an ontoolresult handler.`; if (this.options?.strict) { throw new Error(msg); } From 989ddc756894f2d86d100b267e62a9b6fc67df82 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 13:53:20 +0100 Subject: [PATCH 29/97] 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. --- package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15d88de..9c8f52b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1994,9 +1994,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -6330,9 +6330,9 @@ } }, "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -9465,9 +9465,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", "dependencies": { From b170150a8259f4561e36c734290cf6cce2eff454 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Tue, 21 Apr 2026 07:53:58 -0500 Subject: [PATCH 30/97] 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) --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ca6368..4c364a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,14 @@ We welcome contributions to the MCP Apps SDK! This document outlines the process - 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 Start the development environment with hot reloading: From 5dfc88f113dca1cfe746cde9349e3aae89173d24 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 14:48:52 +0100 Subject: [PATCH 31/97] docs(types): drop stale resourceUri JSDoc referencing deprecated flat key (#626) --- src/generated/schema.ts | 1 - src/spec.types.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 0e0a390..4368737 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -711,7 +711,6 @@ export const McpUiToolVisibilitySchema = z export const McpUiToolMetaSchema = z.object({ /** * URI of the UI resource to display for this tool, if any. - * This is converted to `_meta["ui/resourceUri"]`. * * @example * ```ts diff --git a/src/spec.types.ts b/src/spec.types.ts index ad3d4d2..7a8b337 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -769,7 +769,6 @@ export type McpUiToolVisibility = "model" | "app"; export interface McpUiToolMeta { /** * URI of the UI resource to display for this tool, if any. - * This is converted to `_meta["ui/resourceUri"]`. * * @example * ```ts From 9d68315720d021c448f55b57eac1f4395b3472ad Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 14:49:09 +0100 Subject: [PATCH 32/97] feat(app): default Zod to jitless; add allowUnsafeEval opt-out (#618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/app.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/app.ts b/src/app.ts index d8571ee..6168a1c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -74,6 +74,7 @@ import { standardSchemaToJsonSchema, validateStandardSchema, } from "./standard-schema"; +import { z } from "zod/v4"; export type { StandardSchemaV1, @@ -187,6 +188,20 @@ export type AppOptions = ProtocolOptions & { * @default false */ strict?: boolean; + /** + * Allow code paths that require CSP `unsafe-eval` (e.g. `new Function()`). + * + * Views typically run under a strict CSP without `unsafe-eval`. Zod's JIT + * object parser uses `new Function()` and throws on the first message parse + * under such a policy. By default (`allowUnsafeEval: false`) the + * {@link App `App`} constructor sets `z.config({ jitless: true })` so the + * SDK works out of the box under the spec's default CSP. Set + * `allowUnsafeEval: true` to skip that and keep the faster JIT path when + * the host's CSP permits `unsafe-eval`. + * + * @default false + */ + allowUnsafeEval?: boolean; }; type RequestHandlerExtra = Parameters< @@ -403,6 +418,10 @@ export class App extends ProtocolWithEvents< ) { super(options); + if (!options.allowUnsafeEval) { + z.config({ jitless: true }); + } + this.setRequestHandler(PingRequestSchema, (request) => { console.log("Received ping:", request.params); return {}; From 6fa5d4e6edcfc7e921e7e95230120d937b756d60 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 14:50:17 +0100 Subject: [PATCH 33/97] feat(app): warn when one-shot handlers are registered after connect() (#629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/app-bridge.test.ts | 93 ++++++++++++++++++++++++++++++++++++++++++ src/app.ts | 43 +++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index e7b32b3..4ea4b49 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1965,6 +1965,99 @@ describe("App <-> AppBridge integration", () => { errSpy.mockRestore(); }); + describe("late handler registration", () => { + const lateMsg = + /handler registered after connect\(\) completed the ui\/initialize handshake/; + + it("warns when ontoolresult is set after connect() resolves", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + app.ontoolresult = () => {}; + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatch(lateMsg); + expect(warnSpy.mock.calls[0][0]).toContain('"toolresult"'); + warnSpy.mockRestore(); + }); + + it("warns when addEventListener('toolinput', …) is called after connect()", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + app.addEventListener("toolinput", () => {}); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toContain('"toolinput"'); + warnSpy.mockRestore(); + }); + + it("does not warn for handlers set before connect()", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + app.ontoolinput = () => {}; + app.addEventListener("toolresult", () => {}); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + + it("does not warn for hostcontextchanged (repeating event)", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + app.onhostcontextchanged = () => {}; + app.addEventListener("hostcontextchanged", () => {}); + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + + it("does not warn when clearing a handler (set to undefined)", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + app.ontoolinput = () => {}; + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + app.ontoolinput = undefined; + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + + it("throws instead of warning when strict: true", async () => { + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + const [strictAppT, strictBridgeT] = + InMemoryTransport.createLinkedPair(); + const strictBridge = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + const strictApp = new App( + testAppInfo, + {}, + { autoResize: false, strict: true }, + ); + await strictBridge.connect(strictBridgeT); + await strictApp.connect(strictAppT); + + expect(() => { + strictApp.ontoolresult = () => {}; + }).toThrow(lateMsg); + expect(() => { + strictApp.addEventListener("toolinput", () => {}); + }).toThrow(lateMsg); + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + }); + it("AppBridge warns on tools/call from a View that skipped the handshake", async () => { const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); bridge.oncalltool = async () => ({ content: [] }); diff --git a/src/app.ts b/src/app.ts index 6168a1c..b703c3f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -386,6 +386,49 @@ export class App extends ProtocolWithEvents< hostcontextchanged: McpUiHostContextChangedNotificationSchema, }; + /** + * Events the host typically sends once, shortly after the handshake. + * Registering a handler for one of these *after* {@link connect `connect`} + * resolves risks missing the notification entirely. + */ + private static readonly ONE_SHOT_EVENTS: ReadonlySet = + new Set(["toolinput", "toolinputpartial", "toolresult", "toolcancelled"]); + + /** + * Warn (or throw under `strict`) when a one-shot event handler is registered + * after the `ui/initialize` → `ui/notifications/initialized` handshake has + * completed. The host may have already fired the notification by then. + * + * Mirrors {@link _assertInitialized `_assertInitialized`} (the outbound-side guard). + */ + private _assertHandlerTiming(event: keyof AppEventMap): void { + if (!this._initializedSent || !App.ONE_SHOT_EVENTS.has(event)) return; + const msg = + `[ext-apps] "${String(event)}" handler registered after connect() ` + + `completed the ui/initialize handshake. The host may have already sent ` + + `this notification. Register handlers before calling app.connect().`; + if (this.options?.strict) { + throw new Error(msg); + } + console.warn(msg); + } + + protected override setEventHandler( + event: K, + handler: ((params: AppEventMap[K]) => void) | undefined, + ): void { + if (handler) this._assertHandlerTiming(event); + super.setEventHandler(event, handler); + } + + override addEventListener( + event: K, + handler: (params: AppEventMap[K]) => void, + ): void { + this._assertHandlerTiming(event); + super.addEventListener(event, handler); + } + protected override onEventDispatch( event: K, params: AppEventMap[K], From 9658678eaa9247e67cee523020ce772007621bd8 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 15:06:49 +0100 Subject: [PATCH 34/97] 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. --- src/app-bridge.test.ts | 77 +++++++++++++++++------------------------- src/app.ts | 6 ++-- 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 4ea4b49..15fa007 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1907,9 +1907,18 @@ describe("App <-> AppBridge integration", () => { describe("pre-handshake guard (claude-ai-mcp#149)", () => { const guardMsg = /called before connect\(\) completed the ui\/initialize handshake/; + let warnSpy: ReturnType>; + + beforeEach(() => { + warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + }); + afterEach(() => { + warnSpy.mockRestore(); + }); + + const warnings = () => warnSpy.mock.calls.map((c) => String(c[0])); it("callServerTool warns when called before connect() completes", async () => { - const errSpy = spyOn(console, "error").mockImplementation(() => {}); bridge.oncalltool = async () => ({ content: [] }); await bridge.connect(bridgeTransport); @@ -1917,40 +1926,34 @@ describe("App <-> AppBridge integration", () => { // Handshake is in flight; _initializedSent is still false. await app.callServerTool({ name: "t", arguments: {} }).catch(() => {}); - expect(errSpy).toHaveBeenCalledTimes(1); - expect(errSpy.mock.calls[0][0]).toMatch(guardMsg); - expect(errSpy.mock.calls[0][0]).toContain("App.callServerTool()"); + const appSide = warnings().filter((m) => guardMsg.test(m)); + expect(appSide).toHaveLength(1); + expect(appSide[0]).toContain("App.callServerTool()"); await connecting; - errSpy.mockRestore(); }); it("callServerTool does not warn after connect() resolves", async () => { - const errSpy = spyOn(console, "error").mockImplementation(() => {}); bridge.oncalltool = async () => ({ content: [] }); await bridge.connect(bridgeTransport); await app.connect(appTransport); await app.callServerTool({ name: "t", arguments: {} }); - expect(errSpy).not.toHaveBeenCalled(); - errSpy.mockRestore(); + expect(warnings()).toEqual([]); }); it("sendMessage and readServerResource also warn before handshake", async () => { - const errSpy = spyOn(console, "error").mockImplementation(() => {}); - await app.sendMessage({ role: "user", content: [] }).catch(() => {}); await app.readServerResource({ uri: "test://r" }).catch(() => {}); - expect(errSpy).toHaveBeenCalledTimes(2); - expect(errSpy.mock.calls[0][0]).toContain("App.sendMessage()"); - expect(errSpy.mock.calls[1][0]).toContain("App.readServerResource()"); - errSpy.mockRestore(); + const appSide = warnings().filter((m) => guardMsg.test(m)); + expect(appSide).toHaveLength(2); + expect(appSide[0]).toContain("App.sendMessage()"); + expect(appSide[1]).toContain("App.readServerResource()"); }); it("throws instead of warning when strict: true", async () => { - const errSpy = spyOn(console, "error").mockImplementation(() => {}); const strictApp = new App( testAppInfo, {}, @@ -1960,9 +1963,7 @@ describe("App <-> AppBridge integration", () => { await expect( strictApp.callServerTool({ name: "t", arguments: {} }), ).rejects.toThrow(guardMsg); - expect(errSpy).not.toHaveBeenCalled(); - - errSpy.mockRestore(); + expect(warnings()).toEqual([]); }); describe("late handler registration", () => { @@ -1970,68 +1971,57 @@ describe("App <-> AppBridge integration", () => { /handler registered after connect\(\) completed the ui\/initialize handshake/; it("warns when ontoolresult is set after connect() resolves", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); await bridge.connect(bridgeTransport); await app.connect(appTransport); app.ontoolresult = () => {}; - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toMatch(lateMsg); - expect(warnSpy.mock.calls[0][0]).toContain('"toolresult"'); - warnSpy.mockRestore(); + expect(warnings()).toHaveLength(1); + expect(warnings()[0]).toMatch(lateMsg); + expect(warnings()[0]).toContain('"toolresult"'); }); it("warns when addEventListener('toolinput', …) is called after connect()", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); await bridge.connect(bridgeTransport); await app.connect(appTransport); app.addEventListener("toolinput", () => {}); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain('"toolinput"'); - warnSpy.mockRestore(); + expect(warnings()).toHaveLength(1); + expect(warnings()[0]).toContain('"toolinput"'); }); it("does not warn for handlers set before connect()", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); app.ontoolinput = () => {}; app.addEventListener("toolresult", () => {}); await bridge.connect(bridgeTransport); await app.connect(appTransport); - expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); + expect(warnings()).toEqual([]); }); it("does not warn for hostcontextchanged (repeating event)", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); await bridge.connect(bridgeTransport); await app.connect(appTransport); app.onhostcontextchanged = () => {}; app.addEventListener("hostcontextchanged", () => {}); - expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); + expect(warnings()).toEqual([]); }); it("does not warn when clearing a handler (set to undefined)", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); app.ontoolinput = () => {}; await bridge.connect(bridgeTransport); await app.connect(appTransport); app.ontoolinput = undefined; - expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); + expect(warnings()).toEqual([]); }); it("throws instead of warning when strict: true", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); const [strictAppT, strictBridgeT] = InMemoryTransport.createLinkedPair(); const strictBridge = new AppBridge( @@ -2053,13 +2043,11 @@ describe("App <-> AppBridge integration", () => { expect(() => { strictApp.addEventListener("toolinput", () => {}); }).toThrow(lateMsg); - expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); + expect(warnings()).toEqual([]); }); }); it("AppBridge warns on tools/call from a View that skipped the handshake", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); bridge.oncalltool = async () => ({ content: [] }); await bridge.connect(bridgeTransport); @@ -2073,23 +2061,20 @@ describe("App <-> AppBridge integration", () => { }); await flush(); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain( + expect(warnings()).toHaveLength(1); + expect(warnings()[0]).toContain( "received 'tools/call' before ui/notifications/initialized", ); - warnSpy.mockRestore(); }); it("AppBridge does not warn after initialized is received", async () => { - const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); bridge.oncalltool = async () => ({ content: [] }); await bridge.connect(bridgeTransport); await app.connect(appTransport); await app.callServerTool({ name: "t", arguments: {} }); - expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); + expect(warnings()).toEqual([]); }); }); diff --git a/src/app.ts b/src/app.ts index b703c3f..f35b43d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -176,12 +176,12 @@ export type AppOptions = ProtocolOptions & { */ autoResize?: boolean; /** - * Throw on detected misuse instead of logging a console error. + * Throw on detected misuse instead of logging a console warning. * * Currently this affects calling host-bound methods (e.g. * {@link App.callServerTool `callServerTool`}, {@link App.sendMessage `sendMessage`}) * before {@link App.connect `connect`} has completed the `ui/initialize` - * handshake. With `strict: false` (default) a `console.error` is emitted; + * handshake. With `strict: false` (default) a `console.warn` is emitted; * with `strict: true` an `Error` is thrown. * * @remarks Throwing will become the default in a future release. @@ -375,7 +375,7 @@ export class App extends ProtocolWithEvents< throw new Error(msg); } // TODO(next-minor): make `strict: true` the default. - console.error(`${msg}. This will throw in a future release.`); + console.warn(`${msg}. This will throw in a future release.`); } protected readonly eventSchemas = { From cc02e17e21c34d38c49e6f189e2250d49299e529 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 15:44:57 +0100 Subject: [PATCH 35/97] fix(react): StrictMode cleanup + relax late-handler guard for re-reg (#631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- src/app-bridge.test.ts | 92 +++++++++++++++++++++++++++++++++++++++++- src/app-bridge.ts | 9 +++++ src/app.ts | 22 ++++++++-- src/react/useApp.tsx | 22 ++++++---- 4 files changed, 133 insertions(+), 12 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 15fa007..38f7d30 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -15,6 +15,7 @@ import { import { z } from "zod/v4"; import { App } from "./app"; +import { LATEST_PROTOCOL_VERSION } from "./types"; import { AppBridge, getToolUiResourceUri, @@ -2021,6 +2022,45 @@ describe("App <-> AppBridge integration", () => { expect(warnings()).toEqual([]); }); + it("does not warn on re-registration when a handler existed pre-connect", async () => { + const h = () => {}; + app.addEventListener("toolresult", h); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // React-style: useEffect cleanup removes, dep change re-adds. + app.removeEventListener("toolresult", h); + app.addEventListener("toolresult", () => {}); + + expect(warnings().filter((m) => lateMsg.test(m))).toEqual([]); + }); + + it("warns once for the first late registration, not on subsequent ones", async () => { + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + app.addEventListener("toolinput", () => {}); + app.addEventListener("toolinput", () => {}); + app.addEventListener("toolinput", () => {}); + + const late = warnings().filter((m) => lateMsg.test(m)); + expect(late).toHaveLength(1); + expect(late[0]).toContain('"toolinput"'); + }); + + it("tracks first-handler per event independently", async () => { + app.addEventListener("toolinput", () => {}); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + app.addEventListener("toolinput", () => {}); // had pre-connect handler → silent + app.addEventListener("toolresult", () => {}); // first reg, late → warns + + const late = warnings().filter((m) => lateMsg.test(m)); + expect(late).toHaveLength(1); + expect(late[0]).toContain('"toolresult"'); + }); + it("throws instead of warning when strict: true", async () => { const [strictAppT, strictBridgeT] = InMemoryTransport.createLinkedPair(); @@ -2034,6 +2074,9 @@ describe("App <-> AppBridge integration", () => { {}, { autoResize: false, strict: true }, ); + // Pre-connect registration for toolcancelled — later swap must not throw. + strictApp.addEventListener("toolcancelled", () => {}); + await strictBridge.connect(strictBridgeT); await strictApp.connect(strictAppT); @@ -2043,10 +2086,57 @@ describe("App <-> AppBridge integration", () => { expect(() => { strictApp.addEventListener("toolinput", () => {}); }).toThrow(lateMsg); - expect(warnings()).toEqual([]); + // Swapping a handler that existed pre-connect is allowed under strict. + expect(() => { + strictApp.addEventListener("toolcancelled", () => {}); + }).not.toThrow(); + expect(warnings().filter((m) => lateMsg.test(m))).toEqual([]); }); }); + it("AppBridge warns on a second ui/initialize (View double-mount)", async () => { + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + expect(warnings()).toEqual([]); + + // Simulate a second View instance re-running the handshake. + appTransport.send({ + jsonrpc: "2.0", + id: 99, + method: "ui/initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + appInfo: testAppInfo, + appCapabilities: {}, + }, + }); + await flush(); + + const doubleInit = warnings().filter((m) => + /second ui\/initialize/.test(m), + ); + expect(doubleInit).toHaveLength(1); + }); + + it("close() stops further notification delivery (StrictMode cleanup relies on this)", async () => { + const received: unknown[] = []; + app.addEventListener("toolresult", (r) => received.push(r)); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + await bridge.sendToolResult({ + content: [{ type: "text", text: "before" }], + }); + expect(received).toHaveLength(1); + + await app.close(); + + await bridge + .sendToolResult({ content: [{ type: "text", text: "after" }] }) + .catch(() => {}); + expect(received).toHaveLength(1); + }); + it("AppBridge warns on tools/call from a View that skipped the handshake", async () => { bridge.oncalltool = async () => ({ content: [] }); await bridge.connect(bridgeTransport); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 5186112..23383c4 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1463,6 +1463,15 @@ export class AppBridge extends ProtocolWithEvents< ): Promise { const requestedVersion = request.params.protocolVersion; + if (this._appInfo !== undefined) { + console.warn( + "[ext-apps] AppBridge received a second ui/initialize. The View may " + + "be double-mounting (e.g. React StrictMode in dev) without closing " + + "the previous App instance. Responding normally; the latest " + + "appInfo/appCapabilities replace the previous values.", + ); + } + this._appCapabilities = request.params.appCapabilities; this._appInfo = request.params.appInfo; diff --git a/src/app.ts b/src/app.ts index f35b43d..adfad5c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -395,14 +395,28 @@ export class App extends ProtocolWithEvents< new Set(["toolinput", "toolinputpartial", "toolresult", "toolcancelled"]); /** - * Warn (or throw under `strict`) when a one-shot event handler is registered - * after the `ui/initialize` → `ui/notifications/initialized` handshake has - * completed. The host may have already fired the notification by then. + * One-shot events that have had at least one handler registered (via `on*` + * setter or `addEventListener`) at any point. Once an event is in this set, + * subsequent late registrations are not flagged — only the *first* handler + * matters for the missed-notification race, and re-registration (e.g. React + * `useEffect` cleanup → re-add on dep change) is a legitimate pattern. + */ + private readonly _everHadListener = new Set(); + + /** + * Warn (or throw under `strict`) when the *first* handler for a one-shot + * event is registered after the `ui/initialize` → `ui/notifications/initialized` + * handshake has completed. The host may have already fired the notification + * by then. Subsequent registrations for the same event are not flagged. * * Mirrors {@link _assertInitialized `_assertInitialized`} (the outbound-side guard). */ private _assertHandlerTiming(event: keyof AppEventMap): void { - if (!this._initializedSent || !App.ONE_SHOT_EVENTS.has(event)) return; + if (!App.ONE_SHOT_EVENTS.has(event) || this._everHadListener.has(event)) { + return; + } + this._everHadListener.add(event); + if (!this._initializedSent) return; const msg = `[ext-apps] "${String(event)}" handler registered after connect() ` + `completed the ui/initialize handshake. The host may have already sent ` + diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 518d6ca..6189246 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -138,6 +138,7 @@ export function useApp({ useEffect(() => { let mounted = true; + let appInstance: App | undefined; async function connect() { try { @@ -145,18 +146,24 @@ export function useApp({ window.parent, window.parent, ); - const app = new App(appInfo, capabilities, { autoResize, strict }); + appInstance = new App(appInfo, capabilities, { autoResize, strict }); // Register handlers BEFORE connecting - onAppCreated?.(app); + onAppCreated?.(appInstance); - await app.connect(transport); + await appInstance.connect(transport); - if (mounted) { - setApp(app); - setIsConnected(true); - setError(null); + if (!mounted) { + // Unmounted during the handshake (e.g. React StrictMode dev + // double-invoke). Close so the abandoned transport's window + // `message` listener doesn't keep receiving host postMessages + // alongside the second mount's instance. + void appInstance.close(); + return; } + setApp(appInstance); + setIsConnected(true); + setError(null); } catch (error) { if (mounted) { setApp(null); @@ -172,6 +179,7 @@ export function useApp({ return () => { mounted = false; + void appInstance?.close(); }; }, []); // Intentionally not including options to avoid reconnection From 3e9950062b1345f926a744d9c8250e5f85c12f37 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 16:03:27 +0100 Subject: [PATCH 36/97] build: add npm run bump; parallel npm-publish approval (#568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: github-actions[bot] --- .github/workflows/npm-publish.yml | 20 +++++++++++-- CONTRIBUTING.md | 33 ++++------------------ package.json | 1 + scripts/bump-version.mjs | 47 +++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 scripts/bump-version.mjs diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ece35d3..0667bbd 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -42,6 +42,10 @@ jobs: - 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' @@ -88,7 +92,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'release' environment: Release - needs: [publish] + needs: [build, test] permissions: contents: read @@ -134,8 +138,18 @@ 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 }} @@ -143,7 +157,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'release' environment: Release - needs: [publish-examples] + needs: [build, test] permissions: contents: write diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c364a2..6a3102b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -539,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 diff --git a/package.json b/package.json index d1fa9df..bb824ff 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "prettier": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --check", "prettier:fix": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --write", "check:versions": "node scripts/check-versions.mjs", + "bump": "node scripts/bump-version.mjs", "update-lock:docker": "rm -rf node_modules package-lock.json examples/*/node_modules && docker run --rm --platform linux/amd64 -v $(pwd):/work -w /work -e HOME=/tmp node:latest npm i --registry=https://registry.npmjs.org/ --ignore-scripts && rm -rf node_modules examples/*/node_modules && npm i --registry=https://registry.npmjs.org/" }, "author": "Olivier Chafik", diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs new file mode 100644 index 0000000..65c878f --- /dev/null +++ b/scripts/bump-version.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +/** + * Bump the version in the root package.json and sync all workspace packages + * to the same version. + * + * Usage: + * node scripts/bump-version.mjs patch + * node scripts/bump-version.mjs minor + * node scripts/bump-version.mjs major + * node scripts/bump-version.mjs 1.4.0 + * node scripts/bump-version.mjs prerelease --preid=beta + * + * Writes the new version to stdout (logs go to stderr). + */ + +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const args = process.argv.slice(2); +if (!args[0]) { + console.error( + "Usage: node scripts/bump-version.mjs [--preid=]", + ); + process.exit(1); +} + +const exec = (cmd) => + execSync(cmd, { stdio: ["inherit", "pipe", "inherit"] }) + .toString() + .trim(); + +const pkgName = JSON.parse(readFileSync("package.json", "utf-8")).name; + +const newVersion = exec( + `npm version ${args.join(" ")} --no-git-tag-version`, +).replace(/^v/, ""); +exec(`npm pkg set version=${newVersion} --workspaces`); + +// Keep workspace dependency ranges compatible (needed on major bumps) +const [major, minor] = newVersion.split("."); +exec(`npm pkg set "dependencies.${pkgName}=^${major}.${minor}.0" --workspaces`); + +// Sync package-lock.json so `npm ci` doesn't reject the release PR +exec("npm install --package-lock-only --ignore-scripts"); + +console.error(`Bumped root + workspaces to ${newVersion}`); +console.log(newVersion); From 64b4fa16de4a4694dbec5514b866802a1825f919 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 21 Apr 2026 16:19:22 +0100 Subject: [PATCH 37/97] chore: bump ext-apps to 1.7.0 (#628) --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 54 +++++++++---------- package.json | 2 +- 27 files changed, 53 insertions(+), 53 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 0153242..8045f36 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 9b79e20..dbe0e07 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index b53c2bd..63d7656 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index efb64d4..b39d041 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 238efe8..46c38e6 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index f89331c..d41a72e 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 8423293..4e8022e 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index d2b1221..d8d0e3c 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 08c7048..a42ba0b 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 16d3322..46c8037 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 6c3f52a..b0a7d54 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index fb8cc99..f1173ed 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.6.0", + "version": "1.7.0", "private": true, "type": "module", "scripts": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index e6c2124..d39648b 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 0180f7b..57af047 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index a71f39f..f74e671 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.6.0", + "version": "1.7.0", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 31c4d6f..036b66e 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index c9c268d..283c4d5 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.6.0", + "version": "1.7.0", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index b7fbcef..2d626cf 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 6e5899a..4924d63 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 756ed33..b5f2370 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 5f9d333..05243db 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 50ff990..0c2d5f5 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 0660aa4..f0daa38 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 88f3b97..9e70d24 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index a2aea7a..a6a8f19 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index 9c8f52b..c0c0c47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.6.0", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "workspaces": [ "examples/*" @@ -66,7 +66,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.6.0", + "version": "1.7.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -109,7 +109,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -153,7 +153,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -200,7 +200,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -244,7 +244,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -288,7 +288,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -330,7 +330,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -374,7 +374,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -417,7 +417,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -464,7 +464,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -507,7 +507,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -548,7 +548,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.6.0", + "version": "1.7.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -593,7 +593,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -635,7 +635,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -679,7 +679,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.6.0", + "version": "1.7.0", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -687,7 +687,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -719,7 +719,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -728,7 +728,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -776,7 +776,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -818,7 +818,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -861,7 +861,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -931,7 +931,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -980,7 +980,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1023,7 +1023,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1065,7 +1065,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/package.json b/package.json index bb824ff..5072e5b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From 77b8daa1e8ac86f0c826162af6763acabc6a9990 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 24 Apr 2026 11:27:32 +0100 Subject: [PATCH 38/97] test: add unit coverage for PostMessageTransport source validation (#536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * docs: clarify origin validation lives in sandbox proxy, not transport --- src/message-transport.test.ts | 390 ++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 src/message-transport.test.ts diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts new file mode 100644 index 0000000..d5f5092 --- /dev/null +++ b/src/message-transport.test.ts @@ -0,0 +1,390 @@ +import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; +import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; + +import { PostMessageTransport } from "./message-transport"; + +/** + * Minimal `window` stub for bun's DOM-less test environment. + * + * Captures listeners registered via `addEventListener` so tests can dispatch + * fake `MessageEvent`-like objects directly. We deliberately avoid pulling in + * jsdom/happy-dom — this file tests the transport's contract, not the browser. + */ +type Listener = (event: MessageEvent) => void; + +function createFakeWindow() { + const listeners = new Map>(); + return { + addEventListener(type: string, listener: Listener) { + if (!listeners.has(type)) listeners.set(type, new Set()); + listeners.get(type)!.add(listener); + }, + removeEventListener(type: string, listener: Listener) { + listeners.get(type)?.delete(listener); + }, + /** Test helper: invoke all listeners for `type` with the given event. */ + dispatch(type: string, event: unknown) { + listeners.get(type)?.forEach((l) => l(event as MessageEvent)); + }, + /** Test helper: how many listeners are registered for `type`? */ + listenerCount(type: string) { + return listeners.get(type)?.size ?? 0; + }, + }; +} + +/** + * A valid JSON-RPC request payload. Used by most happy-path tests. + * Kept deliberately minimal — we care that *a* valid message is delivered, + * not about exercising every JSON-RPC shape (that's the schema's job). + */ +const validRequest: JSONRPCMessage = { + jsonrpc: "2.0", + id: 1, + method: "ping", +}; + +describe("PostMessageTransport", () => { + let fakeWindow: ReturnType; + let trustedSource: object; + let untrustedSource: object; + + /** Stand-in for the target window (e.g. window.parent or iframe.contentWindow). */ + let targetPostMessage: ReturnType; + let eventTarget: { postMessage: typeof targetPostMessage }; + + // The transport logs liberally at debug/error level. Silence during tests + // so failures are readable; restore afterwards so we don't leak state. + let restoreConsole: () => void; + + beforeEach(() => { + fakeWindow = createFakeWindow(); + (globalThis as { window?: unknown }).window = fakeWindow; + + // Distinct object identities — the transport compares with `!==`. + trustedSource = { id: "trusted" }; + untrustedSource = { id: "untrusted" }; + + targetPostMessage = mock(() => {}); + eventTarget = { postMessage: targetPostMessage }; + + const origDebug = console.debug; + const origError = console.error; + console.debug = () => {}; + console.error = () => {}; + restoreConsole = () => { + console.debug = origDebug; + console.error = origError; + }; + }); + + afterEach(() => { + restoreConsole(); + delete (globalThis as { window?: unknown }).window; + }); + + /** Create and start a transport wired to the shared fakes. */ + async function createStartedTransport() { + const transport = new PostMessageTransport( + eventTarget as unknown as Window, + trustedSource as MessageEventSource, + ); + await transport.start(); + return transport; + } + + // ========================================================================== + // Source validation — the security boundary at this layer. + // The transport validates `event.source` (window identity), not `event.origin`. + // Origin checks live in the sandbox proxy relay between the two endpoints + // (see examples/basic-host/src/sandbox.ts). Here, source===contentWindow is + // the narrower check; the app side can't know its sandbox's origin anyway. + // ========================================================================== + describe("source validation", () => { + it("delivers messages from the configured eventSource", async () => { + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + transport.onmessage = (msg) => received.push(msg); + + fakeWindow.dispatch("message", { + source: trustedSource, + data: validRequest, + }); + + expect(received).toEqual([validRequest]); + }); + + it("drops messages from a different source", async () => { + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + const errors: Error[] = []; + transport.onmessage = (msg) => received.push(msg); + transport.onerror = (err) => errors.push(err); + + fakeWindow.dispatch("message", { + source: untrustedSource, + data: validRequest, + }); + + // The message is silently dropped — neither delivered nor surfaced as + // an error. An attacker flooding us with forged messages should not be + // able to DoS the error handler. + expect(received).toEqual([]); + expect(errors).toEqual([]); + }); + + it("drops messages with a null source", async () => { + // Some browser-injected messages (extensions, devtools) arrive with + // source === null. These must not reach the protocol layer. + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + transport.onmessage = (msg) => received.push(msg); + + fakeWindow.dispatch("message", { + source: null, + data: validRequest, + }); + + expect(received).toEqual([]); + }); + + it("continues delivering trusted messages after dropping an untrusted one", async () => { + // Regression guard: a rejected message must not break the listener or + // close the transport. + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + transport.onmessage = (msg) => received.push(msg); + + fakeWindow.dispatch("message", { + source: untrustedSource, + data: validRequest, + }); + fakeWindow.dispatch("message", { + source: trustedSource, + data: validRequest, + }); + + expect(received).toEqual([validRequest]); + }); + }); + + // ========================================================================== + // Message format validation. + // Three paths: valid → onmessage, non-JSON-RPC → silent, malformed → onerror. + // ========================================================================== + describe("message format validation", () => { + it("delivers valid JSON-RPC requests via onmessage", async () => { + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + transport.onmessage = (msg) => received.push(msg); + + fakeWindow.dispatch("message", { + source: trustedSource, + data: { jsonrpc: "2.0", id: 42, method: "tools/call" }, + }); + + expect(received).toHaveLength(1); + expect(received[0]).toMatchObject({ id: 42, method: "tools/call" }); + }); + + it("delivers valid JSON-RPC notifications via onmessage", async () => { + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + transport.onmessage = (msg) => received.push(msg); + + fakeWindow.dispatch("message", { + source: trustedSource, + data: { jsonrpc: "2.0", method: "notifications/initialized" }, + }); + + expect(received).toHaveLength(1); + expect(received[0]).toMatchObject({ + method: "notifications/initialized", + }); + }); + + it("silently ignores non-JSON-RPC payloads", async () => { + // Real iframes receive all sorts of ambient postMessage traffic: + // React DevTools, browser extensions, ad frames. These must not crash + // the transport or surface as protocol errors. + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + const errors: Error[] = []; + transport.onmessage = (msg) => received.push(msg); + transport.onerror = (err) => errors.push(err); + + fakeWindow.dispatch("message", { + source: trustedSource, + data: { type: "react-devtools-hook", payload: {} }, + }); + fakeWindow.dispatch("message", { source: trustedSource, data: "hello" }); + fakeWindow.dispatch("message", { source: trustedSource, data: null }); + fakeWindow.dispatch("message", { source: trustedSource, data: 42 }); + + expect(received).toEqual([]); + expect(errors).toEqual([]); + }); + + it("calls onerror for malformed messages claiming to be JSON-RPC", async () => { + // `jsonrpc: "2.0"` but missing required fields — this IS a protocol + // violation worth surfacing, unlike ambient noise. + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + const errors: Error[] = []; + transport.onmessage = (msg) => received.push(msg); + transport.onerror = (err) => errors.push(err); + + fakeWindow.dispatch("message", { + source: trustedSource, + data: { jsonrpc: "2.0" }, // no id/method/result — invalid + }); + + expect(received).toEqual([]); + expect(errors).toHaveLength(1); + expect(errors[0]).toBeInstanceOf(Error); + }); + + it("does not throw when onerror is unset and a malformed message arrives", async () => { + const transport = await createStartedTransport(); + transport.onmessage = () => {}; + // onerror deliberately left unset + + expect(() => + fakeWindow.dispatch("message", { + source: trustedSource, + data: { jsonrpc: "2.0" }, + }), + ).not.toThrow(); + }); + + it("does not throw when onmessage is unset and a valid message arrives", async () => { + await createStartedTransport(); + // onmessage deliberately left unset — may happen briefly during wiring + + expect(() => + fakeWindow.dispatch("message", { + source: trustedSource, + data: validRequest, + }), + ).not.toThrow(); + }); + }); + + // ========================================================================== + // send() + // ========================================================================== + describe("send()", () => { + it("posts the message to the configured eventTarget with '*' origin", async () => { + const transport = await createStartedTransport(); + + const msg: JSONRPCMessage = { + jsonrpc: "2.0", + id: 7, + method: "ui/initialize", + }; + await transport.send(msg); + + expect(targetPostMessage).toHaveBeenCalledTimes(1); + expect(targetPostMessage).toHaveBeenCalledWith(msg, "*"); + }); + + it("posts multiple messages in order", async () => { + const transport = await createStartedTransport(); + + const a: JSONRPCMessage = { jsonrpc: "2.0", id: 1, method: "a" }; + const b: JSONRPCMessage = { jsonrpc: "2.0", id: 2, method: "b" }; + await transport.send(a); + await transport.send(b); + + expect(targetPostMessage).toHaveBeenCalledTimes(2); + expect(targetPostMessage.mock.calls[0][0]).toEqual(a); + expect(targetPostMessage.mock.calls[1][0]).toEqual(b); + }); + }); + + // ========================================================================== + // Lifecycle: start() / close() + // ========================================================================== + describe("lifecycle", () => { + it("start() registers a message listener on window", async () => { + const transport = new PostMessageTransport( + eventTarget as unknown as Window, + trustedSource as MessageEventSource, + ); + + expect(fakeWindow.listenerCount("message")).toBe(0); + await transport.start(); + expect(fakeWindow.listenerCount("message")).toBe(1); + }); + + it("close() removes the message listener", async () => { + const transport = await createStartedTransport(); + + expect(fakeWindow.listenerCount("message")).toBe(1); + await transport.close(); + expect(fakeWindow.listenerCount("message")).toBe(0); + }); + + it("messages dispatched after close() are not delivered", async () => { + const transport = await createStartedTransport(); + const received: JSONRPCMessage[] = []; + transport.onmessage = (msg) => received.push(msg); + + await transport.close(); + fakeWindow.dispatch("message", { + source: trustedSource, + data: validRequest, + }); + + expect(received).toEqual([]); + }); + + it("close() invokes onclose when set", async () => { + const transport = await createStartedTransport(); + const onclose = mock(() => {}); + transport.onclose = onclose; + + await transport.close(); + + expect(onclose).toHaveBeenCalledTimes(1); + }); + + it("close() does not throw when onclose is unset", async () => { + const transport = await createStartedTransport(); + // onclose deliberately left unset + + expect(transport.close()).resolves.toBeUndefined(); + }); + + it("two transports listen independently", async () => { + // Host scenario: multiple iframes, each with its own transport. + // Each transport must only accept messages from its own iframe. + const sourceA = { id: "iframe-a" }; + const sourceB = { id: "iframe-b" }; + + const transportA = new PostMessageTransport( + eventTarget as unknown as Window, + sourceA as unknown as MessageEventSource, + ); + const transportB = new PostMessageTransport( + eventTarget as unknown as Window, + sourceB as unknown as MessageEventSource, + ); + await transportA.start(); + await transportB.start(); + + const receivedA: JSONRPCMessage[] = []; + const receivedB: JSONRPCMessage[] = []; + transportA.onmessage = (msg) => receivedA.push(msg); + transportB.onmessage = (msg) => receivedB.push(msg); + + fakeWindow.dispatch("message", { source: sourceA, data: validRequest }); + + expect(receivedA).toHaveLength(1); + expect(receivedB).toHaveLength(0); + + await transportA.close(); + await transportB.close(); + }); + }); +}); From 0b836799091c758c8de902a43e67a4019b29389a Mon Sep 17 00:00:00 2001 From: Maurizio Albari Date: Fri, 24 Apr 2026 12:35:13 +0200 Subject: [PATCH 39/97] 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. --- examples/qr-server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/qr-server/server.py b/examples/qr-server/server.py index d282d1a..c1c302e 100755 --- a/examples/qr-server/server.py +++ b/examples/qr-server/server.py @@ -26,7 +26,7 @@ VIEW_URI = "ui://qr-server/view.html" HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility PORT = int(os.environ.get("PORT", "3001")) -mcp = FastMCP("QR Code Server", stateless_http=True) +mcp = FastMCP("QR Code Server", stateless_http=True, host=HOST, port=PORT) # Embedded View HTML for self-contained usage (uv run or unbundled) EMBEDDED_VIEW_HTML = """ From 0f6d653b4a511f6c8057842e93399b7b4aba0f2f Mon Sep 17 00:00:00 2001 From: Suriya Prakash Date: Fri, 24 Apr 2026 16:06:00 +0530 Subject: [PATCH 40/97] test: add unit tests for buildAllowAttribute (#541) --- src/app-bridge.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 38f7d30..a4fe1de 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -18,6 +18,7 @@ import { App } from "./app"; import { LATEST_PROTOCOL_VERSION } from "./types"; import { AppBridge, + buildAllowAttribute, getToolUiResourceUri, isToolVisibilityModelOnly, isToolVisibilityAppOnly, @@ -2786,3 +2787,54 @@ describe("isToolVisibilityAppOnly", () => { }); }); }); + +describe("buildAllowAttribute", () => { + describe("returns empty string", () => { + it("when permissions is undefined", () => { + expect(buildAllowAttribute(undefined)).toBe(""); + }); + + it("when permissions object is empty", () => { + expect(buildAllowAttribute({})).toBe(""); + }); + }); + + describe("returns a single permission directive", () => { + it("when only camera is set", () => { + expect(buildAllowAttribute({ camera: {} })).toBe("camera"); + }); + + it("when only microphone is set", () => { + expect(buildAllowAttribute({ microphone: {} })).toBe("microphone"); + }); + + it("when only geolocation is set", () => { + expect(buildAllowAttribute({ geolocation: {} })).toBe("geolocation"); + }); + + it("when only clipboardWrite is set, maps to clipboard-write", () => { + expect(buildAllowAttribute({ clipboardWrite: {} })).toBe( + "clipboard-write", + ); + }); + }); + + describe("returns multiple directives joined with '; '", () => { + it("when camera and microphone are set", () => { + expect(buildAllowAttribute({ camera: {}, microphone: {} })).toBe( + "camera; microphone", + ); + }); + + it("when all permissions are set", () => { + expect( + buildAllowAttribute({ + camera: {}, + microphone: {}, + geolocation: {}, + clipboardWrite: {}, + }), + ).toBe("camera; microphone; geolocation; clipboard-write"); + }); + }); +}); From 7344d4c966b9e9c0aff9524c9a474eeab754fb23 Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Fri, 24 Apr 2026 06:36:43 -0400 Subject: [PATCH 41/97] fix(docs): update contributing guide on package preview (#601) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a3102b..6592106 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -573,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. --- From 30a78b60b4829282656daf10c298e2f5f6510f58 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 24 Apr 2026 15:57:58 +0100 Subject: [PATCH 42/97] 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. --- examples/pdf-server/server.test.ts | 66 +++++++++ examples/pdf-server/server.ts | 215 +++++++++++++---------------- 2 files changed, 165 insertions(+), 116 deletions(-) diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index a2f64ee..f63ae67 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -71,6 +71,72 @@ describe("PDF Cache with Timeouts", () => { }); }); + describe("byte-cap LRU eviction", () => { + const tenBytes = new Uint8Array(10); + + async function fill(cache: PdfCache, url: string): Promise { + const m = spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(tenBytes, { status: 200 }), + ); + try { + await cache.readPdfRange(url, 0, 1024); + } finally { + m.mockRestore(); + } + } + + it("evicts least-recently-used entry when total exceeds cap", async () => { + const cache = createPdfCache(25); + try { + await fill(cache, "https://arxiv.org/pdf/a"); + await fill(cache, "https://arxiv.org/pdf/b"); + expect(cache.getCacheSize()).toBe(2); + + // Touch A so B becomes least-recently-used + await cache.readPdfRange("https://arxiv.org/pdf/a", 0, 1); + + // Inserting C (10B) pushes total to 30 > 25 → evict LRU (B) + await fill(cache, "https://arxiv.org/pdf/c"); + expect(cache.getCacheSize()).toBe(2); + + // A and C still served from cache; B re-fetches + const m = spyOn(globalThis, "fetch").mockResolvedValue( + new Response(tenBytes, { status: 200 }), + ); + try { + await cache.readPdfRange("https://arxiv.org/pdf/a", 0, 1); + await cache.readPdfRange("https://arxiv.org/pdf/c", 0, 1); + expect(m).toHaveBeenCalledTimes(0); + await cache.readPdfRange("https://arxiv.org/pdf/b", 0, 1); + expect(m).toHaveBeenCalledTimes(1); + } finally { + m.mockRestore(); + } + } finally { + cache.clearCache(); + } + }); + + it("evicts multiple entries if a single insert exceeds the cap", async () => { + const cache = createPdfCache(25); + try { + await fill(cache, "https://arxiv.org/pdf/a"); + await fill(cache, "https://arxiv.org/pdf/b"); + const big = spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(new Uint8Array(20), { status: 200 }), + ); + try { + await cache.readPdfRange("https://arxiv.org/pdf/big", 0, 1024); + } finally { + big.mockRestore(); + } + expect(cache.getCacheSize()).toBe(1); + } finally { + cache.clearCache(); + } + }); + }); + describe("readPdfRange caching behavior", () => { const testUrl = "https://arxiv.org/pdf/test-pdf"; const testData = new Uint8Array([0x25, 0x50, 0x44, 0x46]); // %PDF header diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 80a5499..062b263 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -34,6 +34,7 @@ import { VerbosityLevel, version as PDFJS_VERSION, } from "pdfjs-dist/legacy/build/pdf.mjs"; +import type { PDFDocumentProxy } from "pdfjs-dist/types/src/display/api.js"; /** * PDF Standard-14 fonts from CDN. Used by both server and viewer so we @@ -83,6 +84,9 @@ export const CACHE_MAX_LIFETIME_MS = 60_000; // 60 seconds /** Max size for cached PDFs (defensive limit to prevent memory exhaustion) */ export const CACHE_MAX_PDF_SIZE_BYTES = 50 * 1024 * 1024; // 50MB +/** Max total bytes across all cache entries; oldest evicted first when exceeded. */ +export const CACHE_MAX_TOTAL_BYTES = 256 * 1024 * 1024; // 256MB + /** Allowed local file paths (CLI args + file roots — read access). */ export const allowedLocalFiles = new Set(); @@ -695,8 +699,11 @@ export interface PdfCache { * - CACHE_INACTIVITY_TIMEOUT_MS of no access (resets on each access) * - CACHE_MAX_LIFETIME_MS from creation (absolute timeout) */ -export function createPdfCache(): PdfCache { +export function createPdfCache( + maxTotalBytes: number = CACHE_MAX_TOTAL_BYTES, +): PdfCache { const cache = new Map(); + let totalBytes = 0; /** Delete a cache entry and clear its timers */ function deleteCacheEntry(url: string): void { @@ -704,6 +711,7 @@ export function createPdfCache(): PdfCache { if (entry) { clearTimeout(entry.inactivityTimer); clearTimeout(entry.maxLifetimeTimer); + totalBytes -= entry.data.length; cache.delete(url); } } @@ -719,6 +727,10 @@ export function createPdfCache(): PdfCache { deleteCacheEntry(url); }, CACHE_INACTIVITY_TIMEOUT_MS); + // Move to end of insertion order so size-cap eviction is LRU. + cache.delete(url); + cache.set(url, entry); + return entry.data; } @@ -727,6 +739,12 @@ export function createPdfCache(): PdfCache { // Clear any existing entry first deleteCacheEntry(url); + // Evict least-recently-used entries until under the byte cap. + for (const oldest of cache.keys()) { + if (totalBytes + data.length <= maxTotalBytes) break; + deleteCacheEntry(oldest); + } + const entry: CacheEntry = { data, createdAt: Date.now(), @@ -739,6 +757,7 @@ export function createPdfCache(): PdfCache { }; cache.set(url, entry); + totalBytes += data.length; } /** Slice a cached or freshly-fetched full body to the requested range. */ @@ -943,112 +962,69 @@ interface FormFieldInfo { * from a PDF. Bounding boxes are converted to model coordinates (top-left origin). */ async function extractFormFieldInfo( - url: string, - readRange: ( - url: string, - offset: number, - byteCount: number, - ) => Promise<{ data: Uint8Array; totalBytes: number }>, + pdfDoc: PDFDocumentProxy, ): Promise { - const { totalBytes } = await readRange(url, 0, 1); - const { data } = await readRange(url, 0, totalBytes); - - const loadingTask = getDocument({ - data, - standardFontDataUrl: STANDARD_FONT_DATA_URL, - StandardFontDataFactory: FetchStandardFontDataFactory, - // We only introspect form fields (never render) — silence residual - // warnings like "Unimplemented border style: inset". - verbosity: VerbosityLevel.ERRORS, - }); - const pdfDoc = await loadingTask.promise; - const fields: FormFieldInfo[] = []; - try { - for (let i = 1; i <= pdfDoc.numPages; i++) { - const page = await pdfDoc.getPage(i); - const pageHeight = page.getViewport({ scale: 1.0 }).height; - const annotations = await page.getAnnotations(); - for (const ann of annotations) { - // Only include form widgets (annotationType 20) - if (ann.annotationType !== 20) continue; - if (!ann.rect) continue; + for (let i = 1; i <= pdfDoc.numPages; i++) { + const page = await pdfDoc.getPage(i); + const pageHeight = page.getViewport({ scale: 1.0 }).height; + const annotations = await page.getAnnotations(); + for (const ann of annotations) { + // Only include form widgets (annotationType 20) + if (ann.annotationType !== 20) continue; + if (!ann.rect) continue; - const fieldName = ann.fieldName || ""; - const fieldType = ann.fieldType || "unknown"; + const fieldName = ann.fieldName || ""; + const fieldType = ann.fieldType || "unknown"; - // PDF rect is [x1, y1, x2, y2] in bottom-left origin - const x1 = Math.min(ann.rect[0], ann.rect[2]); - const y1 = Math.min(ann.rect[1], ann.rect[3]); - const x2 = Math.max(ann.rect[0], ann.rect[2]); - const y2 = Math.max(ann.rect[1], ann.rect[3]); - const width = x2 - x1; - const height = y2 - y1; + // PDF rect is [x1, y1, x2, y2] in bottom-left origin + const x1 = Math.min(ann.rect[0], ann.rect[2]); + const y1 = Math.min(ann.rect[1], ann.rect[3]); + const x2 = Math.max(ann.rect[0], ann.rect[2]); + const y2 = Math.max(ann.rect[1], ann.rect[3]); + const width = x2 - x1; + const height = y2 - y1; - // Convert to model coords (top-left origin): modelY = pageHeight - pdfY - height - const modelY = pageHeight - y2; + // Convert to model coords (top-left origin): modelY = pageHeight - pdfY - height + const modelY = pageHeight - y2; - // Choice widgets (combo/listbox) carry `options` as - // [{exportValue, displayValue}]. Expose export values — that's - // what fill_form needs. - let options: string[] | undefined; - if (Array.isArray(ann.options) && ann.options.length > 0) { - options = ann.options - .map((o: { exportValue?: string }) => o?.exportValue) - .filter((v: unknown): v is string => typeof v === "string"); - } - - fields.push({ - name: fieldName, - type: fieldType, - page: i, - x: Math.round(x1), - y: Math.round(modelY), - width: Math.round(width), - height: Math.round(height), - ...(ann.alternativeText ? { label: ann.alternativeText } : undefined), - // Radio: buttonValue is the per-widget export value — the only - // thing distinguishing three `size [Btn]` lines from each other. - ...(ann.radioButton && ann.buttonValue != null - ? { exportValue: String(ann.buttonValue) } - : undefined), - ...(options?.length ? { options } : undefined), - }); + // Choice widgets (combo/listbox) carry `options` as + // [{exportValue, displayValue}]. Expose export values — that's + // what fill_form needs. + let options: string[] | undefined; + if (Array.isArray(ann.options) && ann.options.length > 0) { + options = ann.options + .map((o: { exportValue?: string }) => o?.exportValue) + .filter((v: unknown): v is string => typeof v === "string"); } + + fields.push({ + name: fieldName, + type: fieldType, + page: i, + x: Math.round(x1), + y: Math.round(modelY), + width: Math.round(width), + height: Math.round(height), + ...(ann.alternativeText ? { label: ann.alternativeText } : undefined), + // Radio: buttonValue is the per-widget export value — the only + // thing distinguishing three `size [Btn]` lines from each other. + ...(ann.radioButton && ann.buttonValue != null + ? { exportValue: String(ann.buttonValue) } + : undefined), + ...(options?.length ? { options } : undefined), + }); } - } finally { - pdfDoc.destroy(); } return fields; } -async function extractFormSchema( - url: string, - readRange: ( - url: string, - offset: number, - byteCount: number, - ) => Promise<{ data: Uint8Array; totalBytes: number }>, -): Promise<{ +async function extractFormSchema(pdfDoc: PDFDocumentProxy): Promise<{ type: "object"; properties: Record; required?: string[]; } | null> { - // Read full PDF bytes - const { totalBytes } = await readRange(url, 0, 1); - const { data } = await readRange(url, 0, totalBytes); - - const loadingTask = getDocument({ - data, - standardFontDataUrl: STANDARD_FONT_DATA_URL, - StandardFontDataFactory: FetchStandardFontDataFactory, - // We only introspect form fields (never render) — silence residual - // warnings like "Unimplemented border style: inset". - verbosity: VerbosityLevel.ERRORS, - }); - const pdfDoc = await loadingTask.promise; - let fieldObjects: Record | null; try { fieldObjects = (await pdfDoc.getFieldObjects()) as Record< @@ -1056,11 +1032,9 @@ async function extractFormSchema( PdfJsFieldObject[] > | null; } catch { - pdfDoc.destroy(); return null; } if (!fieldObjects || Object.keys(fieldObjects).length === 0) { - pdfDoc.destroy(); return null; } @@ -1131,7 +1105,6 @@ async function extractFormSchema( return /[[\]().]/.test(name) || /^[A-Z0-9_]+$/.test(name); }); - pdfDoc.destroy(); if (Object.keys(properties).length === 0) return null; if (hasMechanicalNames) return null; @@ -1178,6 +1151,12 @@ export interface CreateServerOptions { debug?: boolean; } +// Module-level singletons so they survive across createServer() calls — in +// stateless HTTP deployments a fresh server is created per request, and +// per-instance caches are discarded immediately. +const sharedPdfCache = createPdfCache(); +let cachedAppHtml: string | undefined; + export function createServer(options: CreateServerOptions = {}): McpServer { const { enableInteract = false, useClientRoots = false } = options; const debug = options.debug ?? false; @@ -1197,8 +1176,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { ); } - // Create session-local cache (isolated per server instance) - const { readPdfRange } = createPdfCache(); + const { readPdfRange } = sharedPdfCache; // Tool: list_pdfs - List available PDFs server.tool( @@ -1484,33 +1462,38 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before } } - // Extract form field schema (used for elicitation and field name validation) + // Extract form field schema + detailed field info from a single + // download/parse pass. let formSchema: Awaited> = null; + let fieldInfo: FormFieldInfo[] = []; try { - formSchema = await extractFormSchema(normalized, readPdfRange); + const { data } = await readPdfRange(normalized, 0, totalBytes); + const pdfDoc = await getDocument({ + data, + standardFontDataUrl: STANDARD_FONT_DATA_URL, + StandardFontDataFactory: FetchStandardFontDataFactory, + verbosity: VerbosityLevel.ERRORS, + }).promise; + try { + formSchema = await extractFormSchema(pdfDoc); + fieldInfo = await extractFormFieldInfo(pdfDoc); + } finally { + pdfDoc.destroy(); + } } catch { - // Non-fatal — PDF may not have form fields + // Non-fatal — PDF may not have form fields or may fail to parse } if (formSchema) { viewFieldNames.set(uuid, new Set(Object.keys(formSchema.properties))); } - - // Extract detailed form field info (page, bounding box, label) - let fieldInfo: FormFieldInfo[] = []; - try { - fieldInfo = await extractFormFieldInfo(normalized, readPdfRange); - if (fieldInfo.length > 0) { - viewFieldInfo.set(uuid, fieldInfo); - // Also populate viewFieldNames from field info if not already set - if (!viewFieldNames.has(uuid)) { - viewFieldNames.set( - uuid, - new Set(fieldInfo.map((f) => f.name).filter(Boolean)), - ); - } + if (fieldInfo.length > 0) { + viewFieldInfo.set(uuid, fieldInfo); + if (!viewFieldNames.has(uuid)) { + viewFieldNames.set( + uuid, + new Set(fieldInfo.map((f) => f.name).filter(Boolean)), + ); } - } catch { - // Non-fatal } // Elicit form field values if requested and client supports it @@ -2833,10 +2816,10 @@ Example — add a signature image and a stamp, then screenshot to verify: RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, async (): Promise => { - const html = await fs.promises.readFile( + const html = (cachedAppHtml ??= await fs.promises.readFile( path.join(DIST_DIR, "mcp-app.html"), "utf-8", - ); + )); return { contents: [ { From ba31be1f912f98e0e85049f16fe8a015eb5278c6 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 27 Apr 2026 15:32:53 +0100 Subject: [PATCH 43/97] perf(pdf-server): lazy form extraction via range transport + incremental viewer scans (#639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- examples/pdf-server/server.test.ts | 266 +++++++++++++++++ examples/pdf-server/server.ts | 192 +++++++++---- examples/pdf-server/src/mcp-app.ts | 171 +++++++---- .../pdf-server/src/pdf-annotations.test.ts | 86 ++++++ playwright.config.ts | 4 + tests/e2e/pdf-incremental-load.spec.ts | 271 ++++++++++++++++++ tests/helpers/assets/fw9.pdf | Bin 0 -> 140815 bytes tests/helpers/range-counting-server.ts | 249 ++++++++++++++++ 8 files changed, 1133 insertions(+), 106 deletions(-) create mode 100644 tests/e2e/pdf-incremental-load.spec.ts create mode 100644 tests/helpers/assets/fw9.pdf create mode 100644 tests/helpers/range-counting-server.ts diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index f63ae67..8a0790a 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -4,9 +4,15 @@ import os from "node:os"; import path from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; +import { PDFDocument } from "pdf-lib"; +import { makeRandomJpeg } from "../../tests/helpers/range-counting-server"; import { createPdfCache, createServer, + extractFormSchema, + PdfCacheRangeTransport, + MAX_CHUNK_BYTES, validateUrl, isAncestorDir, allowedLocalFiles, @@ -289,6 +295,266 @@ describe("PDF Cache with Timeouts", () => { // through manual testing or E2E tests. }); +describe("PdfCacheRangeTransport", () => { + it("accumulates ranges larger than MAX_CHUNK_BYTES into one onDataRange call", async () => { + const big = MAX_CHUNK_BYTES * 2 + 100; + const reads: Array<[number, number]> = []; + const t = new PdfCacheRangeTransport("u", big, async (_u, off, n) => { + reads.push([off, n]); + return { + data: new Uint8Array(Math.min(n, MAX_CHUNK_BYTES)), + totalBytes: big, + }; + }); + const delivered: Array<[number, number]> = []; + t.addRangeListener((begin: number, chunk: Uint8Array) => + delivered.push([begin, chunk.length]), + ); + t.requestDataRange(0, big); + await new Promise((r) => setTimeout(r, 10)); + // pdf.js's reader is keyed by the original begin and removed after one + // delivery, so deliver() must call onDataRange exactly once with the + // accumulated buffer — multiple calls would throw inside pdfjs. + expect(delivered).toEqual([[0, big]]); + expect(reads).toEqual([ + [0, MAX_CHUNK_BYTES], + [MAX_CHUNK_BYTES, MAX_CHUNK_BYTES], + [MAX_CHUNK_BYTES * 2, 100], + ]); + }); + + it("rejects .failed when a range fetch errors instead of hanging", async () => { + const t = new PdfCacheRangeTransport("u", 1000, async () => { + throw new Error("network down"); + }); + t.requestDataRange(0, 100); + await expect( + Promise.race([ + t.failed, + new Promise((r) => setTimeout(() => r("timeout"), 200)), + ]), + ).rejects.toThrow("network down"); + }); + + it("rejects .failed on zero-length response (would otherwise spin)", async () => { + const t = new PdfCacheRangeTransport("u", 1000, async () => ({ + data: new Uint8Array(0), + totalBytes: 1000, + })); + t.requestDataRange(0, 100); + await expect(t.failed).rejects.toThrow(/empty range/); + }); + + it("getDocument resolves on a >1MB PDF when readPdfRange clamps to MAX_CHUNK_BYTES", async () => { + // pdfjs coalesces adjacent missing chunks into one requestDataRange that + // can exceed MAX_CHUNK_BYTES. deliver() must accumulate clamped reads and + // hand pdfjs a single onDataRange(begin, fullBuffer). This test fails if + // deliver() either truncates or calls onDataRange more than once per + // requestDataRange (pdf.mjs _onReceiveData matches by exact begin). + const d = await PDFDocument.create(); + const img = await d.embedJpg(makeRandomJpeg(1_100_000)); + const page = d.addPage([612, 792]); + page.drawImage(img, { x: 36, y: 36, width: 540, height: 720 }); + const bytes = await d.save(); + expect(bytes.length).toBeGreaterThan(2 * MAX_CHUNK_BYTES); + + const readClamped: PdfCache["readPdfRange"] = async (_u, off, n) => { + const len = Math.min(n, MAX_CHUNK_BYTES, bytes.length - off); + return { data: bytes.slice(off, off + len), totalBytes: bytes.length }; + }; + // Record the spans pdfjs actually requests so the test fails fast if it + // never asks for >MAX_CHUNK_BYTES (i.e. can't go vacuously green). + const spans: number[] = []; + class RecordingTransport extends PdfCacheRangeTransport { + override requestDataRange(begin: number, end: number): void { + spans.push(end - begin); + super.requestDataRange(begin, end); + } + } + const transport = new RecordingTransport( + "mem://big", + bytes.length, + readClamped, + ); + + const orHang = (p: Promise, what: string): Promise => + Promise.race([ + p, + transport.failed, + new Promise((_, rej) => + setTimeout(() => rej(new Error(`${what} hung`)), 5000), + ), + ]); + + const doc = await orHang( + getDocument({ + range: transport, + length: bytes.length, + disableAutoFetch: true, + disableStream: true, + rangeChunkSize: 64 * 1024, + }).promise, + "getDocument", + ); + const p1 = await orHang(doc.getPage(1), "getPage"); + // getPage() alone doesn't decode the image XObject; getOperatorList() does, + // which is what triggers the >512KB coalesced range request. + await orHang(p1.getOperatorList(), "getOperatorList"); + expect(Math.max(...spans)).toBeGreaterThan(MAX_CHUNK_BYTES); + doc.destroy(); + }); +}); + +describe("display_pdf transport-error handling", () => { + it("returns (does not hang) when range fetches fail mid-load", async () => { + // First fetch = the 1-byte size probe → 206 with Content-Range so + // display_pdf gets totalBytes. Every subsequent fetch (made by + // PdfCacheRangeTransport via readPdfRange) rejects, which must surface + // through transport.failed → orFail() → outer catch, not hang. + let calls = 0; + const mockFetch = spyOn(globalThis, "fetch").mockImplementation( + async () => { + if (calls++ === 0) { + return new Response(new Uint8Array(1), { + status: 206, + headers: { "Content-Range": "bytes 0-0/50000" }, + }); + } + throw new Error("network down"); + }, + ); + + const server = createServer(); + const client = new Client({ name: "t", version: "1" }); + const [ct, st] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(st), client.connect(ct)]); + + try { + const result = await Promise.race([ + client.callTool({ + name: "display_pdf", + arguments: { url: "https://arxiv.org/pdf/err-test" }, + }), + new Promise((_, rej) => + setTimeout( + () => rej(new Error("display_pdf hung on transport error")), + 3000, + ), + ), + ]); + expect(result.isError).toBeFalsy(); + const sc = result.structuredContent as { formFields?: unknown }; + expect(sc.formFields).toBeUndefined(); + expect(calls).toBeGreaterThan(1); + } finally { + mockFetch.mockRestore(); + await client.close(); + await server.close(); + } + }); +}); + +describe("extractFormSchema field-tree handling", () => { + async function schemaFor(bytes: Uint8Array) { + const doc = await getDocument({ data: bytes }).promise; + try { + const fo = (await doc.getFieldObjects()) as Parameters< + typeof extractFormSchema + >[1]; + return await extractFormSchema(doc, fo); + } finally { + doc.destroy(); + } + } + + it("handles pdf-lib separated field/widget structure", async () => { + const d = await PDFDocument.create(); + const form = d.getForm(); + d.addPage([612, 792]); + form + .createTextField("alpha") + .addToPage(d.getPage(0), { x: 50, y: 700, width: 200, height: 20 }); + form + .createCheckBox("agree") + .addToPage(d.getPage(0), { x: 50, y: 660, width: 20, height: 20 }); + form + .createDropdown("choice") + .addToPage(d.getPage(0), { x: 50, y: 620, width: 100, height: 20 }); + + const schema = await schemaFor(await d.save()); + expect(schema).not.toBeNull(); + expect(schema!.properties.alpha).toEqual({ + type: "string", + title: "alpha", + }); + expect(schema!.properties.agree).toEqual({ + type: "boolean", + title: "agree", + }); + expect(schema!.properties.choice.type).toBe("string"); + }); + + it("handles fields with multiple widgets across pages", async () => { + const d = await PDFDocument.create(); + const form = d.getForm(); + d.addPage([612, 792]); + d.addPage([612, 792]); + const tf = form.createTextField("shared"); + tf.addToPage(d.getPage(0), { x: 50, y: 700, width: 200, height: 20 }); + tf.addToPage(d.getPage(1), { x: 50, y: 700, width: 200, height: 20 }); + + const schema = await schemaFor(await d.save()); + expect(schema?.properties.shared).toEqual({ + type: "string", + title: "shared", + }); + }); + + it("skips container nodes and finds leaf fields (W-9 style)", async () => { + const bytes = fs.readFileSync( + path.join(__dirname, "../../tests/helpers/assets/fw9.pdf"), + ); + const doc = await getDocument({ data: new Uint8Array(bytes) }).promise; + try { + const fo = (await doc.getFieldObjects()) as Parameters< + typeof extractFormSchema + >[1]; + // Container nodes (no leaf type) should not crash extraction + expect(fo!["topmostSubform[0]"]).toBeDefined(); + // Schema is null for W-9 (mechanical names), but extraction must not throw + const schema = await extractFormSchema(doc, fo); + expect(schema).toBeNull(); + } finally { + doc.destroy(); + } + }); + + it("returns null when no AcroForm present", async () => { + const d = await PDFDocument.create(); + d.addPage([612, 792]); + const schema = await schemaFor(await d.save()); + expect(schema).toBeNull(); + }); +}); + +describe("validateUrl loopback HTTP allow (PDF_SERVER_ALLOW_LOOPBACK_HTTP)", () => { + it("rejects http://127.0.0.1 by default", () => { + expect(validateUrl("http://127.0.0.1:9999/x.pdf").valid).toBe(false); + }); + + it("accepts http://127.0.0.1 only when the env gate is set, and never non-loopback http", () => { + const prev = process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP; + process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP = "1"; + try { + expect(validateUrl("http://127.0.0.1:9999/x.pdf").valid).toBe(true); + expect(validateUrl("http://169.254.169.254/").valid).toBe(false); + } finally { + if (prev === undefined) delete process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP; + else process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP = prev; + } + }); +}); + describe("validateUrl with MCP roots (allowedLocalDirs)", () => { const savedFiles = new Set(allowedLocalFiles); const savedDirs = new Set(allowedLocalDirs); diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 062b263..93dedfd 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -31,6 +31,7 @@ import { import "./pdfjs-polyfill.js"; import { getDocument, + PDFDataRangeTransport, VerbosityLevel, version as PDFJS_VERSION, } from "pdfjs-dist/legacy/build/pdf.mjs"; @@ -368,9 +369,6 @@ export const viewSourcePaths = new Map(); /** Valid form field names per viewer UUID (populated during display_pdf) */ const viewFieldNames = new Map>(); -/** Detailed form field info per viewer UUID (populated during display_pdf) */ -const viewFieldInfo = new Map(); - /** * Active fs.watch per view. Only created for local files when interact is * enabled (stdio). Watcher is re-established on `rename` events to survive @@ -387,7 +385,7 @@ const viewFileWatches = new Map(); /** * Per-view heartbeat. THIS is what the sweep iterates — not commandQueues. * - * Why not commandQueues: display_pdf populates viewFieldNames/viewFieldInfo/ + * Why not commandQueues: display_pdf populates viewFieldNames/ * viewFileWatches but never touches commandQueues (only enqueueCommand does, * and it's triply gated). And dequeueCommands deletes the entry on every poll, * so even when it exists the sweep's TTL window is ~200ms wide. Net effect: @@ -409,7 +407,6 @@ function pruneStaleQueues(): void { viewLastActivity.delete(uuid); commandQueues.delete(uuid); viewFieldNames.delete(uuid); - viewFieldInfo.delete(uuid); viewsPolled.delete(uuid); viewSourcePaths.delete(uuid); stopFileWatch(uuid); @@ -644,10 +641,19 @@ export function validateUrl(url: string): { // Remote URL - require HTTPS try { const parsed = new URL(url); - if (parsed.protocol !== "https:") { - return { valid: false, error: `Only HTTPS URLs are allowed: ${url}` }; + if (parsed.protocol === "https:") return { valid: true }; + // Loopback HTTP is opt-in (test fixtures, local dev). Off by default so a + // remotely-deployed server can't be made to probe its own internal ports. + if ( + process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP && + parsed.protocol === "http:" && + (parsed.hostname === "127.0.0.1" || + parsed.hostname === "localhost" || + parsed.hostname === "[::1]") + ) { + return { valid: true }; } - return { valid: true }; + return { valid: false, error: `Only HTTPS URLs are allowed: ${url}` }; } catch { return { valid: false, error: `Invalid URL: ${url}` }; } @@ -882,6 +888,61 @@ export function createPdfCache( }; } +/** + * pdf.js range transport backed by {@link PdfCache.readPdfRange}. Lets + * getDocument() fetch only the byte ranges it needs (xref, /AcroForm dict) + * instead of the whole file. With disableAutoFetch, a PDF without form + * fields is opened with ~5% of bytes fetched. + * + * pdf.js has no upstream error channel on PDFDataRangeTransport (its + * `abort()` is a no-op stub it calls *on* us, not the other way). Callers + * must `Promise.race` their pdf.js awaits against {@link failed}, which + * rejects on the first fetch error. + */ +export class PdfCacheRangeTransport extends PDFDataRangeTransport { + /** Rejects on the first range-fetch error; never resolves. */ + readonly failed: Promise; + private fail!: (e: unknown) => void; + + constructor( + private url: string, + length: number, + private readPdfRange: PdfCache["readPdfRange"], + ) { + super(length, null); + this.failed = new Promise((_, reject) => { + this.fail = reject; + }); + // Don't crash the process if no one is racing yet. + this.failed.catch(() => {}); + } + + override requestDataRange(begin: number, end: number): void { + void this.deliver(begin, end).catch((e) => this.fail(e)); + } + + /** + * pdf.js coalesces adjacent missing chunks into one unbounded request, but + * readPdfRange clamps each call to MAX_CHUNK_BYTES. Its reader is keyed by + * the original `begin` and removed after one delivery, so we must accumulate + * slices and call onDataRange exactly once with the full buffer. + */ + private async deliver(begin: number, end: number): Promise { + const buf = new Uint8Array(end - begin); + let off = 0; + while (off < buf.length) { + const want = Math.min(buf.length - off, MAX_CHUNK_BYTES); + const { data } = await this.readPdfRange(this.url, begin + off, want); + if (data.length === 0) { + throw new Error(`empty range at ${begin + off} for ${this.url}`); + } + buf.set(data.subarray(0, Math.min(data.length, buf.length - off)), off); + off += data.length; + } + this.onDataRange(begin, buf); + } +} + // ============================================================================= // MCP Roots // ============================================================================= @@ -957,6 +1018,60 @@ interface FormFieldInfo { options?: string[]; } +/** + * Open `url` via {@link PdfCacheRangeTransport} and return form metadata. + * Uses `disableAutoFetch` so PDFs without an AcroForm are probed with only + * the trailer/xref/catalog (~5-25% of bytes); PDFs with forms still walk + * every page via {@link extractFormFieldInfo} but those are typically small. + * All errors (including range-fetch failures surfaced via + * {@link PdfCacheRangeTransport.failed}) resolve to empty results. + */ +async function probeFormFields( + url: string, + totalBytes: number, + readPdfRange: PdfCache["readPdfRange"], +): Promise<{ + formSchema: Awaited>; + fieldInfo: FormFieldInfo[]; +}> { + // Assigned sequentially below so a throw in extractFormFieldInfo (no per-page + // guard, unlike extractFormSchema) doesn't discard an already-computed schema. + let formSchema: Awaited> = null; + let fieldInfo: FormFieldInfo[] = []; + try { + const transport = new PdfCacheRangeTransport(url, totalBytes, readPdfRange); + const orFail = (p: Promise): Promise => + Promise.race([p, transport.failed]); + const pdfDoc = await orFail( + getDocument({ + range: transport, + length: totalBytes, + disableAutoFetch: true, + disableStream: true, + rangeChunkSize: 64 * 1024, + standardFontDataUrl: STANDARD_FONT_DATA_URL, + StandardFontDataFactory: FetchStandardFontDataFactory, + verbosity: VerbosityLevel.ERRORS, + }).promise, + ); + try { + const fieldObjects = (await orFail(pdfDoc.getFieldObjects())) as Record< + string, + PdfJsFieldObject[] + > | null; + if (fieldObjects && Object.keys(fieldObjects).length > 0) { + formSchema = await orFail(extractFormSchema(pdfDoc, fieldObjects)); + fieldInfo = await orFail(extractFormFieldInfo(pdfDoc)); + } + } finally { + pdfDoc.destroy(); + } + } catch { + // Non-fatal — return whatever was assigned before the throw. + } + return { formSchema, fieldInfo }; +} + /** * Extract detailed form field info (name, type, page, bounding box, label) * from a PDF. Bounding boxes are converted to model coordinates (top-left origin). @@ -1020,27 +1135,25 @@ async function extractFormFieldInfo( return fields; } -async function extractFormSchema(pdfDoc: PDFDocumentProxy): Promise<{ +export async function extractFormSchema( + pdfDoc: PDFDocumentProxy, + fieldObjects: Record | null, +): Promise<{ type: "object"; properties: Record; required?: string[]; } | null> { - let fieldObjects: Record | null; - try { - fieldObjects = (await pdfDoc.getFieldObjects()) as Record< - string, - PdfJsFieldObject[] - > | null; - } catch { - return null; - } if (!fieldObjects || Object.keys(fieldObjects).length === 0) { return null; } const properties: Record = {}; for (const [name, fields] of Object.entries(fieldObjects)) { - const field = fields[0]; // first widget determines the type + // pdfjs returns the full field-tree array: for separated structures + // (pdf-lib) the typed widget is at [1+] behind a container at [0]; for + // merged/leaf entries (W-9, most authoring tools) it's at [0]. Pick the + // first entry that actually has a field type. + const field = fields.find((f) => f.type) ?? fields[0]; if (!field.editable) continue; switch (field.type) { @@ -1434,7 +1547,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before const { totalBytes } = await readPdfRange(normalized, 0, 1); const uuid = randomUUID(); // Start the heartbeat now so the sweep can clean up viewFieldNames/ - // viewFieldInfo/viewFileWatches even if no interact calls ever happen. + // viewFileWatches even if no interact calls ever happen. if (!disableInteract) touchView(uuid); // Check writability (governs save button; see isWritablePath doc). @@ -1462,38 +1575,19 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before } } - // Extract form field schema + detailed field info from a single - // download/parse pass. - let formSchema: Awaited> = null; - let fieldInfo: FormFieldInfo[] = []; - try { - const { data } = await readPdfRange(normalized, 0, totalBytes); - const pdfDoc = await getDocument({ - data, - standardFontDataUrl: STANDARD_FONT_DATA_URL, - StandardFontDataFactory: FetchStandardFontDataFactory, - verbosity: VerbosityLevel.ERRORS, - }).promise; - try { - formSchema = await extractFormSchema(pdfDoc); - fieldInfo = await extractFormFieldInfo(pdfDoc); - } finally { - pdfDoc.destroy(); - } - } catch { - // Non-fatal — PDF may not have form fields or may fail to parse - } + const { formSchema, fieldInfo } = await probeFormFields( + normalized, + totalBytes, + readPdfRange, + ); if (formSchema) { viewFieldNames.set(uuid, new Set(Object.keys(formSchema.properties))); } - if (fieldInfo.length > 0) { - viewFieldInfo.set(uuid, fieldInfo); - if (!viewFieldNames.has(uuid)) { - viewFieldNames.set( - uuid, - new Set(fieldInfo.map((f) => f.name).filter(Boolean)), - ); - } + if (fieldInfo.length > 0 && !viewFieldNames.has(uuid)) { + viewFieldNames.set( + uuid, + new Set(fieldInfo.map((f) => f.name).filter(Boolean)), + ); } // Elicit form field values if requested and client supports it diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index 500879a..a953e24 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -150,6 +150,11 @@ const imageCache = new Map(); /** Annotations imported from the PDF file (baseline for diff computation). */ let pdfBaselineAnnotations: PdfAnnotationDef[] = []; +/** Pages whose native annotations have already been imported into the baseline. */ +const baselineScannedPages = new Set(); +/** Native-annotation ids the user deleted (from restored localStorage diff) — + * the lazy per-page scan must NOT re-add these to annotationMap. */ +const restoredRemovedIds = new Set(); // Dirty flag — tracks unsaved local changes let isDirty = false; @@ -2679,52 +2684,60 @@ function annotationStorageKey(): string | null { } /** - * Import annotations from the loaded PDF to establish the baseline. - * These are the annotations that exist in the PDF file itself. + * Import one page's native annotations into the baseline. Called lazily from + * renderPage() so we don't walk every page (and pull most of the file via + * range requests) before the user sees anything. Idempotent per page. */ -async function loadBaselineAnnotations( - doc: pdfjsLib.PDFDocumentProxy, -): Promise { - pdfBaselineAnnotations = []; - for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) { +function scanPageBaselineAnnotations( + pageNum: number, + annotations: unknown[], +): void { + if (baselineScannedPages.has(pageNum)) return; + baselineScannedPages.add(pageNum); + let imported = 0; + for (let i = 0; i < annotations.length; i++) { + // Isolate each annotation: a malformed one must not bubble up to the + // caller's form-layer try in renderPage() (which would skip + // AnnotationLayer.render and hide form widgets for the whole page). try { - const page = await doc.getPage(pageNum); - const annotations = await page.getAnnotations(); - for (let i = 0; i < annotations.length; i++) { - const ann = annotations[i]; - const def = importPdfjsAnnotation(ann, pageNum, i); - if (def) { - pdfBaselineAnnotations.push(def); - // Add to annotationMap if not already present (from localStorage restore) - if (!annotationMap.has(def.id)) { - annotationMap.set(def.id, { def, elements: [] }); - } - } else if (ann.annotationType !== 20) { - // Widget (type 20) is expected to be skipped; anything else we - // don't import will still be painted by page.render() onto the - // canvas as unselectable pixels. Log so we can diagnose - // "ghost annotations" (visible but not in panel, not clickable). - log.info( - `[WARN] Baseline: skipped PDF annotation on page ${pageNum}`, - `type=${ann.annotationType}`, - `subtype=${ann.subtype ?? "?"}`, - `name=${ann.name ?? "?"}`, - `rect=${ann.rect ? JSON.stringify(ann.rect) : "none"}`, - ); + const ann = annotations[i] as { + annotationType?: number; + subtype?: string; + name?: string; + rect?: number[]; + }; + const def = importPdfjsAnnotation(ann, pageNum, i); + if (def) { + pdfBaselineAnnotations.push(def); + imported++; + if (!annotationMap.has(def.id) && !restoredRemovedIds.has(def.id)) { + annotationMap.set(def.id, { def, elements: [] }); } + } else if (ann.annotationType !== 20) { + // Widget (type 20) is expected to be skipped; anything else we + // don't import will still be painted by page.render() onto the + // canvas as unselectable pixels. Log so we can diagnose + // "ghost annotations" (visible but not in panel, not clickable). + log.info( + `[WARN] Baseline: skipped PDF annotation on page ${pageNum}`, + `type=${ann.annotationType}`, + `subtype=${ann.subtype ?? "?"}`, + `name=${ann.name ?? "?"}`, + `rect=${ann.rect ? JSON.stringify(ann.rect) : "none"}`, + ); } } catch (err) { - // Log the error — a thrown import for one annotation silently - // drops the REST of that page's annotations too. - log.info( - `[WARN] Baseline: page ${pageNum} annotation import failed:`, - err, - ); + log.info(`Baseline: page ${pageNum} annotation import failed`, err); + } + } + if (imported > 0) { + try { + updateAnnotationsBadge(); + renderAnnotationPanel(); + } catch (err) { + log.info(`Baseline: page ${pageNum} panel update failed`, err); } } - log.info( - `Loaded ${pdfBaselineAnnotations.length} baseline annotations from PDF`, - ); } function persistAnnotations(): void { @@ -2740,6 +2753,20 @@ function persistAnnotations(): void { pdfBaselineFormValues, ); + // computeDiff only sees baseline ids from pages we've already scanned. + // Carry forward restored tombstones for unvisited pages so the first + // persist after restore doesn't drop them. Once every page is scanned the + // baseline is complete and computeDiff is authoritative on its own — + // dropping the carry-forward then also stops a stale id (no longer in the + // file) from pinning dirty=true forever. + if (baselineScannedPages.size < totalPages) { + for (const id of restoredRemovedIds) { + if (!annotationMap.has(id) && !diff.removed.includes(id)) { + diff.removed.push(id); + } + } + } + // Dirty tracks whether there are unsaved changes. Undoing back to baseline // yields an empty diff → clean again → save button disables. if (!isRestoring) setDirty(!isDiffEmpty(diff)); @@ -2765,11 +2792,11 @@ function restoreAnnotations(): void { const diff = deserializeDiff(raw); // Merge baseline + diff. The loop below is add-only, so we MUST also - // delete: loadBaselineAnnotations() runs between the two restore calls - // and re-seeds annotationMap with every baseline id — including the - // ones in diff.removed. Without this, the zombie survives the restore, - // and the next persistAnnotations() sees it in currentIds → computeDiff - // produces removed=[] → the deletion is permanently lost from storage. + // delete: the per-page baseline scan re-seeds annotationMap with every + // native id it encounters — including ones in diff.removed. Without the + // deletes here AND the restoredRemovedIds tombstones below, the zombie + // survives, and the next persistAnnotations() sees it in currentIds → + // computeDiff produces removed=[] → the deletion is permanently lost. const merged = mergeAnnotations(pdfBaselineAnnotations, diff); for (const def of merged) { if (!annotationMap.has(def.id)) { @@ -2778,6 +2805,9 @@ function restoreAnnotations(): void { } for (const id of diff.removed) { annotationMap.delete(id); + // Tombstone so the lazy per-page baseline scan (which runs AFTER this + // restore) doesn't resurrect it. + restoredRemovedIds.add(id); } // Restore form fields @@ -2869,6 +2899,14 @@ async function buildFieldNameMap( // getFieldObjects may fail on some PDFs } + // No AcroForm → nothing to map. Skip the per-page widget walk so form-free + // PDFs (the common large case) don't pull every page after first paint. + // getFieldObjects() itself only reads the catalog/AcroForm dict via range + // transport, so this gate is cheap. + if (!cachedFieldObjects || Object.keys(cachedFieldObjects).length === 0) { + return false; + } + // Scan every page's widget annotations to collect the CORRECT storage keys, // plus labels, pages, positions, AND fieldValue (what the widget renders // — which can differ from getFieldObjects().value if the PDF is internally @@ -3074,11 +3112,19 @@ async function getAnnotatedPdfBytes(): Promise { } // Baseline annotations the user deleted: strip their refs from /Annots so - // they don't reappear on reload. Ids without a recoverable ref (page-index - // fallback) can't be removed by-ref and are skipped. - const removedRefs = pdfBaselineAnnotations - .filter((a) => !annotationMap.has(a.id)) - .map((a) => parseAnnotationRef(a.id)) + // they don't reappear on reload. Include restored tombstones for pages we + // haven't scanned yet — those ids aren't in pdfBaselineAnnotations but the + // ref is still parseable from the id string. Ids without a recoverable ref + // (page-index fallback) can't be removed by-ref and are skipped. + const removedIds = new Set(); + for (const a of pdfBaselineAnnotations) { + if (!annotationMap.has(a.id)) removedIds.add(a.id); + } + for (const id of restoredRemovedIds) { + if (!annotationMap.has(id)) removedIds.add(id); + } + const removedRefs = [...removedIds] + .map(parseAnnotationRef) .filter((r): r is NonNullable => r !== null); // Only write fields that actually changed vs. what's already in the PDF. @@ -3362,6 +3408,9 @@ async function renderPage() { formLayerEl.style.setProperty("--total-scale-factor", `${scale}`); try { const annotations = await page.getAnnotations(); + // Lazy baseline import — piggyback on the annotations we just fetched + // for this page instead of walking all pages upfront. + scanPageBaselineAnnotations(pageToRender, annotations); if (annotations.length > 0) { const linkService = { getDestinationHash: () => "#", @@ -4406,6 +4455,8 @@ async function reloadPdf(): Promise { undoStack.length = 0; redoStack.length = 0; pdfBaselineAnnotations = []; + baselineScannedPages.clear(); + restoredRemovedIds.clear(); pdfBaselineFormValues.clear(); pageTextCache.clear(); pageTextItemsCache.clear(); @@ -4449,11 +4500,11 @@ async function reloadPdf(): Promise { log.info("PDF reloaded:", totalPages, "pages,", totalBytes, "bytes"); showViewer(); - // Render immediately — annotation/form scans below are O(numPages) and - // do NOT block the canvas. See same pattern in the initial-load path. + // Render immediately — baseline-annotation scan now happens per-page + // inside renderPage(); buildFieldNameMap below early-returns when no + // AcroForm is present. See same pattern in the initial-load path. await renderPage(); - await loadBaselineAnnotations(document); const seeded = await buildFieldNameMap(document); syncFormValuesToStorage(); if (seeded) await renderPage(); @@ -4509,6 +4560,11 @@ async function loadPdfProgressively(urlToLoad: string): Promise<{ const loadingTask = pdfjsLib.getDocument({ range: transport, standardFontDataUrl: STANDARD_FONT_DATA_URL, + // Only fetch ranges renderPage()/getFieldObjects() actually ask for. + // Without these pdfjs background-prefetches the whole file regardless of + // the per-page lazy scans below. + disableAutoFetch: true, + disableStream: true, }); try { @@ -4673,12 +4729,13 @@ app.ontoolresult = async (result: CallToolResult) => { scale = fitScale; log.info("Initial fit scale:", scale); } - await renderPage(); - - // Import annotations from the PDF to establish baseline - await loadBaselineAnnotations(document); - // Restore any persisted user diff + // Restore any persisted user diff BEFORE first render so the per-page + // baseline scan inside renderPage() can honour the removed-id tombstones + // and not resurrect annotations the user deleted last session. + // restoreAnnotations is sync (localStorage read) so first paint is not + // delayed. restoreAnnotations(); + await renderPage(); // Build field name → annotation ID mapping for form filling const seeded = await buildFieldNameMap(document); diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index 38ef27b..239777a 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -367,6 +367,92 @@ describe("computeDiff", () => { current.map((a) => a.id).sort(), ); }); + + // Backs the post-computeDiff union step in mcp-app.ts persistAnnotations / + // getAnnotatedPdfBytes: with the lazy per-page baseline scan, computeDiff + // alone cannot produce `removed` for natives on pages not yet visited, so + // callers must union restoredRemovedIds. These tests pin the contract those + // call sites depend on. + describe("partial baseline (lazy-scan tombstone preservation)", () => { + const tombstoned = "pdf-5-0"; + const userNote: PdfAnnotationDef = { + type: "note", + id: "u1", + page: 1, + x: 10, + y: 10, + content: "unrelated edit", + }; + + /** Mirrors the union loop in mcp-app.ts persistAnnotations. */ + function unionRestored( + diff: AnnotationDiff, + restored: Iterable, + currentIds: Set, + ): void { + for (const id of restored) { + if (!currentIds.has(id) && !diff.removed.includes(id)) { + diff.removed.push(id); + } + } + } + + it("computeDiff alone drops tombstones for unscanned pages; the union step preserves them", () => { + const baseline: PdfAnnotationDef[] = []; // page with the native not yet scanned + const current = [userNote]; + const diff = computeDiff(baseline, current, new Map()); + expect(diff.removed).toEqual([]); // proves the union step is load-bearing + + unionRestored(diff, [tombstoned], new Set(current.map((a) => a.id))); + + expect(diff.removed).toEqual([tombstoned]); + expect(isDiffEmpty(diff)).toBe(false); + expect(deserializeDiff(serializeDiff(diff)).removed).toEqual([ + tombstoned, + ]); + }); + + it("a tombstone the user re-added is excluded from the union", () => { + const reAdded: PdfAnnotationDef = { + type: "note", + id: tombstoned, + page: 3, + x: 0, + y: 0, + content: "back", + }; + const current = [userNote, reAdded]; + const diff = computeDiff([], current, new Map()); + unionRestored(diff, [tombstoned], new Set(current.map((a) => a.id))); + expect(diff.removed).toEqual([]); + }); + + it("union does not duplicate ids once the page is scanned and computeDiff produces them", () => { + const native: PdfAnnotationDef = { + type: "note", + id: tombstoned, + page: 3, + x: 0, + y: 0, + content: "native", + }; + const diff = computeDiff([native], [userNote], new Map()); + expect(diff.removed).toEqual([tombstoned]); + unionRestored(diff, [tombstoned], new Set([userNote.id])); + expect(diff.removed).toEqual([tombstoned]); + }); + + it("removedRefs from restored tombstones parse for buildAnnotatedPdfBytes; non-ref ids are skipped", () => { + const restored = new Set(["pdf-5-0", "pdf-12R", "pdf-2-idx-7"]); + const removedRefs = [...restored] + .map(parseAnnotationRef) + .filter((r): r is NonNullable => r !== null); + expect(removedRefs).toEqual([ + { objectNumber: 5, generationNumber: 0 }, + { objectNumber: 12, generationNumber: 0 }, + ]); + }); + }); }); // ============================================================================= diff --git a/playwright.config.ts b/playwright.config.ts index 6f6cb65..ecf1e13 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -47,6 +47,10 @@ export default defineConfig({ env: { ...process.env, EXAMPLE: process.env.EXAMPLE ?? "", + // Let pdf-server fetch from the http://127.0.0.1 range-counting fixture + // (validateUrl rejects loopback HTTP unless this is set). Scoped to this + // server's check only — does not touch Node's TLS verification. + PDF_SERVER_ALLOW_LOOPBACK_HTTP: "1", }, }, // Snapshot configuration diff --git a/tests/e2e/pdf-incremental-load.spec.ts b/tests/e2e/pdf-incremental-load.spec.ts new file mode 100644 index 0000000..52a99ee --- /dev/null +++ b/tests/e2e/pdf-incremental-load.spec.ts @@ -0,0 +1,271 @@ +import { test, expect, type Page } from "@playwright/test"; +import { + startRangeServer, + type RangeServer, +} from "../helpers/range-counting-server"; + +/** + * Regression guard for incremental PDF loading. + * + * Asserts that display_pdf does not pull the entire file before the viewer + * starts streaming, that form schema is still returned in the initial response, + * and that no byte range is fetched server-side more than once. + * + * The "noforms <30%" test is the load-bearing regression check: it fails on the + * pre-range-transport implementation (which downloads 100% during display_pdf + * for form analysis) and passes once form extraction uses range transport. + */ + +test.setTimeout(120_000); + +let rangeServer: RangeServer; + +test.beforeAll(async () => { + rangeServer = await startRangeServer(); +}); + +test.afterAll(async () => { + await rangeServer.close(); +}); + +test.beforeEach(() => { + rangeServer.resetStats(); +}); + +function getAppFrame(page: Page) { + return page.frameLocator("iframe").first().frameLocator("iframe").first(); +} + +async function waitForAppLoad(page: Page) { + const outerFrame = page.frameLocator("iframe").first(); + await expect(outerFrame.locator("iframe")).toBeVisible({ timeout: 30_000 }); +} + +/** + * Load basic-host, select PDF Server, call display_pdf with a custom URL. + * Resolves once the tool result panel appears (server-side display_pdf done); + * does NOT wait for the viewer iframe — call waitForAppLoad separately so + * byte-count assertions can isolate server-side fetches from viewer fetches. + */ +async function displayPdf(page: Page, url: string) { + await page.goto("/?theme=hide"); + await expect(page.locator("select").first()).toBeEnabled({ timeout: 30_000 }); + await page.locator("select").first().selectOption({ label: "PDF Server" }); + await page.locator("textarea").fill(JSON.stringify({ url })); + await page.click('button:has-text("Call Tool")'); + await expect(page.locator('text="📤 Tool Result"').first()).toBeVisible({ + timeout: 30_000, + }); +} + +/** Read and parse the most recent tool result's structuredContent. */ +async function readStructuredContent( + page: Page, +): Promise> { + const resultPanel = page.locator('text="📤 Tool Result"').first(); + await expect(resultPanel).toBeVisible({ timeout: 30_000 }); + await resultPanel.click(); + const pre = page.locator("pre").last(); + await expect(pre).toBeVisible({ timeout: 5_000 }); + const raw = (await pre.textContent()) ?? "{}"; + const parsed = JSON.parse(raw) as { structuredContent?: object }; + return (parsed.structuredContent ?? {}) as Record; +} + +async function waitForFirstPageRendered(page: Page) { + const canvas = getAppFrame(page).locator("canvas").first(); + await expect(canvas).toBeVisible({ timeout: 30_000 }); + await expect + .poll(async () => canvas.evaluate((c: HTMLCanvasElement) => c.width)) + .toBeGreaterThan(0); +} + +test.describe("PDF Server — incremental loading", () => { + test("display_pdf on a form PDF returns form fields in initial response", async ({ + page, + }) => { + await displayPdf(page, `${rangeServer.baseUrl}/forms.pdf`); + await waitForAppLoad(page); + const sc = await readStructuredContent(page); + const fields = sc.formFields as Array<{ name: string }> | undefined; + expect(fields?.map((f) => f.name).sort()).toEqual([ + "city", + "email", + "name", + "notes", + "phone", + ]); + }); + + test("display_pdf on a no-forms PDF stays under byte budget and bounded overlap", async ({ + page, + }) => { + const fileSize = rangeServer.fileSizes["/noforms.pdf"]; + await displayPdf(page, `${rangeServer.baseUrl}/noforms.pdf`); + + // Measure before the viewer iframe loads so the count reflects only the + // server-side display_pdf range fetches. + expect(rangeServer.stats().totalBytesServed).toBeLessThan(fileSize * 0.3); + + await waitForAppLoad(page); + const sc = await readStructuredContent(page); + expect(sc.formFields).toBeUndefined(); + + await waitForFirstPageRendered(page); + // Server-side display_pdf and the viewer each open the document + // independently, so the xref/trailer/catalog is fetched twice (≈25%). + // This guards against the pre-range-transport behavior where the server + // alone pulled 100% (then 200% with the double-parse), giving overlap >> + // file size once the viewer also loaded. + expect(rangeServer.stats().overlapBytes).toBeLessThan(fileSize * 0.5); + }); + + test("first page renders under stall, then page 2 renders the >512KB image after release", async ({ + page, + }) => { + const fileSize = rangeServer.fileSizes["/noforms.pdf"]; + // Allow ~40% through (header + trailer/xref + page-1 content) then stall. + // The 1.1MB image stream referenced only by pages 2+ is the bulk. + const budget = Math.floor(fileSize * 0.4); + await displayPdf( + page, + `${rangeServer.baseUrl}/noforms.pdf?stallAfterBytes=${budget}`, + ); + await waitForAppLoad(page); + await waitForFirstPageRendered(page); + expect(rangeServer.stats().totalBytesServed).toBeLessThan(fileSize); + + rangeServer.release(); + const app = getAppFrame(page); + await app.locator("#next-btn").click(); + await expect(app.locator("#page-input")).toHaveValue("2", { + timeout: 30_000, + }); + // Page 2 references the ~1.1MB embedded JPEG. Rendering it exercises the + // viewer's range transport on a >MAX_CHUNK_BYTES object after the stall + // is released. (The server-side PdfCacheRangeTransport accumulate-once + // path is covered by the unit test in server.test.ts; on noforms.pdf + // display_pdf bails at the empty getFieldObjects() check before touching + // the image stream, so this test does not exercise it.) + await waitForFirstPageRendered(page); + expect(rangeServer.stats().totalBytesServed).toBeGreaterThan( + fileSize * 0.9, + ); + }); +}); + +// Kept in this spec because the test needs an HTTP-served PDF and the +// range-counting fixture is the convenient place; it does not use any +// byte-accounting features. +test.describe("PDF Server — annotation tombstone preservation", () => { + // FIXME(https://github.com/modelcontextprotocol/ext-apps/issues/642): + // basic-host doesn't replay the cached tool result on inner-iframe reload, + // and a fresh display_pdf call gets a new toolId → new storage key, so the + // restore-from-localStorage path can't be reached. The fix itself is covered + // by the computeDiff/serializeDiff contract tests in + // src/pdf-annotations.test.ts. + test.fixme("deleted native annotation tombstone survives a persist before its page is scanned", async ({ + page, + }) => { + // Regression for the lazy baseline scan: restoredRemovedIds must be + // unioned into persistAnnotations() and getAnnotatedPdfBytes() so a + // delete on page 2 isn't silently dropped when an unrelated edit on + // page 1 triggers a persist before page 2 has been re-scanned. + + await displayPdf(page, `${rangeServer.baseUrl}/with-native-annot.pdf`); + await waitForAppLoad(page); + await waitForFirstPageRendered(page); + const sc = await readStructuredContent(page); + const viewUUID = sc.viewUUID as string; + expect(viewUUID).toBeTruthy(); + + const app = getAppFrame(page); + + // 1. Go to page 2, open the panel, delete the native annotation via UI. + await app.locator("#next-btn").click(); + await expect(app.locator("#page-input")).toHaveValue("2"); + await app.locator("#annotations-btn").click(); + const nativeCard = app.locator( + '.annotation-card[data-annotation-id^="pdf-"]', + ); + await expect(nativeCard).toBeVisible({ timeout: 10_000 }); + const nativeId = await nativeCard.getAttribute("data-annotation-id"); + expect(nativeId).toMatch(/^pdf-\d+R?$/); + await nativeCard.locator(".annotation-card-delete").click(); + // Deleting a native annotation re-renders the card as a crossed-out + // tombstone (annotation-panel.ts createRemovedAnnotationCard) with the + // same data-annotation-id — it doesn't disappear from the DOM. + await expect(nativeCard).toHaveClass(/annotation-card-cleared/); + + // 2. Back to page 1 so the post-reload viewer restores there (page 2 + // must stay unscanned until the very end). + await app.locator("#page-input").fill("1"); + await app.locator("#page-input").press("Enter"); + await expect(app.locator("#page-input")).toHaveValue("1"); + + // 3. Capture the annotation localStorage key and confirm the delete was + // persisted. + const storageKey = await app + .locator("body") + .evaluate( + () => + Object.keys(localStorage).find( + (k) => k.startsWith("pdf-annot:") || k.endsWith(":annotations"), + ) ?? null, + ); + expect(storageKey).toBeTruthy(); + const diffBefore = await app + .locator("body") + .evaluate((_, k) => localStorage.getItem(k), storageKey!); + expect(JSON.parse(diffBefore!).removed).toContain(nativeId); + + // 4. Reload the inner viewer iframe ONLY (basic-host keeps the same + // cached tool result → same viewUUID/toolId → same storage key). + // restoreAnnotations() now seeds restoredRemovedIds from localStorage + // while the lazy scan has only seen page 1. + await app.locator("body").evaluate(() => location.reload()); + await waitForFirstPageRendered(page); + await expect(app.locator("#page-input")).toHaveValue("1"); + + // 5. Trigger persistAnnotations() via an unrelated edit on page 1 — the + // bug scenario: page 2 has not been scanned yet. + const toolSelect = page.locator("select").nth(1); + await toolSelect.selectOption("interact"); + await page.locator("textarea").fill( + JSON.stringify({ + viewUUID, + action: "add_annotations", + annotations: [ + { + id: "probe-on-page-1", + type: "highlight", + page: 1, + rects: [{ x: 50, y: 700, width: 100, height: 12 }], + }, + ], + }), + ); + await page.click('button:has-text("Call Tool")'); + await expect( + app.locator('[data-annotation-id="probe-on-page-1"]'), + ).toHaveCount(1, { timeout: 10_000 }); + + // 6. Load-bearing assertion: the persisted diff still carries the + // tombstone. Pre-fix, computeDiff() over the page-1-only baseline + // yielded removed=[], overwriting it. + const diffAfter = await app + .locator("body") + .evaluate((_, k) => localStorage.getItem(k), storageKey!); + const removedAfter: string[] = JSON.parse(diffAfter!).removed; + expect(removedAfter).toContain(nativeId); + + // 7. Belt-and-suspenders: navigate to page 2 (lazy scan now sees the + // native annotation) and confirm the panel shows it as a cleared + // tombstone, not a live (resurrected) card. + await app.locator("#next-btn").click(); + await expect(app.locator("#page-input")).toHaveValue("2"); + await expect( + app.locator(`.annotation-card[data-annotation-id="${nativeId}"]`), + ).toHaveClass(/annotation-card-cleared/); + }); +}); diff --git a/tests/helpers/assets/fw9.pdf b/tests/helpers/assets/fw9.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c5eb6467893181c1d481e0748893877b015d5db0 GIT binary patch literal 140815 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
+

Public App

+

+ No auth was needed to render this view. Click below to invoke the + protected + get_secret tool — the host will run the OAuth flow on 401 + and retry. +

+
+ + + +
+
+
+ + + diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json new file mode 100644 index 0000000..74f8b02 --- /dev/null +++ b/examples/lazy-auth-server/package.json @@ -0,0 +1,51 @@ +{ + "name": "@modelcontextprotocol/server-lazy-auth", + "version": "1.7.2", + "type": "module", + "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/ext-apps", + "directory": "examples/lazy-auth-server" + }, + "license": "MIT", + "main": "dist/server.js", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && cross-env INPUT=secret-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", + "watch": "concurrently \"cross-env INPUT=mcp-app.html vite build --watch\" \"cross-env INPUT=secret-app.html vite build --watch\"", + "serve": "bun --watch main.ts", + "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "jose": "^6.0.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "22.10.0", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + }, + "types": "dist/server.d.ts", + "exports": { + ".": { + "types": "./dist/server.d.ts", + "default": "./dist/server.js" + } + }, + "bin": { + "mcp-server-lazy-auth": "dist/index.js" + } +} diff --git a/examples/lazy-auth-server/secret-app.html b/examples/lazy-auth-server/secret-app.html new file mode 100644 index 0000000..b2ab00b --- /dev/null +++ b/examples/lazy-auth-server/secret-app.html @@ -0,0 +1,16 @@ + + + + + + + Lazy Auth Demo — Secret + + +
+

Protected App

+
Waiting for secret data…
+
+ + + diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts new file mode 100644 index 0000000..fa46d67 --- /dev/null +++ b/examples/lazy-auth-server/server.ts @@ -0,0 +1,1068 @@ +/** + * Lazy Auth demo MCP server. + * + * Demonstrates "lazy" (on-demand) OAuth for MCP servers and Apps: + * + * - The server connects without authentication: `initialize`, `tools/list`, + * and public tool calls all succeed with no Bearer token. + * - Protected tools (`get_secret`, `revoke_auth_token`) return 401 with a + * `WWW-Authenticate` header pointing at protected-resource metadata, so the + * host only runs the OAuth flow when a protected tool is actually called. + * - The `show_auth_button` MCP App is public; clicking its button calls the + * protected `get_secret` tool via `callServerTool`. The host sees the 401, + * runs OAuth, retries, and the result renders inline. + * + * The embedded OAuth authorization server is intentionally minimal and + * stateless (HS256 JWTs, auth codes encoded as short-lived JWTs) so the demo + * can run anywhere with a single env var. It is NOT a production AS. + */ +import { + registerAppResource, + registerAppTool, + RESOURCE_MIME_TYPE, +} from "@modelcontextprotocol/ext-apps/server"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js"; +import type { + CallToolResult, + ReadResourceResult, +} from "@modelcontextprotocol/sdk/types.js"; +import cors from "cors"; +import express, { type Express, type Request, type Response } from "express"; +import { SignJWT, jwtVerify } from "jose"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +// Works both from source (server.ts) and compiled (dist/server.js) +const DIST_DIR = import.meta.filename.endsWith(".ts") + ? path.join(import.meta.dirname, "dist") + : import.meta.dirname; + +// ─── Config ────────────────────────────────────────────────────────────────── + +export const PORT = parseInt(process.env.PORT ?? "3097", 10); +const JWT_SECRET = new TextEncoder().encode( + process.env.JWT_SECRET ?? + "dev-insecure-secret-do-not-use-in-production-" + "x".repeat(20), +); +const PROTECTED_TOOLS = new Set(["get_secret", "revoke_auth_token"]); +// Token lifetimes. Defaults are deliberately short so the host's refresh flow +// (30s–5min after auth) and full re-auth (>5min) are easy to observe in a demo. +// Slow or automated clients can request longer-lived tokens per connection by +// connecting to a TTL-scoped endpoint path (/ttl//mcp) — see +// ttlFromResource() and the README. +const ACCESS_TOKEN_TTL_SECONDS = parseInt( + process.env.ACCESS_TOKEN_TTL_SECONDS ?? "30", + 10, +); +const REFRESH_TOKEN_TTL_SECONDS = parseInt( + process.env.REFRESH_TOKEN_TTL_SECONDS ?? "300", + 10, +); +// Cap on per-connection `?token_ttl_s=` requests. +const MAX_TOKEN_TTL_SECONDS = 86400; +// When true: root well-known PRM returns *stub* metadata WITHOUT authorization_servers, +// and root well-known AS is 404. Hosts probing well-known on connect see "resource has +// PRM but no auth configured" → connect without OAuth. Real PRM (with authorization_servers) +// is only at /auth/prm, reachable via WWW-Authenticate on 401. Default false = standard. +const REACTIVE_AUTH_ONLY = process.env.REACTIVE_AUTH_ONLY === "1"; + +/** Hostname is localhost / a loopback address (URL-style, so IPv6 keeps its brackets). */ +function isLoopbackHostname(hostname: string): boolean { + return ( + ["localhost", "127.0.0.1", "[::1]"].includes(hostname) || + hostname.endsWith(".localhost") + ); +} + +/** + * Resolve the public base URL for this deployment. + * + * Precedence: the PUBLIC_URL env var, then the request's Host header (loopback + * hosts only), then http://localhost:PORT. Host / X-Forwarded-* headers naming + * non-loopback hosts are deliberately NOT trusted — a client could spoof them + * to make OAuth metadata and callback URLs point at a host it controls. + * Non-localhost deployments (e.g. behind a public tunnel) must set PUBLIC_URL + * explicitly. + */ +function resolvePublicUrl(req?: Request): URL { + const envUrl = process.env.PUBLIC_URL; + if (envUrl) return new URL(envUrl.endsWith("/") ? envUrl : envUrl + "/"); + const host = req?.headers.host; + if (host) { + try { + const url = new URL(`http://${host}/`); + if (isLoopbackHostname(url.hostname)) return url; + } catch { + // Malformed Host header → fall through to the localhost default. + } + } + return new URL(`http://localhost:${PORT}/`); +} + +/** OAuth issuer. In REACTIVE_AUTH_ONLY mode, uses a /auth subpath so root well-known 404s. + * Otherwise uses the root origin (standard). */ +const ISSUER_SUFFIX = REACTIVE_AUTH_ONLY ? "/auth" : ""; +function resolveIssuer(req?: Request): string { + return resolvePublicUrl(req).origin + ISSUER_SUFFIX; +} + +// ─── Mock OAuth (HS256, stateless codes) ───────────────────────────────────── + +interface CodePayload { + client_id: string; + redirect_uri: string; + code_challenge?: string; + code_challenge_method?: string; + scope?: string; + /** Requested access-token TTL, from `?token_ttl_s=` on the resource indicator. */ + token_ttl_s?: number; +} + +/** Validate + cap a raw TTL value (from a path segment or query param). */ +function parseTtlSeconds(raw: string | undefined): number | undefined { + const ttl = parseInt(raw ?? "", 10); + return Number.isFinite(ttl) && ttl > 0 + ? Math.min(ttl, MAX_TOKEN_TTL_SECONDS) + : undefined; +} + +/** + * Extract the requested access-token TTL from a resource indicator (RFC 8707). + * + * MCP hosts send `resource=` — the URL the client connected + * to — in authorization and token requests. Clients opt into a non-default + * token lifetime by connecting to a TTL-scoped MCP endpoint path: + * + * /ttl//mcp (e.g. /ttl/3600/mcp for 1-hour tokens) + * + * The TTL lives in the PATH because hosts canonicalize resource indicators and + * strip query strings (verified against real hosts), so `?token_ttl_s=` query + * params would never reach this server's OAuth endpoints. The query param is + * still honored as a fallback for clients that preserve it. + */ +function ttlFromResource(resource: string | undefined): number | undefined { + if (!resource) return undefined; + try { + const url = new URL(resource); + const pathMatch = url.pathname.match(/\/ttl\/(\d+)\/mcp$/); + return ( + parseTtlSeconds(pathMatch?.[1]) ?? + parseTtlSeconds(url.searchParams.get("token_ttl_s") ?? undefined) + ); + } catch { + return undefined; + } +} + +interface AuthInfo { + token: string; + sub: string; + sid: string; +} + +// ─── Session revocation ─────────────────────────────────────────────────────── +// +// Each OAuth session (authorize → code → tokens) gets a single `sid`; all access & +// refresh tokens in that session carry it. Revoking the sid invalidates both → host +// must do a full re-OAuth (refresh grant also fails). +// +// Storage is an in-memory Map, which is fine for a single-process demo server. If +// you deploy this to a multi-instance/serverless environment, back it with a +// shared store instead. + +const revokedSids = new Map(); // sid → unix GC time +// Revoked-sid entries need only outlive the longest token TTL; add small clock-skew grace. +const REVOCATION_GC_TTL_SECONDS = + Math.max( + ACCESS_TOKEN_TTL_SECONDS, + REFRESH_TOKEN_TTL_SECONDS, + MAX_TOKEN_TTL_SECONDS, + ) + 10; + +function revokeSession(sid: string): void { + revokedSids.set( + sid, + Math.floor(Date.now() / 1000) + REVOCATION_GC_TTL_SECONDS, + ); + const now = Math.floor(Date.now() / 1000); + for (const [k, gc] of revokedSids) if (gc < now) revokedSids.delete(k); +} + +function isSessionRevoked(sid: string | undefined): boolean { + if (!sid) return false; + return revokedSids.has(sid); +} + +async function signAccessToken( + sub: string, + scope: string, + sid: string, + issuer: string, + audience: string, + ttlSeconds: number = ACCESS_TOKEN_TTL_SECONDS, +): Promise { + return new SignJWT({ sub, scope, sid }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setIssuer(issuer) + .setAudience(audience) + .setExpirationTime(`${ttlSeconds}s`) + .sign(JWT_SECRET); +} + +async function signRefreshToken( + sub: string, + scope: string, + sid: string, + issuer: string, + ttlSeconds: number = REFRESH_TOKEN_TTL_SECONDS, + accessTtlSeconds?: number, +): Promise { + // access_ttl_s carries the grant's requested access-token TTL forward so + // refreshed access tokens keep the same lifetime as the original. + return new SignJWT({ + sub, + scope, + sid, + typ: "refresh", + ...(accessTtlSeconds !== undefined + ? { access_ttl_s: accessTtlSeconds } + : {}), + }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setIssuer(issuer) + .setExpirationTime(`${ttlSeconds}s`) + .sign(JWT_SECRET); +} + +async function verifyRefreshToken( + token: string, + issuer: string, +): Promise< + | { sub: string; scope: string; sid: string; accessTtlSeconds?: number } + | undefined +> { + try { + const { payload } = await jwtVerify(token, JWT_SECRET, { issuer }); + if (payload.typ !== "refresh") return undefined; + const sid = payload.sid as string | undefined; + if (isSessionRevoked(sid)) return undefined; // session revoked → refresh fails + return { + sub: payload.sub ?? "", + scope: (payload.scope as string) ?? "", + sid: sid ?? "", + accessTtlSeconds: + typeof payload.access_ttl_s === "number" + ? payload.access_ttl_s + : undefined, + }; + } catch { + return undefined; + } +} + +/** Encode grant details inside the code itself (5min expiry) — no server storage. */ +async function signAuthCode( + payload: CodePayload, + issuer: string, +): Promise { + return new SignJWT({ ...payload, typ: "code" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setIssuer(issuer) + .setExpirationTime("5m") + .sign(JWT_SECRET); +} + +async function verifyAuthCode( + code: string, + issuer: string, +): Promise { + try { + const { payload } = await jwtVerify(code, JWT_SECRET, { issuer }); + if (payload.typ !== "code") return undefined; + return payload as unknown as CodePayload; + } catch { + return undefined; + } +} + +async function verifyAccessToken( + token: string, + issuer: string, + maxAgeSeconds?: number, +): Promise { + try { + const { payload } = await jwtVerify(token, JWT_SECRET, { issuer }); + if (payload.typ === "code" || payload.typ === "refresh") return undefined; // only plain access tokens + const sid = payload.sid as string | undefined; + if (isSessionRevoked(sid)) return undefined; // session revoked + // Per-connection TTL override (?token_ttl_s= on the MCP URL): treat tokens + // older than maxAgeSeconds as expired even though their exp claim is still + // in the future. Lets one deployment serve both long-TTL (default) and + // short-TTL (refresh-flow testing) connections. + if (maxAgeSeconds !== undefined && typeof payload.iat === "number") { + const ageSeconds = Math.floor(Date.now() / 1000) - payload.iat; + if (ageSeconds > maxAgeSeconds) return undefined; + } + return { token, sub: payload.sub ?? "", sid: sid ?? "" }; + } catch { + return undefined; + } +} + +function escapeHtml(s: string) { + return s.replace( + /[&<>"']/g, + (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ + c + ]!, + ); +} + +/** + * This demo AS has no client registry, so it can't check redirect URIs against + * pre-registered values. At minimum, only allow https targets (or http on + * loopback for local development hosts) so the consent page never links to, + * or redirects the authorization code to, an unexpected scheme. + */ +function isAllowedRedirectUri(redirectUri: string): boolean { + try { + const url = new URL(redirectUri); + if (url.protocol === "https:") return true; + return url.protocol === "http:" && isLoopbackHostname(url.hostname); + } catch { + return false; + } +} + +async function handleAuthorize(req: Request, res: Response) { + const { + client_id, + redirect_uri, + state, + code_challenge, + code_challenge_method, + scope, + approved, + resource, + } = req.query as Record; + if (!redirect_uri) { + res.status(400).json({ + error: "invalid_request", + error_description: "Missing redirect_uri", + }); + return; + } + if (!isAllowedRedirectUri(redirect_uri)) { + res.status(400).json({ + error: "invalid_request", + error_description: "redirect_uri must use https (or http on localhost)", + }); + return; + } + const issuer = resolveIssuer(req); + + if (approved !== "1") { + // Show consent page. Keeps the OAuth popup visible so users can see the flow. + const approveUrl = new URL(resolvePublicUrl(req).origin + "/authorize"); + for (const [k, v] of Object.entries(req.query)) + if (v) approveUrl.searchParams.set(k, String(v)); + approveUrl.searchParams.set("approved", "1"); + const denyUrl = new URL(redirect_uri); + denyUrl.searchParams.set("error", "access_denied"); + if (state) denyUrl.searchParams.set("state", state); + res.type("text/html").send(/*html*/ ` +Authorize + +
`); + return; + } + + // Per-grant token TTL, requested via `?token_ttl_s=` on the resource + // indicator (the MCP URL the host is authorizing for). Encoded into the + // auth code so /token can issue tokens with the requested lifetime. + const tokenTtl = ttlFromResource(resource); + const code = await signAuthCode( + { + client_id, + redirect_uri, + code_challenge, + code_challenge_method, + scope, + ...(tokenTtl !== undefined ? { token_ttl_s: tokenTtl } : {}), + }, + issuer, + ); + const redirectUrl = new URL(redirect_uri); + redirectUrl.searchParams.set("code", code); + if (state) redirectUrl.searchParams.set("state", state); + console.log( + `[auth] issued code for client_id=${client_id}` + + (tokenTtl !== undefined ? ` (token_ttl_s=${tokenTtl})` : ""), + ); + res.redirect(redirectUrl.href); +} + +async function handleToken(req: Request, res: Response) { + const { grant_type, code, code_verifier, refresh_token, resource } = req.body; + const issuer = resolveIssuer(req); + const audience = resolvePublicUrl(req).href; + + if (grant_type === "refresh_token") { + const refreshClaims = await verifyRefreshToken(refresh_token, issuer); + if (!refreshClaims) { + console.log(`[auth] refresh rejected (invalid/revoked)`); + res.status(400).json({ + error: "invalid_grant", + error_description: "Refresh token invalid or revoked", + }); + return; + } + // Refreshed access tokens keep the grant's requested TTL: prefer the + // resource indicator on this request, else the TTL carried in the refresh + // token, else the default. + const accessTtl = + ttlFromResource(resource) ?? + refreshClaims.accessTtlSeconds ?? + ACCESS_TOKEN_TTL_SECONDS; + const access_token = await signAccessToken( + refreshClaims.sub, + refreshClaims.scope, + refreshClaims.sid, + issuer, + audience, + accessTtl, + ); + console.log( + `[auth] refreshed access token (sid=${refreshClaims.sid}, ttl=${accessTtl}s)`, + ); + res.json({ + access_token, + token_type: "Bearer", + expires_in: accessTtl, + scope: refreshClaims.scope, + refresh_token, + }); + return; + } + if (grant_type !== "authorization_code") { + res.status(400).json({ error: "unsupported_grant_type" }); + return; + } + const stored = await verifyAuthCode(code, issuer); + if (!stored) { + res.status(400).json({ + error: "invalid_grant", + error_description: "Invalid or expired code", + }); + return; + } + if (stored.code_challenge) { + if (!code_verifier) { + res.status(400).json({ + error: "invalid_grant", + error_description: "Missing code_verifier", + }); + return; + } + const hash = crypto + .createHash("sha256") + .update(code_verifier) + .digest("base64url"); + if (hash !== stored.code_challenge) { + res.status(400).json({ + error: "invalid_grant", + error_description: "PKCE verification failed", + }); + return; + } + } + const scope = stored.scope ?? "read:secret"; + const sid = crypto.randomBytes(16).toString("hex"); // new session per authorization_code grant + // Per-grant TTL: prefer the resource indicator on this request (RFC 8707), + // else the TTL encoded in the auth code at /authorize, else the default. + // Refresh tokens must outlive the access tokens they can mint. + const accessTtl = + ttlFromResource(resource) ?? stored.token_ttl_s ?? ACCESS_TOKEN_TTL_SECONDS; + const refreshTtl = Math.max(REFRESH_TOKEN_TTL_SECONDS, accessTtl); + const access_token = await signAccessToken( + "mock-user-123", + scope, + sid, + issuer, + audience, + accessTtl, + ); + const refresh = await signRefreshToken( + "mock-user-123", + scope, + sid, + issuer, + refreshTtl, + accessTtl, + ); + console.log( + `[auth] exchanged code → token (client_id=${stored.client_id}, scope=${scope}, sid=${sid}, access_ttl=${accessTtl}s, refresh_ttl=${refreshTtl}s)`, + ); + res.json({ + access_token, + token_type: "Bearer", + expires_in: accessTtl, + scope, + refresh_token: refresh, + }); +} + +// ─── Elicitation state (module-level — survives across SHTTP requests) ────── +// +// Streamable HTTP creates a fresh McpServer per request, so state must live at +// module scope. This is fine for a single-process demo server; back it with a +// shared store if you deploy multiple instances. + +/** elicitationIds the user has completed via /elicitation-callback */ +const completedElicitations = new Set(); +/** elicit_by_error: remember the last-issued eid so retries can check it */ +let lastIssuedElicitationId: string | undefined; + +// ─── MCP Server ────────────────────────────────────────────────────────────── + +/** + * Creates a new MCP server instance with tools and resources registered. + * + * @param authInfo - Verified Bearer token info, if the request carried one. + * @param req - The inbound HTTP request (used to resolve the public URL). + */ +export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { + const server = new McpServer({ name: "Lazy Auth Demo", version: "1.0.0" }); + + const buttonUri = "ui://lazy-auth/mcp-app.html"; + registerAppTool( + server, + "show_auth_button", + { + title: "Show Auth Button", + description: + "Public tool: shows a button that triggers the protected get_secret tool.", + inputSchema: {}, + _meta: { ui: { resourceUri: buttonUri } }, + }, + async (): Promise => ({ + content: [{ type: "text", text: "ok" }], + }), + ); + registerAppResource( + server, + buttonUri, + buttonUri, + { mimeType: RESOURCE_MIME_TYPE }, + async (): Promise => { + const html = await fs.readFile( + path.join(DIST_DIR, "mcp-app.html"), + "utf-8", + ); + return { + contents: [ + { uri: buttonUri, mimeType: RESOURCE_MIME_TYPE, text: html }, + ], + }; + }, + ); + + const secretUri = "ui://lazy-auth/secret-app.html"; + registerAppTool( + server, + "get_secret", + { + title: "Get Secret", + description: + "Protected tool: returns secret data. Requires authentication.", + inputSchema: {}, + _meta: { ui: { resourceUri: secretUri } }, + }, + async (): Promise => { + if (!authInfo) { + return { + isError: true, + content: [{ type: "text", text: "Authentication required." }], + }; + } + const secret = { + subject: authInfo.sub, + secret: "the-answer-is-42", + issuedAt: new Date().toISOString(), + }; + return { + content: [{ type: "text", text: JSON.stringify(secret, null, 2) }], + }; + }, + ); + registerAppResource( + server, + secretUri, + secretUri, + { mimeType: RESOURCE_MIME_TYPE }, + async (): Promise => { + const html = await fs.readFile( + path.join(DIST_DIR, "secret-app.html"), + "utf-8", + ); + return { + contents: [ + { uri: secretUri, mimeType: RESOURCE_MIME_TYPE, text: html }, + ], + }; + }, + ); + + server.registerTool( + "revoke_auth_token", + { + title: "Revoke Auth Token", + description: + "Protected tool: revokes the caller's entire auth session (access + refresh token).", + inputSchema: {}, + }, + async (): Promise => { + if (!authInfo) { + return { + isError: true, + content: [{ type: "text", text: "Authentication required." }], + }; + } + revokeSession(authInfo.sid); + console.log( + `[auth] revoked session sid=${authInfo.sid} (total revoked sessions: ${revokedSids.size})`, + ); + return { + content: [ + { + type: "text", + text: `Session revoked (sid: ${authInfo.sid}). Access + refresh token invalid → full re-auth required.`, + }, + ], + }; + }, + ); + + // ── Elicitation test tools ──────────────────────────────────────────────── + // Public tools — no auth. Used to exercise a host's URL-elicitation flow + // end-to-end over Streamable HTTP. + + const base = resolvePublicUrl(req); + const callbackUrl = (eid: string) => + `${base.origin}/elicitation-callback?id=${encodeURIComponent(eid)}`; + + server.registerTool( + "elicit_url", + { + title: "Elicit URL (session-callback)", + description: + "URL elicitation via elicitInput. Blocks until the client accepts; sends ElicitCompleteNotification before returning.", + inputSchema: {}, + }, + async (_args, extra): Promise => { + const eid = crypto.randomUUID(); + // Defer notifier creation — checks client capabilities which throws if + // client doesn't advertise elicitation. Create lazily after elicitInput + // succeeds (implies client supports it). + console.log(`[elicit_url] eid=${eid} — blocking on elicitInput`); + const result = await server.server.elicitInput( + { + mode: "url", + url: callbackUrl(eid), + message: "Please open this URL to continue.", + elicitationId: eid, + }, + { relatedRequestId: extra.requestId, timeout: 300_000 }, + ); + // Send completion notification before returning — lands on the same SSE + // stream as this tool response. For Streamable HTTP this is the ONLY + // window where the server can push a notification (stream closes right + // after). The notification is optional per spec — skip if the client + // doesn't support it. + try { + const notifier = server.server.createElicitationCompletionNotifier(eid); + await notifier(); + console.log( + `[elicit_url] eid=${eid} — notified, result=${result.action}`, + ); + } catch (e) { + console.log( + `[elicit_url] eid=${eid} — notify skipped: ${e instanceof Error ? e.message : e}, result=${result.action}`, + ); + } + return { + content: [ + { type: "text", text: `elicit_url: ${JSON.stringify(result)}` }, + ], + }; + }, + ); + + server.registerTool( + "elicit_by_error", + { + title: "Elicit by -32042 Error", + description: + "Throws UrlElicitationRequiredError (-32042). Client must open the URL, then retry. On retry, if the last-issued elicitationId was completed, sends ElicitCompleteNotification and succeeds.", + inputSchema: {}, + }, + async (): Promise => { + // Retry path: check if the user completed the elicitation we last issued. + // Module-level state survives across Streamable HTTP requests in a single + // process. Checking ANY completed eid (not just last-issued) would be more + // forgiving, but last-issued is the stricter real-world pattern hosts must + // handle. + if ( + lastIssuedElicitationId && + completedElicitations.has(lastIssuedElicitationId) + ) { + const eid = lastIssuedElicitationId; + completedElicitations.delete(eid); + lastIssuedElicitationId = undefined; + // Send notification DURING this (retry) tool call — only window where + // the Streamable HTTP stream is open. The notification is optional per + // spec — createElicitationCompletionNotifier throws if the client + // doesn't advertise the capability, but the tool still succeeds. + try { + const notifier = + server.server.createElicitationCompletionNotifier(eid); + await notifier(); + console.log( + `[elicit_by_error] eid=${eid} — completed on retry, notified`, + ); + } catch (e) { + console.log( + `[elicit_by_error] eid=${eid} — completed on retry, notify skipped: ${e instanceof Error ? e.message : e}`, + ); + } + return { + content: [ + { + type: "text", + text: `Auth completed (${eid}). Tool succeeded on retry.`, + }, + ], + }; + } + const eid = crypto.randomUUID(); + lastIssuedElicitationId = eid; + console.log(`[elicit_by_error] eid=${eid} — throwing -32042`); + throw new UrlElicitationRequiredError([ + { + mode: "url", + url: callbackUrl(eid), + message: + "This tool requires authentication. Please open the link to continue.", + elicitationId: eid, + }, + ]); + }, + ); + + return server; +} + +// ─── Express app ───────────────────────────────────────────────────────────── + +/** + * Creates the Express app: OAuth endpoints, discovery metadata, and the /mcp + * endpoint with lazy (401-triggered) auth on protected tools. + */ +export function createApp(): Express { + const app = express(); + app.use(cors({ exposedHeaders: ["WWW-Authenticate"] })); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // Request tracing: log every inbound request (incl. query params) so we can + // see what hosts probe during connect and what they send through OAuth. + app.use((req, _res, next) => { + const ua = (req.headers["user-agent"] ?? "").slice(0, 80); + const auth = req.headers.authorization ? "[Bearer]" : "[no-auth]"; + const query = Object.keys(req.query).length + ? ` query=${JSON.stringify(req.query)}` + : ""; + console.log(`[req] ${req.method} ${req.path} ${auth}${query} ua=${ua}`); + next(); + }); + + // OAuth endpoints + app.get("/authorize", handleAuthorize); + app.post("/token", handleToken); + + // OAuth discovery metadata. In REACTIVE_AUTH_ONLY mode, PRM + AS are kept off the + // ROOT /.well-known paths so MCP hosts can't proactively probe them and trigger + // preemptive auth during connection — discovery is purely reactive: + // 401 → WWW-Authenticate → PRM → AS → OAuth flow. + const PRM_PATH = "/auth/prm"; + + function buildAsMetadata(req: Request) { + const base = resolvePublicUrl(req); + return { + issuer: resolveIssuer(req), // subpath issuer → well-known at /.well-known/.../auth + authorization_endpoint: `${base.origin}/authorize`, + token_endpoint: `${base.origin}/token`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + code_challenge_methods_supported: ["S256"], + token_endpoint_auth_methods_supported: ["none"], + scopes_supported: ["read:secret"], + client_id_metadata_document_supported: true, + }; + } + + // AS metadata: serve at the issuer's well-known location. + // - REACTIVE_AUTH_ONLY: only at /.well-known/.../auth (subpath); root 404s. + // - Default: also at root /.well-known/... (standard; some hosts probe this on connect). + if (ISSUER_SUFFIX) { + // Per RFC 8414 §3: issuer /auth → /.well-known/oauth-authorization-server/auth + app.get( + `/.well-known/oauth-authorization-server${ISSUER_SUFFIX}`, + (req, res) => res.json(buildAsMetadata(req)), + ); + // RFC 8615 style (well-known between host and path): + app.get( + `${ISSUER_SUFFIX}/.well-known/oauth-authorization-server`, + (req, res) => res.json(buildAsMetadata(req)), + ); + } + if (!REACTIVE_AUTH_ONLY) { + // Default mode: full AS metadata at root well-known (standard). + app.get("/.well-known/oauth-authorization-server", (req, res) => + res.json(buildAsMetadata(req)), + ); + } + + // PRM: full version at custom path (referenced via WWW-Authenticate on 401). + function buildPrm(req: Request, includeAuth: boolean, resourcePath = "/mcp") { + const base = resolvePublicUrl(req); + return { + resource: `${base.origin}${resourcePath}`, + ...(includeAuth + ? { + authorization_servers: [resolveIssuer(req)], + scopes_supported: ["read:secret"], + bearer_methods_supported: ["header"], + } + : {}), + }; + } + /** Resource path for a TTL-scoped MCP endpoint, validating the TTL segment. */ + function ttlResourcePath(rawTtl: string): string | undefined { + const ttl = parseTtlSeconds(rawTtl); + return ttl !== undefined ? `/ttl/${ttl}/mcp` : undefined; + } + app.get(PRM_PATH, (req, res) => res.json(buildPrm(req, true))); + app.get(`${PRM_PATH}/ttl/:ttl`, (req, res) => { + const resourcePath = ttlResourcePath(String(req.params.ttl)); + if (!resourcePath) { + res.status(404).json({ error: "invalid_ttl" }); + return; + } + res.json(buildPrm(req, true, resourcePath)); + }); + // Root + path-dependent well-known PRM: in REACTIVE mode these 404 so hosts don't + // interpret their presence as "server has OAuth". In default mode, full PRM. + if (!REACTIVE_AUTH_ONLY) { + app.get("/.well-known/oauth-protected-resource", (req, res) => + res.json(buildPrm(req, true)), + ); + app.get("/.well-known/oauth-protected-resource/mcp", (req, res) => + res.json(buildPrm(req, true)), + ); + // Well-known PRM for TTL-scoped MCP endpoints (RFC 9728 derives this URL + // from the resource path, so each TTL path has its own). + app.get( + "/.well-known/oauth-protected-resource/ttl/:ttl/mcp", + (req, res) => { + const resourcePath = ttlResourcePath(String(req.params.ttl)); + if (!resourcePath) { + res.status(404).json({ error: "invalid_ttl" }); + return; + } + res.json(buildPrm(req, true, resourcePath)); + }, + ); + } + + // /register: some MCP hosts fall through to OAuth dynamic client registration (RFC 7591) + // when all well-known probes 404. We don't support it — return a proper OAuth error so + // the host concludes "auth not available here" and connects without OAuth, instead of + // treating a generic 404 as a connection failure. + app.post("/register", (_req, res) => { + res.status(400).json({ + error: "invalid_request", + error_description: + "Dynamic client registration is not supported. This server uses per-tool auth triggered via WWW-Authenticate on 401.", + }); + }); + + // MCP endpoint handler, shared by the default and TTL-scoped paths. + // pathTtl (from /ttl//mcp) doubles as the acceptance window for + // Bearer tokens: tokens older than it are rejected even if their issued + // lifetime was longer. + async function handleMcp( + req: Request, + res: Response, + pathTtl: number | undefined, + ) { + const base = resolvePublicUrl(req); + const resourceMetadataUrl = + pathTtl !== undefined + ? `${base.origin}${PRM_PATH}/ttl/${pathTtl}` + : `${base.origin}${PRM_PATH}`; + + const body = req.body; + const messages = Array.isArray(body) ? body : body ? [body] : []; + const needsAuth = messages.some( + (msg: { method?: string; params?: { name?: string } }) => + msg?.method === "tools/call" && + PROTECTED_TOOLS.has(msg.params?.name ?? ""), + ); + + // Acceptance window: the path TTL, else the `?token_ttl_s=` query param + // (fallback for clients that preserve query strings). + const tokenMaxAgeSeconds = + pathTtl ?? parseTtlSeconds(String(req.query.token_ttl_s ?? "")); + + let authInfo: AuthInfo | undefined; + const authHeader = req.headers.authorization; + if (authHeader?.startsWith("Bearer ")) { + authInfo = await verifyAccessToken( + authHeader.slice(7), + resolveIssuer(req), + tokenMaxAgeSeconds, + ); + if (!authInfo && needsAuth) { + console.log(`[mcp] 401: invalid token for protected tool`); + res + .status(401) + .set( + "WWW-Authenticate", + `Bearer error="invalid_token", error_description="The access token is invalid", resource_metadata="${resourceMetadataUrl}"`, + ) + .json({ + error: "invalid_token", + error_description: "The access token is invalid", + }); + return; + } + } else if (needsAuth) { + console.log(`[mcp] 401: no token for protected tool`); + res + .status(401) + .set( + "WWW-Authenticate", + `Bearer resource_metadata="${resourceMetadataUrl}"`, + ) + .json({ + error: "invalid_token", + error_description: "Authorization required", + }); + return; + } + + const server = createServer(authInfo, req); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + res.on("close", () => { + transport.close().catch(() => {}); + server.close().catch(() => {}); + }); + + try { + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("[mcp] Error:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + } + + // Default MCP endpoint (default token TTLs). + app.all("/mcp", (req: Request, res: Response) => + handleMcp(req, res, undefined), + ); + // TTL-scoped MCP endpoint: tokens for connections made through this path are + // issued with (and limited to) the requested lifetime, e.g. /ttl/3600/mcp. + app.all("/ttl/:ttl/mcp", (req: Request, res: Response) => { + const pathTtl = parseTtlSeconds(String(req.params.ttl)); + if (pathTtl === undefined) { + res.status(404).json({ error: "invalid_ttl" }); + return; + } + return handleMcp(req, res, pathTtl); + }); + + // Elicitation callback — marks an eid as completed. Module-level state; + // the next elicit_by_error retry reads it. Minimal confirmation page so + // the popup shows something. + app.get("/elicitation-callback", (req, res) => { + const id = String(req.query.id ?? ""); + if (!id) { + res.status(400).type("text/plain").send("missing ?id="); + return; + } + completedElicitations.add(id); + console.log( + `[callback] eid=${id} completed (total: ${completedElicitations.size})`, + ); + res + .type("text/html") + .send( + `Done` + + `` + + `

✓ Elicitation completed

` + + `

${escapeHtml(id)}

` + + `

You can close this tab. The tool will succeed on the next retry.

`, + ); + }); + + // Simple landing page + app.get("/", (req, res) => { + const base = resolvePublicUrl(req); + res + .type("text/plain") + .send( + `Lazy Auth Demo — MCP server\n\n` + + ` MCP endpoint: ${base.origin}/mcp\n` + + ` ${base.origin}/ttl//mcp (custom token TTL, e.g. /ttl/3600/mcp)\n` + + ` AS metadata: ${base.origin}/.well-known/oauth-authorization-server${ISSUER_SUFFIX}\n` + + ` PRM metadata: ${base.origin}${PRM_PATH}\n\n` + + `Tools:\n` + + ` - show_auth_button (public)\n` + + ` - get_secret (protected, requires Bearer token)\n` + + ` - revoke_auth_token (protected, revokes caller's auth session)\n` + + ` - elicit_url (public, URL elicitation via elicitInput)\n` + + ` - elicit_by_error (public, URL elicitation via -32042 error)\n`, + ); + }); + + return app; +} diff --git a/examples/lazy-auth-server/src/global.css b/examples/lazy-auth-server/src/global.css new file mode 100644 index 0000000..54a9860 --- /dev/null +++ b/examples/lazy-auth-server/src/global.css @@ -0,0 +1,100 @@ +:root { + color-scheme: light dark; + + /* + * Fallbacks for host style variables used by this app. + * The host may provide these (and many more) via the host context. + */ + --color-text-primary: light-dark(#1f2937, #f3f4f6); + --color-text-inverse: light-dark(#f3f4f6, #1f2937); + --color-text-info: light-dark(#1d4ed8, #60a5fa); + --color-background-primary: light-dark(#ffffff, #1a1a1a); + --color-background-inverse: light-dark(#1a1a1a, #ffffff); + --color-background-info: light-dark(#eff6ff, #1e3a5f); + --color-ring-primary: light-dark(#3b82f6, #60a5fa); + --border-radius-md: 6px; + --border-width-regular: 1px; + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --font-weight-normal: 400; + --font-weight-bold: 700; + --font-text-md-size: 1rem; + --font-text-md-line-height: 1.5; + --font-heading-3xl-size: 2.25rem; + --font-heading-3xl-line-height: 1.1; + --font-heading-2xl-size: 1.875rem; + --font-heading-2xl-line-height: 1.2; + --font-heading-xl-size: 1.5rem; + --font-heading-xl-line-height: 1.25; + --font-heading-lg-size: 1.25rem; + --font-heading-lg-line-height: 1.3; + --font-heading-md-size: 1rem; + --font-heading-md-line-height: 1.4; + --font-heading-sm-size: 0.875rem; + --font-heading-sm-line-height: 1.4; + + /* Spacing derived from host typography */ + --spacing-unit: var(--font-text-md-size); + --spacing-xs: calc(var(--spacing-unit) * 0.25); + --spacing-sm: calc(var(--spacing-unit) * 0.5); + --spacing-md: var(--spacing-unit); + --spacing-lg: calc(var(--spacing-unit) * 1.5); + + /* App accent color (customize for your brand) */ + --color-accent: #2563eb; + --color-text-on-accent: #ffffff; +} + +* { + box-sizing: border-box; +} + +html, +body { + font-family: var(--font-sans); + font-size: var(--font-text-md-size); + font-weight: var(--font-weight-normal); + line-height: var(--font-text-md-line-height); + color: var(--color-text-primary); +} + +h1 { + font-size: var(--font-heading-3xl-size); + line-height: var(--font-heading-3xl-line-height); +} +h2 { + font-size: var(--font-heading-2xl-size); + line-height: var(--font-heading-2xl-line-height); +} +h3 { + font-size: var(--font-heading-xl-size); + line-height: var(--font-heading-xl-line-height); +} +h4 { + font-size: var(--font-heading-lg-size); + line-height: var(--font-heading-lg-line-height); +} +h5 { + font-size: var(--font-heading-md-size); + line-height: var(--font-heading-md-line-height); +} +h6 { + font-size: var(--font-heading-sm-size); + line-height: var(--font-heading-sm-line-height); +} + +code, +pre, +kbd { + font-family: var(--font-mono); + font-size: 1em; +} + +b, +strong { + font-weight: var(--font-weight-bold); +} diff --git a/examples/lazy-auth-server/src/mcp-app.css b/examples/lazy-auth-server/src/mcp-app.css new file mode 100644 index 0000000..ba4f323 --- /dev/null +++ b/examples/lazy-auth-server/src/mcp-app.css @@ -0,0 +1,66 @@ +.main { + width: 100%; + max-width: 480px; + box-sizing: border-box; + + > * { + margin-top: 0; + margin-bottom: 0; + } + + > * + * { + margin-top: var(--spacing-md); + } +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); +} + +button { + padding: var(--spacing-sm) var(--spacing-md); + border: none; + border-radius: var(--border-radius-md); + color: var(--color-text-on-accent); + font-weight: var(--font-weight-bold); + background-color: var(--color-accent); + cursor: pointer; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:hover:not(:disabled) { + background-color: color-mix( + in srgb, + var(--color-accent) 85%, + var(--color-background-inverse) + ); + } + + &:focus-visible { + outline: calc(var(--border-width-regular) * 2) solid + var(--color-ring-primary); + outline-offset: var(--border-width-regular); + } +} + +.result { + padding: var(--spacing-sm) var(--spacing-md); + border: var(--border-width-regular) solid + color-mix(in srgb, var(--color-text-primary) 25%, transparent); + border-radius: var(--border-radius-md); + font-family: var(--font-mono); + font-size: 0.875em; + white-space: pre-wrap; + word-break: break-word; +} + +.error { + color: var(--color-text-danger, light-dark(#b91c1c, #f87171)); + white-space: pre-wrap; + word-break: break-word; +} diff --git a/examples/lazy-auth-server/src/mcp-app.ts b/examples/lazy-auth-server/src/mcp-app.ts new file mode 100644 index 0000000..f959d71 --- /dev/null +++ b/examples/lazy-auth-server/src/mcp-app.ts @@ -0,0 +1,126 @@ +/** + * @file Public app view: buttons that call the protected tools. + * + * The view itself renders without auth. Clicking "Auth me" calls the protected + * `get_secret` tool via `callServerTool`; the host receives a 401 from the + * server, runs the OAuth flow, retries the call, and the result renders here. + */ +import { + App, + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, + type McpUiHostContext, +} from "@modelcontextprotocol/ext-apps"; +import "./global.css"; +import "./mcp-app.css"; + +const mainEl = document.querySelector(".main") as HTMLElement; +const authBtn = document.getElementById("auth-btn") as HTMLButtonElement; +const revokeBtn = document.getElementById("revoke-btn") as HTMLButtonElement; +const fullscreenBtn = document.getElementById( + "fullscreen-btn", +) as HTMLButtonElement; +const outputEl = document.getElementById("output")!; + +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + if (ctx.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + +const app = new App( + { name: "Lazy Auth Demo", version: "1.0.0" }, + { availableDisplayModes: ["inline", "fullscreen"] }, +); + +// The host echoes the result of the originating `show_auth_button` call; nothing to render. +app.ontoolresult = () => {}; +app.onerror = console.error; +app.onhostcontextchanged = handleHostContextChanged; + +function renderOutput(text: string, isError: boolean) { + const div = document.createElement("div"); + div.className = isError ? "error" : "result"; + div.textContent = text; + outputEl.replaceChildren(div); +} + +/** + * Calls a (protected) server tool. The interesting part happens on the host: + * the server answers 401 + WWW-Authenticate, the host runs the OAuth flow, + * then retries the call and returns the result here. + * + * The default MCP request timeout is 60s; the OAuth flow involves a human + * approving a consent page, which can easily take longer. Use a generous + * timeout so the pending call survives the sign-in round trip. + */ +const AUTH_FLOW_TIMEOUT_MS = 5 * 60_000; + +async function callTool(name: string) { + outputEl.replaceChildren(); + try { + console.info(`Calling ${name} tool...`); + const result = await app.callServerTool( + { name, arguments: {} }, + { timeout: AUTH_FLOW_TIMEOUT_MS }, + ); + const text = + result.content?.find((c) => c.type === "text")?.text ?? "(no content)"; + renderOutput(text, result.isError === true); + } catch (e) { + renderOutput(String(e instanceof Error ? e.message : e), true); + } +} + +authBtn.addEventListener("click", async () => { + authBtn.disabled = true; + try { + await callTool("get_secret"); + } finally { + authBtn.disabled = false; + } +}); + +revokeBtn.addEventListener("click", async () => { + revokeBtn.disabled = true; + try { + await callTool("revoke_auth_token"); + } finally { + revokeBtn.disabled = false; + } +}); + +let displayMode = "inline"; +fullscreenBtn.addEventListener("click", async () => { + fullscreenBtn.disabled = true; + try { + const next = displayMode === "fullscreen" ? "inline" : "fullscreen"; + const { mode } = await app.requestDisplayMode({ mode: next }); + displayMode = mode; + } catch (e) { + console.warn("Display mode not supported:", e); + } finally { + fullscreenBtn.disabled = false; + } +}); + +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + displayMode = ctx.displayMode ?? "inline"; + } +}); diff --git a/examples/lazy-auth-server/src/secret-app.ts b/examples/lazy-auth-server/src/secret-app.ts new file mode 100644 index 0000000..aeb6857 --- /dev/null +++ b/examples/lazy-auth-server/src/secret-app.ts @@ -0,0 +1,57 @@ +/** + * @file Protected app view: renders the result of the `get_secret` tool. + * + * This view is only reached after the host has completed the OAuth flow + * (the tool call that produces it requires a Bearer token). + */ +import { + App, + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, + type McpUiHostContext, +} from "@modelcontextprotocol/ext-apps"; +import "./global.css"; +import "./mcp-app.css"; + +const mainEl = document.querySelector(".main") as HTMLElement; +const outputEl = document.getElementById("output")!; + +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + if (ctx.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + +const app = new App({ name: "Lazy Auth Demo (Secret)", version: "1.0.0" }); + +app.onerror = console.error; +app.onhostcontextchanged = handleHostContextChanged; + +app.ontoolresult = (result) => { + const text = + result.content?.find((c) => c.type === "text")?.text ?? "(no content)"; + const div = document.createElement("div"); + div.className = result.isError ? "error" : "result"; + div.textContent = text; + outputEl.replaceChildren(div); +}; + +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } +}); diff --git a/examples/lazy-auth-server/tsconfig.json b/examples/lazy-auth-server/tsconfig.json new file mode 100644 index 0000000..6c553b5 --- /dev/null +++ b/examples/lazy-auth-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts", "main.ts"] +} diff --git a/examples/lazy-auth-server/tsconfig.server.json b/examples/lazy-auth-server/tsconfig.server.json new file mode 100644 index 0000000..05ddd8e --- /dev/null +++ b/examples/lazy-auth-server/tsconfig.server.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["server.ts"] +} diff --git a/examples/lazy-auth-server/vite.config.ts b/examples/lazy-auth-server/vite.config.ts new file mode 100644 index 0000000..6ff6d99 --- /dev/null +++ b/examples/lazy-auth-server/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/package-lock.json b/package-lock.json index 54af40d..be26ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -591,6 +591,48 @@ "dev": true, "license": "MIT" }, + "examples/lazy-auth-server": { + "name": "@modelcontextprotocol/server-lazy-auth", + "version": "1.7.2", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "jose": "^6.0.0" + }, + "bin": { + "mcp-server-lazy-auth": "dist/index.js" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "22.10.0", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/lazy-auth-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/lazy-auth-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", "version": "1.7.2", @@ -2678,6 +2720,10 @@ "resolved": "examples/debug-server", "link": true }, + "node_modules/@modelcontextprotocol/server-lazy-auth": { + "resolved": "examples/lazy-auth-server", + "link": true + }, "node_modules/@modelcontextprotocol/server-map": { "resolved": "examples/map-server", "link": true From 7d4434e991516d91286dded5bb10266cd6cddd02 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 1 Jun 2026 12:10:47 +0100 Subject: [PATCH 49/97] 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). --- .github/workflows/npm-publish.yml | 1 + .github/workflows/publish.yml | 1 + examples/basic-host/package.json | 4 +- examples/basic-server-preact/package.json | 4 +- examples/basic-server-react/package.json | 4 +- examples/basic-server-solid/package.json | 4 +- examples/basic-server-svelte/package.json | 4 +- examples/basic-server-vanillajs/package.json | 4 +- examples/basic-server-vue/package.json | 4 +- examples/budget-allocator-server/package.json | 4 +- examples/cohort-heatmap-server/package.json | 4 +- .../customer-segmentation-server/package.json | 4 +- examples/debug-server/package.json | 4 +- examples/integration-server/package.json | 4 +- examples/lazy-auth-server/package.json | 4 +- examples/map-server/package.json | 4 +- examples/pdf-server/package.json | 4 +- examples/qr-server/package.json | 4 +- examples/quickstart/package.json | 4 +- examples/say-server/package.json | 4 +- examples/scenario-modeler-server/package.json | 4 +- examples/shadertoy-server/package.json | 4 +- examples/sheet-music-server/package.json | 4 +- examples/system-monitor-server/package.json | 4 +- examples/threejs-server/package.json | 4 +- examples/transcript-server/package.json | 4 +- examples/video-resource-server/package.json | 4 +- examples/wiki-explorer-server/package.json | 4 +- package-lock.json | 108 +++++++++--------- package.json | 2 +- 30 files changed, 109 insertions(+), 107 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 0667bbd..ed36c15 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -112,6 +112,7 @@ jobs: - cohort-heatmap-server - customer-segmentation-server - debug-server + - lazy-auth-server - map-server - pdf-server - scenario-modeler-server diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5f1d3ac..478c5b8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,6 +34,7 @@ jobs: ./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 \ diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index a7939a6..117beca 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", @@ -11,7 +11,7 @@ "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 9d0b4d5..353d5f0 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 1be79c5..a8bdfa8 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Basic MCP App Server example using React", "repository": { @@ -34,7 +34,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index ac4c197..1925ae1 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 4f124f3..c7fef6b 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index cfb7c1b..72b6ed2 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 4f7d19c..cb924b4 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index ad217c8..ded5814 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 4c20844..9f82c64 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index fada75e..aa52b35 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 5bbbad4..1c505e2 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { @@ -34,7 +34,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.1.13" }, diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 0244bad..2104382 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.2", + "version": "1.7.3", "private": true, "type": "module", "scripts": { @@ -15,7 +15,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 74f8b02..8b7cb3f 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", "repository": { @@ -22,7 +22,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 3050e1e..7283dd2 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index fc4cdb1..619f082 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { @@ -26,7 +26,7 @@ }, "dependencies": { "@cantoo/pdf-lib": "^2.6.5", - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index 3c183d0..e86607b 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.2", + "version": "1.7.3", "private": true, "scripts": { "start": "uv run server.py", @@ -8,7 +8,7 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0" } } diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 5063a6a..81e03b7 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "private": true, "description": "Quickstart MCP App Server example", @@ -15,7 +15,7 @@ "start": "concurrently --raw \"cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch\" \"tsx watch main.ts\"" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0" diff --git a/examples/say-server/package.json b/examples/say-server/package.json index aca5e3d..c044d09 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.2", + "version": "1.7.3", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { @@ -15,7 +15,7 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0" } } diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index e7e1d28..a91b2f4 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 1aa1fd3..80a4c76 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index ffc5c8a..cfad907 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 5a55817..102e96d 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index be3b293..372a41b 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index d4e4ce9..aeb149c 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 68c890e..86f437d 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 96ce612..2d3f1f7 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.2", + "version": "1.7.3", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { @@ -26,7 +26,7 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", diff --git a/package-lock.json b/package-lock.json index be26ff8..b3f7a65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.2", + "version": "1.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "workspaces": [ "examples/*" @@ -66,9 +66,9 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.2", + "version": "1.7.3", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -109,10 +109,10 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -153,10 +153,10 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -200,10 +200,10 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -244,10 +244,10 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -288,10 +288,10 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -330,10 +330,10 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -374,10 +374,10 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -417,10 +417,10 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -464,10 +464,10 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -507,10 +507,10 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.1.13" }, @@ -548,9 +548,9 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.7.2", + "version": "1.7.3", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -593,10 +593,10 @@ }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -635,10 +635,10 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -677,11 +677,11 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -721,18 +721,18 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.2", + "version": "1.7.3", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0" } }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0" @@ -761,19 +761,19 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0" } }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -818,10 +818,10 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -860,10 +860,10 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", @@ -903,10 +903,10 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -973,10 +973,10 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -1022,10 +1022,10 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -1065,10 +1065,10 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -1107,10 +1107,10 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", diff --git a/package.json b/package.json index 2f31c24..6738b68 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.2", + "version": "1.7.3", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From a9907802937f1da067cbc4aa48b283cd4cfa7dc8 Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Tue, 2 Jun 2026 05:30:49 -0700 Subject: [PATCH 50/97] Require PKCE and bind redirect_uri in lazy-auth-server token exchange (#681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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). --- examples/lazy-auth-server/server.ts | 74 +++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts index fa46d67..1026340 100644 --- a/examples/lazy-auth-server/server.ts +++ b/examples/lazy-auth-server/server.ts @@ -273,18 +273,28 @@ async function signAuthCode( return new SignJWT({ ...payload, typ: "code" }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() + .setJti(crypto.randomUUID()) .setIssuer(issuer) .setExpirationTime("5m") .sign(JWT_SECRET); } +// Authorization codes are single-use (RFC 6749 §4.1.2): remember redeemed code +// IDs until the code's own 5-minute expiry makes replay impossible anyway. +const redeemedCodeJtis = new Map(); // jti → unix GC time + async function verifyAuthCode( code: string, issuer: string, ): Promise { try { const { payload } = await jwtVerify(code, JWT_SECRET, { issuer }); - if (payload.typ !== "code") return undefined; + if (payload.typ !== "code" || !payload.jti) return undefined; + const now = Math.floor(Date.now() / 1000); + for (const [k, gc] of redeemedCodeJtis) + if (gc < now) redeemedCodeJtis.delete(k); + if (redeemedCodeJtis.has(payload.jti)) return undefined; // already redeemed + redeemedCodeJtis.set(payload.jti, now + 5 * 60 + 10); return payload as unknown as CodePayload; } catch { return undefined; @@ -366,6 +376,16 @@ async function handleAuthorize(req: Request, res: Response) { }); return; } + // PKCE is mandatory (the MCP auth spec requires it of clients, and this AS + // only advertises S256). Rejecting up front keeps stolen-code attacks out of + // the demo even though it has no real data to protect. + if (!code_challenge || code_challenge_method !== "S256") { + res.status(400).json({ + error: "invalid_request", + error_description: "PKCE with S256 code_challenge is required", + }); + return; + } const issuer = resolveIssuer(req); if (approved !== "1") { @@ -475,25 +495,34 @@ async function handleToken(req: Request, res: Response) { }); return; } - if (stored.code_challenge) { - if (!code_verifier) { - res.status(400).json({ - error: "invalid_grant", - error_description: "Missing code_verifier", - }); - return; - } - const hash = crypto - .createHash("sha256") - .update(code_verifier) - .digest("base64url"); - if (hash !== stored.code_challenge) { - res.status(400).json({ - error: "invalid_grant", - error_description: "PKCE verification failed", - }); - return; - } + // PKCE verification (challenges are always present — /authorize requires them). + if (!code_verifier) { + res.status(400).json({ + error: "invalid_grant", + error_description: "Missing code_verifier", + }); + return; + } + const hash = crypto + .createHash("sha256") + .update(code_verifier) + .digest("base64url"); + if (hash !== stored.code_challenge) { + res.status(400).json({ + error: "invalid_grant", + error_description: "PKCE verification failed", + }); + return; + } + // RFC 6749 §4.1.3 redirect_uri binding: if the client includes redirect_uri + // in the token request it must match the one from the authorization request. + // (OAuth 2.1 clients may omit it and rely on PKCE, which is enforced above.) + if (req.body.redirect_uri && req.body.redirect_uri !== stored.redirect_uri) { + res.status(400).json({ + error: "invalid_grant", + error_description: "redirect_uri does not match authorization request", + }); + return; } const scope = stored.scope ?? "read:secret"; const sid = crypto.randomBytes(16).toString("hex"); // new session per authorization_code grant @@ -789,6 +818,11 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { */ export function createApp(): Express { const app = express(); + // Wildcard CORS is deliberate: browser-based MCP hosts connect to this demo + // from arbitrary origins and must read WWW-Authenticate to drive the lazy + // auth flow. There are no cookies or ambient credentials to protect — all + // auth is an explicit Bearer header, and /token is a credential-less PKCE + // exchange — so cross-origin reads expose nothing a direct request wouldn't. app.use(cors({ exposedHeaders: ["WWW-Authenticate"] })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); From 0ffe16781ea29fc018b45ab6c1967d2abcfb2434 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 4 Jun 2026 19:47:57 +0100 Subject: [PATCH 51/97] lazy-auth-server: support mounting under a base path (#683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- examples/lazy-auth-server/README.md | 27 +++++++++++++ examples/lazy-auth-server/server.ts | 63 ++++++++++++++++++----------- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/examples/lazy-auth-server/README.md b/examples/lazy-auth-server/README.md index 769ac18..f2e7545 100644 --- a/examples/lazy-auth-server/README.md +++ b/examples/lazy-auth-server/README.md @@ -51,6 +51,33 @@ https:///ttl/3600/mcp ← tokens for this connection live 1 hour This works through [RFC 8707 resource indicators](https://www.rfc-editor.org/rfc/rfc8707): MCP hosts send the MCP server URL as the `resource` parameter in OAuth authorization and token requests, and this server issues tokens for that grant with the lifetime encoded in the path (refresh tokens are extended to at least match). The TTL is a _path_ segment rather than a query param because hosts canonicalize resource indicators and strip query strings. Each TTL endpoint also enforces its value as a maximum token age, so connecting to a path with a _lower_ TTL than a token's issued lifetime forces the refresh flow. To exercise the full **re-auth** flow, call the `revoke_auth_token` tool. +## Mounting under a base path + +`createApp()` can also be mounted inside another Express app, so an existing server can host this example at a sub-path of its own origin: + +```ts +import { createApp } from "@modelcontextprotocol/server-lazy-auth"; + +hostApp.use("/lazy-auth", createApp()); +// → MCP endpoint at https:///lazy-auth/mcp +``` + +All advertised URLs (OAuth metadata, `WWW-Authenticate` `resource_metadata`, PRM `resource`, elicitation callbacks) include the mount path automatically, derived from Express's `req.baseUrl`. When `PUBLIC_URL` is set, it must include the mount path (e.g. `https://example.com/lazy-auth`). + +One thing the mounted app cannot do for itself: [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414#section-3) / [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) put well-known discovery documents at the _root_ of the origin with the path inserted after the well-known prefix (`/.well-known/oauth-authorization-server/lazy-auth`), and MCP SDK clients only try that insertion form. The host app must rewrite those root paths into the mount before its other routes: + +```ts +hostApp.use((req, _res, next) => { + const m = req.url.match( + /^\/\.well-known\/(oauth-authorization-server|oauth-protected-resource)\/lazy-auth(\/.*)?$/, + ); + if (m) req.url = `/lazy-auth/.well-known/${m[1]}${m[2] ?? ""}`; + next(); +}); +``` + +Rewriting into the mount (rather than calling the sub-app directly) keeps `req.baseUrl` — and therefore every advertised URL — consistent. + ## How It Works 1. **Connect without auth** — `initialize`, `tools/list`, and public tool calls succeed with no `Authorization` header. diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts index 1026340..fe7b1ac 100644 --- a/examples/lazy-auth-server/server.ts +++ b/examples/lazy-auth-server/server.ts @@ -34,11 +34,16 @@ import { SignJWT, jwtVerify } from "jose"; import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; -// Works both from source (server.ts) and compiled (dist/server.js) -const DIST_DIR = import.meta.filename.endsWith(".ts") - ? path.join(import.meta.dirname, "dist") - : import.meta.dirname; +// Works both from source (server.ts) and compiled (dist/server.js). Derived +// from import.meta.url rather than import.meta.filename/dirname, which are +// undefined in some module-VM contexts (e.g. importing this package from +// jest). +const SERVER_FILE = fileURLToPath(import.meta.url); +const DIST_DIR = SERVER_FILE.endsWith(".ts") + ? path.join(path.dirname(SERVER_FILE), "dist") + : path.dirname(SERVER_FILE); // ─── Config ────────────────────────────────────────────────────────────────── @@ -88,25 +93,35 @@ function isLoopbackHostname(hostname: string): boolean { * explicitly. */ function resolvePublicUrl(req?: Request): URL { + // Mount path when this app is mounted inside another Express app + // (e.g. app.use("/lazy-auth", createApp())). Empty when standalone. + // PUBLIC_URL, when set, must already include any mount path. + const basePath = req?.baseUrl ?? ""; const envUrl = process.env.PUBLIC_URL; if (envUrl) return new URL(envUrl.endsWith("/") ? envUrl : envUrl + "/"); const host = req?.headers.host; if (host) { try { - const url = new URL(`http://${host}/`); + const url = new URL(`http://${host}${basePath}/`); if (isLoopbackHostname(url.hostname)) return url; } catch { // Malformed Host header → fall through to the localhost default. } } - return new URL(`http://localhost:${PORT}/`); + return new URL(`http://localhost:${PORT}${basePath}/`); +} + +/** Public base URL as a string with no trailing slash (may include a base path). */ +function publicBaseHref(req?: Request): string { + const href = resolvePublicUrl(req).href; + return href.endsWith("/") ? href.slice(0, -1) : href; } /** OAuth issuer. In REACTIVE_AUTH_ONLY mode, uses a /auth subpath so root well-known 404s. - * Otherwise uses the root origin (standard). */ + * Otherwise uses the public base URL (origin + any mount path). */ const ISSUER_SUFFIX = REACTIVE_AUTH_ONLY ? "/auth" : ""; function resolveIssuer(req?: Request): string { - return resolvePublicUrl(req).origin + ISSUER_SUFFIX; + return publicBaseHref(req) + ISSUER_SUFFIX; } // ─── Mock OAuth (HS256, stateless codes) ───────────────────────────────────── @@ -390,7 +405,7 @@ async function handleAuthorize(req: Request, res: Response) { if (approved !== "1") { // Show consent page. Keeps the OAuth popup visible so users can see the flow. - const approveUrl = new URL(resolvePublicUrl(req).origin + "/authorize"); + const approveUrl = new URL(publicBaseHref(req) + "/authorize"); for (const [k, v] of Object.entries(req.query)) if (v) approveUrl.searchParams.set(k, String(v)); approveUrl.searchParams.set("approved", "1"); @@ -695,9 +710,9 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { // Public tools — no auth. Used to exercise a host's URL-elicitation flow // end-to-end over Streamable HTTP. - const base = resolvePublicUrl(req); + const base = publicBaseHref(req); const callbackUrl = (eid: string) => - `${base.origin}/elicitation-callback?id=${encodeURIComponent(eid)}`; + `${base}/elicitation-callback?id=${encodeURIComponent(eid)}`; server.registerTool( "elicit_url", @@ -850,11 +865,11 @@ export function createApp(): Express { const PRM_PATH = "/auth/prm"; function buildAsMetadata(req: Request) { - const base = resolvePublicUrl(req); + const base = publicBaseHref(req); return { issuer: resolveIssuer(req), // subpath issuer → well-known at /.well-known/.../auth - authorization_endpoint: `${base.origin}/authorize`, - token_endpoint: `${base.origin}/token`, + authorization_endpoint: `${base}/authorize`, + token_endpoint: `${base}/token`, response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"], code_challenge_methods_supported: ["S256"], @@ -888,9 +903,9 @@ export function createApp(): Express { // PRM: full version at custom path (referenced via WWW-Authenticate on 401). function buildPrm(req: Request, includeAuth: boolean, resourcePath = "/mcp") { - const base = resolvePublicUrl(req); + const base = publicBaseHref(req); return { - resource: `${base.origin}${resourcePath}`, + resource: `${base}${resourcePath}`, ...(includeAuth ? { authorization_servers: [resolveIssuer(req)], @@ -959,11 +974,11 @@ export function createApp(): Express { res: Response, pathTtl: number | undefined, ) { - const base = resolvePublicUrl(req); + const base = publicBaseHref(req); const resourceMetadataUrl = pathTtl !== undefined - ? `${base.origin}${PRM_PATH}/ttl/${pathTtl}` - : `${base.origin}${PRM_PATH}`; + ? `${base}${PRM_PATH}/ttl/${pathTtl}` + : `${base}${PRM_PATH}`; const body = req.body; const messages = Array.isArray(body) ? body : body ? [body] : []; @@ -1080,15 +1095,15 @@ export function createApp(): Express { // Simple landing page app.get("/", (req, res) => { - const base = resolvePublicUrl(req); + const base = publicBaseHref(req); res .type("text/plain") .send( `Lazy Auth Demo — MCP server\n\n` + - ` MCP endpoint: ${base.origin}/mcp\n` + - ` ${base.origin}/ttl//mcp (custom token TTL, e.g. /ttl/3600/mcp)\n` + - ` AS metadata: ${base.origin}/.well-known/oauth-authorization-server${ISSUER_SUFFIX}\n` + - ` PRM metadata: ${base.origin}${PRM_PATH}\n\n` + + ` MCP endpoint: ${base}/mcp\n` + + ` ${base}/ttl//mcp (custom token TTL, e.g. /ttl/3600/mcp)\n` + + ` AS metadata: ${base}/.well-known/oauth-authorization-server${ISSUER_SUFFIX}\n` + + ` PRM metadata: ${base}${PRM_PATH}\n\n` + `Tools:\n` + ` - show_auth_button (public)\n` + ` - get_secret (protected, requires Bearer token)\n` + From ca1d29894fabbd1558885a9ec8620dcb01d7457e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 4 Jun 2026 22:10:06 +0100 Subject: [PATCH 52/97] 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. --- examples/basic-host/package.json | 5 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/lazy-auth-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 4 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 669 +++--------------- package.json | 2 +- 28 files changed, 134 insertions(+), 594 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 117beca..1b45dcf 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", @@ -29,7 +29,6 @@ "prettier": "^3.6.2", "typescript": "^5.9.3", "vite": "^6.0.0", - "vite-plugin-singlefile": "^2.3.0", - "vitest": "^3.2.4" + "vite-plugin-singlefile": "^2.3.0" } } diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 353d5f0..5127202 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index a8bdfa8..e4d1233 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 1925ae1..8f75ac8 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index c7fef6b..f877bf7 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 72b6ed2..5d47f2c 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index cb924b4..d27ddc9 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index ded5814..19a1d25 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 9f82c64..8d1268b 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index aa52b35..4511de9 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 1c505e2..3417a8f 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 2104382..f6715a4 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.3", + "version": "1.7.4", "private": true, "type": "module", "scripts": { diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 8b7cb3f..f3ed6cd 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", "repository": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 7283dd2..5dc23fe 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 619f082..4d960f1 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index e86607b..a5cbf5b 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.3", + "version": "1.7.4", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 81e03b7..99948cc 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index c044d09..a0076e6 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.3", + "version": "1.7.4", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index a91b2f4..aa1e668 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 80a4c76..ccf15a4 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index cfad907..ca9fa90 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 102e96d..12e8cea 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { @@ -31,7 +31,7 @@ "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "systeminformation": "^5.31.5", + "systeminformation": "^5.31.6", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 372a41b..cfb5686 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index aeb149c..dcba3d7 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 86f437d..b0d04af 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 2d3f1f7..fe511ad 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.3", + "version": "1.7.4", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index b3f7a65..29e9058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "workspaces": [ "examples/*" @@ -66,7 +66,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.3", + "version": "1.7.4", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -86,8 +86,7 @@ "prettier": "^3.6.2", "typescript": "^5.9.3", "vite": "^6.0.0", - "vite-plugin-singlefile": "^2.3.0", - "vitest": "^3.2.4" + "vite-plugin-singlefile": "^2.3.0" } }, "examples/basic-host/node_modules/@types/node": { @@ -109,7 +108,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -153,7 +152,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -200,7 +199,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -244,7 +243,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -288,7 +287,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -330,7 +329,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -374,7 +373,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -417,7 +416,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -464,7 +463,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -507,7 +506,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -548,7 +547,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.7.3", + "version": "1.7.4", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -593,7 +592,7 @@ }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -635,7 +634,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -677,7 +676,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", @@ -721,7 +720,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.3", + "version": "1.7.4", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -729,7 +728,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -761,7 +760,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -770,7 +769,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -818,7 +817,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -860,7 +859,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -903,7 +902,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -911,7 +910,7 @@ "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "systeminformation": "^5.31.5", + "systeminformation": "^5.31.6", "zod": "^4.1.13" }, "bin": { @@ -938,32 +937,6 @@ "undici-types": "~6.20.0" } }, - "examples/system-monitor-server/node_modules/systeminformation": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.5.tgz", - "integrity": "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==", - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, "examples/system-monitor-server/node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -973,7 +946,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -1022,7 +995,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -1065,7 +1038,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -1107,7 +1080,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -3770,9 +3743,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", - "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -3891,17 +3864,6 @@ "bun-types": "1.3.6" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -3922,13 +3884,6 @@ "@types/node": "*" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/dom-speech-recognition": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.7.tgz", @@ -4092,19 +4047,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript/vfs": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz", @@ -4153,131 +4095,6 @@ "vue": "^3.2.25" } }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vue/compiler-core": { "version": "3.5.27", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", @@ -4431,9 +4248,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4560,16 +4377,6 @@ "node": ">= 0.4" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -4844,16 +4651,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4917,23 +4714,6 @@ "node": ">=12" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4976,16 +4756,6 @@ "pnpm": ">=8" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/cheerio": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", @@ -5643,16 +5413,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5683,9 +5443,9 @@ } }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "license": "MIT" }, "node_modules/dom-serializer": { @@ -5858,13 +5618,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5955,13 +5708,20 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", - "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", + "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, "node_modules/estree-walker": { @@ -6007,16 +5767,6 @@ "node": ">=18.0.0" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -6061,12 +5811,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -6085,9 +5835,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -6421,9 +6171,9 @@ } }, "node_modules/hono": { - "version": "4.12.14", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", - "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -6562,9 +6312,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -7014,13 +6764,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7197,9 +6940,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -7465,23 +7208,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pdfjs-dist": { "version": "5.4.530", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.530.tgz", @@ -7570,9 +7296,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -7589,7 +7315,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7654,9 +7380,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -8124,13 +7850,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -8281,13 +8000,6 @@ "node": ">=16" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -8297,13 +8009,6 @@ "node": ">= 0.8" } }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8368,26 +8073,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -8405,23 +8090,23 @@ } }, "node_modules/svelte": { - "version": "5.55.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz", - "integrity": "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==", + "version": "5.56.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz", + "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.4", + "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -8431,6 +8116,32 @@ "node": ">=18" } }, + "node_modules/systeminformation": { + "version": "5.31.7", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.7.tgz", + "integrity": "sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/text-camel-case": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/text-camel-case/-/text-camel-case-1.2.9.tgz", @@ -8649,13 +8360,6 @@ "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", "license": "MIT" }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -8663,13 +8367,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -8718,36 +8415,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9634,29 +9301,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-singlefile": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz", @@ -9768,92 +9412,6 @@ } } }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vue": { "version": "3.5.27", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", @@ -9912,23 +9470,6 @@ "node": ">= 8" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index 6738b68..8c6ead5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.3", + "version": "1.7.4", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From fa1274490873f869c9a084e1abb9cf3031d288c7 Mon Sep 17 00:00:00 2001 From: Aqil Aziz Date: Wed, 17 Jun 2026 22:51:57 +0700 Subject: [PATCH 53/97] docs: fix map-server readme and code of conduct links (#657) --- CONTRIBUTING.md | 2 +- examples/map-server/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6592106..e28dcd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -502,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 diff --git a/examples/map-server/README.md b/examples/map-server/README.md index 4c4de5f..e1e044a 100644 --- a/examples/map-server/README.md +++ b/examples/map-server/README.md @@ -122,7 +122,7 @@ Vanilla TypeScript app that: - [`server.ts`](server.ts) - MCP server with geocode and show-map tools - [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - CesiumJS globe UI -- [`server-utils.ts`](server-utils.ts) - HTTP server utilities +- [`main.ts`](main.ts) - HTTP and stdio server utilities ## Notes From ea8775dcb5534cc8977c5137fee5572b0cd94f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20G=C3=B6k?= <116498667+ensargx@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:43:37 +0300 Subject: [PATCH 54/97] docs: API Docs link for ext-apps/server (#697) Updated API documentation link for ext-apps/server. The link `/server.html` does not exists. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7348e15..d0677ef 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ UI metadata. | `@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.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) From f0d9582eaa63faeec754c8b026ec4a9b3f626cfe Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Tue, 7 Jul 2026 14:02:07 -0400 Subject: [PATCH 55/97] spec: add missing HostCapabilities fields (#653) --- specification/draft/apps.mdx | 39 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 1a14d3f..96c8286 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -659,6 +659,22 @@ Example: They describe the features and capabilities that the Host supports. ```typescript +/** Content block modalities that the host supports for a given capability. */ +interface SupportedContentBlockModalities { + /** Host supports text content blocks. */ + text?: {}; + /** Host supports image content blocks. */ + image?: {}; + /** Host supports audio content blocks. */ + audio?: {}; + /** Host supports resource content blocks. */ + resource?: {}; + /** Host supports resource link content blocks. */ + resourceLink?: {}; + /** Host supports structured content. */ + structuredContent?: {}; +} + interface HostCapabilities { /** Experimental features (structure TBD). */ experimental?: {}; @@ -678,14 +694,6 @@ interface HostCapabilities { }; /** Host accepts log messages. */ logging?: {}; - /** - * Host supports LLM sampling (sampling/createMessage) from the view. - * Mirrors MCP ClientCapabilities.sampling so hosts can pass it through. - */ - sampling?: { - /** Host supports tool use via `tools` and `toolChoice` params (SEP-1577). */ - tools?: {}; - }; /** Sandbox configuration applied by the host. */ sandbox?: { /** Permissions granted by the host (camera, microphone, geolocation, clipboard-write). */ @@ -707,6 +715,21 @@ interface HostCapabilities { baseUriDomains?: string[]; }; }; + /** + * Host accepts context updates (ui/update-model-context) to be included in + * the model's context for future turns. + */ + updateModelContext?: SupportedContentBlockModalities; + /** Host supports receiving content messages (ui/message) from the view. */ + message?: SupportedContentBlockModalities; + /** + * Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors MCP ClientCapabilities.sampling so hosts can pass it through. + */ + sampling?: { + /** Host supports tool use via `tools` and `toolChoice` params (SEP-1577). */ + tools?: {}; + }; } ``` From cd969dbc312923a0a407ed76f8679666be45e801 Mon Sep 17 00:00:00 2001 From: Andrew Khadder <54488379+khandrew1@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:05:17 -0700 Subject: [PATCH 56/97] docs: add mcp-use inspector badge to supported clients (#650) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d0677ef..506b255 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ it will guide you through the rest. Goose Postman MCPJam + mcp-use

> [!NOTE] From cf87f2a2c2581b2bc45ff4848aac9fa7e106a576 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:29:16 +0300 Subject: [PATCH 57/97] 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> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 506b255..2fd89ff 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ UI metadata. | `@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) | +| `@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) From b383847bf2c7f4f5fa37e3bd054205f71618e035 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 13:35:51 -0700 Subject: [PATCH 58/97] chore: prepare SDK v2 dependencies --- examples/basic-server-preact/package.json | 2 + examples/basic-server-react/package.json | 2 + examples/basic-server-solid/package.json | 2 + examples/basic-server-svelte/package.json | 2 + examples/basic-server-vanillajs/package.json | 2 + examples/basic-server-vue/package.json | 2 + examples/budget-allocator-server/package.json | 2 + examples/cohort-heatmap-server/package.json | 2 + .../customer-segmentation-server/package.json | 2 + examples/debug-server/package.json | 2 + examples/integration-server/package.json | 2 + examples/lazy-auth-server/package.json | 1 + examples/map-server/package.json | 2 + examples/pdf-server/package.json | 2 + examples/quickstart/package.json | 2 + examples/scenario-modeler-server/package.json | 2 + examples/shadertoy-server/package.json | 2 + examples/sheet-music-server/package.json | 2 + examples/system-monitor-server/package.json | 2 + examples/threejs-server/package.json | 2 + examples/transcript-server/package.json | 2 + examples/video-resource-server/package.json | 2 + examples/wiki-explorer-server/package.json | 2 + package-lock.json | 537 +++++------------- package.json | 10 +- 25 files changed, 187 insertions(+), 405 deletions(-) diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 5127202..1a63232 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index e4d1233..821fc96 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -35,6 +35,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 8f75ac8..997bc60 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index f877bf7..e83772f 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 5d47f2c..64e1758 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index d27ddc9..e4cd0b4 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 19a1d25..8af1a6c 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 8d1268b..99e9d9f 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 4511de9..59e3eb1 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 3417a8f..02e9fbf 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -35,6 +35,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.1.13" }, diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index f6715a4..3f9d8bb 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -16,6 +16,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index f3ed6cd..086f20a 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 5dc23fe..b0c629a 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 4d960f1..aeb52c7 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -27,6 +27,8 @@ "dependencies": { "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 99948cc..1003637 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -16,6 +16,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0" diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index aa1e668..a7e8a9a 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index ccf15a4..0c27501 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index ca9fa90..6cf175a 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 12e8cea..760d07f 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index cfb5686..9dda288 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index dcba3d7..ce5b737 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index b0d04af..605b8de 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -25,6 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index fe511ad..62f1de8 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", diff --git a/package-lock.json b/package-lock.json index 29e9058..5514bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,10 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -44,16 +47,19 @@ "typedoc": "^0.28.14", "typedoc-github-theme": "^0.4.0", "typescript": "^5.9.3", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "engines": { "node": ">=20" }, "peerDependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" + "zod": "^4.2.0" }, "peerDependenciesMeta": { "react": { @@ -89,29 +95,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-host/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-host/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -133,29 +124,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-preact/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-preact/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -180,29 +156,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-react/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-react/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -224,29 +185,14 @@ "vite-plugin-solid": "^2.11.12" } }, - "examples/basic-server-solid/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-solid/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -268,29 +214,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-svelte/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-svelte/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -310,29 +241,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-vanillajs/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-vanillajs/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -354,29 +270,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-vue/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-vue/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -397,29 +298,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/budget-allocator-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/budget-allocator-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -444,29 +330,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/cohort-heatmap-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/cohort-heatmap-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -487,29 +358,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/customer-segmentation-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/customer-segmentation-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.1.13" }, @@ -529,27 +385,12 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/debug-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/debug-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/integration-server": { "version": "1.7.4", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -573,29 +414,13 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/integration-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/integration-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -615,29 +440,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/lazy-auth-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/lazy-auth-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -657,30 +467,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/map-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/map-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", "version": "1.7.4", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -701,23 +496,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/pdf-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/pdf-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", "version": "1.7.4", @@ -731,7 +509,9 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0" @@ -748,16 +528,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/quickstart/node_modules/@types/node": { - "version": "22.19.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", - "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "1.7.4", @@ -772,7 +542,9 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -798,29 +570,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/scenario-modeler-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/scenario-modeler-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -840,29 +597,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/shadertoy-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/shadertoy-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", @@ -883,29 +625,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/sheet-music-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/sheet-music-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", @@ -927,29 +654,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/system-monitor-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/system-monitor-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -976,29 +688,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/threejs-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/threejs-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -1019,29 +716,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/transcript-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/transcript-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -1061,29 +743,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/video-resource-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/video-resource-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", @@ -1105,23 +772,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/wiki-explorer-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/wiki-explorer-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -2576,6 +2226,53 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz", + "integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0-beta.4", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz", + "integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/express": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0-beta.4.tgz", + "integrity": "sha512-7hI8bEomz6rMn8sKYRVIZgQJsrWIjIS9WiPyIp3X2pMyLSCC7hB5km0kg+9SygWygTIRnPfte0tXevRB3mXEHQ==", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-beta.4", + "express": "^4.18.0 || ^5.0.0" + } + }, "node_modules/@modelcontextprotocol/ext-apps": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", @@ -2609,6 +2306,27 @@ "resolved": "examples/basic-host", "link": true }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-beta.4.tgz", + "integrity": "sha512-OqbrRVNYNfLxBB2BRkP5oIHKHa0IPHluMbaLgw7YCRKcj5be9ExzyWFJY3KVMb+1F2AVn+0SRoH0XKcNzh3HYQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-beta.4", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/quickstart": { "resolved": "examples/quickstart", "link": true @@ -2653,6 +2371,19 @@ } } }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz", + "integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0-beta.4", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/server-basic-preact": { "resolved": "examples/basic-server-preact", "link": true diff --git a/package.json b/package.json index 8c6ead5..35bcdf0 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,10 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -105,13 +108,16 @@ "typedoc": "^0.28.14", "typedoc-github-theme": "^0.4.0", "typescript": "^5.9.3", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "peerDependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" + "zod": "^4.2.0" }, "peerDependenciesMeta": { "react": { From 209cfc62413b7eea46c127ddd756ce58bd2da095 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 13:49:10 -0700 Subject: [PATCH 59/97] refactor: compose protocol-neutral event dispatch --- src/app-bridge.test.ts | 2 +- src/app-bridge.ts | 120 ++++++++++++++++--- src/app.ts | 139 +++++++++++++++++----- src/events.test.ts | 82 +++++++++++++ src/events.ts | 261 ++++++----------------------------------- typedoc.config.mjs | 2 +- 6 files changed, 335 insertions(+), 271 deletions(-) create mode 100644 src/events.test.ts diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index a4fe1de..7adbb16 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -2713,7 +2713,7 @@ describe("isToolVisibilityAppOnly", () => { expect(a).toEqual([]); }); - it("App.onEventDispatch merges hostcontext before listeners fire", async () => { + it("App merges hostcontext before listeners fire", async () => { let seen: unknown; app.addEventListener("hostcontextchanged", () => { seen = app.getHostContext(); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 23383c4..18d7671 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -47,7 +47,10 @@ import { ProtocolOptions, RequestOptions, } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { ProtocolWithEvents } from "./events"; +import { EventDispatcher } from "./events"; +import type { ZodLiteral, ZodObject } from "zod/v4"; + +type MethodSchema = ZodObject<{ method: ZodLiteral }>; import { type AppNotification, @@ -303,16 +306,36 @@ export type AppBridgeEventMap = { * await bridge.connect(transport); * ``` */ -export class AppBridge extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppBridgeEventMap -> { +export class AppBridge extends Protocol { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; private _initializedReceived = false; + private readonly _registeredMethods = new Set(); + private readonly _registeredEvents = new Set(); + private readonly _events = new EventDispatcher(); + + // ProtocolWithEvents previously owned these registration guards. Keep them + // on AppBridge to preserve the existing double-registration behavior while + // event fan-out moves to protocol-independent composition. Arrow-function + // fields let Protocol's constructor run before the guards initialize. + override setRequestHandler: Protocol< + AppRequest, + AppNotification, + AppResult + >["setRequestHandler"] = (schema, handler) => { + this._assertMethodNotRegistered(schema, "setRequestHandler"); + super.setRequestHandler(schema, handler); + }; + + override setNotificationHandler: Protocol< + AppRequest, + AppNotification, + AppResult + >["setNotificationHandler"] = (schema, handler) => { + this._assertMethodNotRegistered(schema, "setNotificationHandler"); + super.setNotificationHandler(schema, handler); + }; /** * Wrap every handler registered via `replaceRequestHandler` with a check @@ -324,13 +347,14 @@ export class AppBridge extends ProtocolWithEvents< * * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} */ - private _baseReplaceRequestHandler = this.replaceRequestHandler; - protected override replaceRequestHandler: Protocol< + protected replaceRequestHandler: Protocol< AppRequest, AppNotification, AppResult >["setRequestHandler"] = (schema, handler) => { - this._baseReplaceRequestHandler(schema, (request, extra) => { + const method = (schema as MethodSchema).shape.method.value; + this._registeredMethods.add(method); + super.setRequestHandler(schema, (request, extra) => { if (!this._initializedReceived) { console.warn( `[ext-apps] AppBridge received '${request.method}' before ` + @@ -343,7 +367,9 @@ export class AppBridge extends ProtocolWithEvents< }); }; - protected readonly eventSchemas = { + private readonly eventSchemas: { + [K in keyof AppBridgeEventMap]: MethodSchema; + } = { sizechange: McpUiSizeChangedNotificationSchema, sandboxready: McpUiSandboxProxyReadyNotificationSchema, initialized: McpUiInitializedNotificationSchema, @@ -351,6 +377,74 @@ export class AppBridge extends ProtocolWithEvents< loggingmessage: LoggingMessageNotificationSchema, }; + protected setEventHandler( + event: K, + handler: ((params: AppBridgeEventMap[K]) => void) | undefined, + ): void { + this._ensureEventSlot(event); + this._events.setHandler(event, handler); + } + + protected getEventHandler( + event: K, + ): ((params: AppBridgeEventMap[K]) => void) | undefined { + return this._events.getHandler(event); + } + + addEventListener( + event: K, + handler: (params: AppBridgeEventMap[K]) => void, + ): void { + this._ensureEventSlot(event); + this._events.addEventListener(event, handler); + } + + removeEventListener( + event: K, + handler: (params: AppBridgeEventMap[K]) => void, + ): void { + this._events.removeEventListener(event, handler); + } + + private _ensureEventSlot( + event: K, + ): void { + if (this._registeredEvents.has(event)) return; + const schema = this.eventSchemas[event]; + if (!schema) throw new Error(`Unknown event: ${String(event)}`); + this.setNotificationHandler(schema, (notification) => { + const params = (notification as { params: AppBridgeEventMap[K] }).params; + if (event === "initialized") this._initializedReceived = true; + this._events.dispatch(event, params); + }); + this._registeredEvents.add(event); + } + + protected warnIfRequestHandlerReplaced( + name: string, + previous: unknown, + next: unknown, + ): void { + if (previous && next) { + console.warn( + `[MCP Apps] ${name} handler replaced. ` + + `Previous handler will no longer be called.`, + ); + } + } + + private _assertMethodNotRegistered(schema: unknown, via: string): void { + const method = (schema as MethodSchema).shape.method.value; + if (this._registeredMethods.has(method)) { + throw new Error( + `Handler for "${method}" already registered (via ${via}). ` + + `Use addEventListener() to attach multiple listeners, ` + + `or the on* setter for replace semantics.`, + ); + } + this._registeredMethods.add(method); + } + /** * Create a new AppBridge instance. * @@ -392,9 +486,7 @@ export class AppBridge extends ProtocolWithEvents< ) { super(options); - this.addEventListener("initialized", () => { - this._initializedReceived = true; - }); + this._ensureEventSlot("initialized"); this._hostContext = options?.hostContext || {}; diff --git a/src/app.ts b/src/app.ts index adfad5c..c586c31 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import { type RequestOptions, mergeCapabilities, + Protocol, ProtocolOptions, } from "@modelcontextprotocol/sdk/shared/protocol.js"; @@ -32,8 +33,8 @@ import { ToolListChangedNotification, } from "@modelcontextprotocol/sdk/types.js"; import { AppNotification, AppRequest, AppResult } from "./types"; -import { ProtocolWithEvents } from "./events"; -export { ProtocolWithEvents }; +import { EventDispatcher } from "./events"; +export { EventDispatcher } from "./events"; import { PostMessageTransport } from "./message-transport"; import { LATEST_PROTOCOL_VERSION, @@ -74,7 +75,9 @@ import { standardSchemaToJsonSchema, validateStandardSchema, } from "./standard-schema"; -import { z } from "zod/v4"; +import { z, type ZodLiteral, type ZodObject } from "zod/v4"; + +type MethodSchema = ZodObject<{ method: ZodLiteral }>; export type { StandardSchemaV1, @@ -271,9 +274,9 @@ export type RegisteredAppTool = { /** * Maps DOM-style event names to their notification `params` types. * - * Used by {@link App `App`} (which extends {@link ProtocolWithEvents `ProtocolWithEvents`}) - * to provide type-safe `addEventListener` / `removeEventListener` and - * singular `on*` handler support. + * Used by {@link App `App`} and its composed {@link EventDispatcher} to + * provide type-safe `addEventListener` / `removeEventListener` and singular + * `on*` handler support. */ export type AppEventMap = { toolinput: McpUiToolInputNotification["params"]; @@ -304,16 +307,14 @@ export type AppEventMap = { * 3. **Interactive**: Send requests, receive notifications, call tools * 4. **Teardown**: Host sends teardown request before unmounting * - * ## Inherited Methods + * ## Protocol and event methods * - * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits: + * As a subclass of the MCP SDK's `Protocol`, `App` provides: * - `setRequestHandler()` - Register handlers for requests from host * - `setNotificationHandler()` - Register handlers for notifications from host * - `addEventListener()` - Append a listener for a notification event (multi-listener) * - `removeEventListener()` - Remove a previously added listener * - * @see {@link ProtocolWithEvents `ProtocolWithEvents`} for the DOM-model event system - * * ## Notification Setters (DOM-model `on*` handlers) * * For common notifications, the `App` class provides getter/setter properties @@ -342,17 +343,37 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppEventMap -> { +export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; private _registeredTools: { [name: string]: RegisteredAppTool } = {}; private _initializedSent = false; + private readonly _registeredMethods = new Set(); + private readonly _registeredEvents = new Set(); + private readonly _events = new EventDispatcher(); + + // ProtocolWithEvents previously owned these registration guards. Keep them + // on App to preserve the existing double-registration behavior while event + // fan-out moves to protocol-independent composition. Arrow-function fields + // ensure Protocol's constructor uses its own methods before the guards init. + override setRequestHandler: Protocol< + AppRequest, + AppNotification, + AppResult + >["setRequestHandler"] = (schema, handler) => { + this._assertMethodNotRegistered(schema, "setRequestHandler"); + super.setRequestHandler(schema, handler); + }; + + override setNotificationHandler: Protocol< + AppRequest, + AppNotification, + AppResult + >["setNotificationHandler"] = (schema, handler) => { + this._assertMethodNotRegistered(schema, "setNotificationHandler"); + super.setNotificationHandler(schema, handler); + }; /** * Warn if a host-bound method is called before {@link connect `connect`} has @@ -378,7 +399,9 @@ export class App extends ProtocolWithEvents< console.warn(`${msg}. This will throw in a future release.`); } - protected readonly eventSchemas = { + private readonly eventSchemas: { + [K in keyof AppEventMap]: MethodSchema; + } = { toolinput: McpUiToolInputNotificationSchema, toolinputpartial: McpUiToolInputPartialNotificationSchema, toolresult: McpUiToolResultNotificationSchema, @@ -427,31 +450,86 @@ export class App extends ProtocolWithEvents< console.warn(msg); } - protected override setEventHandler( + protected setEventHandler( event: K, handler: ((params: AppEventMap[K]) => void) | undefined, ): void { if (handler) this._assertHandlerTiming(event); - super.setEventHandler(event, handler); + this._ensureEventSlot(event); + this._events.setHandler(event, handler); } - override addEventListener( + addEventListener( event: K, handler: (params: AppEventMap[K]) => void, ): void { this._assertHandlerTiming(event); - super.addEventListener(event, handler); + this._ensureEventSlot(event); + this._events.addEventListener(event, handler); } - protected override onEventDispatch( + removeEventListener( event: K, - params: AppEventMap[K], + handler: (params: AppEventMap[K]) => void, ): void { - if (event === "hostcontextchanged") { - this._hostContext = { ...this._hostContext, ...params }; + this._events.removeEventListener(event, handler); + } + + private _ensureEventSlot(event: K): void { + if (this._registeredEvents.has(event)) return; + const schema = this.eventSchemas[event]; + if (!schema) throw new Error(`Unknown event: ${String(event)}`); + this.setNotificationHandler(schema, (notification) => { + const params = (notification as { params: AppEventMap[K] }).params; + if (event === "hostcontextchanged") { + this._hostContext = { ...this._hostContext, ...params }; + } + this._events.dispatch(event, params); + }); + this._registeredEvents.add(event); + } + + protected getEventHandler( + event: K, + ): ((params: AppEventMap[K]) => void) | undefined { + return this._events.getHandler(event); + } + + protected warnIfRequestHandlerReplaced( + name: string, + previous: unknown, + next: unknown, + ): void { + if (previous && next) { + console.warn( + `[MCP Apps] ${name} handler replaced. ` + + `Previous handler will no longer be called.`, + ); } } + protected replaceRequestHandler: Protocol< + AppRequest, + AppNotification, + AppResult + >["setRequestHandler"] = (schema, handler) => { + const method = (schema as MethodSchema).shape.method.value; + this._registeredMethods.add(method); + super.setRequestHandler(schema, handler); + }; + + private _assertMethodNotRegistered(schema: unknown, via: string): void { + const method = (schema as MethodSchema).shape.method.value; + if (this._registeredMethods.has(method)) { + throw new Error( + `Handler for "${method}" already registered (via ${via}). ` + + `Use addEventListener() to attach multiple listeners, ` + + `or the on* setter for replace semantics.`, + ); + } + this._registeredMethods.add(method); + } + /** * Create a new MCP App instance. * @@ -484,9 +562,8 @@ export class App extends ProtocolWithEvents< return {}; }); - // Eagerly register the hostcontextchanged event slot so that - // onEventDispatch (which merges into _hostContext) fires even if the - // user never assigns onhostcontextchanged or calls addEventListener. + // Eagerly register hostcontextchanged so the owner-side context merge runs + // even if the user never assigns a handler or adds a listener. this.setEventHandler("hostcontextchanged", undefined); } @@ -924,9 +1001,9 @@ export class App extends ProtocolWithEvents< * without replacing. * * Notification params are automatically merged into the internal host context - * via {@link onEventDispatch `onEventDispatch`} before any handler or listener - * fires. This means {@link getHostContext `getHostContext`} will return the - * updated values even before your callback runs. + * before any handler or listener fires. This means + * {@link getHostContext `getHostContext`} will return the updated values even + * before your callback runs. * * Register handlers before calling {@link connect `connect`} to avoid missing notifications. * diff --git a/src/events.test.ts b/src/events.test.ts new file mode 100644 index 0000000..7fd8708 --- /dev/null +++ b/src/events.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, spyOn } from "bun:test"; + +import { EventDispatcher } from "./events"; + +type TestEventMap = { + change: { value: number }; +}; + +describe("EventDispatcher", () => { + it("replaces the singular handler", () => { + const dispatcher = new EventDispatcher(); + const calls: string[] = []; + const first = () => calls.push("first"); + const second = () => calls.push("second"); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + + try { + dispatcher.setHandler("change", first); + dispatcher.setHandler("change", second); + + expect(dispatcher.getHandler("change")).toBe(second); + dispatcher.dispatch("change", { value: 1 }); + expect(calls).toEqual(["second"]); + } finally { + warn.mockRestore(); + } + }); + + it("dispatches the singular handler before listeners in insertion order", () => { + const dispatcher = new EventDispatcher(); + const calls: string[] = []; + + dispatcher.setHandler("change", () => calls.push("handler")); + dispatcher.addEventListener("change", () => calls.push("listener-1")); + dispatcher.addEventListener("change", () => calls.push("listener-2")); + + dispatcher.dispatch("change", { value: 1 }); + + expect(calls).toEqual(["handler", "listener-1", "listener-2"]); + }); + + it("uses a listener snapshot when a listener is removed during dispatch", () => { + const dispatcher = new EventDispatcher(); + const calls: string[] = []; + const second = () => calls.push("second"); + const first = () => { + calls.push("first"); + dispatcher.removeEventListener("change", second); + }; + + dispatcher.addEventListener("change", first); + dispatcher.addEventListener("change", second); + + dispatcher.dispatch("change", { value: 1 }); + dispatcher.dispatch("change", { value: 2 }); + + expect(calls).toEqual(["first", "second", "first"]); + }); + + it("warns only when one singular handler replaces another", () => { + const dispatcher = new EventDispatcher(); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + const first = () => {}; + const second = () => {}; + + try { + dispatcher.setHandler("change", first); + dispatcher.setHandler("change", undefined); + dispatcher.setHandler("change", first); + expect(warn).not.toHaveBeenCalled(); + + dispatcher.setHandler("change", second); + + expect(warn).toHaveBeenCalledTimes(1); + expect(String(warn.mock.calls[0]?.[0])).toContain( + "onchange handler replaced", + ); + } finally { + warn.mockRestore(); + } + }); +}); diff --git a/src/events.ts b/src/events.ts index 4d17ca4..b891867 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,17 +1,6 @@ -import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - Request, - Notification, - Result, -} from "@modelcontextprotocol/sdk/types.js"; -import { ZodLiteral, ZodObject } from "zod/v4"; - -type MethodSchema = ZodObject<{ method: ZodLiteral }>; - /** - * Per-event state: a singular `on*` handler (replace semantics) plus a - * listener array (`addEventListener` semantics), mirroring the DOM model - * where `el.onclick` and `el.addEventListener("click", …)` coexist. + * Per-event state: a singular `on*` handler (replace semantics) plus an + * ordered listener array (`addEventListener` semantics). */ interface EventSlot { onHandler?: ((params: T) => void) | undefined; @@ -19,118 +8,19 @@ interface EventSlot { } /** - * Intermediate base class that adds DOM-style event support on top of the - * MCP SDK's `Protocol`. + * Protocol-independent DOM-style event fan-out. * - * The base `Protocol` class stores one handler per method: - * `setRequestHandler()` and `setNotificationHandler()` replace any existing - * handler for the same method silently. This class introduces a two-channel - * event model inspired by the DOM: + * Owners perform event-specific state updates before calling + * {@link dispatch}. Dispatch then invokes the singular handler followed by a + * snapshot of listeners in insertion order. * - * ### Singular `on*` handler (like `el.onclick`) - * - * Subclasses expose `get`/`set` pairs that delegate to - * {@link setEventHandler `setEventHandler`} / - * {@link getEventHandler `getEventHandler`}. Assigning replaces the previous - * handler; assigning `undefined` clears it. `addEventListener` listeners are - * unaffected. - * - * ### Multi-listener (`addEventListener` / `removeEventListener`) - * - * Append to a per-event listener array. Listeners fire in insertion order - * after the singular `on*` handler. - * - * ### Dispatch order - * - * When a notification arrives for a mapped event: - * 1. {@link onEventDispatch `onEventDispatch`} (subclass side-effects) - * 2. The singular `on*` handler (if set) - * 3. All `addEventListener` listeners in insertion order - * - * ### Double-set protection - * - * Direct calls to {@link setRequestHandler `setRequestHandler`} / - * {@link setNotificationHandler `setNotificationHandler`} throw if a handler - * for the same method has already been registered (through any path), so - * accidental overwrites surface as errors instead of silent bugs. - * - * @typeParam EventMap - Maps event names to the listener's `params` type. + * @typeParam EventMap - Maps event names to their listener parameter types. */ -export abstract class ProtocolWithEvents< - SendRequestT extends Request, - SendNotificationT extends Notification, - SendResultT extends Result, - EventMap extends Record, -> extends Protocol { - private _registeredMethods = new Set(); - private _eventSlots = new Map(); +export class EventDispatcher> { + private readonly _eventSlots = new Map(); - /** - * Event name → notification schema. Subclasses populate this so that - * the event system can lazily register a dispatcher with the correct - * schema on first use. - */ - protected abstract readonly eventSchemas: { - [K in keyof EventMap]: MethodSchema; - }; - - /** - * Called once per incoming notification, before any handlers or listeners - * fire. Subclasses may override to perform side effects such as merging - * notification params into cached state. - */ - protected onEventDispatch( - _event: K, - _params: EventMap[K], - ): void {} - - // ── Event system (DOM model) ──────────────────────────────────────── - - /** - * Lazily create the event slot and register a single dispatcher with the - * base `Protocol`. The dispatcher fans out to the `on*` handler and all - * `addEventListener` listeners. - */ - private _ensureEventSlot( - event: K, - ): EventSlot { - let slot = this._eventSlots.get(event) as - | EventSlot - | undefined; - if (!slot) { - const schema = this.eventSchemas[event]; - if (!schema) { - throw new Error(`Unknown event: ${String(event)}`); - } - slot = { listeners: [] }; - this._eventSlots.set(event, slot as EventSlot); - - // Claim this method so direct setNotificationHandler calls throw. - const method = schema.shape.method.value; - this._registeredMethods.add(method); - - const s = slot; // stable reference for the closure - super.setNotificationHandler(schema, (n) => { - const params = (n as { params: EventMap[K] }).params; - this.onEventDispatch(event, params); - // 1. Singular on* handler - s.onHandler?.(params); - // 2. addEventListener listeners — snapshot to tolerate removal during - // dispatch (e.g., a listener that calls removeEventListener on itself) - for (const l of [...s.listeners]) l(params); - }); - } - return slot; - } - - /** - * Set or clear the singular `on*` handler for an event. - * - * Replace semantics — like the DOM's `el.onclick = fn`. Assigning - * `undefined` clears the handler without affecting `addEventListener` - * listeners. - */ - protected setEventHandler( + /** Set, replace, or clear the singular handler for an event. */ + setHandler( event: K, handler: ((params: EventMap[K]) => void) | undefined, ): void { @@ -144,31 +34,16 @@ export abstract class ProtocolWithEvents< slot.onHandler = handler; } - /** - * Get the singular `on*` handler for an event, or `undefined` if none is - * set. `addEventListener` listeners are not reflected here. - */ - protected getEventHandler( + /** Get the singular handler for an event, if one is set. */ + getHandler( event: K, ): ((params: EventMap[K]) => void) | undefined { - return (this._eventSlots.get(event) as EventSlot | undefined) - ?.onHandler; + return (this._eventSlots.get(event) as + | EventSlot + | undefined)?.onHandler; } - /** - * Add a listener for a notification event. - * - * Unlike the singular `on*` handler, calling this multiple times appends - * listeners rather than replacing them. All registered listeners fire in - * insertion order after the `on*` handler when the notification arrives. - * - * Registration is lazy: the first call (for a given event, from either - * this method or the `on*` setter) registers a dispatcher with the base - * `Protocol`. - * - * @param event - Event name (a key of the `EventMap` type parameter). - * @param handler - Listener invoked with the notification `params`. - */ + /** Add a listener. Listeners run in insertion order after the handler. */ addEventListener( event: K, handler: (params: EventMap[K]) => void, @@ -176,11 +51,7 @@ export abstract class ProtocolWithEvents< this._ensureEventSlot(event).listeners.push(handler); } - /** - * Remove a previously registered event listener. The dispatcher stays - * registered even if the listener array becomes empty; future - * notifications simply have no listeners to call. - */ + /** Remove the first matching listener, if present. */ removeEventListener( event: K, handler: (params: EventMap[K]) => void, @@ -193,87 +64,29 @@ export abstract class ProtocolWithEvents< if (idx !== -1) slot.listeners.splice(idx, 1); } - // ── Handler registration with double-set protection ───────────────── - - // The two overrides below are arrow-function class fields rather than - // prototype methods so that Protocol's constructor — which registers its - // own ping/cancelled/progress handlers via `this.setRequestHandler` - // before our fields initialize — hits the base implementation and skips - // tracking. Converting these to proper methods would crash with - // `_registeredMethods` undefined during super(). - /** - * Registers a request handler. Throws if a handler for the same method - * has already been registered — use the `on*` setter (replace semantics) - * or `addEventListener` (multi-listener) for notification events. - * - * @throws {Error} if a handler for this method is already registered. + * Dispatch an event to its singular handler and a listener snapshot. + * Listener mutations during dispatch take effect on the next dispatch. */ - override setRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; - - /** - * Registers a notification handler. Throws if a handler for the same - * method has already been registered — use the `on*` setter (replace - * semantics) or `addEventListener` (multi-listener) for mapped events. - * - * @throws {Error} if a handler for this method is already registered. - */ - override setNotificationHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setNotificationHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setNotificationHandler"); - super.setNotificationHandler(schema, handler); - }; - - /** - * Warn if a request handler `on*` setter is replacing a previously-set - * handler. Call from each request setter before updating the backing field. - */ - protected warnIfRequestHandlerReplaced( - name: string, - previous: unknown, - next: unknown, - ): void { - if (previous && next) { - console.warn( - `[MCP Apps] ${name} handler replaced. ` + - `Previous handler will no longer be called.`, - ); - } + dispatch(event: K, params: EventMap[K]): void { + const slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) return; + slot.onHandler?.(params); + for (const l of [...slot.listeners]) l(params); } - /** - * Replace a request handler, bypassing double-set protection. Used by - * `on*` request-handler setters that need replace semantics. - */ - protected replaceRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - const method = (schema as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(schema, handler); - }; - - private _assertMethodNotRegistered(schema: unknown, via: string): void { - const method = (schema as MethodSchema).shape.method.value; - if (this._registeredMethods.has(method)) { - throw new Error( - `Handler for "${method}" already registered (via ${via}). ` + - `Use addEventListener() to attach multiple listeners, ` + - `or the on* setter for replace semantics.`, - ); + private _ensureEventSlot( + event: K, + ): EventSlot { + let slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) { + slot = { listeners: [] }; + this._eventSlots.set(event, slot as EventSlot); } - this._registeredMethods.add(method); + return slot; } } diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 4c5baa3..00babc4 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -28,7 +28,7 @@ const config = { ], excludePrivate: true, excludeInternal: false, - intentionallyNotExported: ["MethodSchema", "RequestHandlerExtra"], + intentionallyNotExported: ["RequestHandlerExtra"], blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false, From 59e6ffc6d8822f5139efc4d09fae46350761ebd2 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 15:02:25 -0700 Subject: [PATCH 60/97] refactor: migrate App to SDK v2 client --- src/app-bridge.test.ts | 44 +++--- src/app.test.ts | 300 +++++++++++++++++++++++++++++++++++++++++ src/app.ts | 261 ++++++++++++++++------------------- 3 files changed, 434 insertions(+), 171 deletions(-) create mode 100644 src/app.test.ts diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 7adbb16..8f432b4 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -7,10 +7,7 @@ import { ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, - PromptListChangedNotificationSchema, ReadResourceResultSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod/v4"; @@ -2313,9 +2310,12 @@ describe("App <-> AppBridge integration", () => { it("sendToolListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + app.setNotificationHandler( + "notifications/tools/list_changed", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2328,9 +2328,12 @@ describe("App <-> AppBridge integration", () => { it("sendResourceListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + app.setNotificationHandler( + "notifications/resources/list_changed", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2343,9 +2346,12 @@ describe("App <-> AppBridge integration", () => { it("sendPromptListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + app.setNotificationHandler( + "notifications/prompts/list_changed", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2771,20 +2777,6 @@ describe("isToolVisibilityAppOnly", () => { ); }).toThrow(/already registered/); }); - - it("direct setNotificationHandler throws for event-mapped methods", () => { - const app2 = new App(testAppInfo, {}, { autoResize: false }); - app2.addEventListener("toolinput", () => {}); - expect(() => { - app2.setNotificationHandler( - // @ts-expect-error — exercising throw path with raw schema - { - shape: { method: { value: "ui/notifications/tool-input" } }, - }, - () => {}, - ); - }).toThrow(/already registered/); - }); }); }); diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 0000000..dcf2d52 --- /dev/null +++ b/src/app.test.ts @@ -0,0 +1,300 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + InMemoryTransport, + Server, + type JSONRPCMessage, +} from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import { App } from "./app"; +import { + LATEST_PROTOCOL_VERSION, + McpUiInitializeRequestSchema, + McpUiInitializeResultSchema, + McpUiInitializedNotificationSchema, +} from "./types"; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; + +type ConnectedPair = { + app: App; + server: Server; + sentByApp: JSONRPCMessage[]; + appsInitialized: Promise; +}; + +async function connectPair( + app = new App( + { name: "view-app", version: "1.0.0" }, + { tools: { listChanged: true } }, + { autoResize: false }, + ), +): Promise { + const server = new Server( + { name: "inner-mcp-server", version: "0.0.0" }, + { + capabilities: { resources: {}, tools: {} }, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }, + ); + const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const sentByApp: JSONRPCMessage[] = []; + const send = appTransport.send.bind(appTransport); + appTransport.send = async (message, options) => { + sentByApp.push(message); + await send(message, options); + }; + + let resolveAppsInitialized!: () => void; + const appsInitialized = new Promise((resolve) => { + resolveAppsInitialized = resolve; + }); + + server.setRequestHandler( + "ui/initialize", + { + params: McpUiInitializeRequestSchema.shape.params, + result: McpUiInitializeResultSchema, + }, + (params) => { + expect(params.protocolVersion).toBe(LATEST_PROTOCOL_VERSION); + return { + protocolVersion: LATEST_PROTOCOL_VERSION, + hostCapabilities: { openLinks: {} }, + hostInfo: { name: "apps-host", version: "2.0.0" }, + hostContext: { theme: "dark" as const, locale: "en-US" }, + }; + }, + ); + server.setNotificationHandler( + "ui/notifications/initialized", + { params: McpUiInitializedNotificationSchema.shape.params }, + () => resolveAppsInitialized(), + ); + server.setRequestHandler("tools/call", (request) => ({ + content: [{ type: "text", text: `called ${request.params.name}` }], + })); + server.setRequestHandler("resources/read", (request) => ({ + contents: [{ uri: request.params.uri, text: "resource body" }], + })); + server.setRequestHandler("resources/list", () => ({ + resources: [{ uri: "test://resource", name: "Test resource" }], + })); + server.setRequestHandler("sampling/createMessage", () => ({ + model: "test-model", + role: "assistant", + content: { type: "text", text: "sampled" }, + })); + + await server.connect(serverTransport); + await app.connect(appTransport); + await appsInitialized; + + return { app, server, sentByApp, appsInitialized }; +} + +const connected: Array> = []; + +afterEach(async () => { + await Promise.all( + connected + .splice(0) + .flatMap(({ app, server }) => [app.close(), server.close()]), + ); +}); + +describe("App base MCP SDK v2 Client migration", () => { + it("completes the legacy MCP handshake before the authoritative Apps handshake", async () => { + const pair = await connectPair(); + connected.push(pair); + + const lifecycleMethods = pair.sentByApp + .filter( + (message): message is JSONRPCMessage & { method: string } => + "method" in message, + ) + .map((message) => message.method) + .filter((method) => + [ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ].includes(method), + ); + + expect(lifecycleMethods).toEqual([ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ]); + expect(pair.app.getNegotiatedProtocolVersion()).toBe( + INNER_MCP_PROTOCOL_VERSION, + ); + }); + + it("uses ui/initialize as the authority for Apps host state", async () => { + const pair = await connectPair(); + connected.push(pair); + + expect(pair.app.getServerVersion()).toEqual({ + name: "inner-mcp-server", + version: "0.0.0", + }); + expect(pair.app.getHostVersion()).toEqual({ + name: "apps-host", + version: "2.0.0", + }); + expect(pair.app.getHostCapabilities()).toEqual({ openLinks: {} }); + expect(pair.app.getHostContext()).toEqual({ + theme: "dark", + locale: "en-US", + }); + }); + + it("preserves registered App tools with the base MCP SDK v2 Client handler context", async () => { + const pair = await connectPair(); + connected.push(pair); + let receivedSignal: AbortSignal | undefined; + + pair.app.registerTool( + "greet", + { description: "Greets a person" }, + async (extra) => { + receivedSignal = extra.mcpReq.signal; + return { content: [{ type: "text", text: "hello" }] }; + }, + ); + + const listed = await pair.server.request({ + method: "tools/list", + params: {}, + }); + const result = await pair.server.request({ + method: "tools/call", + params: { name: "greet", arguments: {} }, + }); + + expect(listed.tools.map((tool) => tool.name)).toEqual(["greet"]); + expect(result.content).toEqual([{ type: "text", text: "hello" }]); + expect(receivedSignal).toBeInstanceOf(AbortSignal); + }); + + it("uses base MCP SDK v2 method-keyed sends for standard MCP requests", async () => { + const pair = await connectPair(); + connected.push(pair); + + const toolResult = await pair.app.callServerTool({ + name: "refresh", + arguments: {}, + }); + const resource = await pair.app.readServerResource({ + uri: "test://resource", + }); + const resources = await pair.app.listServerResources(); + const sample = await pair.app.createSamplingMessage({ + messages: [{ role: "user", content: { type: "text", text: "hello" } }], + maxTokens: 20, + }); + + expect(toolResult.content).toEqual([ + { type: "text", text: "called refresh" }, + ]); + expect(resource.contents[0]).toEqual({ + uri: "test://resource", + text: "resource body", + }); + expect(resources.resources.map(({ uri }) => uri)).toEqual([ + "test://resource", + ]); + expect(sample.content).toEqual({ type: "text", text: "sampled" }); + }); + + it("preserves reconnect behavior after close", async () => { + const first = await connectPair(); + await first.app.close(); + await first.server.close(); + + const second = await connectPair(first.app); + connected.push(second); + + expect(second.app.getNegotiatedProtocolVersion()).toBe( + INNER_MCP_PROTOCOL_VERSION, + ); + expect(second.app.getHostVersion()).toEqual({ + name: "apps-host", + version: "2.0.0", + }); + }); + + it("preserves auto-resize setup after Apps initialization", async () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: true }, + ); + let setupCalls = 0; + app.setupSizeChangedNotifications = () => { + setupCalls += 1; + return () => {}; + }; + + const pair = await connectPair(app); + connected.push(pair); + + expect(setupCalls).toBe(1); + }); + + it("preserves strict late-handler timing errors", async () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false, strict: true }, + ); + const pair = await connectPair(app); + connected.push(pair); + + expect(() => { + app.ontoolinput = () => {}; + }).toThrow(/handler registered after connect/); + }); + + it("uses base MCP SDK replacement semantics for request handlers", async () => { + const pair = await connectPair(); + connected.push(pair); + let handledBy = 0; + + pair.app.setRequestHandler("ping", () => { + handledBy = 1; + return {}; + }); + pair.app.setRequestHandler("ping", () => { + handledBy = 2; + return {}; + }); + + await pair.server.request({ method: "ping", params: {} }); + expect(handledBy).toBe(2); + }); + + it("uses base MCP SDK replacement semantics for non-event notifications", async () => { + const pair = await connectPair(); + connected.push(pair); + const calls: number[] = []; + const params = z.object({}); + + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(1); + }); + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(2); + }); + await pair.server.notification({ + method: "test/notification", + params: {}, + }); + + expect(calls).toEqual([2]); + }); +}); diff --git a/src/app.ts b/src/app.ts index c586c31..3c8f5b7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,38 +1,27 @@ import { + Client, + type ClientContext, + type ClientOptions, type RequestOptions, - mergeCapabilities, - Protocol, - ProtocolOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; - -import { - CallToolRequest, - CallToolRequestSchema, - CallToolResult, - CallToolResultSchema, - CreateMessageRequest, - CreateMessageResult, - CreateMessageResultSchema, - CreateMessageResultWithTools, - CreateMessageResultWithToolsSchema, - EmptyResultSchema, - Implementation, - ListResourcesRequest, - ListResourcesResult, - ListResourcesResultSchema, - ListToolsRequest, - ListToolsRequestSchema, - ListToolsResult, - LoggingMessageNotification, - PingRequestSchema, - ReadResourceRequest, - ReadResourceResult, - ReadResourceResultSchema, - Tool, - ToolAnnotations, - ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; -import { AppNotification, AppRequest, AppResult } from "./types"; + type CallToolRequest, + type CallToolResult, + type CreateMessageRequest, + type CreateMessageResult, + type CreateMessageResultWithTools, + type Implementation, + type ListResourcesRequest, + type ListResourcesResult, + type ListToolsRequest, + type ListToolsResult, + type LoggingMessageNotification, + type ReadResourceRequest, + type ReadResourceResult, + type Tool, + type ToolAnnotations, + type ToolListChangedNotification, + type Transport, +} from "@modelcontextprotocol/client"; +import { EmptyResultSchema } from "@modelcontextprotocol/core"; import { EventDispatcher } from "./events"; export { EventDispatcher } from "./events"; import { PostMessageTransport } from "./message-transport"; @@ -56,6 +45,7 @@ import { McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, + McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, @@ -69,15 +59,36 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeResultSchema, } from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { StandardSchemaV1, standardSchemaToJsonSchema, validateStandardSchema, } from "./standard-schema"; -import { z, type ZodLiteral, type ZodObject } from "zod/v4"; +import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; -type MethodSchema = ZodObject<{ method: ZodLiteral }>; +type MethodSchema = ZodObject<{ + method: ZodLiteral; + params: ZodType; +}>; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; + +function mergeAppCapabilities( + base: McpUiAppCapabilities, + additional: McpUiAppCapabilities, +): McpUiAppCapabilities { + const result = { ...base, ...additional }; + if (additional.experimental !== undefined) { + result.experimental = { + ...base.experimental, + ...additional.experimental, + }; + } + if (additional.tools !== undefined) { + result.tools = { ...base.tools, ...additional.tools }; + } + return result; +} export type { StandardSchemaV1, @@ -167,7 +178,10 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; * * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options */ -export type AppOptions = ProtocolOptions & { +export type AppOptions = Omit< + ClientOptions, + "capabilities" | "supportedProtocolVersions" | "versionNegotiation" +> & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -207,9 +221,7 @@ export type AppOptions = ProtocolOptions & { allowUnsafeEval?: boolean; }; -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +type RequestHandlerExtra = ClientContext; /** * Result of an app-registered tool callback. When `Out` is provided, @@ -343,38 +355,15 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends Protocol { +export class App extends Client { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; private _registeredTools: { [name: string]: RegisteredAppTool } = {}; private _initializedSent = false; - private readonly _registeredMethods = new Set(); private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); - // ProtocolWithEvents previously owned these registration guards. Keep them - // on App to preserve the existing double-registration behavior while event - // fan-out moves to protocol-independent composition. Arrow-function fields - // ensure Protocol's constructor uses its own methods before the guards init. - override setRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; - - override setNotificationHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setNotificationHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setNotificationHandler"); - super.setNotificationHandler(schema, handler); - }; - /** * Warn if a host-bound method is called before {@link connect `connect`} has * completed the `ui/initialize` → `ui/notifications/initialized` handshake. @@ -479,13 +468,17 @@ export class App extends Protocol { if (this._registeredEvents.has(event)) return; const schema = this.eventSchemas[event]; if (!schema) throw new Error(`Unknown event: ${String(event)}`); - this.setNotificationHandler(schema, (notification) => { - const params = (notification as { params: AppEventMap[K] }).params; - if (event === "hostcontextchanged") { - this._hostContext = { ...this._hostContext, ...params }; - } - this._events.dispatch(event, params); - }); + this.setNotificationHandler( + schema.shape.method.value, + { params: schema.shape.params }, + (params) => { + const eventParams = params as AppEventMap[K]; + if (event === "hostcontextchanged") { + this._hostContext = { ...this._hostContext, ...eventParams }; + } + this._events.dispatch(event, eventParams); + }, + ); this._registeredEvents.add(event); } @@ -508,33 +501,11 @@ export class App extends Protocol { } } - protected replaceRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - const method = (schema as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(schema, handler); - }; - - private _assertMethodNotRegistered(schema: unknown, via: string): void { - const method = (schema as MethodSchema).shape.method.value; - if (this._registeredMethods.has(method)) { - throw new Error( - `Handler for "${method}" already registered (via ${via}). ` + - `Use addEventListener() to attach multiple listeners, ` + - `or the on* setter for replace semantics.`, - ); - } - this._registeredMethods.add(method); - } - /** * Create a new MCP App instance. * * @param _appInfo - App identification (name and version) - * @param _capabilities - Features and capabilities this app provides + * @param _appCapabilities - Features and capabilities this app provides * @param options - Configuration options including `autoResize` behavior * * @example @@ -548,16 +519,29 @@ export class App extends Protocol { */ constructor( private _appInfo: Implementation, - private _capabilities: McpUiAppCapabilities = {}, + private _appCapabilities: McpUiAppCapabilities = {}, private options: AppOptions = { autoResize: true }, ) { - super(options); + // TEMPORARY INNER-CHANNEL NEGOTIATION: + // App and AppBridge use the public v2 Client/Server roles, so the iframe + // channel first completes the SDK's standard legacy MCP initialization. + // ui/initialize remains authoritative for Apps capabilities, identity, and + // host context, and ui/notifications/initialized remains the View-ready gate. + // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only + // after the MCP Apps negotiation specification is resolved: + // https://github.com/modelcontextprotocol/ext-apps/issues/708 + super(_appInfo, { + ...options, + capabilities: {}, + versionNegotiation: { mode: "legacy" }, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }); if (!options.allowUnsafeEval) { z.config({ jitless: true }); } - this.setRequestHandler(PingRequestSchema, (request) => { + this.setRequestHandler("ping", (request) => { console.log("Received ping:", request.params); return {}; }); @@ -567,13 +551,16 @@ export class App extends Protocol { this.setEventHandler("hostcontextchanged", undefined); } - private registerCapabilities(capabilities: McpUiAppCapabilities): void { + private registerAppCapabilities(capabilities: McpUiAppCapabilities): void { if (this.transport) { throw new Error( "Cannot register capabilities after transport is established", ); } - this._capabilities = mergeCapabilities(this._capabilities, capabilities); + this._appCapabilities = mergeAppCapabilities( + this._appCapabilities, + capabilities, + ); } registerTool< @@ -596,7 +583,7 @@ export class App extends Protocol { } const app = this; const notify = () => { - if (app._initializedSent && app._capabilities.tools?.listChanged) { + if (app._initializedSent && app._appCapabilities.tools?.listChanged) { void app.sendToolListChanged(); } }; @@ -668,8 +655,8 @@ export class App extends Protocol { // to declare { tools: {} } in the constructor just to use registerTool. // Only do this pre-connect; post-connect the capability was already // advertised (or wasn't) and can't change. - if (!this._capabilities.tools && !this.transport) { - this.registerCapabilities({ tools: { listChanged: true } }); + if (!this._appCapabilities.tools && !this.transport) { + this.registerAppCapabilities({ tools: { listChanged: true } }); } this.ensureToolHandlersInitialized(); @@ -1081,11 +1068,15 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); this._onteardown = callback; - this.replaceRequestHandler( - McpUiResourceTeardownRequestSchema, - (request, extra) => { + this.setRequestHandler( + "ui/resource-teardown", + { + params: McpUiResourceTeardownRequestSchema.shape.params, + result: McpUiResourceTeardownResultSchema, + }, + (params, extra) => { if (!this._onteardown) throw new Error("No onteardown handler set"); - return this._onteardown(request.params, extra); + return this._onteardown(params, extra); }, ); } @@ -1135,7 +1126,7 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => { + this.setRequestHandler("tools/call", (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1206,7 +1197,7 @@ export class App extends Protocol { callback, ); this._onlisttools = callback; - this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => { + this.setRequestHandler("tools/list", (request, extra) => { if (!this._onlisttools) throw new Error("No onlisttools handler set"); return this._onlisttools(request.params, extra); }); @@ -1216,7 +1207,7 @@ export class App extends Protocol { * Verify that the host supports the capability required for the given request method. * @internal */ - assertCapabilityForMethod(method: AppRequest["method"]): void { + protected override assertCapabilityForMethod(method: string): void { switch (method) { case "sampling/createMessage": if (!this._hostCapabilities?.sampling) { @@ -1232,11 +1223,11 @@ export class App extends Protocol { * Verify that the app declared the capability required for the given request method. * @internal */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { + protected override assertRequestHandlerCapability(method: string): void { switch (method) { case "tools/call": case "tools/list": - if (!this._capabilities.tools) { + if (!this._appCapabilities.tools) { throw new Error( `Client does not support tool capability (required for ${method})`, ); @@ -1254,26 +1245,10 @@ export class App extends Protocol { * Verify that the app supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(_method: AppNotification["method"]): void { + protected override assertNotificationCapability(_method: string): void { // TODO } - /** - * Verify that task creation is supported for the given request method. - * @internal - */ - protected assertTaskCapability(_method: string): void { - throw new Error("Tasks are not supported in MCP Apps"); - } - - /** - * Verify that task handler is supported for the given method. - * @internal - */ - protected assertTaskHandlerCapability(_method: string): void { - throw new Error("Task handlers are not supported in MCP Apps"); - } - /** * Call a tool on the originating MCP server (proxied through the host). * @@ -1322,7 +1297,6 @@ export class App extends Protocol { } return await this.request( { method: "tools/call", params }, - CallToolResultSchema, { // Hosts may interpose long-running or user-interactive steps before the // tool result arrives. Opting in here lets a host heartbeat keep the @@ -1379,11 +1353,7 @@ export class App extends Protocol { options?: RequestOptions, ): Promise { this._assertInitialized("readServerResource"); - return await this.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - options, - ); + return await this.request({ method: "resources/read", params }, options); } /** @@ -1427,11 +1397,7 @@ export class App extends Protocol { options?: RequestOptions, ): Promise { this._assertInitialized("listServerResources"); - return await this.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - options, - ); + return await this.request({ method: "resources/list", params }, options); } /** @@ -1508,12 +1474,8 @@ export class App extends Protocol { options?: RequestOptions, ): Promise { this._assertInitialized("createSamplingMessage"); - const resultSchema = params.tools - ? CreateMessageResultWithToolsSchema - : CreateMessageResultSchema; return await this.request( { method: "sampling/createMessage", params }, - resultSchema, options, ); } @@ -2030,14 +1992,23 @@ export class App extends Protocol { ); } this._initializedSent = false; - await super.connect(transport); + // TEMPORARY INNER-CHANNEL NEGOTIATION: + // App and AppBridge use the public v2 Client/Server roles, so the iframe + // channel first completes the SDK's standard legacy MCP initialization. + // ui/initialize remains authoritative for Apps capabilities, identity, and + // host context, and ui/notifications/initialized remains the View-ready gate. + // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only + // after the MCP Apps negotiation specification is resolved: + // https://github.com/modelcontextprotocol/ext-apps/issues/708 try { + await super.connect(transport); + const result = await this.request( { method: "ui/initialize", params: { - appCapabilities: this._capabilities, + appCapabilities: this._appCapabilities, appInfo: this._appInfo, protocolVersion: LATEST_PROTOCOL_VERSION, }, From d19a1fb83ade586d6e2eb46535e7d84a7f7468a8 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 15:34:33 -0700 Subject: [PATCH 61/97] refactor: migrate AppBridge to SDK v2 server --- src/app-bridge.test.ts | 336 ++++++++++++++++++++++---- src/app-bridge.ts | 527 ++++++++++++++++++++--------------------- 2 files changed, 546 insertions(+), 317 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 8f432b4..81e072f 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1,14 +1,6 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; -import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js"; -import { - EmptyResultSchema, - ListPromptsResultSchema, - ListResourcesResultSchema, - ListResourceTemplatesResultSchema, - ReadResourceResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Server, type ServerCapabilities } from "@modelcontextprotocol/server"; import { z } from "zod/v4"; import { App } from "./app"; @@ -19,6 +11,7 @@ import { getToolUiResourceUri, isToolVisibilityModelOnly, isToolVisibilityAppOnly, + McpUiOpenLinkResultSchema, type McpUiHostCapabilities, } from "./app-bridge"; @@ -31,11 +24,18 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); */ function createMockClient( serverCapabilities: ServerCapabilities = {}, -): Pick { +): Pick< + Client, + | "getServerCapabilities" + | "request" + | "notification" + | "setNotificationHandler" +> { return { getServerCapabilities: () => serverCapabilities, request: async () => ({}) as never, notification: async () => {}, + setNotificationHandler: () => {}, }; } @@ -82,6 +82,58 @@ describe("App <-> AppBridge integration", () => { expect(initializedFired).toBe(true); }); + it("keeps standard initialization separate from the Apps-ready gate", async () => { + const methods: string[] = []; + let releaseAppsInitialized!: () => void; + let reachedAppsInitialized!: () => void; + const appsInitializedGate = new Promise((resolve) => { + releaseAppsInitialized = resolve; + }); + const reachedGate = new Promise((resolve) => { + reachedAppsInitialized = resolve; + }); + const originalSend = appTransport.send.bind(appTransport); + appTransport.send = async (message, options) => { + if ("method" in message) methods.push(message.method); + if ( + "method" in message && + message.method === "ui/notifications/initialized" + ) { + reachedAppsInitialized(); + await appsInitializedGate; + } + return originalSend(message, options); + }; + + let singularCalls = 0; + let listenerCalls = 0; + bridge.oninitialized = () => singularCalls++; + bridge.addEventListener("initialized", () => listenerCalls++); + + await bridge.connect(bridgeTransport); + const connecting = app.connect(appTransport); + await reachedGate; + + expect(methods).toEqual([ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ]); + expect(app.getNegotiatedProtocolVersion()).toBe("2025-11-25"); + expect(bridge.getNegotiatedProtocolVersion()).toBe("2025-11-25"); + expect(bridge.getClientVersion()).toEqual(testAppInfo); + expect(bridge.getAppVersion()).toEqual(testAppInfo); + expect(singularCalls).toBe(0); + expect(listenerCalls).toBe(0); + + releaseAppsInitialized(); + await connecting; + + expect(singularCalls).toBe(1); + expect(listenerCalls).toBe(1); + }); + it("App receives host info and capabilities after connect", async () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -91,6 +143,8 @@ describe("App <-> AppBridge integration", () => { const hostCaps = app.getHostCapabilities(); expect(hostCaps).toEqual(testHostCapabilities); + expect(bridge.getHostCapabilities()).toEqual(testHostCapabilities); + expect(bridge.getCapabilities()).toEqual({}); }); it("Bridge receives app info and capabilities after initialization", async () => { @@ -611,6 +665,30 @@ describe("App <-> AppBridge integration", () => { expect(result.isError).toBe(true); }); + + it("rejects invalid params for a custom ui request", async () => { + bridge.onopenlink = async () => ({}); + await app.connect(appTransport); + + await expect( + app.request( + { + method: "ui/open-link", + params: { url: 42 }, + }, + McpUiOpenLinkResultSchema, + ), + ).rejects.toThrow(); + }); + + it("rejects invalid results from a custom ui handler", async () => { + bridge.onopenlink = async () => ({ isError: "yes" }) as never; + await app.connect(appTransport); + + await expect( + app.openLink({ url: "https://example.com" }), + ).rejects.toThrow(); + }); }); describe("deprecated method aliases", () => { @@ -680,10 +758,7 @@ describe("App <-> AppBridge integration", () => { await app.connect(appTransport); // Bridge can send ping via the protocol's request method - const result = await bridge.request( - { method: "ping", params: {} }, - EmptyResultSchema, - ); + const result = await bridge.request({ method: "ping", params: {} }); expect(result).toEqual({}); }); @@ -798,7 +873,7 @@ describe("App <-> AppBridge integration", () => { }); await bridge.callTool({ name: "noargs", arguments: {} }); expect(receivedExtra).toBeDefined(); - expect(receivedExtra.signal).toBeInstanceOf(AbortSignal); + expect(receivedExtra.mcpReq.signal).toBeInstanceOf(AbortSignal); }); it("isError result skips output schema validation", async () => { @@ -2180,10 +2255,10 @@ describe("App <-> AppBridge integration", () => { await app.connect(appTransport); // App sends resources/list request via the protocol's request method - const result = await app.request( - { method: "resources/list", params: requestParams }, - ListResourcesResultSchema, - ); + const result = await app.request({ + method: "resources/list", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2224,10 +2299,10 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - const result = await app.request( - { method: "resources/read", params: requestParams }, - ReadResourceResultSchema, - ); + const result = await app.request({ + method: "resources/read", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2275,10 +2350,10 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - const result = await app.request( - { method: "resources/templates/list", params: requestParams }, - ListResourceTemplatesResultSchema, - ); + const result = await app.request({ + method: "resources/templates/list", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2298,10 +2373,10 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - const result = await app.request( - { method: "prompts/list", params: requestParams }, - ListPromptsResultSchema, - ); + const result = await app.request({ + method: "prompts/list", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2310,6 +2385,7 @@ describe("App <-> AppBridge integration", () => { it("sendToolListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; + bridge.oncalltool = async () => ({ content: [] }); app.setNotificationHandler( "notifications/tools/list_changed", (notification) => { @@ -2328,6 +2404,7 @@ describe("App <-> AppBridge integration", () => { it("sendResourceListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; + bridge.onlistresources = async () => ({ resources: [] }); app.setNotificationHandler( "notifications/resources/list_changed", (notification) => { @@ -2346,6 +2423,7 @@ describe("App <-> AppBridge integration", () => { it("sendPromptListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; + bridge.onlistprompts = async () => ({ prompts: [] }); app.setNotificationHandler( "notifications/prompts/list_changed", (notification) => { @@ -2364,6 +2442,184 @@ describe("App <-> AppBridge integration", () => { }); }); +describe("AppBridge outer v2 Client proxy", () => { + let outerClient: Client; + let outerServer: Server; + let outerClientTransport: InMemoryTransport; + let outerServerTransport: InMemoryTransport; + let outerInitializeCount: number; + + beforeEach(async () => { + [outerClientTransport, outerServerTransport] = + InMemoryTransport.createLinkedPair(); + outerInitializeCount = 0; + const originalSend = outerClientTransport.send.bind(outerClientTransport); + outerClientTransport.send = async (message, options) => { + if ("method" in message && message.method === "initialize") { + outerInitializeCount++; + } + return originalSend(message, options); + }; + + outerServer = new Server( + { name: "ActualServer", version: "1.0.0" }, + { + capabilities: { + tools: { listChanged: true }, + resources: { listChanged: true }, + prompts: { listChanged: true }, + }, + supportedProtocolVersions: ["2025-11-25"], + }, + ); + outerServer.setRequestHandler("tools/call", async (request) => ({ + content: [ + { + type: "text", + text: `outer:${request.params.name}`, + }, + ], + })); + + outerClient = new Client( + { name: "HostOuterClient", version: "1.0.0" }, + { + capabilities: {}, + versionNegotiation: { mode: "legacy" }, + supportedProtocolVersions: ["2025-11-25"], + }, + ); + await outerServer.connect(outerServerTransport); + await outerClient.connect(outerClientTransport); + }); + + afterEach(async () => { + await outerClient.close().catch(() => {}); + await outerServer.close().catch(() => {}); + }); + + it("proxies through the outer Client without reconnecting it", async () => { + const bridge = new AppBridge( + outerClient, + testHostInfo, + testHostCapabilities, + ); + const app = new App(testAppInfo, {}, { autoResize: false }); + let [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + expect( + await app.callServerTool({ name: "first", arguments: {} }), + ).toMatchObject({ + content: [{ type: "text", text: "outer:first" }], + }); + expect(outerInitializeCount).toBe(1); + + await app.close(); + await bridge.close(); + [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + expect( + await app.callServerTool({ name: "second", arguments: {} }), + ).toMatchObject({ + content: [{ type: "text", text: "outer:second" }], + }); + expect(outerInitializeCount).toBe(1); + expect(outerClient.getServerVersion()).toMatchObject({ + name: "ActualServer", + version: "1.0.0", + }); + + await app.close(); + await bridge.close(); + }); + + it("propagates View cancellation to the actual outer Server", async () => { + let started!: () => void; + let aborted!: () => void; + const startedPromise = new Promise((resolve) => { + started = resolve; + }); + const abortedPromise = new Promise((resolve) => { + aborted = resolve; + }); + outerServer.setRequestHandler("tools/call", async (_request, context) => { + started(); + await new Promise((resolve) => { + context.mcpReq.signal.addEventListener( + "abort", + () => { + aborted(); + resolve(); + }, + { once: true }, + ); + }); + return { content: [] }; + }); + + const bridge = new AppBridge( + outerClient, + testHostInfo, + testHostCapabilities, + ); + const app = new App(testAppInfo, {}, { autoResize: false }); + const [appTransport, bridgeTransport] = + InMemoryTransport.createLinkedPair(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + const controller = new AbortController(); + const result = app.callServerTool( + { name: "cancel-me", arguments: {} }, + { signal: controller.signal }, + ); + await startedPromise; + controller.abort("test cancellation"); + await abortedPromise; + await expect(result).rejects.toThrow(); + + await app.close(); + await bridge.close(); + }); + + it("forwards outer list-changed notifications to the App", async () => { + const bridge = new AppBridge( + outerClient, + testHostInfo, + testHostCapabilities, + ); + const app = new App(testAppInfo, {}, { autoResize: false }); + const [appTransport, bridgeTransport] = + InMemoryTransport.createLinkedPair(); + const received: string[] = []; + app.setNotificationHandler("notifications/tools/list_changed", () => { + received.push("tools"); + }); + app.setNotificationHandler("notifications/resources/list_changed", () => { + received.push("resources"); + }); + app.setNotificationHandler("notifications/prompts/list_changed", () => { + received.push("prompts"); + }); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + await outerServer.sendToolListChanged(); + await outerServer.sendResourceListChanged(); + await outerServer.sendPromptListChanged(); + await flush(); + + expect(received).toEqual(["tools", "resources", "prompts"]); + + await app.close(); + await bridge.close(); + }); +}); + describe("getToolUiResourceUri", () => { describe("new nested format (_meta.ui.resourceUri)", () => { it("extracts resourceUri from _meta.ui.resourceUri", () => { @@ -2758,24 +3014,16 @@ describe("isToolVisibilityAppOnly", () => { expect(app.onteardown).toBe(handler); }); - it("direct setRequestHandler throws when called twice", () => { + it("direct setRequestHandler uses base SDK replacement semantics", () => { const bridge2 = new AppBridge( createMockClient() as Client, testHostInfo, testHostCapabilities, ); - bridge2.setRequestHandler( - // @ts-expect-error — exercising throw path with raw schema - { shape: { method: { value: "test/method" } } }, - () => ({}), - ); + bridge2.setRequestHandler("ping", () => ({})); expect(() => { - bridge2.setRequestHandler( - // @ts-expect-error — exercising throw path with raw schema - { shape: { method: { value: "test/method" } } }, - () => ({}), - ); - }).toThrow(/already registered/); + bridge2.setRequestHandler("ping", () => ({})); + }).not.toThrow(); }); }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 18d7671..c65510d 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,61 +1,51 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { type Client } from "@modelcontextprotocol/client"; import { - CallToolRequest, - CallToolRequestSchema, - CallToolResult, - CallToolResultSchema, - CreateMessageRequest, - CreateMessageRequestSchema, - CreateMessageResult, - CreateMessageResultWithTools, - EmptyResult, - Implementation, - ListPromptsRequest, - ListPromptsRequestSchema, - ListPromptsResult, - ListPromptsResultSchema, - ListResourcesRequest, - ListResourcesRequestSchema, - ListResourcesResult, - ListResourcesResultSchema, - ListResourceTemplatesRequest, - ListResourceTemplatesRequestSchema, - ListResourceTemplatesResult, - ListResourceTemplatesResultSchema, - ListToolsRequest, - ListToolsRequestSchema, - ListToolsResultSchema, - LoggingMessageNotification, + Server, + type CallToolRequest, + type CallToolResult, + type CreateMessageRequest, + type CreateMessageResult, + type CreateMessageResultWithTools, + type EmptyResult, + type Implementation, + type JSONRPCRequest, + type ListPromptsRequest, + type ListPromptsResult, + type ListResourcesRequest, + type ListResourcesResult, + type ListResourceTemplatesRequest, + type ListResourceTemplatesResult, + type ListToolsRequest, + type LoggingMessageNotification, + type PingRequest, + type PromptListChangedNotification, + type ReadResourceRequest, + type ReadResourceResult, + type RequestOptions, + type ResourceListChangedNotification, + type Result, + type ServerCapabilities, + type ServerContext, + type ServerOptions, + type Tool, + type ToolListChangedNotification, + type Transport, +} from "@modelcontextprotocol/server"; +import { + EmptyResultSchema, LoggingMessageNotificationSchema, - PingRequest, - PingRequestSchema, - PromptListChangedNotification, - PromptListChangedNotificationSchema, - ReadResourceRequest, - ReadResourceRequestSchema, - ReadResourceResult, - ReadResourceResultSchema, - ResourceListChangedNotification, - ResourceListChangedNotificationSchema, - Tool, - ToolListChangedNotification, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { - Protocol, - ProtocolOptions, - RequestOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; +} from "@modelcontextprotocol/core"; import { EventDispatcher } from "./events"; -import type { ZodLiteral, ZodObject } from "zod/v4"; +import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; -type MethodSchema = ZodObject<{ method: ZodLiteral }>; +type MethodSchema = ZodObject<{ + method: ZodLiteral; + params: ZodType; +}>; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; import { - type AppNotification, - type AppRequest, - type AppResult, type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, type McpUiToolCancelledNotification, @@ -74,15 +64,19 @@ import { McpUiInitializeRequest, McpUiInitializeRequestSchema, McpUiInitializeResult, + McpUiInitializeResultSchema, McpUiMessageRequest, McpUiMessageRequestSchema, McpUiMessageResult, + McpUiMessageResultSchema, McpUiOpenLinkRequest, McpUiOpenLinkRequestSchema, McpUiOpenLinkResult, + McpUiOpenLinkResultSchema, McpUiDownloadFileRequest, McpUiDownloadFileRequestSchema, McpUiDownloadFileResult, + McpUiDownloadFileResultSchema, McpUiResourceTeardownRequest, McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, @@ -93,6 +87,7 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResult, + McpUiRequestDisplayModeResultSchema, McpUiResourcePermissions, McpUiToolMeta, } from "./types"; @@ -101,6 +96,20 @@ export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; import { RESOURCE_URI_META_KEY } from "./app"; export { PostMessageTransport } from "./message-transport"; +function proxiedInnerCapabilities( + capabilities: ServerCapabilities, +): ServerCapabilities { + return { + ...(capabilities.tools && { tools: capabilities.tools }), + ...(capabilities.resources && { + resources: { + listChanged: capabilities.resources.listChanged, + }, + }), + ...(capabilities.prompts && { prompts: capabilities.prompts }), + }; +} + /** * Extract UI resource URI from tool metadata. * @@ -206,10 +215,13 @@ export function buildAllowAttribute( * * @property hostContext - Optional initial host context to provide to the view * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options + * @see `ServerOptions` from @modelcontextprotocol/server for available options * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure */ -export type HostOptions = ProtocolOptions & { +export type HostOptions = Omit< + ServerOptions, + "capabilities" | "supportedProtocolVersions" +> & { hostContext?: McpUiHostContext; }; @@ -224,15 +236,12 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; /** * Extra metadata passed to request handlers. * - * This type represents the additional context provided by the `Protocol` class - * when handling requests, including abort signals and session information. - * It is extracted from the MCP SDK's request handler signature. + * This type represents the additional context provided by the base MCP SDK + * `Server` when handling requests. * * @internal */ -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +type RequestHandlerExtra = ServerContext; /** * Maps DOM-style event names to their notification `params` types. @@ -306,66 +315,53 @@ export type AppBridgeEventMap = { * await bridge.connect(transport); * ``` */ -export class AppBridge extends Protocol { +export class AppBridge extends Server { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; private _initializedReceived = false; - private readonly _registeredMethods = new Set(); private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); - // ProtocolWithEvents previously owned these registration guards. Keep them - // on AppBridge to preserve the existing double-registration behavior while - // event fan-out moves to protocol-independent composition. Arrow-function - // fields let Protocol's constructor run before the guards initialize. - override setRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; - - override setNotificationHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setNotificationHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setNotificationHandler"); - super.setNotificationHandler(schema, handler); - }; - /** - * Wrap every handler registered via `replaceRequestHandler` with a check - * that the View has sent `ui/notifications/initialized`. Warns (never - * throws) so lenient hosts keep working while still surfacing the - * misordering that leaves strict hosts with a permanently hidden iframe. - * `ui/initialize` and `ping` use `setRequestHandler` directly and are - * intentionally exempt. + * The base MCP SDK calls this hook for every standard and custom request + * handler registration. AppBridge uses it to preserve the existing warning + * when a View calls host methods before `ui/notifications/initialized`, + * without adding overload-forwarding adapters around `setRequestHandler`. + * Chaining through `super._wrapHandler()` retains the base `Server`'s own + * role-specific behavior, including tool-result validation. The two + * initialization methods and `ping` are exempt because they are valid before + * the Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch. * * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} */ - protected replaceRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - const method = (schema as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(schema, (request, extra) => { + protected override _wrapHandler( + method: string, + handler: ( + request: JSONRPCRequest, + context: ServerContext, + ) => Promise, + ): (request: JSONRPCRequest, context: ServerContext) => Promise { + const wrapped = super._wrapHandler(method, handler); + if ( + method === "initialize" || + method === "ui/initialize" || + method === "ping" + ) { + return wrapped; + } + return async (request, context) => { if (!this._initializedReceived) { console.warn( - `[ext-apps] AppBridge received '${request.method}' before ` + + `[ext-apps] AppBridge received '${method}' before ` + `ui/notifications/initialized. The View is calling host ` + `methods before completing the handshake; it should await ` + `app.connect() first.`, ); } - return handler(request, extra); - }); - }; + return wrapped(request, context); + }; + } private readonly eventSchemas: { [K in keyof AppBridgeEventMap]: MethodSchema; @@ -406,17 +402,27 @@ export class AppBridge extends Protocol this._events.removeEventListener(event, handler); } - private _ensureEventSlot( - event: K, - ): void { + private _ensureEventSlot(event: K): void { if (this._registeredEvents.has(event)) return; const schema = this.eventSchemas[event]; if (!schema) throw new Error(`Unknown event: ${String(event)}`); - this.setNotificationHandler(schema, (notification) => { - const params = (notification as { params: AppBridgeEventMap[K] }).params; + + const dispatch = (params: AppBridgeEventMap[K]) => { if (event === "initialized") this._initializedReceived = true; this._events.dispatch(event, params); - }); + }; + + if (event === "loggingmessage") { + this.setNotificationHandler("notifications/message", (notification) => { + dispatch(notification.params as AppBridgeEventMap[K]); + }); + } else { + this.setNotificationHandler( + schema.shape.method.value, + { params: schema.shape.params }, + (params) => dispatch(params as AppBridgeEventMap[K]), + ); + } this._registeredEvents.add(event); } @@ -433,16 +439,12 @@ export class AppBridge extends Protocol } } - private _assertMethodNotRegistered(schema: unknown, via: string): void { - const method = (schema as MethodSchema).shape.method.value; - if (this._registeredMethods.has(method)) { - throw new Error( - `Handler for "${method}" already registered (via ${via}). ` + - `Use addEventListener() to attach multiple listeners, ` + - `or the on* setter for replace semantics.`, - ); - } - this._registeredMethods.add(method); + private ensureInnerCapabilities(capabilities: ServerCapabilities): void { + const current = super.getCapabilities(); + const missing = Object.keys(capabilities).some( + (key) => current[key as keyof ServerCapabilities] === undefined, + ); + if (missing) this.registerCapabilities(capabilities); } /** @@ -453,8 +455,8 @@ export class AppBridge extends Protocol * between the View and the server. When `null`, you must register handlers * manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters. * @param _hostInfo - Host application identification (name and version) - * @param _capabilities - Features and capabilities the host supports - * @param options - Configuration options (inherited from Protocol) + * @param _hostCapabilities - Features and capabilities the host supports + * @param options - Configuration options (inherited from Server) * * @example With MCP client (automatic forwarding) * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient" @@ -481,29 +483,61 @@ export class AppBridge extends Protocol constructor( private _client: Client | null, private _hostInfo: Implementation, - private _capabilities: McpUiHostCapabilities, + private _hostCapabilities: McpUiHostCapabilities, options?: HostOptions, ) { - super(options); + // TEMPORARY INNER-CHANNEL NEGOTIATION: + // App and AppBridge use the public v2 Client/Server roles, so the iframe + // channel first completes the SDK's standard legacy MCP initialization. + // ui/initialize remains authoritative for Apps capabilities, identity, and + // host context, and ui/notifications/initialized remains the View-ready gate. + // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only + // after the MCP Apps negotiation specification is resolved: + // https://github.com/modelcontextprotocol/ext-apps/issues/708 + super(_hostInfo, { + ...options, + capabilities: {}, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }); + + // The base Server's standard initialized callback is MCP-ready state. The + // public AppBridge callback remains exclusively the Apps View-ready gate. + super.setNotificationHandler("notifications/initialized", () => {}); + Object.defineProperty(this, "oninitialized", { + configurable: true, + enumerable: true, + get: () => this.getEventHandler("initialized"), + set: (callback: AppBridge["oninitialized"]) => + this.setEventHandler("initialized", callback), + }); this._ensureEventSlot("initialized"); this._hostContext = options?.hostContext || {}; - this.setRequestHandler(McpUiInitializeRequestSchema, (request) => - this._oninitialize(request), + this.setRequestHandler( + "ui/initialize", + { + params: McpUiInitializeRequestSchema.shape.params, + result: McpUiInitializeResultSchema, + }, + (params) => this._onAppsInitialize(params), ); - this.setRequestHandler(PingRequestSchema, (request, extra) => { + this.setRequestHandler("ping", (request, extra) => { this.onping?.(request.params, extra); return {}; }); // Default handler for requestDisplayMode - returns current mode from host context. // Hosts can override this by setting bridge.onrequestdisplaymode = ... - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - (request) => { + this.setRequestHandler( + "ui/request-display-mode", + { + params: McpUiRequestDisplayModeRequestSchema.shape.params, + result: McpUiRequestDisplayModeResultSchema, + }, + () => { const currentMode = this._hostContext.displayMode ?? "inline"; return { mode: currentMode }; }, @@ -680,18 +714,9 @@ export class AppBridge extends Protocol * @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ - get oninitialized(): - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined { - return this.getEventHandler("initialized"); - } - set oninitialized( - callback: - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("initialized", callback); - } + override oninitialized?: ( + params?: McpUiInitializedNotification["params"], + ) => void; /** * Register a handler for message requests from the view. @@ -744,11 +769,15 @@ export class AppBridge extends Protocol ) { this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); this._onmessage = callback; - this.replaceRequestHandler( - McpUiMessageRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/message", + { + params: McpUiMessageRequestSchema.shape.params, + result: McpUiMessageResultSchema, + }, + async (params, extra) => { if (!this._onmessage) throw new Error("No onmessage handler set"); - return this._onmessage(request.params, extra); + return this._onmessage(params, extra); }, ); } @@ -813,11 +842,15 @@ export class AppBridge extends Protocol ) { this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); this._onopenlink = callback; - this.replaceRequestHandler( - McpUiOpenLinkRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/open-link", + { + params: McpUiOpenLinkRequestSchema.shape.params, + result: McpUiOpenLinkResultSchema, + }, + async (params, extra) => { if (!this._onopenlink) throw new Error("No onopenlink handler set"); - return this._onopenlink(request.params, extra); + return this._onopenlink(params, extra); }, ); } @@ -885,12 +918,16 @@ export class AppBridge extends Protocol callback, ); this._ondownloadfile = callback; - this.replaceRequestHandler( - McpUiDownloadFileRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/download-file", + { + params: McpUiDownloadFileRequestSchema.shape.params, + result: McpUiDownloadFileResultSchema, + }, + async (params, extra) => { if (!this._ondownloadfile) throw new Error("No ondownloadfile handler set"); - return this._ondownloadfile(request.params, extra); + return this._ondownloadfile(params, extra); }, ); } @@ -985,12 +1022,16 @@ export class AppBridge extends Protocol callback, ); this._onrequestdisplaymode = callback; - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/request-display-mode", + { + params: McpUiRequestDisplayModeRequestSchema.shape.params, + result: McpUiRequestDisplayModeResultSchema, + }, + async (params, extra) => { if (!this._onrequestdisplaymode) throw new Error("No onrequestdisplaymode handler set"); - return this._onrequestdisplaymode(request.params, extra); + return this._onrequestdisplaymode(params, extra); }, ); } @@ -1082,12 +1123,16 @@ export class AppBridge extends Protocol callback, ); this._onupdatemodelcontext = callback; - this.replaceRequestHandler( - McpUiUpdateModelContextRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/update-model-context", + { + params: McpUiUpdateModelContextRequestSchema.shape.params, + result: EmptyResultSchema, + }, + async (params, extra) => { if (!this._onupdatemodelcontext) throw new Error("No onupdatemodelcontext handler set"); - return this._onupdatemodelcontext(request.params, extra); + return this._onupdatemodelcontext(params, extra); }, ); } @@ -1135,13 +1180,11 @@ export class AppBridge extends Protocol ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.replaceRequestHandler( - CallToolRequestSchema, - async (request, extra) => { - if (!this._oncalltool) throw new Error("No oncalltool handler set"); - return this._oncalltool(request.params, extra); - }, - ); + this.ensureInnerCapabilities({ tools: {} }); + this.setRequestHandler("tools/call", async (request, extra) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(request.params, extra); + }); } /** @@ -1179,12 +1222,9 @@ export class AppBridge extends Protocol extra: RequestHandlerExtra, ) => Promise, ) { - this.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - return callback(request.params, extra); - }, - ); + this.setRequestHandler("sampling/createMessage", async (request, extra) => { + return callback(request.params, extra); + }); } /** @@ -1260,14 +1300,12 @@ export class AppBridge extends Protocol callback, ); this._onlistresources = callback; - this.replaceRequestHandler( - ListResourcesRequestSchema, - async (request, extra) => { - if (!this._onlistresources) - throw new Error("No onlistresources handler set"); - return this._onlistresources(request.params, extra); - }, - ); + this.ensureInnerCapabilities({ resources: {} }); + this.setRequestHandler("resources/list", async (request, extra) => { + if (!this._onlistresources) + throw new Error("No onlistresources handler set"); + return this._onlistresources(request.params, extra); + }); } /** @@ -1317,8 +1355,9 @@ export class AppBridge extends Protocol callback, ); this._onlistresourcetemplates = callback; - this.replaceRequestHandler( - ListResourceTemplatesRequestSchema, + this.ensureInnerCapabilities({ resources: {} }); + this.setRequestHandler( + "resources/templates/list", async (request, extra) => { if (!this._onlistresourcetemplates) throw new Error("No onlistresourcetemplates handler set"); @@ -1374,14 +1413,12 @@ export class AppBridge extends Protocol callback, ); this._onreadresource = callback; - this.replaceRequestHandler( - ReadResourceRequestSchema, - async (request, extra) => { - if (!this._onreadresource) - throw new Error("No onreadresource handler set"); - return this._onreadresource(request.params, extra); - }, - ); + this.ensureInnerCapabilities({ resources: {} }); + this.setRequestHandler("resources/read", async (request, extra) => { + if (!this._onreadresource) + throw new Error("No onreadresource handler set"); + return this._onreadresource(request.params, extra); + }); } /** @@ -1459,14 +1496,11 @@ export class AppBridge extends Protocol callback, ); this._onlistprompts = callback; - this.replaceRequestHandler( - ListPromptsRequestSchema, - async (request, extra) => { - if (!this._onlistprompts) - throw new Error("No onlistprompts handler set"); - return this._onlistprompts(request.params, extra); - }, - ); + this.ensureInnerCapabilities({ prompts: {} }); + this.setRequestHandler("prompts/list", async (request, extra) => { + if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); + return this._onlistprompts(request.params, extra); + }); } /** @@ -1495,46 +1529,6 @@ export class AppBridge extends Protocol }); } - /** - * Verify that the guest supports the capability required for the given request method. - * @internal - */ - assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that a request handler is registered and supported for the given method. - * @internal - */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that the host supports the capability required for the given notification method. - * @internal - */ - assertNotificationCapability(method: AppNotification["method"]): void { - // TODO - } - - /** - * Verify that task creation is supported for the given request method. - * @internal - */ - protected assertTaskCapability(_method: string): void { - throw new Error("Tasks are not supported in MCP Apps"); - } - - /** - * Verify that task handler is supported for the given method. - * @internal - */ - protected assertTaskHandlerCapability(_method: string): void { - throw new Error("Task handlers are not supported in MCP Apps"); - } - /** * Get the host capabilities passed to the constructor. * @@ -1542,18 +1536,18 @@ export class AppBridge extends Protocol * * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure */ - getCapabilities(): McpUiHostCapabilities { - return this._capabilities; + getHostCapabilities(): McpUiHostCapabilities { + return this._hostCapabilities; } /** * Handle the ui/initialize request from the guest. * @internal */ - private async _oninitialize( - request: McpUiInitializeRequest, + private async _onAppsInitialize( + params: McpUiInitializeRequest["params"], ): Promise { - const requestedVersion = request.params.protocolVersion; + const requestedVersion = params.protocolVersion; if (this._appInfo !== undefined) { console.warn( @@ -1564,8 +1558,8 @@ export class AppBridge extends Protocol ); } - this._appCapabilities = request.params.appCapabilities; - this._appInfo = request.params.appInfo; + this._appCapabilities = params.appCapabilities; + this._appInfo = params.appInfo; const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes( requestedVersion, @@ -1575,7 +1569,7 @@ export class AppBridge extends Protocol return { protocolVersion, - hostCapabilities: this.getCapabilities(), + hostCapabilities: this.getHostCapabilities(), hostInfo: this._hostInfo, hostContext: this._hostContext, }; @@ -1857,11 +1851,7 @@ export class AppBridge extends Protocol * @returns Promise resolving to the tool call result */ callTool(params: CallToolRequest["params"], options?: RequestOptions) { - return this.request( - { method: "tools/call", params }, - CallToolResultSchema, - options, - ); + return this.request({ method: "tools/call", params }, options); } /** @@ -1874,11 +1864,7 @@ export class AppBridge extends Protocol * @returns Promise resolving to the list of tools */ listTools(params: ListToolsRequest["params"], options?: RequestOptions) { - return this.request( - { method: "tools/list", params }, - ListToolsResultSchema, - options, - ); + return this.request({ method: "tools/list", params }, options); } /** @@ -1949,18 +1935,18 @@ export class AppBridge extends Protocol if (!serverCapabilities) { throw new Error("Client server capabilities not available"); } + this.registerCapabilities(proxiedInnerCapabilities(serverCapabilities)); if (serverCapabilities.tools) { this.oncalltool = async (params, extra) => { return this._client!.request( { method: "tools/call", params }, - CallToolResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; if (serverCapabilities.tools.listChanged) { this._client.setNotificationHandler( - ToolListChangedNotificationSchema, + "notifications/tools/list_changed", (n) => this.sendToolListChanged(n.params), ); } @@ -1969,27 +1955,24 @@ export class AppBridge extends Protocol this.onlistresources = async (params, extra) => { return this._client!.request( { method: "resources/list", params }, - ListResourcesResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; this.onlistresourcetemplates = async (params, extra) => { return this._client!.request( { method: "resources/templates/list", params }, - ListResourceTemplatesResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; this.onreadresource = async (params, extra) => { return this._client!.request( { method: "resources/read", params }, - ReadResourceResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; if (serverCapabilities.resources.listChanged) { this._client.setNotificationHandler( - ResourceListChangedNotificationSchema, + "notifications/resources/list_changed", (n) => this.sendResourceListChanged(n.params), ); } @@ -1998,22 +1981,20 @@ export class AppBridge extends Protocol this.onlistprompts = async (params, extra) => { return this._client!.request( { method: "prompts/list", params }, - ListPromptsResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; if (serverCapabilities.prompts.listChanged) { this._client.setNotificationHandler( - PromptListChangedNotificationSchema, + "notifications/prompts/list_changed", (n) => this.sendPromptListChanged(n.params), ); } } } - // MCP-UI specific handlers are registered by the host component - // after the proxy is created. The standard MCP initialization - // (via oninitialized callback set in constructor) handles the ready signal. + // The standard MCP handshake only establishes the temporary inner Server + // role. ui/notifications/initialized remains the public View-ready signal. return super.connect(transport); } From a76e3a76913b9482a5d00342184004ccaab44885 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 15:46:56 -0700 Subject: [PATCH 62/97] refactor: migrate shared SDK surfaces to v2 --- scripts/generate-schemas.ts | 6 +- src/generated/schema.json | 182 +++++++++++++++++++++++------------- src/generated/schema.ts | 2 +- src/message-transport.ts | 8 +- src/react/useApp.tsx | 3 +- src/server/index.ts | 54 +++-------- src/spec.types.ts | 22 ++--- src/types.ts | 130 -------------------------- 8 files changed, 149 insertions(+), 258 deletions(-) diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 109f352..85225f5 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -12,7 +12,7 @@ * * ts-to-zod generates `import { z } from "zod"`. We rewrite this to * `import { z } from "zod/v4"` because the generated schemas compose with - * schemas imported from `@modelcontextprotocol/sdk/types.js`, which the SDK + * schemas imported from `@modelcontextprotocol/core`, which the SDK * constructs via `zod/v4`. Mixing schemas from the v3 and v4 APIs at runtime * fails with errors like `keyValidator._parse is not a function` (v3 internals * calling into v4 objects, or vice versa). The `zod/v4` subpath is exported by @@ -22,7 +22,7 @@ * * **Problem**: ts-to-zod cannot resolve types imported from external packages. * When it encounters types like `ContentBlock`, `CallToolResult`, `Implementation`, - * `RequestId`, and `Tool` from `@modelcontextprotocol/sdk`, it generates `z.any()` + * `RequestId`, and `Tool` from `@modelcontextprotocol/core`, it generates `z.any()` * as a placeholder. * * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders. @@ -193,7 +193,7 @@ function postProcess(content: string): string { `import { z } from "zod/v4"; import { ${mcpImports}, -} from "@modelcontextprotocol/sdk/types.js";`, +} from "@modelcontextprotocol/core";`, ); // 2. Remove z.any() placeholders for external types (now imported from MCP SDK) diff --git a/src/generated/schema.json b/src/generated/schema.json index 80b4ac6..5f2124a 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -596,7 +596,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -611,25 +613,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -1286,7 +1273,40 @@ } }, "required": ["method", "params"], - "additionalProperties": false + "additionalProperties": false, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiHostContext": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -1359,7 +1379,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -1374,25 +1396,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -2044,7 +2051,40 @@ "additionalProperties": false } }, - "additionalProperties": {} + "additionalProperties": {}, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiHostCss": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -2942,7 +2982,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -2957,25 +2999,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -3637,7 +3664,40 @@ "hostCapabilities", "hostContext" ], - "additionalProperties": {} + "additionalProperties": {}, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiInitializedNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -5669,13 +5729,7 @@ ] } }, - "structuredContent": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, + "structuredContent": {}, "isError": { "type": "boolean" } diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 4368737..341c7b4 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -10,7 +10,7 @@ import { RequestIdSchema, ResourceLinkSchema, ToolSchema, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/core"; /** * @description Color theme preference for the host environment. diff --git a/src/message-transport.ts b/src/message-transport.ts index 9d19543..9483540 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -1,12 +1,10 @@ -import { +import { JSONRPCMessageSchema } from "@modelcontextprotocol/core"; +import type { JSONRPCMessage, - JSONRPCMessageSchema, MessageExtraInfo, -} from "@modelcontextprotocol/sdk/types.js"; -import { Transport, TransportSendOptions, -} from "@modelcontextprotocol/sdk/shared/transport.js"; +} from "@modelcontextprotocol/client"; import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types"; /** diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 6189246..0e64a74 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; -import { Implementation } from "@modelcontextprotocol/sdk/types.js"; -import { Client } from "@modelcontextprotocol/sdk/client"; +import type { Implementation } from "@modelcontextprotocol/client"; import { App, AppOptions, diff --git a/src/server/index.ts b/src/server/index.ts index c90514a..b8b3ad0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -40,24 +40,17 @@ import { McpUiClientCapabilities, } from "../app.js"; import type { - BaseToolCallback, + ClientCapabilities, McpServer, + ReadResourceResult, RegisteredTool, ResourceMetadata, + StandardSchemaWithJSON, + ToolAnnotations, ToolCallback, ReadResourceCallback as _ReadResourceCallback, RegisteredResource, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - AnySchema, - ZodRawShapeCompat, -} from "@modelcontextprotocol/sdk/server/zod-compat.js"; -import type { StandardSchemaWithJSON } from "../standard-schema"; -import type { - ClientCapabilities, - ReadResourceResult, - ToolAnnotations, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; @@ -70,8 +63,8 @@ export type { ResourceMetadata, ToolCallback }; export interface ToolConfig { title?: string; description?: string; - inputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON; - outputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON; + inputSchema?: StandardSchemaWithJSON; + outputSchema?: StandardSchemaWithJSON; annotations?: ToolAnnotations; _meta?: Record; } @@ -215,9 +208,8 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool */ export function registerAppTool< - OutputArgs extends ZodRawShapeCompat | StandardSchemaWithJSON, - InputArgs extends undefined | ZodRawShapeCompat | StandardSchemaWithJSON = - undefined, + OutputArgs extends StandardSchemaWithJSON, + InputArgs extends StandardSchemaWithJSON | undefined = undefined, >( server: Pick, name: string, @@ -225,15 +217,7 @@ export function registerAppTool< inputSchema?: InputArgs; outputSchema?: OutputArgs; }, - // The widened constraint signals the v2 API shape, but NOTE: McpServer in - // sdk@1.x still calls zod internals at runtime, so non-zod schemas will fail - // here until we depend on sdk v2. Zod (which all current callers use) is - // unaffected. The cast below bridges the 1.x type signature. - cb: ToolCallback< - InputArgs extends undefined | ZodRawShapeCompat | AnySchema - ? InputArgs - : AnySchema - >, + cb: ToolCallback, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key @@ -251,14 +235,7 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - // Cast bridges the widened StandardSchemaWithJSON constraint to the - // sdk@1.x zod-typed signature. Drops once we depend on sdk v2. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return server.registerTool( - name, - { ...config, _meta: normalizedMeta } as any, - cb as any, - ); + return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } export type McpUiReadResourceResult = ReadResourceResult & { @@ -418,10 +395,6 @@ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; * This helper retrieves the capability object from the `extensions` field * where MCP Apps advertises its support. * - * Note: The `clientCapabilities` parameter extends the SDK's `ClientCapabilities` - * type with an `extensions` field (pending SEP-1724). Once `extensions` is added - * to the SDK, this can use `ClientCapabilities` directly. - * * @param clientCapabilities - The client capabilities from the initialize response * @returns The MCP Apps capability settings, or `undefined` if not supported * @@ -456,10 +429,7 @@ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; * ``` */ export function getUiCapability( - clientCapabilities: - | (ClientCapabilities & { extensions?: Record }) - | null - | undefined, + clientCapabilities: ClientCapabilities | null | undefined, ): McpUiClientCapabilities | undefined { if (!clientCapabilities) { return undefined; diff --git a/src/spec.types.ts b/src/spec.types.ts index 7a8b337..8b03de9 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -18,7 +18,7 @@ import type { RequestId, ResourceLink, Tool, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; /** * Current protocol version supported by this SDK. @@ -163,8 +163,8 @@ export interface McpUiOpenLinkResult { /** @description True if the host failed to open the URL (e.g., due to security policy). */ isError?: boolean; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -195,8 +195,8 @@ export interface McpUiDownloadFileResult { /** @description True if the download failed (e.g., user cancelled or host denied). */ isError?: boolean; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -223,8 +223,8 @@ export interface McpUiMessageResult { /** @description True if the host rejected or failed to deliver the message. */ isError?: boolean; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -454,7 +454,7 @@ export interface McpUiResourceTeardownRequest { */ export interface McpUiResourceTeardownResult { /** - * Index signature required for MCP SDK `Protocol` class compatibility. + * Additional result properties are preserved for protocol extensibility. */ [key: string]: unknown; } @@ -577,8 +577,8 @@ export interface McpUiInitializeResult { /** @description Rich context about the host environment. */ hostContext: McpUiHostContext; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -752,7 +752,7 @@ export interface McpUiRequestDisplayModeResult { /** @description The display mode that was actually set. May differ from requested if not supported. */ mode: McpUiDisplayMode; /** - * Index signature required for MCP SDK `Protocol` class compatibility. + * Additional result properties are preserved for protocol extensibility. * Note: The generated schema uses passthrough() to allow additional properties. */ [key: string]: unknown; diff --git a/src/types.ts b/src/types.ts index 7fc6b71..655812b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,33 +69,6 @@ export { type McpUiClientCapabilities, } from "./spec.types.js"; -// Import types needed for protocol type unions (not re-exported, just used internally) -import type { - McpUiInitializeRequest, - McpUiOpenLinkRequest, - McpUiDownloadFileRequest, - McpUiMessageRequest, - McpUiUpdateModelContextRequest, - McpUiResourceTeardownRequest, - McpUiRequestDisplayModeRequest, - McpUiHostContextChangedNotification, - McpUiToolInputNotification, - McpUiToolInputPartialNotification, - McpUiToolResultNotification, - McpUiToolCancelledNotification, - McpUiSandboxResourceReadyNotification, - McpUiInitializedNotification, - McpUiSizeChangedNotification, - McpUiSandboxProxyReadyNotification, - McpUiRequestTeardownNotification, - McpUiInitializeResult, - McpUiOpenLinkResult, - McpUiDownloadFileResult, - McpUiMessageResult, - McpUiResourceTeardownResult, - McpUiRequestDisplayModeResult, -} from "./spec.types.js"; - // Re-export all schemas from generated/schema.ts (already PascalCase) export { McpUiThemeSchema, @@ -135,106 +108,3 @@ export { McpUiToolVisibilitySchema, McpUiToolMetaSchema, } from "./generated/schema.js"; - -// Re-export SDK types used in protocol type unions -import { - CallToolRequest, - CallToolResult, - CreateMessageRequest, - CreateMessageResult, - CreateMessageResultWithTools, - EmptyResult, - ListPromptsRequest, - ListPromptsResult, - ListResourcesRequest, - ListResourcesResult, - ListResourceTemplatesRequest, - ListResourceTemplatesResult, - ListToolsRequest, - ListToolsResult, - LoggingMessageNotification, - PingRequest, - PromptListChangedNotification, - ReadResourceRequest, - ReadResourceResult, - ResourceListChangedNotification, - ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; - -/** - * All request types in the MCP Apps protocol. - * - * Includes: - * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode) - * - MCP server requests forwarded from the app (tools/call, tools/list, resources/list, - * resources/templates/list, resources/read, prompts/list) - * - MCP client requests forwarded to the host (sampling/createMessage) - * - Protocol requests (ping) - */ -export type AppRequest = - | McpUiInitializeRequest - | McpUiOpenLinkRequest - | McpUiDownloadFileRequest - | McpUiMessageRequest - | McpUiUpdateModelContextRequest - | McpUiResourceTeardownRequest - | McpUiRequestDisplayModeRequest - | CallToolRequest - | ListToolsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | ListPromptsRequest - | CreateMessageRequest - | PingRequest; - -/** - * All notification types in the MCP Apps protocol. - * - * Host to app: - * - Tool lifecycle (input, input-partial, result, cancelled) - * - Host context changes - * - MCP list changes (tools, resources, prompts) - * - Sandbox resource ready - * - * App to host: - * - Initialized, size-changed, sandbox-proxy-ready, request-teardown - * - Logging messages - */ -export type AppNotification = - // Sent to app - | McpUiHostContextChangedNotification - | McpUiToolInputNotification - | McpUiToolInputPartialNotification - | McpUiToolResultNotification - | McpUiToolCancelledNotification - | McpUiSandboxResourceReadyNotification - | ToolListChangedNotification - | ResourceListChangedNotification - | PromptListChangedNotification - // Received from app - | McpUiInitializedNotification - | McpUiSizeChangedNotification - | McpUiSandboxProxyReadyNotification - | McpUiRequestTeardownNotification - | LoggingMessageNotification; - -/** - * All result types in the MCP Apps protocol. - */ -export type AppResult = - | McpUiInitializeResult - | McpUiOpenLinkResult - | McpUiDownloadFileResult - | McpUiMessageResult - | McpUiResourceTeardownResult - | McpUiRequestDisplayModeResult - | CallToolResult - | ListToolsResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | CreateMessageResult - | CreateMessageResultWithTools - | EmptyResult; From f94cb38b0b6453476cb45e3edccf606505be7708 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 17:07:30 -0700 Subject: [PATCH 63/97] refactor: finish MCP SDK v2 migration --- AGENTS.md | 8 +- README.md | 10 +- build.bun.ts | 12 +- docs/authorization.md | 2 +- docs/migrate_from_openai_apps.md | 8 +- docs/overview.md | 8 +- docs/patterns.md | 11 +- docs/patterns.tsx | 17 +-- docs/quickstart.md | 19 +-- docs/testing-mcp-apps.md | 4 + examples/basic-host/package.json | 2 +- examples/basic-host/src/implementation.ts | 12 +- examples/basic-host/src/index.tsx | 2 +- examples/basic-server-preact/main.ts | 10 +- examples/basic-server-preact/package.json | 3 +- examples/basic-server-preact/server.ts | 10 +- examples/basic-server-preact/src/mcp-app.tsx | 2 +- examples/basic-server-react/main.ts | 10 +- examples/basic-server-react/package.json | 3 +- examples/basic-server-react/server.ts | 10 +- examples/basic-server-react/src/mcp-app.tsx | 2 +- examples/basic-server-solid/main.ts | 10 +- examples/basic-server-solid/package.json | 3 +- examples/basic-server-solid/server.ts | 10 +- examples/basic-server-solid/src/mcp-app.tsx | 2 +- examples/basic-server-svelte/main.ts | 10 +- examples/basic-server-svelte/package.json | 3 +- examples/basic-server-svelte/server.ts | 10 +- examples/basic-server-svelte/src/App.svelte | 2 +- examples/basic-server-vanillajs/main.ts | 10 +- examples/basic-server-vanillajs/package.json | 3 +- examples/basic-server-vanillajs/server.ts | 9 +- .../basic-server-vanillajs/src/mcp-app.ts | 2 +- examples/basic-server-vue/main.ts | 10 +- examples/basic-server-vue/package.json | 3 +- examples/basic-server-vue/server.ts | 10 +- examples/basic-server-vue/src/App.vue | 2 +- examples/budget-allocator-server/main.ts | 10 +- examples/budget-allocator-server/package.json | 2 +- examples/budget-allocator-server/server.ts | 12 +- examples/cohort-heatmap-server/main.ts | 10 +- examples/cohort-heatmap-server/package.json | 2 +- examples/cohort-heatmap-server/server.ts | 10 +- examples/customer-segmentation-server/main.ts | 10 +- .../customer-segmentation-server/package.json | 2 +- .../customer-segmentation-server/server.ts | 14 +-- examples/debug-server/main.ts | 10 +- examples/debug-server/package.json | 2 +- examples/debug-server/server.ts | 10 +- examples/integration-server/main.ts | 10 +- examples/integration-server/package.json | 3 +- examples/integration-server/server.ts | 14 +-- examples/integration-server/src/mcp-app.tsx | 2 +- examples/lazy-auth-server/package.json | 5 +- examples/lazy-auth-server/server.ts | 29 ++--- examples/map-server/main.ts | 10 +- examples/map-server/package.json | 3 +- examples/map-server/server.ts | 18 +-- examples/map-server/src/mcp-app.ts | 2 +- examples/pdf-server/main.ts | 10 +- examples/pdf-server/package.json | 3 +- examples/pdf-server/server.test.ts | 3 +- examples/pdf-server/server.ts | 57 +++++---- examples/pdf-server/src/mcp-app.ts | 6 +- examples/qr-server/package.json | 3 +- examples/quickstart/main.ts | 10 +- examples/quickstart/package.json | 5 +- examples/quickstart/server.ts | 5 +- examples/say-server/package.json | 3 +- examples/scenario-modeler-server/main.ts | 10 +- examples/scenario-modeler-server/package.json | 2 +- examples/scenario-modeler-server/server.ts | 14 +-- examples/shadertoy-server/main.ts | 10 +- examples/shadertoy-server/package.json | 2 +- examples/shadertoy-server/server.ts | 10 +- examples/sheet-music-server/main.ts | 10 +- examples/sheet-music-server/package.json | 2 +- examples/sheet-music-server/server.ts | 10 +- examples/system-monitor-server/main.ts | 10 +- examples/system-monitor-server/package.json | 2 +- examples/system-monitor-server/server.ts | 18 +-- examples/threejs-server/main.ts | 10 +- examples/threejs-server/package.json | 3 +- examples/threejs-server/server.ts | 12 +- .../threejs-server/src/mcp-app-wrapper.tsx | 2 +- examples/transcript-server/main.ts | 10 +- examples/transcript-server/package.json | 2 +- examples/transcript-server/server.ts | 13 +- examples/video-resource-server/main.ts | 10 +- examples/video-resource-server/package.json | 3 +- examples/video-resource-server/server.ts | 12 +- examples/video-resource-server/src/mcp-app.ts | 2 +- examples/wiki-explorer-server/main.ts | 10 +- examples/wiki-explorer-server/package.json | 3 +- examples/wiki-explorer-server/server.ts | 10 +- examples/wiki-explorer-server/src/mcp-app.ts | 2 +- package-lock.json | 95 +++++++++------ package.json | 2 - .../skills/add-app-to-server/SKILL.md | 111 +++++++++++------- .../mcp-apps/skills/convert-web-app/SKILL.md | 57 +++++---- .../mcp-apps/skills/create-mcp-app/SKILL.md | 5 +- scripts/typedoc-plugin-base-sdk-links.mjs | 16 +++ src/app-bridge.examples.ts | 36 +++--- src/app-bridge.ts | 62 +++++----- src/app.examples.ts | 9 +- src/app.ts | 19 ++- src/events.ts | 5 +- src/message-transport.test.ts | 2 +- src/server/index.examples.ts | 11 +- src/server/index.test.ts | 2 +- src/server/index.ts | 4 +- typedoc.config.mjs | 25 ++++ 112 files changed, 684 insertions(+), 540 deletions(-) create mode 100644 scripts/typedoc-plugin-base-sdk-links.mjs diff --git a/AGENTS.md b/AGENTS.md index 6bfc8cd..f850aa3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,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 the base MCP SDK `Client`, handles View initialization, tool calls, and messaging +- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Server` for the inner 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 @@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \ ### Protocol Flow ``` -View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server +View (App/Client) <--PostMessageTransport--> Host (AppBridge/Server) <--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 and Host complete the temporary inner base MCP handshake, then the View sends `ui/initialize` 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 diff --git a/README.md b/README.md index 2fd89ff..b3f68e7 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,17 @@ resources: ## Getting Started ```bash -npm install -S @modelcontextprotocol/ext-apps +npm install -S @modelcontextprotocol/ext-apps \ + @modelcontextprotocol/client@2.0.0-beta.4 \ + @modelcontextprotocol/server@2.0.0-beta.4 \ + @modelcontextprotocol/core@2.0.0-beta.4 \ + zod@^4.2.0 ``` +This release uses the split base MCP SDK v2 packages. Install all three at the +exact published beta.4 version so the Apps SDK and your MCP client/server share +one compatible protocol implementation. + **New here?** Start with the [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) to build your first MCP App. diff --git a/build.bun.ts b/build.bun.ts index a129d5d..837279f 100644 --- a/build.bun.ts +++ b/build.bun.ts @@ -31,9 +31,15 @@ function buildJs( }); } -// zod is a peerDependency — keep it external so consumers share a single -// zod instance (instanceof ZodError / schema.extend() break with duplicate copies). -const PEER_EXTERNALS = ["@modelcontextprotocol/sdk", "zod"]; +// 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", { diff --git a/docs/authorization.md b/docs/authorization.md index 59fc26c..df7adc6 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -150,7 +150,7 @@ registerAppTool( "get_account_balance", { description: "Get account balance", - inputSchema: { accountId: z.string() }, + inputSchema: z.object({ accountId: z.string() }), }, async ({ accountId }) => { if (!authInfo) { diff --git a/docs/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md index d91281b..22d5d9f 100644 --- a/docs/migrate_from_openai_apps.md +++ b/docs/migrate_from_openai_apps.md @@ -66,7 +66,7 @@ The server-side changes involve updating metadata structure and using helper fun #### Before (OpenAI) ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { z } from "zod"; function createServer() { @@ -78,7 +78,7 @@ function createServer() { { title: "Shopping Cart", description: "Display the user's shopping cart", - inputSchema: { userId: z.string() }, + inputSchema: z.object({ userId: z.string() }), annotations: { readOnlyHint: true }, _meta: { "openai/outputTemplate": "ui://view/cart.html", @@ -126,7 +126,7 @@ function createServer() { #### After (MCP Apps) ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { registerAppTool, registerAppResource, @@ -144,7 +144,7 @@ function createServer() { { title: "Shopping Cart", description: "Display the user's shopping cart", - inputSchema: { userId: z.string() }, + inputSchema: z.object({ userId: z.string() }), annotations: { readOnlyHint: true }, _meta: { ui: { resourceUri: "ui://view/cart.html" } }, }, diff --git a/docs/overview.md b/docs/overview.md index 28f5b23..317ab43 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -56,7 +56,11 @@ flowchart LR - **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them. - **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat. -The View acts as an MCP client, the Host acts as a proxy, and the Server is a standard MCP server. +The View's `App` subclasses the base MCP SDK `Client`. The Host's `AppBridge` +subclasses the base MCP SDK `Server` for the inner iframe channel, while a +separate outer `Client` connects the Host to the actual MCP Server. Keeping +those two connections separate prevents iframe negotiation and capabilities +from leaking into the server connection. ## Lifecycle @@ -93,7 +97,7 @@ sequenceDiagram ``` 1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server. -2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). This handshake ensures the View is ready before receiving data. +2. **Initialization** — When a UI tool is called, the Host renders the iframe. The SDK first completes its temporary inner base MCP handshake, then the View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). `ui/initialize` remains authoritative for Apps capabilities and identity, and `ui/notifications/initialized` remains the public View-ready signal. 3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context. 4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context. 5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources. diff --git a/docs/patterns.md b/docs/patterns.md index 87a6d82..884de26 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -19,7 +19,7 @@ registerAppTool( "update-quantity", { description: "Update item quantity in cart", - inputSchema: { itemId: z.string(), quantity: z.number() }, + inputSchema: z.object({ itemId: z.string(), quantity: z.number() }), _meta: { ui: { resourceUri: "ui://shop/cart.html", @@ -127,14 +127,14 @@ registerAppTool( { title: "Read Data Bytes", description: "Load binary data in chunks", - inputSchema: { + inputSchema: z.object({ id: z.string().describe("Resource identifier"), offset: z.number().min(0).default(0).describe("Byte offset"), byteCount: z .number() .default(MAX_CHUNK_BYTES) .describe("Bytes to read"), - }, + }), outputSchema: DataChunkSchema, // Hidden from model - only callable by the App _meta: { ui: { visibility: ["app"] } }, @@ -281,10 +281,7 @@ server.registerResource( ```ts source="./patterns.tsx#binaryBlobResourceClient" -const result = await app.request( - { method: "resources/read", params: { uri: `video://${videoId}` } }, - ReadResourceResultSchema, -); +const result = await app.readServerResource({ uri: `video://${videoId}` }); const content = result.contents[0]; if (!content || !("blob" in content)) { diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 6b87692..bb9eaa4 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -17,16 +17,12 @@ import { randomUUID } from "node:crypto"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; -import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import type { McpUiHostContext } from "../src/types.js"; import { useEffect, useState } from "react"; import { useApp } from "../src/react/index.js"; import { registerAppTool } from "../src/server/index.js"; -import { - McpServer, - ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server"; import { z } from "zod"; /** @@ -117,14 +113,14 @@ function chunkedDataServer(server: McpServer) { { title: "Read Data Bytes", description: "Load binary data in chunks", - inputSchema: { + inputSchema: z.object({ id: z.string().describe("Resource identifier"), offset: z.number().min(0).default(0).describe("Byte offset"), byteCount: z .number() .default(MAX_CHUNK_BYTES) .describe("Bytes to read"), - }, + }), outputSchema: DataChunkSchema, // Hidden from model - only callable by the App _meta: { ui: { visibility: ["app"] } }, @@ -252,10 +248,7 @@ function binaryBlobResourceServer( */ async function binaryBlobResourceClient(app: App, videoId: string) { //#region binaryBlobResourceClient - const result = await app.request( - { method: "resources/read", params: { uri: `video://${videoId}` } }, - ReadResourceResultSchema, - ); + const result = await app.readServerResource({ uri: `video://${videoId}` }); const content = result.contents[0]; if (!content || !("blob" in content)) { diff --git a/docs/quickstart.md b/docs/quickstart.md index 3e00796..eb6b096 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -21,7 +21,7 @@ This tutorial assumes you've built an MCP server before and are comfortable with We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) to build the server. -You'll also need Node.js 18+. +You'll also need Node.js 20+. ## 1. Set up the project @@ -37,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` @@ -178,9 +178,10 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -205,7 +206,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () => { @@ -240,10 +241,10 @@ export function createServer(): McpServer { ```ts source="../examples/quickstart/main.ts" -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -263,7 +264,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md index df1396c..e5d82b5 100644 --- a/docs/testing-mcp-apps.md +++ b/docs/testing-mcp-apps.md @@ -28,6 +28,10 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex cd examples/basic-host ``` + The root install runs the package build, including type-checking the + documentation snippets against the exact base MCP SDK beta.4 packages. Do + not bypass a failed build before testing the host. + 2. Start basic-host, pointing it to your MCP server: ```bash diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 1b45dcf..583d2ff 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 31e3698..5f680ff 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -1,8 +1,12 @@ import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js"; +import { + Client, + SSEClientTransport, + StreamableHTTPClientTransport, + type CallToolResult, + type Resource, + type Tool, +} from "@modelcontextprotocol/client"; import { getTheme, onThemeChange } from "./theme"; import { HOST_STYLE_VARIABLES } from "./host-styles"; diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 3d488a7..00c0d52 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -1,5 +1,5 @@ import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge"; -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { Tool } from "@modelcontextprotocol/client"; import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation"; diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts index 7642632..ffdff52 100644 --- a/examples/basic-server-preact/main.ts +++ b/examples/basic-server-preact/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 1a63232..0187827 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -24,10 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts index bf14ea5..dfec94a 100644 --- a/examples/basic-server-preact/server.ts +++ b/examples/basic-server-preact/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx index a8bdc68..351a8ec 100644 --- a/examples/basic-server-preact/src/mcp-app.tsx +++ b/examples/basic-server-preact/src/mcp-app.tsx @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { useCallback, useEffect, useState } from "preact/hooks"; import { render } from "preact"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts index ec187b6..21c5e18 100644 --- a/examples/basic-server-react/main.ts +++ b/examples/basic-server-react/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 821fc96..dae7665 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -34,10 +34,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 23f6dda..d77c908 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -1,8 +1,12 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") @@ -29,7 +33,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx index 6bb5540..2b5fbef 100644 --- a/examples/basic-server-react/src/mcp-app.tsx +++ b/examples/basic-server-react/src/mcp-app.tsx @@ -3,7 +3,7 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts index c8d9de2..095ef8d 100644 --- a/examples/basic-server-solid/main.ts +++ b/examples/basic-server-solid/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 997bc60..5d91407 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -24,10 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts index 2ed2d93..addb522 100644 --- a/examples/basic-server-solid/server.ts +++ b/examples/basic-server-solid/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx index 66d47f3..2d78b7b 100644 --- a/examples/basic-server-solid/src/mcp-app.tsx +++ b/examples/basic-server-solid/src/mcp-app.tsx @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { createEffect, createSignal, onMount, Show } from "solid-js"; import { render } from "solid-js/web"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts index 6c50a25..734ec0c 100644 --- a/examples/basic-server-svelte/main.ts +++ b/examples/basic-server-svelte/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index e83772f..18bf7ab 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -24,10 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts index da603ca..9414285 100644 --- a/examples/basic-server-svelte/server.ts +++ b/examples/basic-server-svelte/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte index fde128d..cabcc31 100644 --- a/examples/basic-server-svelte/src/App.svelte +++ b/examples/basic-server-svelte/src/App.svelte @@ -7,7 +7,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; function extractTime(result: CallToolResult): string { const { text } = result.content?.find((c) => c.type === "text")!; diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts index 286fa34..4de7331 100644 --- a/examples/basic-server-vanillajs/main.ts +++ b/examples/basic-server-vanillajs/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 64e1758..6bb75af 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -24,10 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 5b2daf7..693d861 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -1,6 +1,9 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -29,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), outputSchema: z.object({ time: z.string(), }), diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index c3168c2..6b5084a 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import "./global.css"; import "./mcp-app.css"; diff --git a/examples/basic-server-vue/main.ts b/examples/basic-server-vue/main.ts index 669a071..d774269 100644 --- a/examples/basic-server-vue/main.ts +++ b/examples/basic-server-vue/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index e4cd0b4..3b34aa6 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -24,10 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", diff --git a/examples/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts index 5aa72de..9adb911 100644 --- a/examples/basic-server-vue/server.ts +++ b/examples/basic-server-vue/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue index 196ad64..a45c262 100644 --- a/examples/basic-server-vue/src/App.vue +++ b/examples/basic-server-vue/src/App.vue @@ -7,7 +7,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; function extractTime(result: CallToolResult): string { const { text } = result.content?.find((c) => c.type === "text")!; diff --git a/examples/budget-allocator-server/main.ts b/examples/budget-allocator-server/main.ts index cc488f0..02c03cb 100644 --- a/examples/budget-allocator-server/main.ts +++ b/examples/budget-allocator-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 8af1a6c..5d4fe8e 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -29,7 +29,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 04fba11..01f5dbf 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -4,11 +4,11 @@ * Provides budget configuration, 24 months of historical allocation data, * and industry benchmarks by company stage. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -270,7 +270,7 @@ export function createServer(): McpServer { title: "Get Budget Data", description: "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage. The widget is interactive and exposes tools for reading/modifying allocations, adjusting budgets, and comparing against industry benchmarks.", - inputSchema: {}, + inputSchema: z.object({}), outputSchema: BudgetDataResponseSchema, _meta: { ui: { resourceUri } }, }, diff --git a/examples/cohort-heatmap-server/main.ts b/examples/cohort-heatmap-server/main.ts index d59903b..8b33603 100644 --- a/examples/cohort-heatmap-server/main.ts +++ b/examples/cohort-heatmap-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 99e9d9f..c54040a 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -29,7 +29,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 6d07144..129e2dc 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -1,5 +1,7 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -178,8 +180,8 @@ export function createServer(): McpServer { title: "Get Cohort Retention Data", description: "Returns cohort retention heatmap data showing customer retention over time by signup month", - inputSchema: GetCohortDataInputSchema.shape, - outputSchema: CohortDataSchema.shape, + inputSchema: GetCohortDataInputSchema, + outputSchema: CohortDataSchema, _meta: { ui: { resourceUri } }, }, async ({ metric, periodType, cohortCount, maxPeriods }) => { diff --git a/examples/customer-segmentation-server/main.ts b/examples/customer-segmentation-server/main.ts index 130076c..c788757 100644 --- a/examples/customer-segmentation-server/main.ts +++ b/examples/customer-segmentation-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 59e3eb1..ef14b32 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -29,7 +29,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 244415d..7cc1c1f 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -100,8 +100,8 @@ export function createServer(): McpServer { title: "Get Customer Data", description: "Returns customer data with segment information for visualization. Optionally filter by segment.", - inputSchema: GetCustomerDataInputSchema.shape, - outputSchema: GetCustomerDataOutputSchema.shape, + inputSchema: GetCustomerDataInputSchema, + outputSchema: GetCustomerDataOutputSchema, _meta: { ui: { resourceUri } }, }, async ({ segment }): Promise => { diff --git a/examples/debug-server/main.ts b/examples/debug-server/main.ts index 3c89a97..9fb0825 100644 --- a/examples/debug-server/main.ts +++ b/examples/debug-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 02e9fbf..0e3f508 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -37,7 +37,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts index fd3452a..068f94d 100644 --- a/examples/debug-server/server.ts +++ b/examples/debug-server/server.ts @@ -3,11 +3,11 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import { appendFileSync } from "node:fs"; import path from "node:path"; diff --git a/examples/integration-server/main.ts b/examples/integration-server/main.ts index c45787e..9a42842 100644 --- a/examples/integration-server/main.ts +++ b/examples/integration-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 3f9d8bb..56fc8ed 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -15,10 +15,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index e44689d..346f4f7 100644 --- a/examples/integration-server/server.ts +++ b/examples/integration-server/server.ts @@ -3,11 +3,11 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -33,7 +33,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + inputSchema: z.object({}), outputSchema: z.object({ time: z.string(), }), @@ -72,7 +72,7 @@ export function createServer(): McpServer { ); // Sample downloadable resource — used to demo ResourceLink in ui/download-file - server.resource( + server.registerResource( SAMPLE_DOWNLOAD_URI, SAMPLE_DOWNLOAD_URI, { diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx index 7f0b18c..281a356 100644 --- a/examples/integration-server/src/mcp-app.tsx +++ b/examples/integration-server/src/mcp-app.tsx @@ -3,7 +3,7 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 086f20a..6aef3e1 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -24,10 +24,11 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", - "jose": "^6.0.0" + "jose": "^6.0.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts index fe7b1ac..01535eb 100644 --- a/examples/lazy-auth-server/server.ts +++ b/examples/lazy-auth-server/server.ts @@ -21,13 +21,13 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import { + McpServer, + UrlElicitationRequiredError, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import cors from "cors"; import express, { type Express, type Request, type Response } from "express"; import { SignJWT, jwtVerify } from "jose"; @@ -35,6 +35,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js). Derived // from import.meta.url rather than import.meta.filename/dirname, which are @@ -605,7 +606,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Show Auth Button", description: "Public tool: shows a button that triggers the protected get_secret tool.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri: buttonUri } }, }, async (): Promise => ({ @@ -638,7 +639,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Get Secret", description: "Protected tool: returns secret data. Requires authentication.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri: secretUri } }, }, async (): Promise => { @@ -682,7 +683,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Revoke Auth Token", description: "Protected tool: revokes the caller's entire auth session (access + refresh token).", - inputSchema: {}, + inputSchema: z.object({}), }, async (): Promise => { if (!authInfo) { @@ -720,7 +721,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Elicit URL (session-callback)", description: "URL elicitation via elicitInput. Blocks until the client accepts; sends ElicitCompleteNotification before returning.", - inputSchema: {}, + inputSchema: z.object({}), }, async (_args, extra): Promise => { const eid = crypto.randomUUID(); @@ -735,7 +736,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { message: "Please open this URL to continue.", elicitationId: eid, }, - { relatedRequestId: extra.requestId, timeout: 300_000 }, + { relatedRequestId: extra.mcpReq.id, timeout: 300_000 }, ); // Send completion notification before returning — lands on the same SSE // stream as this tool response. For Streamable HTTP this is the ONLY @@ -767,7 +768,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Elicit by -32042 Error", description: "Throws UrlElicitationRequiredError (-32042). Client must open the URL, then retry. On retry, if the last-issued elicitationId was completed, sends ElicitCompleteNotification and succeeds.", - inputSchema: {}, + inputSchema: z.object({}), }, async (): Promise => { // Retry path: check if the user completed the elicitation we last issued. @@ -1031,7 +1032,7 @@ export function createApp(): Express { } const server = createServer(authInfo, req); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); res.on("close", () => { diff --git a/examples/map-server/main.ts b/examples/map-server/main.ts index f91f57b..8038394 100644 --- a/examples/map-server/main.ts +++ b/examples/map-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/map-server/package.json b/examples/map-server/package.json index b0c629a..a3639ce 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -26,10 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts index 10125a6..5767d46 100644 --- a/examples/map-server/server.ts +++ b/examples/map-server/server.ts @@ -5,11 +5,11 @@ * - geocode: Search for places using OpenStreetMap Nominatim * - show-map: Display an interactive 3D globe at a given location */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -148,7 +148,7 @@ export function createServer(): McpServer { title: "Show Map", description: "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location. The widget is interactive and exposes tools for navigation (fly to locations) and querying the current view.", - inputSchema: { + inputSchema: z.object({ west: z .number() .optional() @@ -173,7 +173,7 @@ export function createServer(): McpServer { .string() .optional() .describe("Optional label to display on the map"), - }, + }), _meta: { ui: { resourceUri } }, }, async ({ west, south, east, north, label }): Promise => ({ @@ -196,13 +196,13 @@ export function createServer(): McpServer { title: "Geocode", description: "Search for places using OpenStreetMap. Returns coordinates and bounding boxes for up to 5 matches.", - inputSchema: { + inputSchema: z.object({ query: z .string() .describe( "Place name or address to search for (e.g., 'Paris', 'Golden Gate Bridge', '1600 Pennsylvania Ave')", ), - }, + }), }, async ({ query }): Promise => { try { diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts index 39d7c2a..d3090c7 100644 --- a/examples/map-server/src/mcp-app.ts +++ b/examples/map-server/src/mcp-app.ts @@ -6,7 +6,7 @@ * a navigate-to tool for the host to control navigation. */ import { App } from "@modelcontextprotocol/ext-apps"; -import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js"; +import type { ContentBlock } from "@modelcontextprotocol/client"; import { z } from "zod"; // TypeScript declaration for Cesium loaded from CDN diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts index 1ff848a..bb7b57f 100644 --- a/examples/pdf-server/main.ts +++ b/examples/pdf-server/main.ts @@ -6,10 +6,10 @@ import fs from "node:fs"; import path from "node:path"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { @@ -39,7 +39,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index aeb52c7..a7eb4bb 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -26,10 +26,11 @@ }, "dependencies": { "@cantoo/pdf-lib": "^2.6.5", + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index 33a03f1..0b2a5eb 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -2,8 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { PDFDocument } from "@cantoo/pdf-lib"; import { makeRandomJpeg } from "../../tests/helpers/range-counting-server"; diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 93dedfd..e67cd47 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -13,18 +13,19 @@ import { randomUUID } from "crypto"; import fs from "node:fs"; import path from "node:path"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import { - RootsListChangedNotificationSchema, + McpServer, type CallToolResult, + type ElicitResult, + type PrimitiveSchemaDefinition, type ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; + type Server, +} from "@modelcontextprotocol/server"; // Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy // build instantiates DOMMatrix at module scope and the @napi-rs/canvas // polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details. @@ -62,10 +63,6 @@ class FetchStandardFontDataFactory { return new Uint8Array(await res.arrayBuffer()); } } -import type { - PrimitiveSchemaDefinition, - ElicitResult, -} from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; // ============================================================================= @@ -1282,7 +1279,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { refreshRoots(server.server); }; server.server.setNotificationHandler( - RootsListChangedNotificationSchema, + "notifications/roots/list_changed", async () => { await refreshRoots(server.server); }, @@ -1292,10 +1289,12 @@ export function createServer(options: CreateServerOptions = {}): McpServer { const { readPdfRange } = sharedPdfCache; // Tool: list_pdfs - List available PDFs - server.tool( + server.registerTool( "list_pdfs", - "List available PDFs that can be displayed", - {}, + { + description: "List available PDFs that can be displayed", + inputSchema: z.object({}), + }, async (): Promise => { const seen = new Set(); const localFiles: string[] = []; @@ -1377,7 +1376,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { title: "Read PDF Bytes", description: "Read a range of bytes from a PDF (max 512KB per request). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ url: z.string().describe("PDF URL or local file path"), offset: z.number().min(0).default(0).describe("Byte offset"), byteCount: z @@ -1386,7 +1385,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { .max(MAX_CHUNK_BYTES) .default(MAX_CHUNK_BYTES) .describe("Bytes to read"), - }, + }), outputSchema: z.object({ url: z.string(), bytes: z.string().describe("Base64 encoded bytes"), @@ -1473,7 +1472,7 @@ Returns a viewUUID in structuredContent. Pass it to \`interact\`: Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL. Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before display.`, - inputSchema: { + inputSchema: z.object({ url: z .string() .default(DEFAULT_PDF) @@ -1489,7 +1488,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before "If true and the PDF has form fields, prompt the user to fill them before displaying", ), }), - }, + }), outputSchema: z.object({ viewUUID: z .string() @@ -2426,7 +2425,7 @@ Example — add a signature image and a stamp, then screenshot to verify: **FORMS** — fill_form: fill fields with \`fields\` array of {name, value}. **SAVE** — save_as: write the annotated PDF (annotations + form values) to a file. Pass \`path\` (absolute path or file://) for a new location, or omit \`path\` to overwrite the original. Set \`overwrite: true\` to replace an existing file (always required when omitting \`path\`).`, - inputSchema: { + inputSchema: z.object({ viewUUID: z .string() .describe( @@ -2523,7 +2522,7 @@ Example — add a signature image and a stamp, then screenshot to verify: .describe( "Array of commands to execute sequentially. More efficient than separate calls. Tip: end with get_pages+getScreenshots to verify changes.", ), - }, + }), }, async ( { @@ -2600,7 +2599,7 @@ Example — add a signature image and a stamp, then screenshot to verify: const result = await processInteractCommand( uuid, commandList[i], - extra.signal, + extra.mcpReq.signal, ); if (result.isError) { const errText = result.content @@ -2657,7 +2656,7 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Submit Page Data", description: "Submit rendered page data for a get_pages request (used by viewer). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ requestId: z .string() .describe("The request ID from the get_pages command"), @@ -2670,7 +2669,7 @@ Example — add a signature image and a stamp, then screenshot to verify: }), ) .describe("Page data entries"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ requestId, pages }): Promise => { @@ -2700,7 +2699,7 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Submit Save Data", description: "Submit annotated PDF bytes for a save_as request (used by viewer). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ requestId: z .string() .describe("The request ID from the save_as command"), @@ -2709,7 +2708,7 @@ Example — add a signature image and a stamp, then screenshot to verify: .string() .optional() .describe("Error message if the viewer failed to build bytes"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ requestId, data, error }): Promise => { @@ -2739,7 +2738,7 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Submit Viewer State", description: "Submit a viewer-state snapshot for a get_viewer_state request (used by viewer). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ requestId: z .string() .describe("The request ID from the get_viewer_state command"), @@ -2751,7 +2750,7 @@ Example — add a signature image and a stamp, then screenshot to verify: .string() .optional() .describe("Error message if the viewer failed to read state"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ requestId, state, error }): Promise => { @@ -2781,9 +2780,9 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Poll PDF Commands", description: "Poll for pending commands for a PDF viewer. The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ viewUUID: z.string().describe("The viewUUID of the PDF viewer"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ viewUUID: uuid }): Promise => { @@ -2828,10 +2827,10 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Save PDF", description: "Save annotated PDF bytes back to a local file. The model should NOT call this tool directly — use interact with action: save_as instead.", - inputSchema: { + inputSchema: z.object({ url: z.string().describe("Original PDF URL or local file path"), data: z.string().describe("Base64-encoded PDF bytes"), - }, + }), outputSchema: z.object({ filePath: z.string(), mtimeMs: z.number(), diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index a953e24..45e0c74 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -12,8 +12,10 @@ import { applyDocumentTheme, applyHostStyleVariables, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js"; +import type { + CallToolResult, + ContentBlock, +} from "@modelcontextprotocol/client"; import { z } from "zod"; import * as pdfjsLib from "pdfjs-dist"; import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist"; diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index a5cbf5b..cb4e010 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -8,7 +8,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } } diff --git a/examples/quickstart/main.ts b/examples/quickstart/main.ts index 06895a4..7f370a8 100644 --- a/examples/quickstart/main.ts +++ b/examples/quickstart/main.ts @@ -1,7 +1,7 @@ -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -21,7 +21,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 1003637..f741c6e 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -18,9 +18,10 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/quickstart/server.ts b/examples/quickstart/server.ts index eb3c3b1..611d0f0 100644 --- a/examples/quickstart/server.ts +++ b/examples/quickstart/server.ts @@ -3,9 +3,10 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -30,7 +31,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () => { diff --git a/examples/say-server/package.json b/examples/say-server/package.json index a0076e6..2ebc099 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -15,7 +15,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } } diff --git a/examples/scenario-modeler-server/main.ts b/examples/scenario-modeler-server/main.ts index dbd3ab6..cc20e63 100644 --- a/examples/scenario-modeler-server/main.ts +++ b/examples/scenario-modeler-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index a7e8a9a..63a5d48 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -29,7 +29,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index b59851c..c5fa05d 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -3,11 +3,11 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -305,8 +305,8 @@ export function createServer(): McpServer { title: "Get Scenario Data", description: "Returns SaaS scenario templates and optionally computes custom projections for given inputs", - inputSchema: GetScenarioDataInputSchema.shape, - outputSchema: GetScenarioDataOutputSchema.shape, + inputSchema: GetScenarioDataInputSchema, + outputSchema: GetScenarioDataOutputSchema, _meta: { ui: { resourceUri } }, }, async (args: { diff --git a/examples/shadertoy-server/main.ts b/examples/shadertoy-server/main.ts index ac8f6ad..7e388f0 100644 --- a/examples/shadertoy-server/main.ts +++ b/examples/shadertoy-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 0c27501..318f017 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -27,7 +27,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/shadertoy-server/server.ts b/examples/shadertoy-server/server.ts index 5238c40..50c27bc 100644 --- a/examples/shadertoy-server/server.ts +++ b/examples/shadertoy-server/server.ts @@ -3,11 +3,11 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; diff --git a/examples/sheet-music-server/main.ts b/examples/sheet-music-server/main.ts index 8b143a7..d7bf3a0 100644 --- a/examples/sheet-music-server/main.ts +++ b/examples/sheet-music-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 6cf175a..10d7c71 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -27,7 +27,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/sheet-music-server/server.ts b/examples/sheet-music-server/server.ts index cfd319b..be7214a 100644 --- a/examples/sheet-music-server/server.ts +++ b/examples/sheet-music-server/server.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; diff --git a/examples/system-monitor-server/main.ts b/examples/system-monitor-server/main.ts index 365a560..c55c799 100644 --- a/examples/system-monitor-server/main.ts +++ b/examples/system-monitor-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 760d07f..5bc4a95 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -29,7 +29,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 8fac5f2..a17bcf6 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -3,11 +3,11 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -135,8 +135,8 @@ export function createServer(): McpServer { title: "Get System Info", description: "Returns system information, including hostname, platform, CPU info, and memory.", - inputSchema: {}, - outputSchema: SystemInfoSchema.shape, + inputSchema: z.object({}), + outputSchema: SystemInfoSchema, _meta: { ui: { resourceUri } }, }, (): CallToolResult => { @@ -156,8 +156,8 @@ export function createServer(): McpServer { title: "Poll System Stats", description: "Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.", - inputSchema: {}, - outputSchema: PollStatsSchema.shape, + inputSchema: z.object({}), + outputSchema: PollStatsSchema, _meta: { ui: { visibility: ["app"] } }, }, async (): Promise => { diff --git a/examples/threejs-server/main.ts b/examples/threejs-server/main.ts index 13a2084..ef16e26 100644 --- a/examples/threejs-server/main.ts +++ b/examples/threejs-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 9dda288..fb97d33 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -26,10 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index ae320df..0a13c19 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -8,8 +8,10 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -161,7 +163,7 @@ export function createServer(): McpServer { title: "Show Three.js Scene", description: "Render an interactive 3D scene with custom Three.js code. Supports transparent backgrounds (alpha: true) for seamless host UI integration. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.", - inputSchema: { + inputSchema: z.object({ code: z .string() .default(DEFAULT_THREEJS_CODE) @@ -172,7 +174,7 @@ export function createServer(): McpServer { .positive() .default(400) .describe("Height in pixels"), - }, + }), outputSchema: z.object({ success: z.boolean(), }), @@ -192,7 +194,7 @@ export function createServer(): McpServer { { title: "Learn Three.js", description: "Get documentation and examples for using the Three.js View", - inputSchema: {}, + inputSchema: z.object({}), }, async () => { return { diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 34194b1..44fee0c 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -6,7 +6,7 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { StrictMode, useState, useCallback, useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; import { z } from "zod"; diff --git a/examples/transcript-server/main.ts b/examples/transcript-server/main.ts index 68db8dc..e86ad7c 100644 --- a/examples/transcript-server/main.ts +++ b/examples/transcript-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -25,7 +25,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index ce5b737..eea4a80 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -27,7 +27,7 @@ "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/transcript-server/server.ts b/examples/transcript-server/server.ts index 56bf88d..863eb29 100644 --- a/examples/transcript-server/server.ts +++ b/examples/transcript-server/server.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { @@ -10,6 +10,7 @@ import { registerAppResource, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -33,7 +34,7 @@ export function createServer(): McpServer { title: "Transcribe Speech", description: "Opens a live speech transcription interface using the Web Speech API.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, }, async (): Promise => { diff --git a/examples/video-resource-server/main.ts b/examples/video-resource-server/main.ts index 082fa5a..1983373 100644 --- a/examples/video-resource-server/main.ts +++ b/examples/video-resource-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 605b8de..ea816b9 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -24,10 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 2cb317e..8ae6240 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -12,11 +12,9 @@ import { import { McpServer, ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -135,14 +133,14 @@ Available videos: ${Object.entries(VIDEO_LIBRARY) .map(([id, v]) => `- ${id}: ${v.description}`) .join("\n")}`, - inputSchema: { + inputSchema: z.object({ videoId: z .enum(Object.keys(VIDEO_LIBRARY) as [string, ...string[]]) .default("bunny-1mb") .describe( `Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`, ), - }, + }), outputSchema: z.object({ videoUri: z.string(), description: z.string(), diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index 88ed913..2a3f8f9 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -5,7 +5,7 @@ * The video is served as a base64 blob and converted to a data URI for playback. */ import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import "./global.css"; import "./mcp-app.css"; diff --git a/examples/wiki-explorer-server/main.ts b/examples/wiki-explorer-server/main.ts index cc7b3a2..bfeeffb 100644 --- a/examples/wiki-explorer-server/main.ts +++ b/examples/wiki-explorer-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 62f1de8..bd31d6f 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -26,10 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 59d5e18..8bc4eb7 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -3,11 +3,11 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import * as cheerio from "cheerio"; import fs from "node:fs/promises"; import path from "node:path"; diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 291292d..9833d9c 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -2,7 +2,7 @@ * Wiki Explorer - Force-directed graph visualization of Wikipedia link networks */ import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { z } from "zod"; import { forceCenter, diff --git a/package-lock.json b/package-lock.json index 5514bf1..c1ae34d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@boneskull/typedoc-plugin-mermaid": "^0.2.0", "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/server": "2.0.0-beta.4", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", @@ -55,7 +54,6 @@ "peerDependencies": { "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/server": "2.0.0-beta.4", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -74,8 +72,8 @@ "name": "@modelcontextprotocol/ext-apps-basic-host", "version": "1.7.4", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" @@ -100,10 +98,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", @@ -129,10 +128,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -161,10 +161,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", @@ -190,10 +191,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", @@ -219,10 +221,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -246,10 +249,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", @@ -278,7 +282,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -306,7 +310,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -338,7 +342,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -366,7 +370,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "zod": "^4.1.13" }, "bin": { @@ -388,10 +392,11 @@ "examples/integration-server": { "version": "1.7.4", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -421,10 +426,11 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", - "jose": "^6.0.0" + "jose": "^6.0.0", + "zod": "^4.1.13" }, "bin": { "mcp-server-lazy-auth": "dist/index.js" @@ -445,10 +451,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -473,10 +480,11 @@ "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", @@ -500,8 +508,7 @@ "name": "@modelcontextprotocol/server-qr", "version": "1.7.4", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } }, "examples/quickstart": { @@ -512,9 +519,10 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", @@ -533,8 +541,7 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } }, "examples/scenario-modeler-server": { @@ -545,7 +552,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -578,7 +585,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -605,7 +612,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", @@ -633,7 +640,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -659,10 +666,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -696,7 +704,7 @@ "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -721,10 +729,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -748,10 +757,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -2230,7 +2240,6 @@ "version": "2.0.0-beta.4", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz", "integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==", - "dev": true, "license": "MIT", "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", @@ -2336,6 +2345,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -3995,6 +4005,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4011,6 +4022,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -5546,6 +5558,7 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^10.2.0" }, @@ -5563,7 +5576,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-uri": { "version": "3.1.2", @@ -5579,7 +5593,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fflate": { "version": "0.8.2", @@ -5906,6 +5921,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6047,6 +6063,7 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 12" } @@ -6242,13 +6259,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-typed": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -7224,6 +7243,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9350,6 +9370,7 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25 || ^4" } diff --git a/package.json b/package.json index 35bcdf0..ecbf386 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "@boneskull/typedoc-plugin-mermaid": "^0.2.0", "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/server": "2.0.0-beta.4", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", @@ -113,7 +112,6 @@ "peerDependencies": { "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/server": "2.0.0-beta.4", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md index 02d09a4..69e41f8 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -77,13 +77,15 @@ Before writing any code, analyze the server's existing tools and determine which ## Step 2: Add Dependencies ```bash -npm install @modelcontextprotocol/ext-apps +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0 npm install -D vite vite-plugin-singlefile ``` Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React). -Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory. +Use the exact base MCP SDK prerelease required by ext-apps. Existing servers +that still import `@modelcontextprotocol/sdk` v1 must migrate those imports and +handler schemas before adding the App integration. ## Step 3: Set Up the Build Pipeline @@ -146,10 +148,14 @@ Transform plain MCP tools into App tools with UI. **Before** (plain MCP tool): ```typescript -server.tool("my-tool", { param: z.string() }, async (args) => { - const data = await fetchData(args.param); - return { content: [{ type: "text", text: JSON.stringify(data) }] }; -}); +server.registerTool( + "my-tool", + { inputSchema: z.object({ param: z.string() }) }, + async ({ param }) => { + const data = await fetchData(param); + return { content: [{ type: "text", text: JSON.stringify(data) }] }; + }, +); ``` **After** (App tool with UI): @@ -158,17 +164,22 @@ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@model const resourceUri = "ui://my-tool/mcp-app.html"; -registerAppTool(server, "my-tool", { - description: "Shows data with an interactive UI", - inputSchema: { param: z.string() }, - _meta: { ui: { resourceUri } }, -}, async (args) => { - const data = await fetchData(args.param); - return { - content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts - structuredContent: { data }, // structured data for the UI - }; -}); +registerAppTool( + server, + "my-tool", + { + description: "Shows data with an interactive UI", + inputSchema: z.object({ param: z.string() }), + _meta: { ui: { resourceUri } }, + }, + async ({ param }) => { + const data = await fetchData(param); + return { + content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts + structuredContent: { data }, // structured data for the UI + }; + }, +); ``` Key guidance: @@ -187,17 +198,21 @@ import path from "node:path"; const resourceUri = "ui://my-tool/mcp-app.html"; -registerAppResource(server, { - uri: resourceUri, - name: "My Tool UI", - mimeType: RESOURCE_MIME_TYPE, -}, async () => { - const html = await fs.readFile( - path.resolve(import.meta.dirname, "dist", "mcp-app.html"), - "utf-8", - ); - return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; -}); +registerAppResource( + server, + "My Tool UI", + resourceUri, + {}, + async () => { + const html = await fs.readFile( + path.resolve(import.meta.dirname, "dist", "mcp-app.html"), + "utf-8", + ); + return { + contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }], + }; + }, +); ``` If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration. @@ -271,25 +286,32 @@ registerAppTool(server, "poll-data", { }); ``` -The UI calls these via `app.callServerTool("poll-data", {})`. +The UI calls these via +`app.callServerTool({ name: "poll-data", arguments: {} })`. ### CSP Configuration If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains: ```typescript -registerAppResource(server, { - uri: resourceUri, - name: "My Tool UI", - mimeType: RESOURCE_MIME_TYPE, - _meta: { - ui: { - connectDomains: ["api.example.com"], // fetch/XHR targets - resourceDomains: ["cdn.example.com"], // scripts, styles, images - frameDomains: ["embed.example.com"], // nested iframes +registerAppResource(server, "My Tool UI", resourceUri, {}, async () => ({ + contents: [ + { + uri: resourceUri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: { + ui: { + csp: { + connectDomains: ["https://api.example.com"], // fetch/XHR targets + resourceDomains: ["https://cdn.example.com"], // scripts, styles, images + frameDomains: ["https://embed.example.com"], // nested iframes + }, + }, + }, }, - }, -}, async () => { /* ... */ }); + ], +})); ``` ### Streaming Partial Input @@ -326,7 +348,14 @@ server.server.oninitialized = () => { }, appToolHandler); } else { // Text-only client — register plain tool - server.tool("my-tool", "Shows data", { param: z.string() }, plainToolHandler); + server.registerTool( + "my-tool", + { + description: "Shows data", + inputSchema: z.object({ param: z.string() }), + }, + plainToolHandler, + ); } }; ``` diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md index 5db2d94..22686ef 100644 --- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md +++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md @@ -115,19 +115,20 @@ Create a new MCP server with tool and resource registration. This wraps the exis ### Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0 npm install -D tsx vite vite-plugin-singlefile ``` -Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory. +Use `npm install` with the exact base MCP SDK prerelease required by ext-apps. +Do not substitute unpublished local packages or guess a different prerelease. ### Server Code Create `server.ts`: ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; import fs from "node:fs/promises"; import path from "node:path"; @@ -140,7 +141,9 @@ const resourceUri = "ui://my-app/mcp-app.html"; // Register the tool — inputSchema maps to the app's data sources registerAppTool(server, "show-app", { description: "Displays the app with the given parameters", - inputSchema: { query: z.string().describe("The search query") }, + inputSchema: z.object({ + query: z.string().describe("The search query"), + }), _meta: { ui: { resourceUri } }, }, async (args) => { // Process args server-side if needed @@ -151,19 +154,29 @@ registerAppTool(server, "show-app", { }); // Register the HTML resource -registerAppResource(server, { - uri: resourceUri, - name: "My App UI", - mimeType: RESOURCE_MIME_TYPE, - // Add CSP domains from Step 2 if needed: - // _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } }, -}, async () => { - const html = await fs.readFile( - path.resolve(import.meta.dirname, "dist", "mcp-app.html"), - "utf-8", - ); - return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; -}); +registerAppResource( + server, + "My App UI", + resourceUri, + {}, + async () => { + const html = await fs.readFile( + path.resolve(import.meta.dirname, "dist", "mcp-app.html"), + "utf-8", + ); + return { + contents: [ + { + uri: resourceUri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + // Add CSP domains from Step 2 if needed: + // _meta: { ui: { csp: { connectDomains: ["https://api.example.com"], resourceDomains: ["https://cdn.example.com"] } } }, + }, + ], + }; + }, +); // Start the server const transport = new StdioServerTransport(); @@ -310,7 +323,10 @@ const data = await fetch("/api/data").then(r => r.json()); // After (hybrid): async function fetchData(): Promise { if (isMcpApp) { - const result = await app.callServerTool("fetch-data", {}); + const result = await app.callServerTool({ + name: "fetch-data", + arguments: {}, + }); return result.structuredContent; } return fetch("/api/data").then(r => r.json()); @@ -437,7 +453,8 @@ registerAppTool(server, "refresh-data", { }); ``` -The UI calls these via `app.callServerTool("refresh-data", {})`. +The UI calls these via +`app.callServerTool({ name: "refresh-data", arguments: {} })`. ### Streaming Partial Input diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md index 5bd1219..fc41e70 100644 --- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md +++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md @@ -110,11 +110,12 @@ See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes: **Always** use `npm install` to add dependencies rather than manually writing version numbers: ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors ``` -This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory. +Use the exact base MCP SDK prerelease required by ext-apps. Do not substitute +unpublished local packages or guess a different prerelease. ### TypeScript Server Execution diff --git a/scripts/typedoc-plugin-base-sdk-links.mjs b/scripts/typedoc-plugin-base-sdk-links.mjs new file mode 100644 index 0000000..6098918 --- /dev/null +++ b/scripts/typedoc-plugin-base-sdk-links.mjs @@ -0,0 +1,16 @@ +/** + * Resolve module-local links inherited from the base MCP SDK declarations. + * + * TypeDoc's externalSymbolLinkMappings handles package-qualified symbols, but + * the base SDK writes this particular reference as `index.inputRequired`. + */ + +const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/"; + +/** @param {import("typedoc").Application} app */ +export function load(app) { + app.converter.addUnknownSymbolResolver((ref) => { + const name = ref.symbolReference?.path?.map((part) => part.path).join("."); + if (name === "index.inputRequired") return BASE_SDK_DOCS; + }); +} diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index d5dc5bc..47ad623 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -7,17 +7,11 @@ * @module */ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { - CallToolResult, - CallToolResultSchema, +import { Client, type Transport } from "@modelcontextprotocol/client"; +import type { CreateMessageRequest, CreateMessageResult, - ListResourcesResultSchema, - ReadResourceResultSchema, - ListPromptsResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import { AppBridge, PostMessageTransport } from "./app-bridge.js"; import type { McpUiDisplayMode } from "./types.js"; @@ -223,8 +217,7 @@ function AppBridge_oncalltool_forwardToServer( bridge.oncalltool = async (params, extra) => { return mcpClient.request( { method: "tools/call", params }, - CallToolResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_oncalltool_forwardToServer @@ -245,7 +238,9 @@ function AppBridge_oncreatesamplingmessage_forwardToLlm( //#region AppBridge_oncreatesamplingmessage_forwardToLlm bridge.oncreatesamplingmessage = async (params, extra) => { // Apply rate limiting, user approval, cost controls here - return await myLlmProvider.complete(params, { signal: extra.signal }); + return await myLlmProvider.complete(params, { + signal: extra.mcpReq.signal, + }); }; //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm } @@ -261,8 +256,7 @@ function AppBridge_onlistresources_returnResources( bridge.onlistresources = async (params, extra) => { return mcpClient.request( { method: "resources/list", params }, - ListResourcesResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_onlistresources_returnResources @@ -279,8 +273,7 @@ function AppBridge_onreadresource_returnResource( bridge.onreadresource = async (params, extra) => { return mcpClient.request( { method: "resources/read", params }, - ReadResourceResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_onreadresource_returnResource @@ -297,8 +290,7 @@ function AppBridge_onlistprompts_returnPrompts( bridge.onlistprompts = async (params, extra) => { return mcpClient.request( { method: "prompts/list", params }, - ListPromptsResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_onlistprompts_returnPrompts @@ -444,10 +436,10 @@ async function AppBridge_sendToolResult_afterExecution( args: Record, ) { //#region AppBridge_sendToolResult_afterExecution - const result = await mcpClient.request( - { method: "tools/call", params: { name: "get_weather", arguments: args } }, - CallToolResultSchema, - ); + const result = await mcpClient.request({ + method: "tools/call", + params: { name: "get_weather", arguments: args }, + }); bridge.sendToolResult(result); //#endregion AppBridge_sendToolResult_afterExecution } diff --git a/src/app-bridge.ts b/src/app-bridge.ts index c65510d..d353cf7 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -261,7 +261,7 @@ export type AppBridgeEventMap = { /** * Host-side bridge for communicating with a single View ({@link app!App `App`}). * - * `AppBridge` extends the MCP SDK's `Protocol` class and acts as a proxy between + * `AppBridge` extends the base MCP SDK's public `Server` class and acts as a proxy between * the host application and a view running in an iframe. When an MCP client * is provided to the constructor, it automatically forwards MCP server capabilities * (tools, resources, prompts) to the view. It also handles the initialization @@ -667,8 +667,7 @@ export class AppBridge extends Server { * ```typescript * bridge.onsandboxready = async () => { * const resource = await mcpClient.request( - * { method: "resources/read", params: { uri: "ui://my-app" } }, - * ReadResourceResultSchema + * { method: "resources/read", params: { uri: "ui://my-app" } } * ); * * bridge.sendSandboxResourceReady({ @@ -1154,14 +1153,13 @@ export class AppBridge extends Server { * bridge.oncalltool = async (params, extra) => { * return mcpClient.request( * { method: "tools/call", params }, - * CallToolResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type - * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type + * @see `CallToolRequest` from @modelcontextprotocol/server for the request type + * @see `CallToolResult` from @modelcontextprotocol/server for the result type */ private _oncalltool?: ( params: CallToolRequest["params"], @@ -1209,12 +1207,14 @@ export class AppBridge extends Server { * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm" * bridge.oncreatesamplingmessage = async (params, extra) => { * // Apply rate limiting, user approval, cost controls here - * return await myLlmProvider.complete(params, { signal: extra.signal }); + * return await myLlmProvider.complete(params, { + * signal: extra.mcpReq.signal, + * }); * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/server for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/server for result types */ set oncreatesamplingmessage( callback: ( @@ -1244,7 +1244,7 @@ export class AppBridge extends Server { * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/server for the notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1270,14 +1270,13 @@ export class AppBridge extends Server { * bridge.onlistresources = async (params, extra) => { * return mcpClient.request( * { method: "resources/list", params }, - * ListResourcesResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type + * @see `ListResourcesRequest` from @modelcontextprotocol/server for the request type + * @see `ListResourcesResult` from @modelcontextprotocol/server for the result type */ private _onlistresources?: ( params: ListResourcesRequest["params"], @@ -1325,14 +1324,13 @@ export class AppBridge extends Server { * bridge.onlistresourcetemplates = async (params, extra) => { * return mcpClient.request( * { method: "resources/templates/list", params }, - * ListResourceTemplatesResultSchema, - * { signal: extra.signal } + * { signal: extra.mcpReq.signal } * ); * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type + * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/server for the request type + * @see `ListResourceTemplatesResult` from @modelcontextprotocol/server for the result type */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], @@ -1383,14 +1381,13 @@ export class AppBridge extends Server { * bridge.onreadresource = async (params, extra) => { * return mcpClient.request( * { method: "resources/read", params }, - * ReadResourceResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type - * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type + * @see `ReadResourceRequest` from @modelcontextprotocol/server for the request type + * @see `ReadResourceResult` from @modelcontextprotocol/server for the result type */ private _onreadresource?: ( params: ReadResourceRequest["params"], @@ -1438,7 +1435,7 @@ export class AppBridge extends Server { * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/server for the notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1466,14 +1463,13 @@ export class AppBridge extends Server { * bridge.onlistprompts = async (params, extra) => { * return mcpClient.request( * { method: "prompts/list", params }, - * ListPromptsResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type + * @see `ListPromptsRequest` from @modelcontextprotocol/server for the request type + * @see `ListPromptsResult` from @modelcontextprotocol/server for the result type */ private _onlistprompts?: ( params: ListPromptsRequest["params"], @@ -1520,7 +1516,7 @@ export class AppBridge extends Server { * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/server for the notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ @@ -1722,10 +1718,10 @@ export class AppBridge extends Server { * * @example * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution" - * const result = await mcpClient.request( - * { method: "tools/call", params: { name: "get_weather", arguments: args } }, - * CallToolResultSchema, - * ); + * const result = await mcpClient.request({ + * method: "tools/call", + * params: { name: "get_weather", arguments: args }, + * }); * bridge.sendToolResult(result); * ``` * diff --git a/src/app.examples.ts b/src/app.examples.ts index 81bdd44..7d475ef 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -7,11 +7,8 @@ * @module */ -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; -import type { - McpServer, - ToolCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { SamplingMessage, Tool } from "@modelcontextprotocol/client"; +import type { McpServer, ToolCallback } from "@modelcontextprotocol/server"; import { App, PostMessageTransport, @@ -369,7 +366,7 @@ async function App_createSamplingMessage_simple(app: App) { */ async function App_createSamplingMessage_withTools( app: App, - messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[], + messages: SamplingMessage[], ) { //#region App_createSamplingMessage_withTools if (!app.getHostCapabilities()?.sampling?.tools) return; diff --git a/src/app.ts b/src/app.ts index 3c8f5b7..7e752ce 100644 --- a/src/app.ts +++ b/src/app.ts @@ -174,9 +174,9 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Options for configuring {@link App `App`} behavior. * - * Extends `ProtocolOptions` from the MCP SDK with `App`-specific configuration. + * Extends `ClientOptions` from the base MCP SDK with `App`-specific configuration. * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options + * @see `ClientOptions` from @modelcontextprotocol/client for inherited options */ export type AppOptions = Omit< ClientOptions, @@ -243,7 +243,7 @@ export type AppToolResult< * validated/parsed input; when `In` is `undefined`, the callback receives only * `extra`. When `Out` is provided, the return's `structuredContent` is typed. * - * Mirrors `ToolCallback` from `@modelcontextprotocol/sdk/server/mcp.js` but is + * Mirrors `ToolCallback` from `@modelcontextprotocol/server` but is * parameterized over {@link StandardSchemaV1} instead of zod, so any * Standard-Schema-compatible library (Zod, ArkType, Valibot, …) can be used. */ @@ -261,7 +261,7 @@ export type AppToolCallback< /** * Handle returned by {@link App.registerTool}. Mirrors `RegisteredTool` from - * `@modelcontextprotocol/sdk/server/mcp.js` but stores + * `@modelcontextprotocol/server` but stores * {@link StandardSchemaV1} schemas. */ export type RegisteredAppTool = { @@ -302,9 +302,8 @@ export type AppEventMap = { * Main class for MCP Apps to communicate with their host. * * The `App` class provides a framework-agnostic way to build interactive MCP Apps - * that run inside host applications. It extends the MCP SDK's `Protocol` class and - * handles the connection lifecycle, initialization handshake, and bidirectional - * communication with the host. + * that run inside host applications. It extends the base MCP SDK's public + * `Client` class and adds the Apps handshake, lifecycle, and high-level APIs. * * ## Architecture * @@ -321,7 +320,7 @@ export type AppEventMap = { * * ## Protocol and event methods * - * As a subclass of the MCP SDK's `Protocol`, `App` provides: + * As a subclass of the base MCP SDK's `Client`, `App` provides: * - `setRequestHandler()` - Register handlers for requests from host * - `setNotificationHandler()` - Register handlers for notifications from host * - `addEventListener()` - Append a listener for a notification event (multi-listener) @@ -1458,8 +1457,8 @@ export class App extends Client { * } * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ async createSamplingMessage( params: CreateMessageRequest["params"] & { tools?: undefined }, diff --git a/src/events.ts b/src/events.ts index b891867..aff1446 100644 --- a/src/events.ts +++ b/src/events.ts @@ -38,9 +38,8 @@ export class EventDispatcher> { getHandler( event: K, ): ((params: EventMap[K]) => void) | undefined { - return (this._eventSlots.get(event) as - | EventSlot - | undefined)?.onHandler; + return (this._eventSlots.get(event) as EventSlot | undefined) + ?.onHandler; } /** Add a listener. Listeners run in insertion order after the handler. */ diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts index d5f5092..2a115f3 100644 --- a/src/message-transport.test.ts +++ b/src/message-transport.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; -import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; +import type { JSONRPCMessage } from "@modelcontextprotocol/client"; import { PostMessageTransport } from "./message-transport"; diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts index d583ae8..9ceebce 100644 --- a/src/server/index.examples.ts +++ b/src/server/index.examples.ts @@ -9,17 +9,14 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs/promises"; -import type { - McpServer, - ToolCallback, - ReadResourceCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer, ToolCallback } from "@modelcontextprotocol/server"; import { z } from "zod"; import { registerAppTool, registerAppResource, getUiCapability, RESOURCE_MIME_TYPE, + type ReadResourceCallback, } from "./index.js"; // Stubs for external functions used in examples @@ -74,7 +71,7 @@ function registerAppTool_basicUsage(server: McpServer) { { title: "Get Weather", description: "Get current weather for a location", - inputSchema: { location: z.string() }, + inputSchema: z.object({ location: z.string() }), _meta: { ui: { resourceUri: "ui://weather/view.html" }, }, @@ -122,7 +119,7 @@ function registerAppTool_appOnlyVisibility(server: McpServer) { "update-quantity", { description: "Update item quantity in cart", - inputSchema: { itemId: z.string(), quantity: z.number() }, + inputSchema: z.object({ itemId: z.string(), quantity: z.number() }), _meta: { ui: { resourceUri: "ui://shop/cart.html", diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 1853cc0..5de341f 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -7,7 +7,7 @@ import { getUiCapability, EXTENSION_ID, } from "./index"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; describe("registerAppTool", () => { it("should pass through config to server.registerTool", () => { diff --git a/src/server/index.ts b/src/server/index.ts index b8b3ad0..61e7438 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -150,7 +150,7 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * { * title: "Get Weather", * description: "Get current weather for a location", - * inputSchema: { location: z.string() }, + * inputSchema: z.object({ location: z.string() }), * _meta: { * ui: { resourceUri: "ui://weather/view.html" }, * }, @@ -190,7 +190,7 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * "update-quantity", * { * description: "Update item quantity in cart", - * inputSchema: { itemId: z.string(), quantity: z.number() }, + * inputSchema: z.object({ itemId: z.string(), quantity: z.number() }), * _meta: { * ui: { * resourceUri: "ui://shop/cart.html", diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 00babc4..041bf5e 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -1,5 +1,7 @@ import { OptionDefaults } from "typedoc"; +const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/"; + /** @type {Partial} */ const config = { name: "MCP Apps", @@ -29,6 +31,28 @@ const config = { excludePrivate: true, excludeInternal: false, intentionallyNotExported: ["RequestHandlerExtra"], + externalSymbolLinkMappings: { + "@modelcontextprotocol/client": { + ClientOptions: BASE_SDK_DOCS, + ConnectOptions: BASE_SDK_DOCS, + DiscoverResult: BASE_SDK_DOCS, + ProtocolError: BASE_SDK_DOCS, + "Protocol.close": BASE_SDK_DOCS, + "Protocol.notification": BASE_SDK_DOCS, + ResponseCacheStore: BASE_SDK_DOCS, + SdkError: BASE_SDK_DOCS, + "SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS, + "__type.enforceStrictCapabilities": BASE_SDK_DOCS, + }, + "@modelcontextprotocol/server": { + LoggingMessageNotification: BASE_SDK_DOCS, + "McpServer.registerTool": BASE_SDK_DOCS, + "MessageExtraInfo.classification": BASE_SDK_DOCS, + "Protocol.close": BASE_SDK_DOCS, + "Protocol.notification": BASE_SDK_DOCS, + "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS, + }, + }, blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false, @@ -49,6 +73,7 @@ const config = { out: "docs/api", plugin: [ "typedoc-github-theme", + "./scripts/typedoc-plugin-base-sdk-links.mjs", "./scripts/typedoc-plugin-fix-mermaid-entities.mjs", "./scripts/typedoc-plugin-seo.mjs", "./scripts/typedoc-plugin-mcpstyle.mjs", From f50ec6ebfbca14bf3d3070b6d01b90f12ba93359 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Tue, 14 Jul 2026 17:16:27 -0700 Subject: [PATCH 64/97] fix: regenerate lockfile for npm 10 --- package-lock.json | 401 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 401 insertions(+) diff --git a/package-lock.json b/package-lock.json index c1ae34d..9e0a71b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-host/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-host/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", "version": "1.7.4", @@ -123,6 +140,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-preact/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-preact/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", "version": "1.7.4", @@ -156,6 +190,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-react/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-react/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", "version": "1.7.4", @@ -186,6 +237,23 @@ "vite-plugin-solid": "^2.11.12" } }, + "examples/basic-server-solid/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-solid/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", "version": "1.7.4", @@ -216,6 +284,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-svelte/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-svelte/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", "version": "1.7.4", @@ -244,6 +329,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-vanillajs/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-vanillajs/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", "version": "1.7.4", @@ -274,6 +376,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-vue/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-vue/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", "version": "1.7.4", @@ -302,6 +421,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/budget-allocator-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/budget-allocator-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", "version": "1.7.4", @@ -334,6 +470,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/cohort-heatmap-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/cohort-heatmap-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", "version": "1.7.4", @@ -362,6 +515,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/customer-segmentation-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/customer-segmentation-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", "version": "1.7.4", @@ -389,6 +559,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/debug-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/debug-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/integration-server": { "version": "1.7.4", "dependencies": { @@ -419,6 +606,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/integration-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/integration-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", "version": "1.7.4", @@ -446,6 +650,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/lazy-auth-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/lazy-auth-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", "version": "1.7.4", @@ -474,6 +695,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/map-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/map-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", "version": "1.7.4", @@ -504,6 +742,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/pdf-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/pdf-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", "version": "1.7.4", @@ -536,6 +791,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/quickstart/node_modules/@types/node": { + "version": "22.19.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", + "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "1.7.4", @@ -577,6 +842,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/scenario-modeler-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/scenario-modeler-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", "version": "1.7.4", @@ -604,6 +886,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/shadertoy-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/shadertoy-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", "version": "1.7.4", @@ -632,6 +931,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/sheet-music-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/sheet-music-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", "version": "1.7.4", @@ -661,6 +977,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/system-monitor-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/system-monitor-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", "version": "1.7.4", @@ -696,6 +1029,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/threejs-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/threejs-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", "version": "1.7.4", @@ -724,6 +1074,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/transcript-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/transcript-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", "version": "1.7.4", @@ -752,6 +1119,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/video-resource-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/video-resource-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", "version": "1.7.4", @@ -782,6 +1166,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/wiki-explorer-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/wiki-explorer-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", From 9778cf97a2643e58d43d3e5cdc86fe6a43773fbc Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Thu, 16 Jul 2026 09:00:17 -0700 Subject: [PATCH 65/97] Make lazy-auth-server usable on mobile (#714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 - -`); + + + +`); return; } diff --git a/examples/lazy-auth-server/src/mcp-app.css b/examples/lazy-auth-server/src/mcp-app.css index ba4f323..800b224 100644 --- a/examples/lazy-auth-server/src/mcp-app.css +++ b/examples/lazy-auth-server/src/mcp-app.css @@ -17,9 +17,17 @@ display: flex; flex-wrap: wrap; gap: var(--spacing-sm); + /* Keep a long press on or near the buttons from starting text selection + or raising the iOS callout menu instead of a click. */ + -webkit-user-select: none; + user-select: none; + -webkit-touch-callout: none; } button { + /* Match the consent page's 56px tap targets (server.ts .btn). */ + min-height: 56px; + min-width: 56px; padding: var(--spacing-sm) var(--spacing-md); border: none; border-radius: var(--border-radius-md); @@ -27,12 +35,20 @@ button { font-weight: var(--font-weight-bold); background-color: var(--color-accent); cursor: pointer; + /* Suppress the double-tap-to-zoom gesture so a tap resolves to a click + immediately instead of waiting to see if a second tap follows. */ + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; &:disabled { opacity: 0.5; cursor: not-allowed; } + &:active:not(:disabled) { + filter: brightness(0.85); + } + &:hover:not(:disabled) { background-color: color-mix( in srgb, From 2ca6a59d2f493b227a83a2e3ce0396db4705621a Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Thu, 16 Jul 2026 20:15:10 -0400 Subject: [PATCH 66/97] spec: sync object-src with default-src (#715) --- specification/draft/apps.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 96c8286..369ef64 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -289,6 +289,7 @@ When `_meta.ui` is present on **both**, the content-item value takes precedence. style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' data:; + object-src 'none'; connect-src 'none'; ``` From ccae093d1ab04b182e17817a3e457f7b6b1c8aea Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 15 Jul 2026 08:30:45 +0000 Subject: [PATCH 67/97] 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. --- AGENTS.md | 8 +-- docs/overview.md | 2 +- package-lock.json | 16 ++--- package.json | 11 +-- src/app-bridge.test.ts | 10 +-- src/app-bridge.ts | 150 +++++++++++++++++------------------------ src/app.test.ts | 22 ++---- src/app.ts | 52 +++++--------- 8 files changed, 105 insertions(+), 166 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f850aa3..4b0faa0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \ ### Key Source Files -- `src/app.ts` - `App` subclasses the base MCP SDK `Client`, handles View initialization, tool calls, and messaging -- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Server` for the inner iframe channel and proxies through a separate outer `Client` +- `src/app.ts` - `App` subclasses the base MCP SDK `Protocol`, handles View initialization, tool calls, and messaging +- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Protocol` 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 @@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \ ### Protocol Flow ``` -View (App/Client) <--PostMessageTransport--> Host (AppBridge/Server) <--separate 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 and Host complete the temporary inner base MCP handshake, then the View sends `ui/initialize` and receives authoritative Apps 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 diff --git a/docs/overview.md b/docs/overview.md index 317ab43..63156ec 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -97,7 +97,7 @@ sequenceDiagram ``` 1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server. -2. **Initialization** — When a UI tool is called, the Host renders the iframe. The SDK first completes its temporary inner base MCP handshake, then the View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). `ui/initialize` remains authoritative for Apps capabilities and identity, and `ui/notifications/initialized` remains the public View-ready signal. +2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` (the only handshake on the iframe channel) and receives host context (theme, capabilities, container dimensions). `ui/initialize` is authoritative for Apps capabilities and identity, and `ui/notifications/initialized` is the public View-ready signal. 3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context. 4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context. 5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources. diff --git a/package-lock.json b/package-lock.json index 9e0a71b..04b0d2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,9 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", + "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", + "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -2639,8 +2639,8 @@ }, "node_modules/@modelcontextprotocol/client": { "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz", - "integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==", + "resolved": "file:../tarballs/mcp-client.tgz", + "integrity": "sha512-aCTBtOjOSOj+SOLsoUfDjhD3Ngs4pMXzAczBmKRTesiz0N/3wUEXEmKgxhfbhF6Wtgk4tatWBUG9LDaj919K2Q==", "license": "MIT", "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", @@ -2657,7 +2657,7 @@ }, "node_modules/@modelcontextprotocol/core": { "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz", + "resolved": "file:../tarballs/mcp-core.tgz", "integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==", "license": "MIT", "dependencies": { @@ -2784,8 +2784,8 @@ }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz", - "integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==", + "resolved": "file:../tarballs/mcp-server.tgz", + "integrity": "sha512-/UJubByzGV+mlU8fN8yyuMI9zgoq4Ic5b1V/6yhzxrKjmXpTbNkY5rD2jRsWBk+At0AVcXLkcDzmLF91oOJtKw==", "license": "MIT", "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", diff --git a/package.json b/package.json index ecbf386..59e1802 100644 --- a/package.json +++ b/package.json @@ -77,9 +77,9 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", + "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", + "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -129,7 +129,10 @@ "seroval": "1.4.1", "seroval-plugins": "1.4.2", "solid-js": "1.9.10", - "@types/node": "20.19.27" + "@types/node": "20.19.27", + "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", + "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", + "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz" }, "dependencies": { "@standard-schema/spec": "^1.1.0" diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 81e072f..a80a961 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -82,7 +82,7 @@ describe("App <-> AppBridge integration", () => { expect(initializedFired).toBe(true); }); - it("keeps standard initialization separate from the Apps-ready gate", async () => { + it("sends only the Apps handshake on the wire and gates the Apps-ready callback", async () => { const methods: string[] = []; let releaseAppsInitialized!: () => void; let reachedAppsInitialized!: () => void; @@ -115,14 +115,9 @@ describe("App <-> AppBridge integration", () => { await reachedGate; expect(methods).toEqual([ - "initialize", - "notifications/initialized", "ui/initialize", "ui/notifications/initialized", ]); - expect(app.getNegotiatedProtocolVersion()).toBe("2025-11-25"); - expect(bridge.getNegotiatedProtocolVersion()).toBe("2025-11-25"); - expect(bridge.getClientVersion()).toEqual(testAppInfo); expect(bridge.getAppVersion()).toEqual(testAppInfo); expect(singularCalls).toBe(0); expect(listenerCalls).toBe(0); @@ -143,8 +138,7 @@ describe("App <-> AppBridge integration", () => { const hostCaps = app.getHostCapabilities(); expect(hostCaps).toEqual(testHostCapabilities); - expect(bridge.getHostCapabilities()).toEqual(testHostCapabilities); - expect(bridge.getCapabilities()).toEqual({}); + expect(bridge.getCapabilities()).toEqual(testHostCapabilities); }); it("Bridge receives app info and capabilities after initialization", async () => { diff --git a/src/app-bridge.ts b/src/app-bridge.ts index d353cf7..467c33d 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,6 +1,8 @@ -import { type Client } from "@modelcontextprotocol/client"; import { - Server, + Protocol, + type BaseContext, + type Client, + type ProtocolOptions, type CallToolRequest, type CallToolResult, type CreateMessageRequest, @@ -24,13 +26,10 @@ import { type RequestOptions, type ResourceListChangedNotification, type Result, - type ServerCapabilities, - type ServerContext, - type ServerOptions, type Tool, type ToolListChangedNotification, type Transport, -} from "@modelcontextprotocol/server"; +} from "@modelcontextprotocol/client"; import { EmptyResultSchema, LoggingMessageNotificationSchema, @@ -43,8 +42,6 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; -const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; - import { type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, @@ -96,20 +93,6 @@ export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; import { RESOURCE_URI_META_KEY } from "./app"; export { PostMessageTransport } from "./message-transport"; -function proxiedInnerCapabilities( - capabilities: ServerCapabilities, -): ServerCapabilities { - return { - ...(capabilities.tools && { tools: capabilities.tools }), - ...(capabilities.resources && { - resources: { - listChanged: capabilities.resources.listChanged, - }, - }), - ...(capabilities.prompts && { prompts: capabilities.prompts }), - }; -} - /** * Extract UI resource URI from tool metadata. * @@ -215,13 +198,10 @@ export function buildAllowAttribute( * * @property hostContext - Optional initial host context to provide to the view * - * @see `ServerOptions` from @modelcontextprotocol/server for available options + * @see `ProtocolOptions` from @modelcontextprotocol/client for available options * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure */ -export type HostOptions = Omit< - ServerOptions, - "capabilities" | "supportedProtocolVersions" -> & { +export type HostOptions = Omit & { hostContext?: McpUiHostContext; }; @@ -241,7 +221,7 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; * * @internal */ -type RequestHandlerExtra = ServerContext; +type RequestHandlerExtra = BaseContext; /** * Maps DOM-style event names to their notification `params` types. @@ -315,7 +295,7 @@ export type AppBridgeEventMap = { * await bridge.connect(transport); * ``` */ -export class AppBridge extends Server { +export class AppBridge extends Protocol { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; @@ -328,26 +308,17 @@ export class AppBridge extends Server { * handler registration. AppBridge uses it to preserve the existing warning * when a View calls host methods before `ui/notifications/initialized`, * without adding overload-forwarding adapters around `setRequestHandler`. - * Chaining through `super._wrapHandler()` retains the base `Server`'s own - * role-specific behavior, including tool-result validation. The two - * initialization methods and `ping` are exempt because they are valid before - * the Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch. + * `ui/initialize` and `ping` are exempt because they are valid before the + * Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch. * * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} */ protected override _wrapHandler( method: string, - handler: ( - request: JSONRPCRequest, - context: ServerContext, - ) => Promise, - ): (request: JSONRPCRequest, context: ServerContext) => Promise { + handler: (request: JSONRPCRequest, context: BaseContext) => Promise, + ): (request: JSONRPCRequest, context: BaseContext) => Promise { const wrapped = super._wrapHandler(method, handler); - if ( - method === "initialize" || - method === "ui/initialize" || - method === "ping" - ) { + if (method === "ui/initialize" || method === "ping") { return wrapped; } return async (request, context) => { @@ -439,14 +410,6 @@ export class AppBridge extends Server { } } - private ensureInnerCapabilities(capabilities: ServerCapabilities): void { - const current = super.getCapabilities(); - const missing = Object.keys(capabilities).some( - (key) => current[key as keyof ServerCapabilities] === undefined, - ); - if (missing) this.registerCapabilities(capabilities); - } - /** * Create a new AppBridge instance. * @@ -486,30 +449,7 @@ export class AppBridge extends Server { private _hostCapabilities: McpUiHostCapabilities, options?: HostOptions, ) { - // TEMPORARY INNER-CHANNEL NEGOTIATION: - // App and AppBridge use the public v2 Client/Server roles, so the iframe - // channel first completes the SDK's standard legacy MCP initialization. - // ui/initialize remains authoritative for Apps capabilities, identity, and - // host context, and ui/notifications/initialized remains the View-ready gate. - // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only - // after the MCP Apps negotiation specification is resolved: - // https://github.com/modelcontextprotocol/ext-apps/issues/708 - super(_hostInfo, { - ...options, - capabilities: {}, - supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], - }); - - // The base Server's standard initialized callback is MCP-ready state. The - // public AppBridge callback remains exclusively the Apps View-ready gate. - super.setNotificationHandler("notifications/initialized", () => {}); - Object.defineProperty(this, "oninitialized", { - configurable: true, - enumerable: true, - get: () => this.getEventHandler("initialized"), - set: (callback: AppBridge["oninitialized"]) => - this.setEventHandler("initialized", callback), - }); + super(options); this._ensureEventSlot("initialized"); @@ -544,6 +484,35 @@ export class AppBridge extends Server { ); } + /** @internal */ + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + + /** + * Verify that the view supports the capability required for the given request method. + * @internal + */ + protected assertCapabilityForMethod(_method: string): void { + // Host-to-view requests are not capability-gated. + } + + /** + * Verify that a request handler is registered and supported for the given method. + * @internal + */ + protected assertRequestHandlerCapability(_method: string): void { + // Host handler registration is not capability-gated. + } + + /** + * Verify that the host supports the capability required for the given notification method. + * @internal + */ + protected assertNotificationCapability(_method: string): void { + // Host notifications are not capability-gated. + } + /** * Get the view's capabilities discovered during initialization. * @@ -713,9 +682,18 @@ export class AppBridge extends Server { * @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ - override oninitialized?: ( - params?: McpUiInitializedNotification["params"], - ) => void; + get oninitialized(): + | ((params?: McpUiInitializedNotification["params"]) => void) + | undefined { + return this.getEventHandler("initialized"); + } + set oninitialized( + callback: + | ((params?: McpUiInitializedNotification["params"]) => void) + | undefined, + ) { + this.setEventHandler("initialized", callback); + } /** * Register a handler for message requests from the view. @@ -1178,7 +1156,6 @@ export class AppBridge extends Server { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.ensureInnerCapabilities({ tools: {} }); this.setRequestHandler("tools/call", async (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); @@ -1299,7 +1276,6 @@ export class AppBridge extends Server { callback, ); this._onlistresources = callback; - this.ensureInnerCapabilities({ resources: {} }); this.setRequestHandler("resources/list", async (request, extra) => { if (!this._onlistresources) throw new Error("No onlistresources handler set"); @@ -1353,7 +1329,6 @@ export class AppBridge extends Server { callback, ); this._onlistresourcetemplates = callback; - this.ensureInnerCapabilities({ resources: {} }); this.setRequestHandler( "resources/templates/list", async (request, extra) => { @@ -1410,7 +1385,6 @@ export class AppBridge extends Server { callback, ); this._onreadresource = callback; - this.ensureInnerCapabilities({ resources: {} }); this.setRequestHandler("resources/read", async (request, extra) => { if (!this._onreadresource) throw new Error("No onreadresource handler set"); @@ -1492,7 +1466,6 @@ export class AppBridge extends Server { callback, ); this._onlistprompts = callback; - this.ensureInnerCapabilities({ prompts: {} }); this.setRequestHandler("prompts/list", async (request, extra) => { if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); return this._onlistprompts(request.params, extra); @@ -1532,7 +1505,7 @@ export class AppBridge extends Server { * * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure */ - getHostCapabilities(): McpUiHostCapabilities { + getCapabilities(): McpUiHostCapabilities { return this._hostCapabilities; } @@ -1565,7 +1538,7 @@ export class AppBridge extends Server { return { protocolVersion, - hostCapabilities: this.getHostCapabilities(), + hostCapabilities: this.getCapabilities(), hostInfo: this._hostInfo, hostContext: this._hostContext, }; @@ -1931,7 +1904,6 @@ export class AppBridge extends Server { if (!serverCapabilities) { throw new Error("Client server capabilities not available"); } - this.registerCapabilities(proxiedInnerCapabilities(serverCapabilities)); if (serverCapabilities.tools) { this.oncalltool = async (params, extra) => { @@ -1989,9 +1961,9 @@ export class AppBridge extends Server { } } - // The standard MCP handshake only establishes the temporary inner Server - // role. ui/notifications/initialized remains the public View-ready signal. - + // Protocol.connect only wires the transport — no MCP handshake happens on + // the iframe channel. The View drives ui/initialize; + // ui/notifications/initialized remains the public View-ready signal. return super.connect(transport); } } diff --git a/src/app.test.ts b/src/app.test.ts index dcf2d52..9057710 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -14,8 +14,6 @@ import { McpUiInitializedNotificationSchema, } from "./types"; -const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; - type ConnectedPair = { app: App; server: Server; @@ -34,7 +32,6 @@ async function connectPair( { name: "inner-mcp-server", version: "0.0.0" }, { capabilities: { resources: {}, tools: {} }, - supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], }, ); const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -103,8 +100,8 @@ afterEach(async () => { ); }); -describe("App base MCP SDK v2 Client migration", () => { - it("completes the legacy MCP handshake before the authoritative Apps handshake", async () => { +describe("App base MCP SDK v2 Protocol migration", () => { + it("performs only the Apps handshake — no MCP initialize on the wire", async () => { const pair = await connectPair(); connected.push(pair); @@ -123,25 +120,19 @@ describe("App base MCP SDK v2 Client migration", () => { ].includes(method), ); + // The filter above includes the core MCP lifecycle methods, so this + // equality also proves no `initialize`/`notifications/initialized` was sent + // — the wire contract deployed v1 hosts depend on. expect(lifecycleMethods).toEqual([ - "initialize", - "notifications/initialized", "ui/initialize", "ui/notifications/initialized", ]); - expect(pair.app.getNegotiatedProtocolVersion()).toBe( - INNER_MCP_PROTOCOL_VERSION, - ); }); it("uses ui/initialize as the authority for Apps host state", async () => { const pair = await connectPair(); connected.push(pair); - expect(pair.app.getServerVersion()).toEqual({ - name: "inner-mcp-server", - version: "0.0.0", - }); expect(pair.app.getHostVersion()).toEqual({ name: "apps-host", version: "2.0.0", @@ -219,9 +210,6 @@ describe("App base MCP SDK v2 Client migration", () => { const second = await connectPair(first.app); connected.push(second); - expect(second.app.getNegotiatedProtocolVersion()).toBe( - INNER_MCP_PROTOCOL_VERSION, - ); expect(second.app.getHostVersion()).toEqual({ name: "apps-host", version: "2.0.0", diff --git a/src/app.ts b/src/app.ts index 7e752ce..c639784 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,7 @@ import { - Client, - type ClientContext, - type ClientOptions, + Protocol, + type BaseContext, + type ProtocolOptions, type RequestOptions, type CallToolRequest, type CallToolResult, @@ -71,8 +71,6 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; -const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; - function mergeAppCapabilities( base: McpUiAppCapabilities, additional: McpUiAppCapabilities, @@ -174,14 +172,11 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Options for configuring {@link App `App`} behavior. * - * Extends `ClientOptions` from the base MCP SDK with `App`-specific configuration. + * Extends `ProtocolOptions` from the base MCP SDK with `App`-specific configuration. * - * @see `ClientOptions` from @modelcontextprotocol/client for inherited options + * @see `ProtocolOptions` from @modelcontextprotocol/client for inherited options */ -export type AppOptions = Omit< - ClientOptions, - "capabilities" | "supportedProtocolVersions" | "versionNegotiation" -> & { +export type AppOptions = Omit & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -221,7 +216,7 @@ export type AppOptions = Omit< allowUnsafeEval?: boolean; }; -type RequestHandlerExtra = ClientContext; +type RequestHandlerExtra = BaseContext; /** * Result of an app-registered tool callback. When `Out` is provided, @@ -354,7 +349,7 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends Client { +export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; @@ -521,20 +516,7 @@ export class App extends Client { private _appCapabilities: McpUiAppCapabilities = {}, private options: AppOptions = { autoResize: true }, ) { - // TEMPORARY INNER-CHANNEL NEGOTIATION: - // App and AppBridge use the public v2 Client/Server roles, so the iframe - // channel first completes the SDK's standard legacy MCP initialization. - // ui/initialize remains authoritative for Apps capabilities, identity, and - // host context, and ui/notifications/initialized remains the View-ready gate. - // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only - // after the MCP Apps negotiation specification is resolved: - // https://github.com/modelcontextprotocol/ext-apps/issues/708 - super(_appInfo, { - ...options, - capabilities: {}, - versionNegotiation: { mode: "legacy" }, - supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], - }); + super(options); if (!options.allowUnsafeEval) { z.config({ jitless: true }); @@ -550,6 +532,11 @@ export class App extends Client { this.setEventHandler("hostcontextchanged", undefined); } + /** @internal */ + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + private registerAppCapabilities(capabilities: McpUiAppCapabilities): void { if (this.transport) { throw new Error( @@ -1992,15 +1979,10 @@ export class App extends Client { } this._initializedSent = false; - // TEMPORARY INNER-CHANNEL NEGOTIATION: - // App and AppBridge use the public v2 Client/Server roles, so the iframe - // channel first completes the SDK's standard legacy MCP initialization. - // ui/initialize remains authoritative for Apps capabilities, identity, and - // host context, and ui/notifications/initialized remains the View-ready gate. - // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only - // after the MCP Apps negotiation specification is resolved: - // https://github.com/modelcontextprotocol/ext-apps/issues/708 try { + // Protocol.connect only wires the transport — no MCP handshake. The + // ui/initialize request below is the sole negotiation on this channel, + // matching the v1 wire behavior deployed hosts expect. await super.connect(transport); const result = await this.request( From 43eed45404d085b63815acd971d7ab097817ec99 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 15 Jul 2026 08:34:35 +0000 Subject: [PATCH 68/97] 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. --- package-lock.json | 20 ++++++++++---------- package.json | 12 ++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04b0d2e..e82271b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,9 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", - "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", - "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz", + "@modelcontextprotocol/client": "https://pkg.pr.new/@modelcontextprotocol/client@2501", + "@modelcontextprotocol/core": "https://pkg.pr.new/@modelcontextprotocol/core@2501", + "@modelcontextprotocol/server": "https://pkg.pr.new/@modelcontextprotocol/server@2501", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -2639,11 +2639,11 @@ }, "node_modules/@modelcontextprotocol/client": { "version": "2.0.0-beta.4", - "resolved": "file:../tarballs/mcp-client.tgz", - "integrity": "sha512-aCTBtOjOSOj+SOLsoUfDjhD3Ngs4pMXzAczBmKRTesiz0N/3wUEXEmKgxhfbhF6Wtgk4tatWBUG9LDaj919K2Q==", + "resolved": "https://pkg.pr.new/@modelcontextprotocol/client@2501", + "integrity": "sha512-5FvmSf2qWlJXlI9ydf8230f1rsDHMEw9Jgcq2ZUGmA3DJShaYV6t49zKJBuawetMcLOzWBGs8+qDzhZOZTk6Fw==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "2.0.0-beta.4", + "@modelcontextprotocol/core": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/core@f60b401", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", @@ -2657,7 +2657,7 @@ }, "node_modules/@modelcontextprotocol/core": { "version": "2.0.0-beta.4", - "resolved": "file:../tarballs/mcp-core.tgz", + "resolved": "https://pkg.pr.new/@modelcontextprotocol/core@2501", "integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==", "license": "MIT", "dependencies": { @@ -2784,11 +2784,11 @@ }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0-beta.4", - "resolved": "file:../tarballs/mcp-server.tgz", - "integrity": "sha512-/UJubByzGV+mlU8fN8yyuMI9zgoq4Ic5b1V/6yhzxrKjmXpTbNkY5rD2jRsWBk+At0AVcXLkcDzmLF91oOJtKw==", + "resolved": "https://pkg.pr.new/@modelcontextprotocol/server@2501", + "integrity": "sha512-mI+V52bTdSCHDrsrVG3TeOzgy+apDQXz6l1B4KuwKV/8xbRAsV9LZ2Nf67SEoLszuYYH0as4pZQUG4cCMYP5/g==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "2.0.0-beta.4", + "@modelcontextprotocol/core": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/core@f60b401", "zod": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index 59e1802..e95e0be 100644 --- a/package.json +++ b/package.json @@ -77,9 +77,9 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", - "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", - "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz", + "@modelcontextprotocol/client": "https://pkg.pr.new/@modelcontextprotocol/client@2501", + "@modelcontextprotocol/core": "https://pkg.pr.new/@modelcontextprotocol/core@2501", + "@modelcontextprotocol/server": "https://pkg.pr.new/@modelcontextprotocol/server@2501", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -130,9 +130,9 @@ "seroval-plugins": "1.4.2", "solid-js": "1.9.10", "@types/node": "20.19.27", - "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", - "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", - "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz" + "@modelcontextprotocol/core": "https://pkg.pr.new/@modelcontextprotocol/core@2501", + "@modelcontextprotocol/client": "https://pkg.pr.new/@modelcontextprotocol/client@2501", + "@modelcontextprotocol/server": "https://pkg.pr.new/@modelcontextprotocol/server@2501" }, "dependencies": { "@standard-schema/spec": "^1.1.0" From 7664bb1010aa364e84c76f8260bf1701d38b0a10 Mon Sep 17 00:00:00 2001 From: Enrico <2827496+tonxxd@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:29:43 -0700 Subject: [PATCH 69/97] test: isolate optional SDK role dependencies --- build.bun.ts | 5 +- package-lock.json | 6 + package.json | 7 ++ scripts/check-dependency-isolation.mjs | 159 +++++++++++++++++++++++++ src/app-bridge.ts | 4 +- src/app.ts | 3 +- src/constants.ts | 5 + src/core-types.ts | 20 ++++ src/server/index.ts | 5 +- src/spec.types.ts | 2 +- typedoc.config.mjs | 2 + 11 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 scripts/check-dependency-isolation.mjs create mode 100644 src/constants.ts create mode 100644 src/core-types.ts diff --git a/build.bun.ts b/build.bun.ts index 837279f..61361e6 100644 --- a/build.bun.ts +++ b/build.bun.ts @@ -1,6 +1,9 @@ #!/usr/bin/env bun import { $ } from "bun"; -import { cpSync, mkdirSync } from "node:fs"; +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`; diff --git a/package-lock.json b/package-lock.json index e82271b..c7d677e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,12 @@ "zod": "^4.2.0" }, "peerDependenciesMeta": { + "@modelcontextprotocol/client": { + "optional": true + }, + "@modelcontextprotocol/server": { + "optional": true + }, "react": { "optional": true }, diff --git a/package.json b/package.json index e95e0be..4f88483 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "prepack": "npm run build", "build:all": "npm run examples:build", "test": "bun test src examples", + "test:dependency-isolation": "npm run build && node scripts/check-dependency-isolation.mjs", "test:e2e": "playwright test", "test:e2e:update": "playwright test --update-snapshots", "test:e2e:ui": "playwright test --ui", @@ -118,6 +119,12 @@ "zod": "^4.2.0" }, "peerDependenciesMeta": { + "@modelcontextprotocol/client": { + "optional": true + }, + "@modelcontextprotocol/server": { + "optional": true + }, "react": { "optional": true }, diff --git a/scripts/check-dependency-isolation.mjs b/scripts/check-dependency-isolation.mjs new file mode 100644 index 0000000..2ec28ce --- /dev/null +++ b/scripts/check-dependency-isolation.mjs @@ -0,0 +1,159 @@ +import { execFileSync } from "node:child_process"; +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const packageJson = JSON.parse( + readFileSync(join(root, "package.json"), "utf8"), +); +const client = "@modelcontextprotocol/client"; +const server = "@modelcontextprotocol/server"; + +for (const role of [client, server]) { + if (!packageJson.peerDependencies?.[role]) { + throw new Error(`${role} must remain a peer dependency`); + } + if (packageJson.peerDependenciesMeta?.[role]?.optional !== true) { + throw new Error(`${role} must be an optional peer dependency`); + } +} + +const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-")); +try { + const npmEnvironment = { + ...process.env, + npm_config_cache: join(temporaryRoot, "npm-cache"), + }; + const packOutput = JSON.parse( + execFileSync( + "npm", + [ + "pack", + "--ignore-scripts", + "--json", + "--pack-destination", + temporaryRoot, + ], + { cwd: root, encoding: "utf8", env: npmEnvironment }, + ), + ); + const tarball = join(temporaryRoot, packOutput[0].filename); + + const consumers = [ + { + name: "app-only", + dependencies: { + "@types/node": packageJson.devDependencies["@types/node"], + [client]: packageJson.devDependencies[client], + "@modelcontextprotocol/ext-apps": `file:${tarball}`, + }, + absent: server, + entry: + 'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; console.log(App, AppBridge);', + }, + { + name: "server-only", + dependencies: { + "@types/node": packageJson.devDependencies["@types/node"], + "@modelcontextprotocol/ext-apps": `file:${tarball}`, + [server]: packageJson.devDependencies[server], + }, + absent: client, + entry: + 'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);', + }, + ]; + + for (const consumer of consumers) { + const directory = join(temporaryRoot, consumer.name); + mkdirSync(directory); + writeFileSync( + join(directory, "package.json"), + JSON.stringify({ + private: true, + type: "module", + dependencies: consumer.dependencies, + }), + ); + execFileSync( + "npm", + [ + "install", + "--ignore-scripts", + "--package-lock=false", + "--no-audit", + "--no-fund", + ], + { cwd: directory, stdio: "pipe", env: npmEnvironment }, + ); + + const absentPath = join( + directory, + "node_modules", + ...consumer.absent.split("/"), + "package.json", + ); + try { + readFileSync(absentPath); + throw new Error( + `${consumer.name} unexpectedly installed ${consumer.absent}`, + ); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } + + writeFileSync(join(directory, "entry.ts"), consumer.entry); + writeFileSync( + join(directory, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + lib: ["ES2020", "DOM"], + module: "ESNext", + moduleResolution: "bundler", + noEmit: true, + skipLibCheck: false, + strict: true, + target: "ES2020", + }, + include: ["entry.ts"], + }), + ); + execFileSync( + process.execPath, + [join(root, "node_modules", "typescript", "bin", "tsc")], + { cwd: directory, stdio: "inherit" }, + ); + + const metafile = join(directory, "bundle-meta.json"); + execFileSync( + join(root, "node_modules", ".bin", "esbuild"), + [ + "entry.ts", + "--bundle", + "--platform=browser", + "--outfile=bundle.js", + `--metafile=${metafile}`, + ], + { cwd: directory, stdio: "pipe" }, + ); + const bundleInputs = Object.keys( + JSON.parse(readFileSync(metafile, "utf8")).inputs, + ).join("\n"); + if (bundleInputs.includes(`/node_modules/${consumer.absent}/`)) { + throw new Error( + `${consumer.name} unexpectedly bundled ${consumer.absent}`, + ); + } + } +} finally { + rmSync(temporaryRoot, { recursive: true, force: true }); +} + +console.log("Role peer dependency isolation checks passed."); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 467c33d..d6a2368 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -89,8 +89,8 @@ import { McpUiToolMeta, } from "./types"; export * from "./types"; -export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; -import { RESOURCE_URI_META_KEY } from "./app"; +export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants"; +import { RESOURCE_URI_META_KEY } from "./constants"; export { PostMessageTransport } from "./message-transport"; /** diff --git a/src/app.ts b/src/app.ts index c639784..0ff369e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -22,6 +22,7 @@ import { type Transport, } from "@modelcontextprotocol/client"; import { EmptyResultSchema } from "@modelcontextprotocol/core"; +export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants"; import { EventDispatcher } from "./events"; export { EventDispatcher } from "./events"; import { PostMessageTransport } from "./message-transport"; @@ -158,7 +159,6 @@ export { * } * ``` */ -export const RESOURCE_URI_META_KEY = "ui/resourceUri"; /** * MIME type for MCP UI resources. @@ -167,7 +167,6 @@ export const RESOURCE_URI_META_KEY = "ui/resourceUri"; * * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources. */ -export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Options for configuring {@link App `App`} behavior. diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..f69cfa5 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,5 @@ +/** Legacy metadata key associating an MCP tool with its App resource. */ +export const RESOURCE_URI_META_KEY = "ui/resourceUri"; + +/** MIME type for MCP App HTML resources. */ +export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; diff --git a/src/core-types.ts b/src/core-types.ts new file mode 100644 index 0000000..19392d2 --- /dev/null +++ b/src/core-types.ts @@ -0,0 +1,20 @@ +import { + CallToolResultSchema, + ContentBlockSchema, + EmbeddedResourceSchema, + ImplementationSchema, + RequestIdSchema, + ResourceLinkSchema, + ToolSchema, +} from "@modelcontextprotocol/core"; +import type { z } from "zod/v4"; + +// Infer shared wire types from the public role-neutral schemas so declarations +// used by every entrypoint do not acquire a client or server package edge. +export type CallToolResult = z.infer; +export type ContentBlock = z.infer; +export type EmbeddedResource = z.infer; +export type Implementation = z.infer; +export type RequestId = z.infer; +export type ResourceLink = z.infer; +export type Tool = z.infer; diff --git a/src/server/index.ts b/src/server/index.ts index 61e7438..dc9a1f3 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -32,13 +32,12 @@ */ import { - RESOURCE_URI_META_KEY, - RESOURCE_MIME_TYPE, McpUiResourceCsp, McpUiResourceMeta, McpUiToolMeta, McpUiClientCapabilities, -} from "../app.js"; +} from "../spec.types.js"; +import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js"; import type { ClientCapabilities, McpServer, diff --git a/src/spec.types.ts b/src/spec.types.ts index 8b03de9..6c158d1 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -18,7 +18,7 @@ import type { RequestId, ResourceLink, Tool, -} from "@modelcontextprotocol/client"; +} from "./core-types"; /** * Current protocol version supported by this SDK. diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 041bf5e..1531bdc 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -36,12 +36,14 @@ const config = { ClientOptions: BASE_SDK_DOCS, ConnectOptions: BASE_SDK_DOCS, DiscoverResult: BASE_SDK_DOCS, + "MessageExtraInfo.classification": BASE_SDK_DOCS, ProtocolError: BASE_SDK_DOCS, "Protocol.close": BASE_SDK_DOCS, "Protocol.notification": BASE_SDK_DOCS, ResponseCacheStore: BASE_SDK_DOCS, SdkError: BASE_SDK_DOCS, "SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS, + "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS, "__type.enforceStrictCapabilities": BASE_SDK_DOCS, }, "@modelcontextprotocol/server": { From c55a3a231fb76303376e059aef9a13367e72731c Mon Sep 17 00:00:00 2001 From: victor-openai Date: Wed, 22 Jul 2026 09:28:57 -0700 Subject: [PATCH 70/97] fix: preserve experimental capability settings (#721) --- specification/draft/apps.mdx | 8 ++--- src/app-bridge.test.ts | 10 +++++- src/generated/schema.json | 60 ++++++++++++++++++++++++++++-------- src/generated/schema.ts | 22 +++++++++---- src/spec.types.ts | 8 ++--- 5 files changed, 81 insertions(+), 27 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 369ef64..f2523f2 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -548,8 +548,8 @@ When the View sends an `ui/initialize` request to the Host, it MUST include its ```typescript interface McpUiAppCapabilities { - /** Experimental features (structure TBD). */ - experimental?: {}; + /** Experimental features keyed by identifier. */ + experimental?: Record; /** App exposes MCP-style tools that the host can call. */ tools?: { /** App supports tools/list_changed notifications. */ @@ -677,8 +677,8 @@ interface SupportedContentBlockModalities { } interface HostCapabilities { - /** Experimental features (structure TBD). */ - experimental?: {}; + /** Experimental features keyed by identifier. */ + experimental?: Record; /** Host supports opening external URLs. */ openLinks?: {}; /** Host supports file downloads via ui/download-file. */ diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index a4fe1de..d327c95 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -45,6 +45,9 @@ function createMockClient( const testHostInfo = { name: "TestHost", version: "1.0.0" }; const testAppInfo = { name: "TestApp", version: "1.0.0" }; const testHostCapabilities: McpUiHostCapabilities = { + experimental: { + "com.example/host-extension": { version: 1 }, + }, openLinks: {}, serverTools: {}, logging: {}, @@ -97,7 +100,12 @@ describe("App <-> AppBridge integration", () => { }); it("Bridge receives app info and capabilities after initialization", async () => { - const appCapabilities = { tools: { listChanged: true } }; + const appCapabilities = { + experimental: { + "com.example/app-extension": { version: 1 }, + }, + tools: { listChanged: true }, + }; app = new App(testAppInfo, appCapabilities, { autoResize: false }); await bridge.connect(bridgeTransport); diff --git a/src/generated/schema.json b/src/generated/schema.json index 80b4ac6..6b66482 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -9,10 +9,19 @@ "type": "object", "properties": { "experimental": { - "description": "Experimental features (structure TBD).", + "description": "Experimental features keyed by identifier.", "type": "object", - "properties": {}, - "additionalProperties": false + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Experimental features keyed by identifier." + } }, "tools": { "description": "App exposes MCP-style tools that the host can call.", @@ -298,10 +307,19 @@ "type": "object", "properties": { "experimental": { - "description": "Experimental features (structure TBD).", + "description": "Experimental features keyed by identifier.", "type": "object", - "properties": {}, - "additionalProperties": false + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Experimental features keyed by identifier." + } }, "openLinks": { "description": "Host supports opening external URLs.", @@ -2537,10 +2555,19 @@ "type": "object", "properties": { "experimental": { - "description": "Experimental features (structure TBD).", + "description": "Experimental features keyed by identifier.", "type": "object", - "properties": {}, - "additionalProperties": false + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Experimental features keyed by identifier." + } }, "tools": { "description": "App exposes MCP-style tools that the host can call.", @@ -2651,10 +2678,19 @@ "type": "object", "properties": { "experimental": { - "description": "Experimental features (structure TBD).", + "description": "Experimental features keyed by identifier.", "type": "object", - "properties": {}, - "additionalProperties": false + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Experimental features keyed by identifier." + } }, "openLinks": { "description": "Host supports opening external URLs.", diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 4368737..a8bc1b6 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -496,11 +496,16 @@ export const McpUiRequestTeardownNotificationSchema = z.object({ * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities */ export const McpUiHostCapabilitiesSchema = z.object({ - /** @description Experimental features (structure TBD). */ + /** @description Experimental features keyed by identifier. */ experimental: z - .object({}) + .record( + z.string(), + z + .record(z.string(), z.any()) + .describe("Experimental features keyed by identifier."), + ) .optional() - .describe("Experimental features (structure TBD)."), + .describe("Experimental features keyed by identifier."), /** @description Host supports opening external URLs. */ openLinks: z .object({}) @@ -583,11 +588,16 @@ export const McpUiHostCapabilitiesSchema = z.object({ * @see {@link McpUiInitializeRequest `McpUiInitializeRequest`} for the initialization request that includes these capabilities */ export const McpUiAppCapabilitiesSchema = z.object({ - /** @description Experimental features (structure TBD). */ + /** @description Experimental features keyed by identifier. */ experimental: z - .object({}) + .record( + z.string(), + z + .record(z.string(), z.any()) + .describe("Experimental features keyed by identifier."), + ) .optional() - .describe("Experimental features (structure TBD)."), + .describe("Experimental features keyed by identifier."), /** @description App exposes MCP-style tools that the host can call. */ tools: z .object({ diff --git a/src/spec.types.ts b/src/spec.types.ts index 7a8b337..bedc1f5 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -492,8 +492,8 @@ export interface McpUiRequestTeardownNotification { * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities */ export interface McpUiHostCapabilities { - /** @description Experimental features (structure TBD). */ - experimental?: {}; + /** @description Experimental features keyed by identifier. */ + experimental?: Record; /** @description Host supports opening external URLs. */ openLinks?: {}; /** @description Host supports file downloads via ui/download-file. */ @@ -536,8 +536,8 @@ export interface McpUiHostCapabilities { * @see {@link McpUiInitializeRequest `McpUiInitializeRequest`} for the initialization request that includes these capabilities */ export interface McpUiAppCapabilities { - /** @description Experimental features (structure TBD). */ - experimental?: {}; + /** @description Experimental features keyed by identifier. */ + experimental?: Record; /** @description App exposes MCP-style tools that the host can call. */ tools?: { /** @description App supports tools/list_changed notifications. */ From 7d498079e8f2d0d385ac2ec88a72e031378e6702 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 22 Jul 2026 19:34:30 +0100 Subject: [PATCH 71/97] chore: clear npm audit findings (15 -> 3) (#725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- package-lock.json | 708 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 418 insertions(+), 292 deletions(-) diff --git a/package-lock.json b/package-lock.json index 29e9058..26be03f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "prettier": "^3.6.2", "react": "^19.2.0", "react-dom": "^19.2.0", - "sharp": "^0.34.5", + "sharp": "^0.35.3", "ts-to-zod": "^5.1.0", "tsx": "^4.21.0", "typedoc": "^0.28.14", @@ -1123,13 +1123,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -1138,9 +1138,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -1148,21 +1148,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -1189,14 +1189,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -1219,14 +1219,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -1246,9 +1246,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -1256,29 +1256,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1298,27 +1298,27 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -1326,26 +1326,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1439,33 +1439,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1473,13 +1473,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1550,9 +1550,9 @@ "license": "Apache-2.0" }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "dev": true, "license": "MIT", "optional": true, @@ -2036,9 +2036,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "dev": true, "license": "MIT", "engines": { @@ -2046,9 +2046,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -2059,19 +2059,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -2082,19 +2082,39 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -2109,9 +2129,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -2126,9 +2146,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -2143,9 +2163,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -2160,9 +2180,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], @@ -2177,9 +2197,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], @@ -2194,9 +2214,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -2211,9 +2231,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -2228,9 +2248,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -2245,9 +2265,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -2262,9 +2282,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -2275,19 +2295,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -2298,19 +2318,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], @@ -2321,19 +2341,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], @@ -2344,19 +2364,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -2367,19 +2387,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -2390,19 +2410,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -2413,19 +2433,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -2436,39 +2456,56 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@emnapi/runtime": "^1.11.1" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -2479,16 +2516,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -2499,16 +2536,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -2519,7 +2556,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -3026,9 +3063,9 @@ } }, "node_modules/@oclif/core/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -4460,13 +4497,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", - "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bezier-js": { @@ -4494,20 +4534,20 @@ } }, "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -4517,6 +4557,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -4540,9 +4593,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4564,9 +4617,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, "funding": [ { @@ -4584,11 +4637,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4597,6 +4650,34 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/browserslist/node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/browserslist/node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, "node_modules/bun": { "version": "1.3.10", "resolved": "https://registry.npmjs.org/bun/-/bun-1.3.10.tgz", @@ -5005,15 +5086,15 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.8.3", + "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -5835,9 +5916,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -5868,9 +5949,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -6171,9 +6252,9 @@ } }, "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "version": "4.12.31", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", + "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -6576,10 +6657,20 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -6791,15 +6882,25 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -6987,11 +7088,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nodemon": { "version": "3.1.11", @@ -7615,9 +7719,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -7700,48 +7804,53 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "dev": true, - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { @@ -7766,9 +7875,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, "license": "MIT", "engines": { @@ -9057,17 +9166,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typedoc": { @@ -9108,9 +9234,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -9162,9 +9288,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", - "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -9227,9 +9353,9 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8c6ead5..b494003 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "prettier": "^3.6.2", "react": "^19.2.0", "react-dom": "^19.2.0", - "sharp": "^0.34.5", + "sharp": "^0.35.3", "ts-to-zod": "^5.1.0", "tsx": "^4.21.0", "typedoc": "^0.28.14", From e7ffe7c738b2db36d76339ec2f1b0d9c16c29812 Mon Sep 17 00:00:00 2001 From: Enrico <2827496+tonxxd@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:24:49 -0700 Subject: [PATCH 72/97] chore: update MCP SDK packages to beta.5 --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 8 +- examples/basic-server-react/package.json | 8 +- examples/basic-server-solid/package.json | 8 +- examples/basic-server-svelte/package.json | 8 +- examples/basic-server-vanillajs/package.json | 8 +- examples/basic-server-vue/package.json | 8 +- examples/budget-allocator-server/package.json | 6 +- examples/cohort-heatmap-server/package.json | 6 +- .../customer-segmentation-server/package.json | 6 +- examples/debug-server/package.json | 6 +- examples/integration-server/package.json | 8 +- examples/lazy-auth-server/package.json | 4 +- examples/map-server/package.json | 8 +- examples/pdf-server/package.json | 8 +- examples/quickstart/package.json | 6 +- examples/scenario-modeler-server/package.json | 6 +- examples/shadertoy-server/package.json | 6 +- examples/sheet-music-server/package.json | 6 +- examples/system-monitor-server/package.json | 6 +- examples/threejs-server/package.json | 8 +- examples/transcript-server/package.json | 6 +- examples/video-resource-server/package.json | 8 +- examples/wiki-explorer-server/package.json | 8 +- package-lock.json | 613 +++--------------- package.json | 17 +- src/generated/schema.json | 56 +- 27 files changed, 235 insertions(+), 613 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 583d2ff..c2a104e 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 0187827..8722a54 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index dae7665..0a860c5 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -34,11 +34,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 5d91407..a0d1bfa 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 18bf7ab..81d73fe 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 6bb75af..1958b73 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 3b34aa6..a872c52 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 5d4fe8e..3b02ddf 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index c54040a..fd4777c 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index ef14b32..0846b40 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 0e3f508..f118717 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -35,9 +35,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 56fc8ed..2993020 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -15,11 +15,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 6aef3e1..257f96c 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -23,8 +23,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index a3639ce..0a56f85 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -26,11 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index a7eb4bb..ac80173 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -26,11 +26,11 @@ }, "dependencies": { "@cantoo/pdf-lib": "^2.6.5", - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index f741c6e..96bcbfa 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -16,9 +16,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 63a5d48..93b6d8a 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 318f017..ef5f563 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -25,9 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 10d7c71..0628f58 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -25,9 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 5bc4a95..c5aa008 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index fb97d33..aae13a7 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -26,11 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index eea4a80..51e7d27 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -25,9 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index ea816b9..e8535cd 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index bd31d6f..11ec18c 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -26,11 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.4", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/package-lock.json b/package-lock.json index c7d677e..e6e524e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,9 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "https://pkg.pr.new/@modelcontextprotocol/client@2501", - "@modelcontextprotocol/core": "https://pkg.pr.new/@modelcontextprotocol/core@2501", - "@modelcontextprotocol/server": "https://pkg.pr.new/@modelcontextprotocol/server@2501", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -52,9 +52,9 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" @@ -78,7 +78,7 @@ "name": "@modelcontextprotocol/ext-apps-basic-host", "version": "1.7.4", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -99,33 +99,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-host/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-host/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", @@ -146,33 +129,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-preact/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-preact/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -196,33 +162,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-react/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-react/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", @@ -243,33 +192,16 @@ "vite-plugin-solid": "^2.11.12" } }, - "examples/basic-server-solid/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-solid/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", @@ -290,33 +222,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-svelte/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-svelte/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -335,33 +250,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-vanillajs/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-vanillajs/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", @@ -382,32 +280,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-vue/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-vue/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -427,32 +308,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/budget-allocator-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/budget-allocator-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -476,32 +340,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/cohort-heatmap-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/cohort-heatmap-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -521,32 +368,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/customer-segmentation-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/customer-segmentation-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "zod": "^4.1.13" }, "bin": { @@ -565,31 +395,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/debug-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/debug-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/integration-server": { "version": "1.7.4", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -612,31 +425,14 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/integration-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/integration-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", "version": "1.7.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", @@ -656,33 +452,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/lazy-auth-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/lazy-auth-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -701,34 +480,17 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/map-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/map-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", "version": "1.7.4", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", @@ -748,23 +510,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/pdf-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/pdf-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", "version": "1.7.4", @@ -777,10 +522,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -797,16 +542,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/quickstart/node_modules/@types/node": { - "version": "22.19.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", - "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "1.7.4", @@ -820,10 +555,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -848,32 +583,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/scenario-modeler-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/scenario-modeler-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -892,32 +610,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/shadertoy-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/shadertoy-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", @@ -937,32 +638,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/sheet-music-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/sheet-music-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -983,33 +667,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/system-monitor-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/system-monitor-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -1035,32 +702,15 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/threejs-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/threejs-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1080,33 +730,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/transcript-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/transcript-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1125,33 +758,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/video-resource-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/video-resource-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0-beta.5", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -1172,23 +788,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/wiki-explorer-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/wiki-explorer-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -2644,12 +2243,12 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/client": { - "version": "2.0.0-beta.4", - "resolved": "https://pkg.pr.new/@modelcontextprotocol/client@2501", - "integrity": "sha512-5FvmSf2qWlJXlI9ydf8230f1rsDHMEw9Jgcq2ZUGmA3DJShaYV6t49zKJBuawetMcLOzWBGs8+qDzhZOZTk6Fw==", + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.5.tgz", + "integrity": "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/core@f60b401", + "@modelcontextprotocol/core": "2.0.0-beta.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", @@ -2662,9 +2261,9 @@ } }, "node_modules/@modelcontextprotocol/core": { - "version": "2.0.0-beta.4", - "resolved": "https://pkg.pr.new/@modelcontextprotocol/core@2501", - "integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==", + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.5.tgz", + "integrity": "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew==", "license": "MIT", "dependencies": { "zod": "^4.2.0" @@ -2674,9 +2273,9 @@ } }, "node_modules/@modelcontextprotocol/express": { - "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0-beta.4.tgz", - "integrity": "sha512-7hI8bEomz6rMn8sKYRVIZgQJsrWIjIS9WiPyIp3X2pMyLSCC7hB5km0kg+9SygWygTIRnPfte0tXevRB3mXEHQ==", + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0-beta.5.tgz", + "integrity": "sha512-SDHAAKCOkHrxuBRyh6fRQJpjWBt5vrRCmvV7OIyWdmIksQzmTYsw7oEX1p9Kje6PSDqKJHpmqLJzZNUnOhrfCQ==", "license": "MIT", "dependencies": { "cors": "^2.8.5" @@ -2685,7 +2284,7 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/server": "^2.0.0-beta.4", + "@modelcontextprotocol/server": "^2.0.0-beta.5", "express": "^4.18.0 || ^5.0.0" } }, @@ -2723,9 +2322,9 @@ "link": true }, "node_modules/@modelcontextprotocol/node": { - "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-beta.4.tgz", - "integrity": "sha512-OqbrRVNYNfLxBB2BRkP5oIHKHa0IPHluMbaLgw7YCRKcj5be9ExzyWFJY3KVMb+1F2AVn+0SRoH0XKcNzh3HYQ==", + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-beta.5.tgz", + "integrity": "sha512-flrOzx5qFLBM9I8IYRGfYUY02erFmxOvYQ9sEE6iSj/ZZpRYKorXWscM/qrfW9rwlcGLyymHhnP2AaDbMB1lIA==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9" @@ -2734,7 +2333,7 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/server": "^2.0.0-beta.4", + "@modelcontextprotocol/server": "^2.0.0-beta.5", "hono": "^4.11.4" }, "peerDependenciesMeta": { @@ -2789,12 +2388,12 @@ } }, "node_modules/@modelcontextprotocol/server": { - "version": "2.0.0-beta.4", - "resolved": "https://pkg.pr.new/@modelcontextprotocol/server@2501", - "integrity": "sha512-mI+V52bTdSCHDrsrVG3TeOzgy+apDQXz6l1B4KuwKV/8xbRAsV9LZ2Nf67SEoLszuYYH0as4pZQUG4cCMYP5/g==", + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.5.tgz", + "integrity": "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/core@f60b401", + "@modelcontextprotocol/core": "2.0.0-beta.5", "zod": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index 4f88483..ac7897c 100644 --- a/package.json +++ b/package.json @@ -78,9 +78,9 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "https://pkg.pr.new/@modelcontextprotocol/client@2501", - "@modelcontextprotocol/core": "https://pkg.pr.new/@modelcontextprotocol/core@2501", - "@modelcontextprotocol/server": "https://pkg.pr.new/@modelcontextprotocol/server@2501", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -111,9 +111,9 @@ "zod": "^4.2.0" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0-beta.5", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" @@ -136,10 +136,7 @@ "seroval": "1.4.1", "seroval-plugins": "1.4.2", "solid-js": "1.9.10", - "@types/node": "20.19.27", - "@modelcontextprotocol/core": "https://pkg.pr.new/@modelcontextprotocol/core@2501", - "@modelcontextprotocol/client": "https://pkg.pr.new/@modelcontextprotocol/client@2501", - "@modelcontextprotocol/server": "https://pkg.pr.new/@modelcontextprotocol/server@2501" + "@types/node": "20.19.27" }, "dependencies": { "@standard-schema/spec": "^1.1.0" diff --git a/src/generated/schema.json b/src/generated/schema.json index 5f2124a..0932dc4 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -5384,26 +5384,52 @@ "_meta": { "type": "object", "properties": { - "progressToken": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - ] - }, - "io.modelcontextprotocol/related-task": { + "io.modelcontextprotocol/serverInfo": { "type": "object", "properties": { - "taskId": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "icons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizes": { + "type": "array", + "items": { + "type": "string" + } + }, + "theme": { + "type": "string", + "enum": ["light", "dark"] + } + }, + "required": ["src"], + "additionalProperties": false + } + }, + "version": { + "type": "string" + }, + "websiteUrl": { + "type": "string" + }, + "description": { "type": "string" } }, - "required": ["taskId"], + "required": ["name", "version"], "additionalProperties": false } }, From c847bc0887d23c90f79a7acfa95eb32af29e0833 Mon Sep 17 00:00:00 2001 From: Enrico <2827496+tonxxd@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:37:22 -0700 Subject: [PATCH 73/97] docs: align Protocol and role-specific setup --- README.md | 22 ++++++++++++++----- docs/overview.md | 10 ++++----- docs/quickstart.md | 2 +- .../skills/add-app-to-server/SKILL.md | 2 +- .../mcp-apps/skills/convert-web-app/SKILL.md | 2 +- .../mcp-apps/skills/create-mcp-app/SKILL.md | 2 +- src/app-bridge.ts | 10 ++++----- src/app.ts | 7 ++++-- 8 files changed, 35 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b3f68e7..a514e19 100644 --- a/README.md +++ b/README.md @@ -121,17 +121,27 @@ resources: ## Getting Started +For a View or host: + ```bash npm install -S @modelcontextprotocol/ext-apps \ - @modelcontextprotocol/client@2.0.0-beta.4 \ - @modelcontextprotocol/server@2.0.0-beta.4 \ - @modelcontextprotocol/core@2.0.0-beta.4 \ + @modelcontextprotocol/client@2.0.0-beta.5 \ + @modelcontextprotocol/core@2.0.0-beta.5 \ zod@^4.2.0 ``` -This release uses the split base MCP SDK v2 packages. Install all three at the -exact published beta.4 version so the Apps SDK and your MCP client/server share -one compatible protocol implementation. +For an MCP server: + +```bash +npm install -S @modelcontextprotocol/ext-apps \ + @modelcontextprotocol/server@2.0.0-beta.5 \ + @modelcontextprotocol/core@2.0.0-beta.5 \ + zod@^4.2.0 +``` + +Applications that implement both roles should install both `client` and +`server`. Keep all installed base MCP SDK packages on the exact same published +beta so they share one compatible protocol implementation. **New here?** Start with the [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) diff --git a/docs/overview.md b/docs/overview.md index 63156ec..5f3852d 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -56,11 +56,11 @@ flowchart LR - **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them. - **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat. -The View's `App` subclasses the base MCP SDK `Client`. The Host's `AppBridge` -subclasses the base MCP SDK `Server` for the inner iframe channel, while a -separate outer `Client` connects the Host to the actual MCP Server. Keeping -those two connections separate prevents iframe negotiation and capabilities -from leaking into the server connection. +The View's `App` and the Host's `AppBridge` both subclass the base MCP SDK's +public `Protocol` for the iframe channel. A separate outer `Client` connects +the Host to the actual MCP Server. Keeping those two connections separate +preserves the Apps-only iframe handshake and prevents iframe capabilities from +leaking into the server connection. ## Lifecycle diff --git a/docs/quickstart.md b/docs/quickstart.md index eb6b096..3cac872 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -37,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 @modelcontextprotocol/node@2.0.0-beta.5 @modelcontextprotocol/express@2.0.0-beta.5 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md index 69e41f8..fda72b0 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -77,7 +77,7 @@ Before writing any code, analyze the server's existing tools and determine which ## Step 2: Add Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0 +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 zod@^4.2.0 npm install -D vite vite-plugin-singlefile ``` diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md index 22686ef..03fc8d7 100644 --- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md +++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md @@ -115,7 +115,7 @@ Create a new MCP server with tool and resource registration. This wraps the exis ### Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0 +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 zod@^4.2.0 npm install -D tsx vite vite-plugin-singlefile ``` diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md index fc41e70..6f1c466 100644 --- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md +++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md @@ -110,7 +110,7 @@ See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes: **Always** use `npm install` to add dependencies rather than manually writing version numbers: ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 @modelcontextprotocol/node@2.0.0-beta.5 @modelcontextprotocol/express@2.0.0-beta.5 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors ``` diff --git a/src/app-bridge.ts b/src/app-bridge.ts index d6a2368..59ea586 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -241,11 +241,11 @@ export type AppBridgeEventMap = { /** * Host-side bridge for communicating with a single View ({@link app!App `App`}). * - * `AppBridge` extends the base MCP SDK's public `Server` class and acts as a proxy between - * the host application and a view running in an iframe. When an MCP client - * is provided to the constructor, it automatically forwards MCP server capabilities - * (tools, resources, prompts) to the view. It also handles the initialization - * handshake. + * `AppBridge` extends the base MCP SDK's public `Protocol` class and acts as a + * proxy between the host application and a view running in an iframe. When a + * separate MCP client is provided to the constructor, it automatically forwards + * MCP server capabilities (tools, resources, prompts) to the view. It also + * handles the Apps initialization handshake. * * ## Architecture * diff --git a/src/app.ts b/src/app.ts index 0ff369e..34ef284 100644 --- a/src/app.ts +++ b/src/app.ts @@ -297,7 +297,8 @@ export type AppEventMap = { * * The `App` class provides a framework-agnostic way to build interactive MCP Apps * that run inside host applications. It extends the base MCP SDK's public - * `Client` class and adds the Apps handshake, lifecycle, and high-level APIs. + * `Protocol` class, composes an {@link EventDispatcher}, and adds the Apps + * handshake, lifecycle, and high-level APIs. * * ## Architecture * @@ -314,9 +315,11 @@ export type AppEventMap = { * * ## Protocol and event methods * - * As a subclass of the base MCP SDK's `Client`, `App` provides: + * From the base MCP SDK's `Protocol`, `App` provides: * - `setRequestHandler()` - Register handlers for requests from host * - `setNotificationHandler()` - Register handlers for notifications from host + * + * From its composed {@link EventDispatcher}, `App` also provides: * - `addEventListener()` - Append a listener for a notification event (multi-listener) * - `removeEventListener()` - Remove a previously added listener * From 92f46a574568a3ddac7600343b7d3c4c4ed7b588 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 23 Jul 2026 12:25:07 +0100 Subject: [PATCH 74/97] chore: bump ext-apps to 1.7.5 (#724) --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/lazy-auth-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 56 +++++++++---------- package.json | 2 +- 28 files changed, 55 insertions(+), 55 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 1b45dcf..f54b237 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 5127202..81ed0de 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index e4d1233..5ae783e 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 8f75ac8..c2950d9 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index f877bf7..8238feb 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 5d47f2c..8ca330e 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index d27ddc9..9c0eb7b 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 19a1d25..0cd94d0 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 8d1268b..9988679 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 4511de9..2fc85f9 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 3417a8f..16d2f4c 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index f6715a4..a8b5421 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.4", + "version": "1.7.5", "private": true, "type": "module", "scripts": { diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index f3ed6cd..8e96906 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", "repository": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 5dc23fe..5de1754 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 4d960f1..8942a90 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index a5cbf5b..8152866 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.4", + "version": "1.7.5", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 99948cc..e1dcf2d 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index a0076e6..d51dabd 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.4", + "version": "1.7.5", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index aa1e668..716c7b5 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index ccf15a4..0505f47 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index ca9fa90..e9e39cc 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 12e8cea..9506c61 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index cfb5686..2121460 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index dcba3d7..8e9d495 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index b0d04af..8d8b8ec 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index fe511ad..af02eae 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.4", + "version": "1.7.5", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index 26be03f..9dc76e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.4", + "version": "1.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "workspaces": [ "examples/*" @@ -66,7 +66,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.4", + "version": "1.7.5", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -108,7 +108,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -152,7 +152,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -199,7 +199,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -243,7 +243,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -287,7 +287,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -329,7 +329,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -373,7 +373,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -416,7 +416,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -463,7 +463,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -506,7 +506,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -547,7 +547,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.7.4", + "version": "1.7.5", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -592,7 +592,7 @@ }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -634,7 +634,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -676,7 +676,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", @@ -720,7 +720,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.4", + "version": "1.7.5", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -728,7 +728,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -760,7 +760,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -769,7 +769,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -817,7 +817,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -859,7 +859,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -902,7 +902,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -946,7 +946,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -995,7 +995,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -1038,7 +1038,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", @@ -1080,7 +1080,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", diff --git a/package.json b/package.json index b494003..15ef5d7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.4", + "version": "1.7.5", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From 4e4886c5ff7125f5332bd29685e94076caeaac24 Mon Sep 17 00:00:00 2001 From: Enrico <2827496+tonxxd@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:02:48 -0700 Subject: [PATCH 75/97] chore: update MCP SDK packages to stable 2.0.0 --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 8 +- examples/basic-server-react/package.json | 8 +- examples/basic-server-solid/package.json | 8 +- examples/basic-server-svelte/package.json | 8 +- examples/basic-server-vanillajs/package.json | 8 +- examples/basic-server-vue/package.json | 8 +- examples/budget-allocator-server/package.json | 6 +- examples/cohort-heatmap-server/package.json | 6 +- .../customer-segmentation-server/package.json | 6 +- examples/debug-server/package.json | 6 +- examples/integration-server/package.json | 8 +- examples/lazy-auth-server/package.json | 4 +- examples/map-server/package.json | 8 +- examples/pdf-server/package.json | 8 +- examples/quickstart/package.json | 6 +- examples/scenario-modeler-server/package.json | 6 +- examples/shadertoy-server/package.json | 6 +- examples/sheet-music-server/package.json | 6 +- examples/system-monitor-server/package.json | 6 +- examples/threejs-server/package.json | 8 +- examples/transcript-server/package.json | 6 +- examples/video-resource-server/package.json | 8 +- examples/wiki-explorer-server/package.json | 8 +- package-lock.json | 212 +++++++++--------- package.json | 12 +- 26 files changed, 193 insertions(+), 193 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index c2a104e..7bce111 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 8722a54..0043486 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 0a860c5..69a06df 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -34,11 +34,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index a0d1bfa..6cfa17f 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 81d73fe..dd88b97 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 1958b73..4a37592 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index a872c52..e8f929e 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 3b02ddf..4d2b977 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index fd4777c..0761130 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 0846b40..6163424 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index f118717..3499abf 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -35,9 +35,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 2993020..64a3286 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -15,11 +15,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 257f96c..4691a2e 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -23,8 +23,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 0a56f85..4577636 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -26,11 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index ac80173..e004394 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -26,11 +26,11 @@ }, "dependencies": { "@cantoo/pdf-lib": "^2.6.5", - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 96bcbfa..4312b75 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -16,9 +16,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 93b6d8a..723491c 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index ef5f563..556699d 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -25,9 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 0628f58..a8075e6 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -25,9 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index c5aa008..cc18d4b 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index aae13a7..b4cc6de 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -26,11 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 51e7d27..f3617c4 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -25,9 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index e8535cd..64a07e9 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -24,11 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 11ec18c..e4bd274 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -26,11 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/express": "2.0.0-beta.5", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/package-lock.json b/package-lock.json index e6e524e..d4e84ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,9 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/core": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -52,9 +52,9 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/core": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" @@ -78,7 +78,7 @@ "name": "@modelcontextprotocol/ext-apps-basic-host", "version": "1.7.4", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -104,11 +104,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", @@ -134,11 +134,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -167,11 +167,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", @@ -197,11 +197,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", @@ -227,11 +227,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -255,11 +255,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", @@ -285,10 +285,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -313,10 +313,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -345,10 +345,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -373,10 +373,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "zod": "^4.1.13" }, "bin": { @@ -398,11 +398,11 @@ "examples/integration-server": { "version": "1.7.4", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -431,8 +431,8 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", @@ -457,11 +457,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -486,11 +486,11 @@ "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", @@ -522,10 +522,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -555,10 +555,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -588,10 +588,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -615,10 +615,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", @@ -643,10 +643,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -672,11 +672,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -707,10 +707,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -735,11 +735,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -763,11 +763,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/express": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/express": "2.0.0", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/node": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -2243,12 +2243,12 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/client": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.5.tgz", - "integrity": "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", @@ -2261,9 +2261,9 @@ } }, "node_modules/@modelcontextprotocol/core": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.5.tgz", - "integrity": "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", "license": "MIT", "dependencies": { "zod": "^4.2.0" @@ -2273,9 +2273,9 @@ } }, "node_modules/@modelcontextprotocol/express": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0-beta.5.tgz", - "integrity": "sha512-SDHAAKCOkHrxuBRyh6fRQJpjWBt5vrRCmvV7OIyWdmIksQzmTYsw7oEX1p9Kje6PSDqKJHpmqLJzZNUnOhrfCQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0.tgz", + "integrity": "sha512-Snlr8j9FR9LcVvEJPF7qJ7d5zTL4Bes2dk7RcacN9eSZ7OLohwxqhEvWu1+UxELyScgLbLLOdeVEGdwKI1iwVQ==", "license": "MIT", "dependencies": { "cors": "^2.8.5" @@ -2284,7 +2284,7 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/server": "^2.0.0-beta.5", + "@modelcontextprotocol/server": "^2.0.0", "express": "^4.18.0 || ^5.0.0" } }, @@ -2322,9 +2322,9 @@ "link": true }, "node_modules/@modelcontextprotocol/node": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-beta.5.tgz", - "integrity": "sha512-flrOzx5qFLBM9I8IYRGfYUY02erFmxOvYQ9sEE6iSj/ZZpRYKorXWscM/qrfW9rwlcGLyymHhnP2AaDbMB1lIA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz", + "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9" @@ -2333,7 +2333,7 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/server": "^2.0.0-beta.5", + "@modelcontextprotocol/server": "^2.0.0", "hono": "^4.11.4" }, "peerDependenciesMeta": { @@ -2388,12 +2388,12 @@ } }, "node_modules/@modelcontextprotocol/server": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.5.tgz", - "integrity": "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index ac7897c..5e0d3c5 100644 --- a/package.json +++ b/package.json @@ -78,9 +78,9 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/core": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -111,9 +111,9 @@ "zod": "^4.2.0" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0-beta.5", - "@modelcontextprotocol/core": "2.0.0-beta.5", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" From 10195ad91851502134930e9b80ec2c04e277a720 Mon Sep 17 00:00:00 2001 From: Quentin Churet <45853629+qchuchu@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:04:54 +0200 Subject: [PATCH 76/97] docs: add Alpic Playground to supported clients (#729) Co-authored-by: Claude Opus 4.8 (1M context) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2fd89ff..77183ab 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ it will guide you through the rest. Postman MCPJam mcp-use + Alpic Playground

> [!NOTE] From 264d9cae448fd07bf5f718d188e5528936796fa0 Mon Sep 17 00:00:00 2001 From: Ken Jo Date: Sat, 11 Jul 2026 21:03:43 +0900 Subject: [PATCH 77/97] 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 --- src/app-bridge.test.ts | 6 +++--- src/app-bridge.ts | 12 ++++++------ src/app.ts | 20 ++++++++++---------- src/message-transport.test.ts | 2 +- src/message-transport.ts | 2 +- src/react/index.tsx | 8 ++++---- src/react/useApp.tsx | 4 ++-- src/react/useAutoResize.ts | 2 +- src/react/useDocumentTheme.ts | 4 ++-- src/react/useHostStyles.ts | 6 +++--- src/server/index.test.ts | 2 +- src/styles.ts | 2 +- 12 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index e538f26..397e12f 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -3,8 +3,8 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Server, type ServerCapabilities } from "@modelcontextprotocol/server"; import { z } from "zod/v4"; -import { App } from "./app"; -import { LATEST_PROTOCOL_VERSION } from "./types"; +import { App } from "./app.js"; +import { LATEST_PROTOCOL_VERSION } from "./types.js"; import { AppBridge, buildAllowAttribute, @@ -13,7 +13,7 @@ import { isToolVisibilityAppOnly, McpUiOpenLinkResultSchema, type McpUiHostCapabilities, -} from "./app-bridge"; +} from "./app-bridge.js"; /** Wait for pending microtasks to complete */ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 59ea586..8e48677 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -34,7 +34,7 @@ import { EmptyResultSchema, LoggingMessageNotificationSchema, } from "@modelcontextprotocol/core"; -import { EventDispatcher } from "./events"; +import { EventDispatcher } from "./events.js"; import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; type MethodSchema = ZodObject<{ @@ -87,11 +87,11 @@ import { McpUiRequestDisplayModeResultSchema, McpUiResourcePermissions, McpUiToolMeta, -} from "./types"; -export * from "./types"; -export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants"; -import { RESOURCE_URI_META_KEY } from "./constants"; -export { PostMessageTransport } from "./message-transport"; +} from "./types.js"; +export * from "./types.js"; +export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js"; +import { RESOURCE_URI_META_KEY } from "./constants.js"; +export { PostMessageTransport } from "./message-transport.js"; /** * Extract UI resource URI from tool metadata. diff --git a/src/app.ts b/src/app.ts index 34ef284..3fe84d6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -22,10 +22,10 @@ import { type Transport, } from "@modelcontextprotocol/client"; import { EmptyResultSchema } from "@modelcontextprotocol/core"; -export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants"; -import { EventDispatcher } from "./events"; -export { EventDispatcher } from "./events"; -import { PostMessageTransport } from "./message-transport"; +export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; +import { EventDispatcher } from "./events.js"; +export { EventDispatcher } from "./events.js"; +import { PostMessageTransport } from "./message-transport.js"; import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, @@ -59,12 +59,12 @@ import { McpUiToolResultNotificationSchema, McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeResultSchema, -} from "./types"; +} from "./types.js"; import { StandardSchemaV1, standardSchemaToJsonSchema, validateStandardSchema, -} from "./standard-schema"; +} from "./standard-schema.js"; import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; type MethodSchema = ZodObject<{ @@ -92,16 +92,16 @@ function mergeAppCapabilities( export type { StandardSchemaV1, StandardSchemaWithJSON, -} from "./standard-schema"; +} from "./standard-schema.js"; -export { PostMessageTransport } from "./message-transport"; -export * from "./types"; +export { PostMessageTransport } from "./message-transport.js"; +export * from "./types.js"; export { applyHostStyleVariables, applyHostFonts, getDocumentTheme, applyDocumentTheme, -} from "./styles"; +} from "./styles.js"; /** * Metadata key for associating a UI resource URI with a tool. diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts index 2a115f3..e2518cc 100644 --- a/src/message-transport.test.ts +++ b/src/message-transport.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; import type { JSONRPCMessage } from "@modelcontextprotocol/client"; -import { PostMessageTransport } from "./message-transport"; +import { PostMessageTransport } from "./message-transport.js"; /** * Minimal `window` stub for bun's DOM-less test environment. diff --git a/src/message-transport.ts b/src/message-transport.ts index 9483540..afc85e9 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -5,7 +5,7 @@ import type { Transport, TransportSendOptions, } from "@modelcontextprotocol/client"; -import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types"; +import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types.js"; /** * JSON-RPC transport using `window.postMessage` for iframe↔parent communication. diff --git a/src/react/index.tsx b/src/react/index.tsx index e76e197..9693fb1 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -31,7 +31,7 @@ * } * ``` */ -export * from "./useApp"; -export * from "./useAutoResize"; -export * from "./useDocumentTheme"; -export * from "./useHostStyles"; +export * from "./useApp.js"; +export * from "./useAutoResize.js"; +export * from "./useDocumentTheme.js"; +export * from "./useHostStyles.js"; diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 0e64a74..a8e1a09 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -5,8 +5,8 @@ import { AppOptions, McpUiAppCapabilities, PostMessageTransport, -} from "../app"; -export * from "../app"; +} from "../app.js"; +export * from "../app.js"; /** * Options for configuring the {@link useApp `useApp`} hook. diff --git a/src/react/useAutoResize.ts b/src/react/useAutoResize.ts index 78217d6..66af07e 100644 --- a/src/react/useAutoResize.ts +++ b/src/react/useAutoResize.ts @@ -1,5 +1,5 @@ import { useEffect, RefObject } from "react"; -import { App } from "../app"; +import { App } from "../app.js"; /** * React hook that automatically reports UI size changes to the host. diff --git a/src/react/useDocumentTheme.ts b/src/react/useDocumentTheme.ts index 2fdfcc2..3dc851e 100644 --- a/src/react/useDocumentTheme.ts +++ b/src/react/useDocumentTheme.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { getDocumentTheme } from "../styles"; -import { McpUiTheme } from "../types"; +import { getDocumentTheme } from "../styles.js"; +import { McpUiTheme } from "../types.js"; /** * React hook that provides the current document theme reactively. diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts index e380efa..d8376a0 100644 --- a/src/react/useHostStyles.ts +++ b/src/react/useHostStyles.ts @@ -1,11 +1,11 @@ import { useEffect, useRef } from "react"; -import { App } from "../app"; +import { App } from "../app.js"; import { applyDocumentTheme, applyHostFonts, applyHostStyleVariables, -} from "../styles"; -import { McpUiHostContext } from "../types"; +} from "../styles.js"; +import { McpUiHostContext } from "../types.js"; /** * React hook that applies host style variables and theme as CSS custom properties. diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 5de341f..389cde1 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -6,7 +6,7 @@ import { RESOURCE_MIME_TYPE, getUiCapability, EXTENSION_ID, -} from "./index"; +} from "./index.js"; import type { McpServer } from "@modelcontextprotocol/server"; describe("registerAppTool", () => { diff --git a/src/styles.ts b/src/styles.ts index ad900e2..895073b 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,4 +1,4 @@ -import { McpUiStyles, McpUiTheme } from "./types"; +import { McpUiStyles, McpUiTheme } from "./types.js"; /** * Get the current document theme from the root HTML element. From 90123733bbc7854a543a05db64c1a6a9b8ee0613 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 11:49:33 +0100 Subject: [PATCH 78/97] test: regenerate lockfile (re-add nested @types/node entries) --- package-lock.json | 401 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 401 insertions(+) diff --git a/package-lock.json b/package-lock.json index 49b1315..ca37f4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,6 +99,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-host/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-host/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", "version": "1.7.5", @@ -129,6 +146,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-preact/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-preact/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", "version": "1.7.5", @@ -162,6 +196,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-react/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-react/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", "version": "1.7.5", @@ -192,6 +243,23 @@ "vite-plugin-solid": "^2.11.12" } }, + "examples/basic-server-solid/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-solid/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", "version": "1.7.5", @@ -222,6 +290,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-svelte/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-svelte/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", "version": "1.7.5", @@ -250,6 +335,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-vanillajs/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-vanillajs/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", "version": "1.7.5", @@ -280,6 +382,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-vue/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-vue/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", "version": "1.7.5", @@ -308,6 +427,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/budget-allocator-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/budget-allocator-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", "version": "1.7.5", @@ -340,6 +476,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/cohort-heatmap-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/cohort-heatmap-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", "version": "1.7.5", @@ -368,6 +521,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/customer-segmentation-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/customer-segmentation-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", "version": "1.7.5", @@ -395,6 +565,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/debug-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/debug-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/integration-server": { "version": "1.7.5", "dependencies": { @@ -425,6 +612,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/integration-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/integration-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", "version": "1.7.5", @@ -452,6 +656,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/lazy-auth-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/lazy-auth-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", "version": "1.7.5", @@ -480,6 +701,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/map-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/map-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", "version": "1.7.5", @@ -510,6 +748,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/pdf-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/pdf-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", "version": "1.7.5", @@ -542,6 +797,16 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/quickstart/node_modules/@types/node": { + "version": "22.19.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", + "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "1.7.5", @@ -583,6 +848,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/scenario-modeler-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/scenario-modeler-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", "version": "1.7.5", @@ -610,6 +892,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/shadertoy-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/shadertoy-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", "version": "1.7.5", @@ -638,6 +937,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/sheet-music-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/sheet-music-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", "version": "1.7.5", @@ -667,6 +983,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/system-monitor-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/system-monitor-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", "version": "1.7.5", @@ -702,6 +1035,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/threejs-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/threejs-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", "version": "1.7.5", @@ -730,6 +1080,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/transcript-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/transcript-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", "version": "1.7.5", @@ -758,6 +1125,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/video-resource-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/video-resource-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", "version": "1.7.5", @@ -788,6 +1172,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/wiki-explorer-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/wiki-explorer-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", From f28f5ddb3c3d451923d25e5591841ba76ef21df0 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:20:52 +0100 Subject: [PATCH 79/97] 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. --- .github/workflows/ci.yml | 15 +++++++ scripts/check-dependency-isolation.mjs | 60 ++++++++++++++++++-------- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb41938..756f259 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,21 @@ jobs: 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: diff --git a/scripts/check-dependency-isolation.mjs b/scripts/check-dependency-isolation.mjs index 2ec28ce..2dfe577 100644 --- a/scripts/check-dependency-isolation.mjs +++ b/scripts/check-dependency-isolation.mjs @@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process"; import { mkdtempSync, mkdirSync, + readdirSync, readFileSync, rmSync, writeFileSync, @@ -25,33 +26,56 @@ for (const role of [client, server]) { } } +/** + * Exact version for a synthetic consumer dependency. Read from + * devDependencies so the consumers exercise the same SDK version the + * repository tests against; the check would silently drift if a range were + * allowed here. + */ +function exactDevDependency(name) { + const version = packageJson.devDependencies?.[name]; + if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version ?? "")) { + throw new Error( + `devDependencies["${name}"] must be an exact version, got ${JSON.stringify(version)}`, + ); + } + return version; +} + const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-")); try { + // Minimal environment: nothing inherited from the caller's npm config + // (registry overrides, auth tokens, npm_config_* set by an outer `npm run`) + // can leak into the synthetic consumers. const npmEnvironment = { - ...process.env, + PATH: process.env.PATH ?? process.env.Path, + HOME: process.env.HOME ?? process.env.USERPROFILE, npm_config_cache: join(temporaryRoot, "npm-cache"), }; - const packOutput = JSON.parse( - execFileSync( - "npm", - [ - "pack", - "--ignore-scripts", - "--json", - "--pack-destination", - temporaryRoot, - ], - { cwd: root, encoding: "utf8", env: npmEnvironment }, - ), + const packDestination = join(temporaryRoot, "pack"); + mkdirSync(packDestination); + // `npm pack` runs the package's `prepare` script even with --ignore-scripts + // (pacote's directory fetcher), and its output can pollute stdout, so do not + // rely on `--json`: locate the tarball on disk instead. + execFileSync( + "npm", + ["pack", "--ignore-scripts", "--pack-destination", packDestination], + { cwd: root, stdio: "pipe", env: npmEnvironment }, ); - const tarball = join(temporaryRoot, packOutput[0].filename); + const tarballs = readdirSync(packDestination).filter((name) => + name.endsWith(".tgz"), + ); + if (tarballs.length !== 1) { + throw new Error(`expected exactly one tarball, found ${tarballs}`); + } + const tarball = join(packDestination, tarballs[0]); const consumers = [ { name: "app-only", dependencies: { - "@types/node": packageJson.devDependencies["@types/node"], - [client]: packageJson.devDependencies[client], + "@types/node": exactDevDependency("@types/node"), + [client]: exactDevDependency(client), "@modelcontextprotocol/ext-apps": `file:${tarball}`, }, absent: server, @@ -61,9 +85,9 @@ try { { name: "server-only", dependencies: { - "@types/node": packageJson.devDependencies["@types/node"], + "@types/node": exactDevDependency("@types/node"), "@modelcontextprotocol/ext-apps": `file:${tarball}`, - [server]: packageJson.devDependencies[server], + [server]: exactDevDependency(server), }, absent: client, entry: From 50eb30ec8dff82b0b505ec5351b1bbf52cd9d2fc Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:24:13 +0100 Subject: [PATCH 80/97] 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. --- examples/basic-host/package.json | 6 +- examples/basic-server-preact/package.json | 6 +- examples/basic-server-react/package.json | 6 +- examples/basic-server-solid/package.json | 6 +- examples/basic-server-svelte/package.json | 6 +- examples/basic-server-vanillajs/package.json | 6 +- examples/basic-server-vue/package.json | 6 +- examples/budget-allocator-server/package.json | 6 +- examples/cohort-heatmap-server/package.json | 6 +- .../customer-segmentation-server/package.json | 6 +- examples/debug-server/package.json | 6 +- examples/integration-server/package.json | 6 +- examples/lazy-auth-server/package.json | 6 +- examples/map-server/package.json | 6 +- examples/pdf-server/package.json | 6 +- examples/qr-server/package.json | 4 +- examples/quickstart/package.json | 6 +- examples/say-server/package.json | 4 +- examples/scenario-modeler-server/package.json | 6 +- examples/shadertoy-server/package.json | 6 +- examples/sheet-music-server/package.json | 6 +- examples/system-monitor-server/package.json | 6 +- examples/threejs-server/package.json | 6 +- examples/transcript-server/package.json | 6 +- examples/video-resource-server/package.json | 6 +- examples/wiki-explorer-server/package.json | 6 +- package-lock.json | 357 ++++-------------- package.json | 11 +- 28 files changed, 163 insertions(+), 357 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 06d1a05..cefe9d5 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", @@ -11,11 +11,11 @@ "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/express": "^5.0.0", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 82a30bb..c88a306 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@preact/preset-vite": "^2.0.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index b2b9540..07a9d3b 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { @@ -35,15 +35,15 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 2e3bc79..a459ebc 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index cd7fb41..3e0279f 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 50f0f42..1404b65 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { @@ -25,13 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 2ff3e0a..32083c6 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 9208e91..e44a147 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { @@ -26,14 +26,14 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index eb58415..de20f13 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { @@ -26,15 +26,15 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index f6999ee..643db17 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { @@ -26,14 +26,14 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 33cede4..5be9291 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { @@ -34,11 +34,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 18f22e6..7e04f4a 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.5", + "version": "2.0.0", "private": true, "type": "module", "scripts": { @@ -16,15 +16,15 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 6e7aaa7..b12b29b 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", "repository": { @@ -22,13 +22,13 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index a59445e..dc6a37f 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { @@ -27,13 +27,13 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 8c2c2ee..ae0870a 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { @@ -27,14 +27,14 @@ "dependencies": { "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index e1b849b..425728e 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.5", + "version": "2.0.0", "private": true, "scripts": { "start": "uv run server.py", @@ -8,6 +8,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } } diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 6874b49..eecb080 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "private": true, "description": "Quickstart MCP App Server example", @@ -15,13 +15,13 @@ "start": "concurrently --raw \"cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch\" \"tsx watch main.ts\"" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 5f0f738..cff7024 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.5", + "version": "2.0.0", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { @@ -15,6 +15,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } } diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 76d692a..d82424d 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { @@ -26,8 +26,8 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", @@ -35,7 +35,7 @@ "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 57edf08..3e16a92 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { @@ -24,13 +24,13 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 55c2870..1b233bb 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { @@ -24,14 +24,14 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index b0c2f20..f3a5763 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { @@ -26,15 +26,15 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", "systeminformation": "^5.31.6", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 40d1c25..f8df10f 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { @@ -27,8 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", @@ -36,7 +36,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "three": "^0.181.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index e7c5dba..405f85f 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { @@ -24,13 +24,13 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 199a373..dea8fcb 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { @@ -25,13 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 090a851..0831670 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { @@ -27,14 +27,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/package-lock.json b/package-lock.json index ca37f4b..f63b3d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.5", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "workspaces": [ "examples/*" @@ -52,17 +52,14 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/core": "2.0.0", - "@modelcontextprotocol/server": "2.0.0", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" }, "peerDependenciesMeta": { - "@modelcontextprotocol/client": { - "optional": true - }, "@modelcontextprotocol/server": { "optional": true }, @@ -76,13 +73,13 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.5", + "version": "2.0.0", "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/express": "^5.0.0", @@ -118,18 +115,18 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-preact": "dist/index.js" @@ -165,19 +162,19 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-react": "dist/index.js" @@ -215,18 +212,18 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-solid": "dist/index.js" @@ -262,18 +259,18 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-svelte": "dist/index.js" @@ -309,17 +306,17 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-vanillajs": "dist/index.js" @@ -354,18 +351,18 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-vue": "dist/index.js" @@ -401,17 +398,17 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-budget-allocator-server": "dist/index.js" @@ -446,18 +443,18 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-cohort-heatmap-server": "dist/index.js" @@ -495,17 +492,17 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-customer-segmentation-server": "dist/index.js" @@ -540,14 +537,14 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-debug": "dist/index.js" @@ -583,18 +580,18 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.7.5", + "version": "2.0.0", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-integration-server": "dist/index.js" @@ -631,16 +628,16 @@ }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-lazy-auth": "dist/index.js" @@ -675,17 +672,17 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-map-server": "dist/index.js" @@ -720,19 +717,19 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-pdf-server": "dist/index.js" @@ -767,23 +764,23 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.5", + "version": "2.0.0", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", @@ -809,19 +806,19 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", @@ -829,7 +826,7 @@ "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-scenario-modeler-server": "dist/index.js" @@ -867,16 +864,16 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-shadertoy-server": "dist/index.js" @@ -911,17 +908,17 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-sheet-music-server": "dist/index.js" @@ -956,18 +953,18 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", "systeminformation": "^5.31.6", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-system-monitor-server": "dist/index.js" @@ -1002,12 +999,12 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", @@ -1015,7 +1012,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "three": "^0.181.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-threejs-server": "dist/index.js" @@ -1054,16 +1051,16 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-transcript-server": "dist/index.js" @@ -1099,17 +1096,17 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-video-resource-server": "dist/index.js" @@ -1144,18 +1141,18 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-wiki-explorer-server": "dist/index.js" @@ -2727,33 +2724,8 @@ } }, "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", - "integrity": "sha512-J3WdG1A4JSSKnSWKyU+895dBVYBV2Utgtf7fUsUK45mlkETm53a/1DR6Pm3hUGKqLLQthZLmpxOg8VPzJi/lyg==", - "license": "MIT", - "workspaces": [ - "examples/*" - ], - "dependencies": { - "@standard-schema/spec": "^1.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } + "resolved": "", + "link": true }, "node_modules/@modelcontextprotocol/ext-apps-basic-host": { "resolved": "examples/basic-host", @@ -2784,47 +2756,6 @@ "resolved": "examples/quickstart", "link": true }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", @@ -4444,41 +4375,6 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6041,49 +5937,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT", - "peer": true - }, - "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -6546,16 +6399,6 @@ "node": ">=12" } }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6743,20 +6586,6 @@ "node": ">=6" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause", - "peer": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7749,16 +7578,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -9897,16 +9716,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peer": true, - "peerDependencies": { - "zod": "^3.25 || ^4" - } } } } diff --git a/package.json b/package.json index fc887fa..da82a66 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", @@ -111,17 +111,14 @@ "zod": "^4.2.0" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/core": "2.0.0", - "@modelcontextprotocol/server": "2.0.0", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" }, "peerDependenciesMeta": { - "@modelcontextprotocol/client": { - "optional": true - }, "@modelcontextprotocol/server": { "optional": true }, From 040942b3af064bf76b705021e3472b6593ae839e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:26:00 +0100 Subject: [PATCH 81/97] 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. --- scripts/generate-schemas.ts | 8 +++++--- src/core-types.ts | 20 -------------------- src/spec.types.ts | 2 +- 3 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 src/core-types.ts diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 85225f5..0ff9f04 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -22,10 +22,12 @@ * * **Problem**: ts-to-zod cannot resolve types imported from external packages. * When it encounters types like `ContentBlock`, `CallToolResult`, `Implementation`, - * `RequestId`, and `Tool` from `@modelcontextprotocol/core`, it generates `z.any()` - * as a placeholder. + * `RequestId`, and `Tool` (imported by spec.types.ts from + * `@modelcontextprotocol/client`), it generates `z.any()` as a placeholder. * - * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders. + * **Solution**: Import the corresponding Zod schemas from + * `@modelcontextprotocol/core` (the role-neutral package both client and + * server re-export) and remove the z.any() placeholders. * * ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough()`) * diff --git a/src/core-types.ts b/src/core-types.ts deleted file mode 100644 index 19392d2..0000000 --- a/src/core-types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - CallToolResultSchema, - ContentBlockSchema, - EmbeddedResourceSchema, - ImplementationSchema, - RequestIdSchema, - ResourceLinkSchema, - ToolSchema, -} from "@modelcontextprotocol/core"; -import type { z } from "zod/v4"; - -// Infer shared wire types from the public role-neutral schemas so declarations -// used by every entrypoint do not acquire a client or server package edge. -export type CallToolResult = z.infer; -export type ContentBlock = z.infer; -export type EmbeddedResource = z.infer; -export type Implementation = z.infer; -export type RequestId = z.infer; -export type ResourceLink = z.infer; -export type Tool = z.infer; diff --git a/src/spec.types.ts b/src/spec.types.ts index 5d3fda6..0c45396 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -18,7 +18,7 @@ import type { RequestId, ResourceLink, Tool, -} from "./core-types"; +} from "@modelcontextprotocol/client"; /** * Current protocol version supported by this SDK. From d6ebcca9a0f4536779666b2223916d64f3700a64 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:27:11 +0100 Subject: [PATCH 82/97] 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. --- src/server/index.test.ts | 121 ++++++++++++++++++++++++++++++++++++++- src/server/index.ts | 78 ++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 389cde1..489d0b0 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -7,7 +7,13 @@ import { getUiCapability, EXTENSION_ID, } from "./index.js"; -import type { McpServer } from "@modelcontextprotocol/server"; +import { + InMemoryTransport, + McpServer as RealMcpServer, + type McpServer, +} from "@modelcontextprotocol/server"; +import { Client } from "@modelcontextprotocol/client"; +import { z } from "zod/v4"; describe("registerAppTool", () => { it("should pass through config to server.registerTool", () => { @@ -197,6 +203,119 @@ describe("registerAppTool", () => { }); }); +describe("registerAppTool schema forms", () => { + /** Registers tools on a fresh server, then connects a client to it. */ + async function connect(register: (server: RealMcpServer) => void) { + const server = new RealMcpServer({ name: "test", version: "0.0.0" }); + register(server); + const client = new Client({ name: "client", version: "0.0.0" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + return client; + } + + it("accepts a z.object() input schema and validates arguments", async () => { + const client = await connect((server) => + registerAppTool( + server, + "greet", + { + inputSchema: z.object({ name: z.string() }), + _meta: { ui: { resourceUri: "ui://greet/view.html" } }, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `hello ${name}` }], + }), + ), + ); + + const { tools } = await client.listTools(); + expect(tools).toHaveLength(1); + expect(tools[0].inputSchema).toMatchObject({ + type: "object", + properties: { name: { type: "string" } }, + }); + expect(tools[0]._meta).toMatchObject({ + ui: { resourceUri: "ui://greet/view.html" }, + [RESOURCE_URI_META_KEY]: "ui://greet/view.html", + }); + + const result = await client.callTool({ + name: "greet", + arguments: { name: "world" }, + }); + expect(result.content).toEqual([{ type: "text", text: "hello world" }]); + + const invalid = await client.callTool({ + name: "greet", + arguments: { name: 42 }, + }); + expect(invalid.isError).toBe(true); + }); + + it("accepts a raw zod shape (deprecated form) and wraps it with z.object()", async () => { + const client = await connect((server) => + registerAppTool( + server, + "greet", + { + inputSchema: { name: z.string() }, + outputSchema: { greeting: z.string() }, + _meta: { ui: { resourceUri: "ui://greet/view.html" } }, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `hello ${name}` }], + structuredContent: { greeting: `hello ${name}` }, + }), + ), + ); + + const { tools } = await client.listTools(); + expect(tools[0].inputSchema).toMatchObject({ + type: "object", + properties: { name: { type: "string" } }, + }); + expect(tools[0].outputSchema).toMatchObject({ + type: "object", + properties: { greeting: { type: "string" } }, + }); + + const result = await client.callTool({ + name: "greet", + arguments: { name: "world" }, + }); + expect(result.structuredContent).toEqual({ greeting: "hello world" }); + + const invalid = await client.callTool({ + name: "greet", + arguments: { name: 42 }, + }); + expect(invalid.isError).toBe(true); + }); + + it("accepts the raw-shape form without an input schema", async () => { + const client = await connect((server) => + registerAppTool( + server, + "ping", + { + outputSchema: { ok: z.boolean() }, + _meta: { ui: { resourceUri: "ui://ping/view.html" } }, + }, + async () => ({ + content: [{ type: "text", text: "pong" }], + structuredContent: { ok: true }, + }), + ), + ); + + const result = await client.callTool({ name: "ping", arguments: {} }); + expect(result.structuredContent).toEqual({ ok: true }); + }); +}); + describe("registerAppResource", () => { it("should register a resource with default MIME type", () => { let capturedName: string | undefined; diff --git a/src/server/index.ts b/src/server/index.ts index dc9a1f3..5fad055 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -39,31 +39,69 @@ import { } from "../spec.types.js"; import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js"; import type { + CallToolResult, ClientCapabilities, + InputRequiredResult, McpServer, ReadResourceResult, RegisteredTool, ResourceMetadata, + ServerContext, StandardSchemaWithJSON, ToolAnnotations, ToolCallback, ReadResourceCallback as _ReadResourceCallback, RegisteredResource, } from "@modelcontextprotocol/server"; +import type { z } from "zod/v4"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; export type { ResourceMetadata, ToolCallback }; +/** + * A plain `{ field: z.string() }` record accepted by the deprecated raw-shape + * form of {@link registerAppTool `registerAppTool`}; the SDK auto-wraps it with `z.object()`. + * + * @deprecated Wrap with `z.object({...})` instead. + */ +export type ZodRawShape = Record; + +/** + * {@link ToolCallback `ToolCallback`} variant used when `inputSchema` is a {@link ZodRawShape `ZodRawShape`}. + * Mirrors the callback type of the SDK's deprecated `registerTool` overload. + * + * @deprecated Wrap with `z.object({...})` instead and use {@link ToolCallback `ToolCallback`}. + */ +export type LegacyToolCallback = + Args extends ZodRawShape + ? ( + args: z.infer>, + ctx: ServerContext, + ) => + | CallToolResult + | InputRequiredResult + | Promise + : ( + ctx: ServerContext, + ) => + | CallToolResult + | InputRequiredResult + | Promise; + /** * Base tool configuration matching the standard MCP server tool options. * Extended by {@link McpUiAppToolConfig `McpUiAppToolConfig`} to add UI metadata requirements. + * + * `inputSchema`/`outputSchema` accept any Standard JSON Schema (zod v4, ArkType, + * Valibot, ...). A raw zod shape (`{ field: z.string() }`) is still accepted for + * backward compatibility but deprecated. */ export interface ToolConfig { title?: string; description?: string; - inputSchema?: StandardSchemaWithJSON; - outputSchema?: StandardSchemaWithJSON; + inputSchema?: ZodRawShape | StandardSchemaWithJSON; + outputSchema?: ZodRawShape | StandardSchemaWithJSON; annotations?: ToolAnnotations; _meta?: Record; } @@ -217,6 +255,31 @@ export function registerAppTool< outputSchema?: OutputArgs; }, cb: ToolCallback, +): RegisteredTool; +/** + * @deprecated Wrap with `z.object({...})` instead. Raw-shape form: + * `inputSchema`/`outputSchema` may be a plain `{ field: z.string() }` record; + * the SDK auto-wraps it with `z.object()`. + */ +export function registerAppTool< + InputArgs extends ZodRawShape, + OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = + undefined, +>( + server: Pick, + name: string, + config: McpUiAppToolConfig & { + inputSchema?: InputArgs; + outputSchema?: OutputArgs; + }, + cb: LegacyToolCallback, +): RegisteredTool; +export function registerAppTool( + server: Pick, + name: string, + config: McpUiAppToolConfig, + // Widest callback shape accepted by either overload above. + cb: (...args: never[]) => unknown, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key @@ -234,7 +297,16 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); + // The public overloads above guarantee `config`/`cb` match one of the SDK's + // own registerTool overloads; the union-typed implementation cannot select + // between them, so widen for the forwarding call. + return server.registerTool( + name, + { ...config, _meta: normalizedMeta } as Parameters< + McpServer["registerTool"] + >[1], + cb as ToolCallback, + ); } export type McpUiReadResourceResult = ReadResourceResult & { From 7f686ff2b47b518db2f106db1710ead2ceea3227 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:28:23 +0100 Subject: [PATCH 83/97] 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. --- scripts/check-dependency-isolation.mjs | 62 ++++++++++++++++++-------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/scripts/check-dependency-isolation.mjs b/scripts/check-dependency-isolation.mjs index 2dfe577..3809224 100644 --- a/scripts/check-dependency-isolation.mjs +++ b/scripts/check-dependency-isolation.mjs @@ -17,13 +17,20 @@ const packageJson = JSON.parse( const client = "@modelcontextprotocol/client"; const server = "@modelcontextprotocol/server"; +// App and AppBridge extend the client package's Protocol class, so client is +// a required peer for every consumer. The server helpers are only needed by +// server authors, so server stays optional: View-only consumers must not have +// it installed or bundled. for (const role of [client, server]) { if (!packageJson.peerDependencies?.[role]) { throw new Error(`${role} must remain a peer dependency`); } - if (packageJson.peerDependenciesMeta?.[role]?.optional !== true) { - throw new Error(`${role} must be an optional peer dependency`); - } +} +if (packageJson.peerDependenciesMeta?.[client]?.optional) { + throw new Error(`${client} must be a required peer dependency`); +} +if (packageJson.peerDependenciesMeta?.[server]?.optional !== true) { + throw new Error(`${server} must be an optional peer dependency`); } /** @@ -42,6 +49,13 @@ function exactDevDependency(name) { return version; } +/** Exact version of a package as installed in this repository's node_modules. */ +function installedVersion(name) { + return JSON.parse( + readFileSync(join(root, "node_modules", name, "package.json"), "utf8"), + ).version; +} + const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-")); try { // Minimal environment: nothing inherited from the caller's npm config @@ -72,17 +86,25 @@ try { const consumers = [ { + // View / host author: ext-apps + client (+ react for the hooks entry). name: "app-only", dependencies: { "@types/node": exactDevDependency("@types/node"), + "@types/react": installedVersion("@types/react"), [client]: exactDevDependency(client), "@modelcontextprotocol/ext-apps": `file:${tarball}`, + react: installedVersion("react"), }, absent: server, + // server must be neither installed (it is an optional peer) nor bundled. + mustNotInstall: true, entry: - 'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; console.log(App, AppBridge);', + 'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; console.log(App, AppBridge, useApp);', }, { + // Server author: ext-apps + server. npm auto-installs client as a + // required peer (its types back the shared wire types), but the server + // entry must not pull it into a runtime bundle. name: "server-only", dependencies: { "@types/node": exactDevDependency("@types/node"), @@ -90,6 +112,7 @@ try { [server]: exactDevDependency(server), }, absent: client, + mustNotInstall: false, entry: 'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);', }, @@ -115,22 +138,24 @@ try { "--no-audit", "--no-fund", ], - { cwd: directory, stdio: "pipe", env: npmEnvironment }, + { cwd: directory, stdio: "inherit", env: npmEnvironment }, ); - const absentPath = join( - directory, - "node_modules", - ...consumer.absent.split("/"), - "package.json", - ); - try { - readFileSync(absentPath); - throw new Error( - `${consumer.name} unexpectedly installed ${consumer.absent}`, + if (consumer.mustNotInstall) { + const absentPath = join( + directory, + "node_modules", + ...consumer.absent.split("/"), + "package.json", ); - } catch (error) { - if (error?.code !== "ENOENT") throw error; + try { + readFileSync(absentPath); + throw new Error( + `${consumer.name} unexpectedly installed ${consumer.absent}`, + ); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } } writeFileSync(join(directory, "entry.ts"), consumer.entry); @@ -138,6 +163,7 @@ try { join(directory, "tsconfig.json"), JSON.stringify({ compilerOptions: { + jsx: "react-jsx", lib: ["ES2020", "DOM"], module: "ESNext", moduleResolution: "bundler", @@ -165,7 +191,7 @@ try { "--outfile=bundle.js", `--metafile=${metafile}`, ], - { cwd: directory, stdio: "pipe" }, + { cwd: directory, stdio: "inherit" }, ); const bundleInputs = Object.keys( JSON.parse(readFileSync(metafile, "utf8")).inputs, From 0a477bb25da7c5b0fdb88ab4f917c071f26c3729 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:28:53 +0100 Subject: [PATCH 84/97] 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. --- src/events.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events.test.ts b/src/events.test.ts index 7fd8708..60e7c35 100644 --- a/src/events.test.ts +++ b/src/events.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, spyOn } from "bun:test"; -import { EventDispatcher } from "./events"; +import { EventDispatcher } from "./events.js"; type TestEventMap = { change: { value: number }; From 09bf2aae3cc3d7911894300efe95e6099c43238f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:29:42 +0100 Subject: [PATCH 85/97] 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. --- src/standard-schema.ts | 44 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/src/standard-schema.ts b/src/standard-schema.ts index 4bb85de..a06864a 100644 --- a/src/standard-schema.ts +++ b/src/standard-schema.ts @@ -4,36 +4,22 @@ import type { StandardTypedV1, } from "@standard-schema/spec"; -export type { StandardJSONSchemaV1, StandardSchemaV1, StandardTypedV1 }; - -// TODO(sdk-v2): once @modelcontextprotocol/core v2 is stable, import -// StandardSchemaWithJSON / standardSchemaToJsonSchema / validateStandardSchema -// from there and delete this file. At that point decide whether to tighten -// App.registerTool to StandardSchemaWithJSON (drops zod 3 from the peer range -// and the lazy z.toJSONSchema fallback below). +import type { StandardSchemaWithJSON } from "@modelcontextprotocol/client"; /** * A schema that implements both Standard Schema (validation) and Standard JSON * Schema (serialization). Zod v4, ArkType, and Valibot (via - * `@valibot/to-json-schema`) all satisfy this. - * - * Mirrors the type of the same name in `@modelcontextprotocol/core` v2 so that - * bumping to that package later is a drop-in import swap. + * `@valibot/to-json-schema`) all satisfy this. Re-exported from the SDK so + * View authors can import it alongside {@link app!App `App`}. * * @see https://standardschema.dev/ - * @see https://github.com/modelcontextprotocol/typescript-sdk/pull/1689 */ -export interface StandardSchemaWithJSON { - readonly "~standard": StandardSchemaV1.Props & - StandardJSONSchemaV1.Props; -} - -export namespace StandardSchemaWithJSON { - export type InferInput = - StandardTypedV1.InferInput; - export type InferOutput = - StandardTypedV1.InferOutput; -} +export type { + StandardJSONSchemaV1, + StandardSchemaV1, + StandardSchemaWithJSON, + StandardTypedV1, +}; /** JSON-Schema target draft used for tool input/output schemas (matches core MCP). */ const TARGET = { target: "draft-2020-12" } as const; @@ -41,11 +27,8 @@ const TARGET = { target: "draft-2020-12" } as const; /** * Serialize a Standard Schema to JSON Schema for the given direction. * - * Uses `~standard.jsonSchema` when present (zod v4, ArkType, Valibot, …). - * Falls back to a lazy `zod/v4` import for zod v3.25.x — which implements - * `~standard.validate` but not yet `~standard.jsonSchema` — so the existing - * `^3.25.0 || ^4.0.0` peer range keeps working. Non-zod schemas without - * `jsonSchema` throw. + * Requires `~standard.jsonSchema` (zod v4, ArkType, Valibot, …); schemas + * without it throw. */ export async function standardSchemaToJsonSchema( schema: StandardSchemaV1, @@ -57,11 +40,6 @@ export async function standardSchemaToJsonSchema( if (std.jsonSchema) { return std.jsonSchema[io](TARGET); } - if (std.vendor === "zod") { - const { z } = await import("zod/v4"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridging StandardSchemaV1 → zod's $ZodType for the v3.25 fallback - return z.toJSONSchema(schema as any, { io }); - } throw new Error( `Schema (vendor: ${std.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). ` + `Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`, From c5b25431155d72a0a2cb4f45f635e243ad1ea844 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:31:22 +0100 Subject: [PATCH 86/97] 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. --- AGENTS.md | 4 +- README.md | 22 +++-- docs/migrate-to-2.md | 94 +++++++++++++++++++ docs/overview.md | 4 +- docs/quickstart.md | 2 +- docs/testing-mcp-apps.md | 2 +- examples/transcript-server/README.md | 2 +- .../skills/add-app-to-server/SKILL.md | 22 ++++- .../mcp-apps/skills/convert-web-app/SKILL.md | 8 +- .../mcp-apps/skills/create-mcp-app/SKILL.md | 8 +- typedoc.config.mjs | 2 + 11 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 docs/migrate-to-2.md diff --git a/AGENTS.md b/AGENTS.md index 4b0faa0..219b947 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \ ### Key Source Files -- `src/app.ts` - `App` subclasses the base MCP SDK `Protocol`, handles View initialization, tool calls, and messaging -- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Protocol` for the iframe channel and proxies through a separate outer `Client` +- `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 diff --git a/README.md b/README.md index ff47e1e..4b92f0c 100644 --- a/README.md +++ b/README.md @@ -122,27 +122,33 @@ resources: ## Getting Started +Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of +`ext-apps` (`@modelcontextprotocol/core` is pulled in transitively). + For a View or host: ```bash npm install -S @modelcontextprotocol/ext-apps \ - @modelcontextprotocol/client@2.0.0-beta.5 \ - @modelcontextprotocol/core@2.0.0-beta.5 \ + @modelcontextprotocol/client@^2.0.0 \ zod@^4.2.0 ``` -For an MCP server: +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/server@2.0.0-beta.5 \ - @modelcontextprotocol/core@2.0.0-beta.5 \ + @modelcontextprotocol/client@^2.0.0 \ + @modelcontextprotocol/server@^2.0.0 \ + @modelcontextprotocol/node@^2.0.0 \ + @modelcontextprotocol/express@^2.0.0 \ zod@^4.2.0 ``` -Applications that implement both roles should install both `client` and -`server`. Keep all installed base MCP SDK packages on the exact same published -beta so they share one compatible protocol implementation. +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) diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md new file mode 100644 index 0000000..3f6db7b --- /dev/null +++ b/docs/migrate-to-2.md @@ -0,0 +1,94 @@ +--- +title: Migrate to v2 +group: Getting Started +description: Upgrade from ext-apps 1.x to 2.x — new base MCP SDK peer packages, API changes for Views and hosts, and what stays wire-compatible. +--- + +# Migrating from ext-apps 1.x to 2.x + +ext-apps 2.x is built on the base MCP TypeScript SDK 2.x, which replaced the +single `@modelcontextprotocol/sdk` package with `@modelcontextprotocol/client`, +`server`, `core`, `node` and `express`. The MCP Apps wire protocol did not +change; the breaking changes are in dependencies and in the TypeScript API. + +## Host compatibility + +The `ui/*` messages exchanged over the iframe channel are byte-identical to +1.x. A 2.x View runs in a 1.x host and a 2.x host renders 1.x Views. The only +host-side deltas are in error responses (see below). + +## Peer dependencies by role + +| Role | Install | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) | +| Host author | same as View author | +| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; `@modelcontextprotocol/node` and `express` for HTTP transports | +| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) | + +`@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend +its `Protocol` class); `@modelcontextprotocol/core` is installed transitively; +`@modelcontextprotocol/server` stays optional and is only needed for the +`./server` helpers. Node.js 20+ is required. + +## Breaking changes + +- **Peer packages.** `@modelcontextprotocol/sdk@^1` is replaced by the split + 2.x packages above, all at `^2.0.0`. Remove the 1.x package from your + project; the two SDKs do not interoperate. +- **zod 3 dropped.** The peer range is `zod@^4.2.0`. Tool schemas must + implement Standard JSON Schema (`~standard.jsonSchema`): zod v4, ArkType, + Valibot. Raw zod shapes (`{ q: z.string() }`) still work with + `registerAppTool` but are deprecated; wrap them with `z.object({...})`. +- **`App` / `AppBridge` extend `Protocol` from `@modelcontextprotocol/client`.** + `ProtocolWithEvents`, `AppRequest`, `AppNotification` and `AppResult` are + gone; use the SDK's `Protocol`, `Request`, `Notification` and `Result`. +- **Handler context.** Custom handlers receive the SDK 2.x context: + `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` → + `extra.mcpReq.id`. +- **`setRequestHandler` / `setNotificationHandler` take method names.** + `app.setRequestHandler(SomeRequestSchema, handler)` becomes + `app.setRequestHandler("some/method", handler)` (pass the params schema as the + optional extra argument for validation). +- **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`); + local conditions are `SdkError` with a string `code`: request timeout → + `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a + request with an `AbortSignal` also rejects with `"REQUEST_TIMEOUT"` (the + message is the abort reason). Messages no longer carry the `MCP error N:` + prefix. + +## Host-side wire deltas + +Observed when a 2.x `AppBridge` answers a View; a 1.x View sees these too. + +| Situation | 1.x host | 2.x host | +| --------------------------------------------------------- | ------------------------------ | --------------------------------------------------- | +| A handler throws `-32002` (resource not found) | `error.code: -32002` | `error.code: -32602` (the SDK never emits `-32002`) | +| Invalid params on a `ui/*` request | `-32603` with a zod issue dump | `-32602 Invalid params for : …` | +| Error message text | `MCP error -32602: …` prefix | plain message | +| `tools/call` to an unknown tool through a 2.x `McpServer` | `result.isError: true` | JSON-RPC error `-32602` (`callServerTool` rejects) | + +## `schema.json` + +The published `./schema.json` export is regenerated from the 2.x core schemas: + +- `McpUiToolResultNotification.params.structuredContent` is any JSON value (was + `type: "object"`). +- Result `_meta` documents `io.modelcontextprotocol/serverInfo` and no longer + lists `progressToken` / `related-task` (both still pass through). +- `McpUiHostContext.toolInfo.tool.inputSchema` is a loose object. +- A recursive `$defs.__schema0` JSON-value definition is added. + +## Checklist + +1. `npm uninstall @modelcontextprotocol/sdk` and install the packages for your + role from the table above. +2. Replace `sdk/...` imports with the split packages (`sdk/server/mcp.js` → + `@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` → + `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`, + `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js` + → `@modelcontextprotocol/client` or `server`). +3. Wrap raw zod shapes with `z.object({...})`. +4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed + `setRequestHandler` calls. +5. Replace `McpError` / numeric-code checks with `ProtocolError` / `SdkError`. diff --git a/docs/overview.md b/docs/overview.md index 5f3852d..8eda95d 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -56,8 +56,8 @@ flowchart LR - **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them. - **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat. -The View's `App` and the Host's `AppBridge` both subclass the base MCP SDK's -public `Protocol` for the iframe channel. A separate outer `Client` connects +The View's `App` and the Host's `AppBridge` both subclass the `Protocol` class +exported by `@modelcontextprotocol/client` for the iframe channel. A separate outer `Client` connects the Host to the actual MCP Server. Keeping those two connections separate preserves the Apps-only iframe handshake and prevents iframe capabilities from leaking into the server connection. diff --git a/docs/quickstart.md b/docs/quickstart.md index 3cac872..2039dc1 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -37,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 @modelcontextprotocol/node@2.0.0-beta.5 @modelcontextprotocol/express@2.0.0-beta.5 zod@^4.2.0 express cors +npm install @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 express cors npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md index e5d82b5..a8eb333 100644 --- a/docs/testing-mcp-apps.md +++ b/docs/testing-mcp-apps.md @@ -29,7 +29,7 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex ``` The root install runs the package build, including type-checking the - documentation snippets against the exact base MCP SDK beta.4 packages. Do + documentation snippets against the pinned base MCP SDK 2.x packages. Do not bypass a failed build before testing the host. 2. Start basic-host, pointing it to your MCP server: diff --git a/examples/transcript-server/README.md b/examples/transcript-server/README.md index 3b3fa42..559ffad 100644 --- a/examples/transcript-server/README.md +++ b/examples/transcript-server/README.md @@ -56,7 +56,7 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ### Prerequisites -- Node.js 18+ +- Node.js 20+ - Chrome, Edge, or Safari (Web Speech API support) ### Installation diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md index fda72b0..df7d931 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -77,15 +77,27 @@ Before writing any code, analyze the server's existing tools and determine which ## Step 2: Add Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 zod@^4.2.0 +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0 npm install -D vite vite-plugin-singlefile ``` -Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React). +Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React), and `@modelcontextprotocol/node` / `@modelcontextprotocol/express` if the server uses HTTP transports. -Use the exact base MCP SDK prerelease required by ext-apps. Existing servers -that still import `@modelcontextprotocol/sdk` v1 must migrate those imports and -handler schemas before adding the App integration. +ext-apps 2.x requires the split base MCP SDK packages at `^2.0.0` +(`@modelcontextprotocol/core` comes in transitively). Existing servers that +still import `@modelcontextprotocol/sdk` v1 must migrate those imports and +handler schemas before adding the App integration: + +| SDK v1 (`@modelcontextprotocol/sdk`) | SDK v2 | +|---|---| +| `sdk/server/mcp.js` (`McpServer`) | `@modelcontextprotocol/server` | +| `sdk/server/streamableHttp.js` (`StreamableHTTPServerTransport`) | `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` | +| Express wiring by hand | `createMcpExpressApp` from `@modelcontextprotocol/express` | +| `sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` | +| `sdk/types.js` (types, schemas) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| Raw zod shapes: `inputSchema: { q: z.string() }` | `inputSchema: z.object({ q: z.string() })` | +| `extra.signal` in tool callbacks | `extra.mcpReq.signal` | +| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", handler)` | ## Step 3: Set Up the Build Pipeline diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md index 03fc8d7..8fb7fa0 100644 --- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md +++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md @@ -115,12 +115,14 @@ Create a new MCP server with tool and resource registration. This wraps the exis ### Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 zod@^4.2.0 +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0 npm install -D tsx vite vite-plugin-singlefile ``` -Use `npm install` with the exact base MCP SDK prerelease required by ext-apps. -Do not substitute unpublished local packages or guess a different prerelease. +Use `npm install` so the package manager resolves versions; ext-apps 2.x needs +the split base MCP SDK packages at `^2.0.0` (`@modelcontextprotocol/core` comes +in transitively). Do not add the legacy `@modelcontextprotocol/sdk` v1 package +or substitute unpublished local packages. ### Server Code diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md index 6f1c466..d73dc90 100644 --- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md +++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md @@ -110,12 +110,14 @@ See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes: **Always** use `npm install` to add dependencies rather than manually writing version numbers: ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 @modelcontextprotocol/node@2.0.0-beta.5 @modelcontextprotocol/express@2.0.0-beta.5 zod@^4.2.0 express cors +npm install @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 express cors npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors ``` -Use the exact base MCP SDK prerelease required by ext-apps. Do not substitute -unpublished local packages or guess a different prerelease. +ext-apps 2.x requires the split base MCP SDK packages (`@modelcontextprotocol/client`, +`server`, `node`, `express`) at `^2.0.0`; `@modelcontextprotocol/core` comes in +transitively. Do not add the legacy `@modelcontextprotocol/sdk` v1 package or +substitute unpublished local packages. ### TypeScript Server Execution diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 1531bdc..c9b9476 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -19,6 +19,7 @@ const config = { "docs/authorization.md", "docs/csp-cors.md", "docs/migrate_from_openai_apps.md", + "docs/migrate-to-2.md", ], entryPoints: [ "src/server/index.ts", @@ -43,6 +44,7 @@ const config = { ResponseCacheStore: BASE_SDK_DOCS, SdkError: BASE_SDK_DOCS, "SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS, + StandardJSONSchemaV1: BASE_SDK_DOCS, "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS, "__type.enforceStrictCapabilities": BASE_SDK_DOCS, }, From ea6b89c9e2670b9ef73ad8b61d989cf6f2f1cc05 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:33:01 +0100 Subject: [PATCH 87/97] 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. --- src/wire-compat.test.ts | 576 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 src/wire-compat.test.ts diff --git a/src/wire-compat.test.ts b/src/wire-compat.test.ts new file mode 100644 index 0000000..5fdca8b --- /dev/null +++ b/src/wire-compat.test.ts @@ -0,0 +1,576 @@ +/** + * Cross-version wire compatibility. + * + * Replays JSON-RPC messages captured from ext-apps 1.7.x (built on + * `@modelcontextprotocol/sdk` 1.x) through the 2.x `App` and `AppBridge` over + * a raw in-memory transport, and asserts what 2.x emits back. The MCP Apps + * wire protocol is meant to be unchanged across the major bump; the few + * host-side error deltas are pinned here so they stay deliberate. + */ +import { describe, it, expect, afterEach } from "bun:test"; +import { + Client, + InMemoryTransport, + ProtocolError, + type JSONRPCMessage, + type Transport, +} from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import { App } from "./app.js"; +import { + AppBridge, + type McpUiHostCapabilities, + type McpUiHostContext, +} from "./app-bridge.js"; +import { LATEST_PROTOCOL_VERSION } from "./types.js"; + +/** Wait for pending microtasks/timers to complete. */ +const flush = () => new Promise((resolve) => setTimeout(resolve, 5)); + +/** + * One end of a raw in-memory channel. The 2.x side gets `transport`; the test + * plays the role of the 1.x peer by calling `inject()` with captured JSON and + * reading `sent` to see exactly what 2.x put on the wire. + */ +function createRawChannel() { + const sent: JSONRPCMessage[] = []; + let closed = false; + const transport: Transport = { + async start() {}, + async send(message) { + // Serialize like a real transport would so we assert on plain JSON. + sent.push(JSON.parse(JSON.stringify(message))); + }, + async close() { + if (closed) return; + closed = true; + transport.onclose?.(); + }, + }; + return { + transport, + sent, + /** Deliver a raw message (as the 1.x peer would have sent it). */ + inject(message: unknown) { + transport.onmessage?.(message as JSONRPCMessage); + }, + /** Messages sent after the given index. */ + since(index: number) { + return sent.slice(index); + }, + }; +} + +const hostInfo = { name: "TestHost", version: "9.9.9" }; +const hostCapabilities: McpUiHostCapabilities = { + openLinks: {}, + serverTools: {}, + serverResources: {}, + logging: {}, + updateModelContext: {}, +}; +const hostContext: McpUiHostContext = { + theme: "dark", + displayMode: "inline", + locale: "en-US", +}; + +/** Captured from ext-apps 1.7.x View (sdk 1.x key order, numeric ids from 0). */ +const v1View = { + initialize: { + method: "ui/initialize", + params: { + appCapabilities: { tools: { listChanged: true } }, + appInfo: { name: "TestView", version: "1.2.3" }, + protocolVersion: "2026-01-26", + }, + jsonrpc: "2.0", + id: 0, + }, + initialized: { method: "ui/notifications/initialized", jsonrpc: "2.0" }, + callTool: { + method: "tools/call", + params: { name: "echo", arguments: { a: 1 }, _meta: { progressToken: 6 } }, + jsonrpc: "2.0", + id: 6, + }, + readResource: { + method: "resources/read", + params: { uri: "test://x" }, + jsonrpc: "2.0", + id: 12, + }, + toolsListResult: (id: number) => ({ + result: { + tools: [ + { + name: "viewtool", + description: "view tool", + inputSchema: { + type: "object", + properties: { x: { type: "number" } }, + }, + }, + ], + }, + jsonrpc: "2.0", + id, + }), +}; + +/** Captured from ext-apps 1.7.x AppBridge (sdk 1.x). */ +const v1Host = { + initializeResult: (id: number) => ({ + result: { + protocolVersion: "2026-01-26", + hostCapabilities, + hostInfo, + hostContext, + }, + jsonrpc: "2.0", + id, + }), + toolInput: { + method: "ui/notifications/tool-input", + params: { arguments: { location: "NYC" } }, + jsonrpc: "2.0", + }, + callToolResult: (id: number) => ({ + result: { content: [{ type: "text", text: "echo 1" }] }, + jsonrpc: "2.0", + id, + }), + /** McpServer 1.x reports an unknown tool as an isError result, not an error. */ + callToolUnknownResult: (id: number) => ({ + result: { + content: [ + { type: "text", text: "MCP error -32602: Tool nope not found" }, + ], + isError: true, + }, + jsonrpc: "2.0", + id, + }), + resourceNotFoundError: (id: number) => ({ + jsonrpc: "2.0", + id, + error: { + code: -32002, + message: "MCP error -32002: Resource not found (host-thrown -32002)", + }, + }), + toolsList: { method: "tools/list", jsonrpc: "2.0", id: 0 }, + callViewTool: { + method: "tools/call", + params: { name: "viewtool", arguments: { x: 7 } }, + jsonrpc: "2.0", + id: 1, + }, +}; + +describe("wire compatibility: 2.x AppBridge with a 1.x View", () => { + let bridge: AppBridge; + + afterEach(async () => { + await bridge?.close().catch(() => {}); + }); + + async function connectBridge() { + const channel = createRawChannel(); + bridge = new AppBridge(null, hostInfo, hostCapabilities, { hostContext }); + await bridge.connect(channel.transport); + return channel; + } + + async function handshake() { + const channel = await connectBridge(); + let initialized = 0; + bridge.oninitialized = () => { + initialized++; + }; + channel.inject(v1View.initialize); + await flush(); + channel.inject(v1View.initialized); + await flush(); + return { channel, initialized: () => initialized }; + } + + it("sends nothing on connect: no MCP initialize toward the View", async () => { + const channel = await connectBridge(); + await flush(); + expect(channel.sent).toEqual([]); + }); + + it("answers a 1.x ui/initialize with the 1.x result shape and same id", async () => { + const channel = await connectBridge(); + channel.inject(v1View.initialize); + await flush(); + + expect(channel.sent).toEqual([ + { + jsonrpc: "2.0", + id: 0, + result: { + protocolVersion: LATEST_PROTOCOL_VERSION, + hostCapabilities, + hostInfo, + hostContext, + }, + }, + ]); + expect(bridge.getAppCapabilities()).toEqual({ + tools: { listChanged: true }, + }); + expect(bridge.getAppVersion()).toEqual({ + name: "TestView", + version: "1.2.3", + }); + }); + + it("accepts ui/notifications/initialized without params", async () => { + const { channel, initialized } = await handshake(); + expect(initialized()).toBe(1); + // Only the initialize response went out; the notification gets no reply + // and the bridge never starts an MCP handshake of its own. + expect(channel.sent).toHaveLength(1); + expect(channel.sent.some((m) => "method" in m)).toBe(false); + }); + + it("answers a 1.x tools/call request through oncalltool", async () => { + const { channel } = await handshake(); + bridge.oncalltool = async (params) => ({ + content: [{ type: "text", text: `echo ${params.arguments?.a}` }], + }); + const mark = channel.sent.length; + channel.inject(v1View.callTool); + await flush(); + + expect(channel.since(mark)).toEqual([ + { + jsonrpc: "2.0", + id: 6, + result: { content: [{ type: "text", text: "echo 1" }] }, + }, + ]); + }); + + it("re-encodes a handler-thrown -32002 as -32602 without the MCP error prefix", async () => { + // Documented host-side delta: SDK 2.x never emits -32002 on the wire. + const { channel } = await handshake(); + bridge.onreadresource = async () => { + throw new ProtocolError( + -32002, + "Resource not found (host-thrown -32002)", + ); + }; + const mark = channel.sent.length; + channel.inject(v1View.readResource); + await flush(); + + const [reply] = channel.since(mark); + expect(reply).toMatchObject({ + jsonrpc: "2.0", + id: 12, + error: { code: -32602 }, + }); + const message = (reply as { error: { message: string } }).error.message; + expect(message).toBe("Resource not found (host-thrown -32002)"); + expect(message).not.toMatch(/^MCP error/); + }); + + it("rejects invalid params on a ui/* request with -32602", async () => { + const { channel } = await handshake(); + bridge.onopenlink = async () => ({}); + const mark = channel.sent.length; + channel.inject({ + method: "ui/open-link", + params: { url: 42 }, + jsonrpc: "2.0", + id: 14, + }); + await flush(); + + expect(channel.since(mark)).toHaveLength(1); + expect(channel.since(mark)[0]).toMatchObject({ + jsonrpc: "2.0", + id: 14, + error: { code: -32602 }, + }); + }); + + it("lists and calls View tools with 1.x-shaped responses", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const listPromise = bridge.listTools({}); + await flush(); + + const [listRequest] = channel.since(mark) as Array<{ + id: number; + method: string; + }>; + expect(listRequest).toMatchObject({ jsonrpc: "2.0", method: "tools/list" }); + channel.inject(v1View.toolsListResult(listRequest.id)); + const list = await listPromise; + expect(list.tools.map((t) => t.name)).toEqual(["viewtool"]); + + const callPromise = bridge.callTool({ + name: "viewtool", + arguments: { x: 7 }, + }); + await flush(); + const [callRequest] = channel.since(mark + 1) as Array<{ id: number }>; + expect(callRequest).toMatchObject({ + jsonrpc: "2.0", + method: "tools/call", + params: { name: "viewtool", arguments: { x: 7 } }, + }); + channel.inject({ + result: { content: [{ type: "text", text: "view got 7" }] }, + jsonrpc: "2.0", + id: callRequest.id, + }); + expect(await callPromise).toEqual({ + content: [{ type: "text", text: "view got 7" }], + }); + }); + + it("only ever emits jsonrpc 2.0 messages, never an initialize request", async () => { + const { channel } = await handshake(); + bridge.oncalltool = async () => ({ content: [] }); + channel.inject(v1View.callTool); + bridge.sendToolInput({ arguments: { q: "x" } }); + bridge.sendToolResult({ content: [] }); + await flush(); + + for (const message of channel.sent) { + expect(message.jsonrpc).toBe("2.0"); + } + const methods = channel.sent + .filter((m): m is JSONRPCMessage & { method: string } => "method" in m) + .map((m) => m.method); + expect(methods).toEqual([ + "ui/notifications/tool-input", + "ui/notifications/tool-result", + ]); + expect(methods).not.toContain("initialize"); + expect(methods).not.toContain("notifications/initialized"); + }); +}); + +describe("wire compatibility: 2.x App with a 1.x host", () => { + let app: App; + + afterEach(async () => { + await app?.close().catch(() => {}); + }); + + /** Connect the App and answer its ui/initialize the way a 1.x host did. */ + async function handshake( + capabilities: ConstructorParameters[1] = {}, + setup: (app: App) => void = () => {}, + ) { + const channel = createRawChannel(); + app = new App({ name: "TestView", version: "1.2.3" }, capabilities, { + autoResize: false, + }); + setup(app); + const connected = app.connect(channel.transport); + await flush(); + + const [initialize] = channel.sent as Array<{ id: number }>; + channel.inject(v1Host.initializeResult(initialize.id)); + await connected; + await flush(); + return { channel, initialize }; + } + + it("sends a 1.x-compatible ui/initialize and ui/notifications/initialized", async () => { + const { channel } = await handshake({ tools: { listChanged: true } }); + + expect(channel.sent).toEqual([ + { + jsonrpc: "2.0", + id: 0, + method: "ui/initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + appInfo: { name: "TestView", version: "1.2.3" }, + appCapabilities: { tools: { listChanged: true } }, + }, + }, + { jsonrpc: "2.0", method: "ui/notifications/initialized" }, + ]); + expect(app.getHostContext()).toEqual(hostContext); + expect(app.getHostCapabilities()).toEqual(hostCapabilities); + expect(app.getHostVersion()).toEqual(hostInfo); + }); + + it("delivers a 1.x notification with sdk-1 key order to ontoolinput", async () => { + const received: unknown[] = []; + const { channel } = await handshake({}, (app) => { + app.ontoolinput = (params) => { + received.push(params); + }; + }); + channel.inject(v1Host.toolInput); + await flush(); + expect(received).toEqual([{ arguments: { location: "NYC" } }]); + }); + + it("resolves callServerTool from a 1.x tools/call response", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const promise = app.callServerTool({ name: "echo", arguments: { a: 1 } }); + await flush(); + + const [request] = channel.since(mark) as Array<{ id: number }>; + expect(request).toMatchObject({ + jsonrpc: "2.0", + method: "tools/call", + params: { + name: "echo", + arguments: { a: 1 }, + _meta: { progressToken: request.id }, + }, + }); + channel.inject(v1Host.callToolResult(request.id)); + expect(await promise).toEqual({ + content: [{ type: "text", text: "echo 1" }], + }); + }); + + it("passes through a 1.x isError tool result unchanged", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const promise = app.callServerTool({ name: "nope", arguments: {} }); + await flush(); + const [request] = channel.since(mark) as Array<{ id: number }>; + channel.inject(v1Host.callToolUnknownResult(request.id)); + expect(await promise).toEqual({ + content: [ + { type: "text", text: "MCP error -32602: Tool nope not found" }, + ], + isError: true, + }); + }); + + it("surfaces a 1.x -32002 error as a ProtocolError with code -32002", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const promise = app.readServerResource({ uri: "test://x" }); + await flush(); + const [request] = channel.since(mark) as Array<{ id: number }>; + channel.inject(v1Host.resourceNotFoundError(request.id)); + + const error = await promise.then( + () => undefined, + (e: unknown) => e, + ); + expect(error).toBeInstanceOf(ProtocolError); + expect((error as ProtocolError).code).toBe(-32002); + expect((error as ProtocolError).message).toContain( + "Resource not found (host-thrown -32002)", + ); + }); + + it("answers 1.x tools/list (no params) and tools/call requests from the host", async () => { + const { channel } = await handshake({ tools: { listChanged: true } }); + app.registerTool( + "viewtool", + { description: "view tool", inputSchema: z.object({ x: z.number() }) }, + async ({ x }) => ({ + content: [{ type: "text", text: `view got ${x}` }], + }), + ); + await flush(); + const mark = channel.sent.length; + + channel.inject(v1Host.toolsList); + await flush(); + const [listReply] = channel.since(mark) as unknown as Array<{ + result: { tools: Array<{ name: string; inputSchema: unknown }> }; + }>; + expect(listReply).toMatchObject({ jsonrpc: "2.0", id: 0 }); + expect(listReply.result.tools).toHaveLength(1); + expect(listReply.result.tools[0]).toMatchObject({ + name: "viewtool", + description: "view tool", + inputSchema: { + type: "object", + properties: { x: { type: "number" } }, + required: ["x"], + }, + }); + + channel.inject(v1Host.callViewTool); + await flush(); + expect(channel.since(mark + 1)).toEqual([ + { + jsonrpc: "2.0", + id: 1, + result: { content: [{ type: "text", text: "view got 7" }] }, + }, + ]); + }); +}); + +describe("AppBridge proxy over a default (modern era) 2.x Client/Server", () => { + let client: Client; + let server: McpServer; + let app: App; + let bridge: AppBridge; + + afterEach(async () => { + await app?.close().catch(() => {}); + await bridge?.close().catch(() => {}); + await client?.close().catch(() => {}); + await server?.close().catch(() => {}); + }); + + it("forwards tools/call and tools/list_changed with default negotiation", async () => { + server = new McpServer({ name: "ActualServer", version: "1.0.0" }); + server.registerTool( + "echo", + { inputSchema: z.object({ a: z.number() }) }, + async ({ a }) => ({ content: [{ type: "text", text: `echo ${a}` }] }), + ); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + + client = new Client({ name: "HostOuterClient", version: "1.0.0" }); + await client.connect(clientTransport); + expect(client.getServerCapabilities()?.tools).toEqual({ + listChanged: true, + }); + + bridge = new AppBridge(client, hostInfo, hostCapabilities, { + hostContext, + }); + app = new App( + { name: "TestView", version: "1.2.3" }, + {}, + { + autoResize: false, + }, + ); + const listChanged: string[] = []; + app.setNotificationHandler("notifications/tools/list_changed", () => { + listChanged.push("tools"); + }); + const [appTransport, bridgeTransport] = + InMemoryTransport.createLinkedPair(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + expect( + await app.callServerTool({ name: "echo", arguments: { a: 1 } }), + ).toMatchObject({ content: [{ type: "text", text: "echo 1" }] }); + + server.sendToolListChanged(); + await flush(); + expect(listChanged).toEqual(["tools"]); + }); +}); From cfdc0c725f03b41ba9f3ccb2299386a2369cb9ed Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:21:49 +0100 Subject: [PATCH 88/97] 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 --- src/app-bridge.test.ts | 85 +++++++++++++++++++- src/app-bridge.ts | 176 ++++++++++++++++++++++++++++++++--------- src/app.test.ts | 111 +++++++++++++++++++++++--- src/app.ts | 159 ++++++++++++++++++++----------------- src/constants.ts | 65 ++++++++++++++- src/events.ts | 41 ++++++++++ 6 files changed, 510 insertions(+), 127 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 397e12f..ea85320 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -3016,16 +3016,95 @@ describe("isToolVisibilityAppOnly", () => { expect(app.onteardown).toBe(handler); }); - it("direct setRequestHandler uses base SDK replacement semantics", () => { + it("direct setRequestHandler throws when called twice", () => { const bridge2 = new AppBridge( createMockClient() as Client, testHostInfo, testHostCapabilities, ); - bridge2.setRequestHandler("ping", () => ({})); + const params = z.object({}); + bridge2.setRequestHandler("test/method", { params }, () => ({})); expect(() => { - bridge2.setRequestHandler("ping", () => ({})); + bridge2.setRequestHandler("test/method", { params }, () => ({})); + }).toThrow(/already registered/); + }); + + it("direct setRequestHandler cannot silently replace an on* host handler", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + bridge2.onopenlink = async () => ({}); + expect(() => { + bridge2.setRequestHandler( + "ui/open-link", + { params: z.object({}) }, + () => ({}), + ); + }).toThrow(/already registered/); + }); + + it("direct setNotificationHandler throws for event-mapped methods", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + bridge2.onsizechange = () => {}; + expect(() => { + bridge2.setNotificationHandler( + "ui/notifications/size-changed", + { params: z.object({}) }, + () => {}, + ); + }).toThrow(/already registered/); + }); + + it("removeRequestHandler releases the method so an on* setter can re-register", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + bridge2.removeRequestHandler("tools/call"); + expect(() => { + bridge2.setRequestHandler("tools/call", async () => ({ content: [] })); }).not.toThrow(); + expect(() => { + bridge2.oncalltool = async () => ({ content: [] }); + }).not.toThrow(); + }); + + it("oncreatesamplingmessage has a getter, replace semantics, and a replace warning", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + expect(bridge2.oncreatesamplingmessage).toBeUndefined(); + const first = async () => ({ + role: "assistant" as const, + content: { type: "text" as const, text: "" }, + model: "m", + }); + bridge2.oncreatesamplingmessage = first; + expect(bridge2.oncreatesamplingmessage).toBe(first); + + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + expect(() => { + bridge2.oncreatesamplingmessage = first; + }).not.toThrow(); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("oncreatesamplingmessage handler replaced"), + ); + } finally { + warn.mockRestore(); + } + + bridge2.oncreatesamplingmessage = undefined; + expect(bridge2.oncreatesamplingmessage).toBeUndefined(); }); }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 8e48677..04ccb4f 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -34,7 +34,7 @@ import { EmptyResultSchema, LoggingMessageNotificationSchema, } from "@modelcontextprotocol/core"; -import { EventDispatcher } from "./events.js"; +import { EventDispatcher, MethodRegistry } from "./events.js"; import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; type MethodSchema = ZodObject<{ @@ -42,6 +42,8 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; + import { type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, @@ -216,8 +218,8 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; /** * Extra metadata passed to request handlers. * - * This type represents the additional context provided by the base MCP SDK - * `Server` when handling requests. + * This type represents the additional context (`BaseContext`) provided by the + * base MCP SDK `Protocol` when handling requests. * * @internal */ @@ -302,6 +304,81 @@ export class AppBridge extends Protocol { private _initializedReceived = false; private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); + private readonly _methods = new MethodRegistry(); + + // ── Handler registration with double-set protection ───────────────── + // + // The base SDK `Protocol` silently replaces an existing handler. The four + // overrides below restore the v1 behaviour: a direct `setRequestHandler` / + // `setNotificationHandler` for a method that already has a handler throws, + // so a stray registration cannot silently replace a host's `on*` handler + // (for example the URL allow-listing in `onopenlink`) or disconnect + // `addEventListener` listeners. They are arrow-function class fields rather + // than prototype methods so that `Protocol`'s constructor — which registers + // its own ping/cancelled/progress handlers before our fields initialize — + // hits the base implementation and skips tracking. + + /** + * Registers a request handler. Throws if a handler for the same method has + * already been registered — use the `on*` setter for replace semantics. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setRequestHandler: Protocol["setRequestHandler"] = ( + method: string, + ...rest: unknown[] + ) => { + this._methods.claim(method, "setRequestHandler"); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + /** + * Registers a notification handler. Throws if a handler for the same method + * has already been registered — use the `on*` setter (replace semantics) or + * `addEventListener` (multi-listener) for mapped events. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setNotificationHandler: Protocol["setNotificationHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.claim(method, "setNotificationHandler"); + (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + override removeRequestHandler: Protocol["removeRequestHandler"] = + (method: string) => { + this._methods.release(method); + super.removeRequestHandler(method); + }; + + override removeNotificationHandler: Protocol["removeNotificationHandler"] = + (method: string) => { + this._methods.release(method); + super.removeNotificationHandler(method); + }; + + /** + * Register a request handler with replace semantics, bypassing the + * double-set protection of {@link setRequestHandler `setRequestHandler`}. + * Used by the `on*` request-handler setters. + */ + protected replaceRequestHandler: Protocol["setRequestHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.replace(method); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; /** * The base MCP SDK calls this hook for every standard and custom request @@ -419,7 +496,7 @@ export class AppBridge extends Protocol { * manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters. * @param _hostInfo - Host application identification (name and version) * @param _hostCapabilities - Features and capabilities the host supports - * @param options - Configuration options (inherited from Server) + * @param options - Configuration options (`ProtocolOptions` from the base MCP SDK plus `hostContext`) * * @example With MCP client (automatic forwarding) * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient" @@ -455,7 +532,7 @@ export class AppBridge extends Protocol { this._hostContext = options?.hostContext || {}; - this.setRequestHandler( + this.replaceRequestHandler( "ui/initialize", { params: McpUiInitializeRequestSchema.shape.params, @@ -464,14 +541,14 @@ export class AppBridge extends Protocol { (params) => this._onAppsInitialize(params), ); - this.setRequestHandler("ping", (request, extra) => { + this.replaceRequestHandler("ping", (request, extra) => { this.onping?.(request.params, extra); return {}; }); // Default handler for requestDisplayMode - returns current mode from host context. // Hosts can override this by setting bridge.onrequestdisplaymode = ... - this.setRequestHandler( + this.replaceRequestHandler( "ui/request-display-mode", { params: McpUiRequestDisplayModeRequestSchema.shape.params, @@ -746,7 +823,7 @@ export class AppBridge extends Protocol { ) { this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); this._onmessage = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/message", { params: McpUiMessageRequestSchema.shape.params, @@ -819,7 +896,7 @@ export class AppBridge extends Protocol { ) { this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); this._onopenlink = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/open-link", { params: McpUiOpenLinkRequestSchema.shape.params, @@ -895,7 +972,7 @@ export class AppBridge extends Protocol { callback, ); this._ondownloadfile = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/download-file", { params: McpUiDownloadFileRequestSchema.shape.params, @@ -999,7 +1076,7 @@ export class AppBridge extends Protocol { callback, ); this._onrequestdisplaymode = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/request-display-mode", { params: McpUiRequestDisplayModeRequestSchema.shape.params, @@ -1100,7 +1177,7 @@ export class AppBridge extends Protocol { callback, ); this._onupdatemodelcontext = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/update-model-context", { params: McpUiUpdateModelContextRequestSchema.shape.params, @@ -1136,8 +1213,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/server for the request type - * @see `CallToolResult` from @modelcontextprotocol/server for the result type + * @see `CallToolRequest` from @modelcontextprotocol/client forthe request type + * @see `CallToolResult` from @modelcontextprotocol/client forthe result type */ private _oncalltool?: ( params: CallToolRequest["params"], @@ -1156,7 +1233,7 @@ export class AppBridge extends Protocol { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.setRequestHandler("tools/call", async (request, extra) => { + this.replaceRequestHandler("tools/call", async (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1190,18 +1267,39 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/server for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/server for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/client forthe request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client forresult types */ + private _oncreatesamplingmessage?: ( + params: CreateMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get oncreatesamplingmessage() { + return this._oncreatesamplingmessage; + } set oncreatesamplingmessage( - callback: ( - params: CreateMessageRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: CreateMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler("sampling/createMessage", async (request, extra) => { - return callback(request.params, extra); - }); + this.warnIfRequestHandlerReplaced( + "oncreatesamplingmessage", + this._oncreatesamplingmessage, + callback, + ); + this._oncreatesamplingmessage = callback; + this.replaceRequestHandler( + "sampling/createMessage", + async (request, extra) => { + if (!this._oncreatesamplingmessage) { + throw new Error("No oncreatesamplingmessage handler set"); + } + return this._oncreatesamplingmessage(request.params, extra); + }, + ); } /** @@ -1221,7 +1319,7 @@ export class AppBridge extends Protocol { * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/server for the notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/client forthe notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1252,8 +1350,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/server for the request type - * @see `ListResourcesResult` from @modelcontextprotocol/server for the result type + * @see `ListResourcesRequest` from @modelcontextprotocol/client forthe request type + * @see `ListResourcesResult` from @modelcontextprotocol/client forthe result type */ private _onlistresources?: ( params: ListResourcesRequest["params"], @@ -1276,7 +1374,7 @@ export class AppBridge extends Protocol { callback, ); this._onlistresources = callback; - this.setRequestHandler("resources/list", async (request, extra) => { + this.replaceRequestHandler("resources/list", async (request, extra) => { if (!this._onlistresources) throw new Error("No onlistresources handler set"); return this._onlistresources(request.params, extra); @@ -1305,8 +1403,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/server for the request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/server for the result type + * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client forthe request type + * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client forthe result type */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], @@ -1329,7 +1427,7 @@ export class AppBridge extends Protocol { callback, ); this._onlistresourcetemplates = callback; - this.setRequestHandler( + this.replaceRequestHandler( "resources/templates/list", async (request, extra) => { if (!this._onlistresourcetemplates) @@ -1361,8 +1459,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/server for the request type - * @see `ReadResourceResult` from @modelcontextprotocol/server for the result type + * @see `ReadResourceRequest` from @modelcontextprotocol/client forthe request type + * @see `ReadResourceResult` from @modelcontextprotocol/client forthe result type */ private _onreadresource?: ( params: ReadResourceRequest["params"], @@ -1385,7 +1483,7 @@ export class AppBridge extends Protocol { callback, ); this._onreadresource = callback; - this.setRequestHandler("resources/read", async (request, extra) => { + this.replaceRequestHandler("resources/read", async (request, extra) => { if (!this._onreadresource) throw new Error("No onreadresource handler set"); return this._onreadresource(request.params, extra); @@ -1409,7 +1507,7 @@ export class AppBridge extends Protocol { * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/server for the notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/client forthe notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1442,8 +1540,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/server for the request type - * @see `ListPromptsResult` from @modelcontextprotocol/server for the result type + * @see `ListPromptsRequest` from @modelcontextprotocol/client forthe request type + * @see `ListPromptsResult` from @modelcontextprotocol/client forthe result type */ private _onlistprompts?: ( params: ListPromptsRequest["params"], @@ -1466,7 +1564,7 @@ export class AppBridge extends Protocol { callback, ); this._onlistprompts = callback; - this.setRequestHandler("prompts/list", async (request, extra) => { + this.replaceRequestHandler("prompts/list", async (request, extra) => { if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); return this._onlistprompts(request.params, extra); }); @@ -1489,7 +1587,7 @@ export class AppBridge extends Protocol { * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/server for the notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/client forthe notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ diff --git a/src/app.test.ts b/src/app.test.ts index 9057710..35a924f 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -248,25 +248,62 @@ describe("App base MCP SDK v2 Protocol migration", () => { }).toThrow(/handler registered after connect/); }); - it("uses base MCP SDK replacement semantics for request handlers", async () => { + it("direct setRequestHandler throws when a handler is already registered", async () => { const pair = await connectPair(); connected.push(pair); + const params = z.object({}); let handledBy = 0; - pair.app.setRequestHandler("ping", () => { + pair.app.setRequestHandler("test/method", { params }, () => { handledBy = 1; return {}; }); - pair.app.setRequestHandler("ping", () => { + expect(() => { + pair.app.setRequestHandler("test/method", { params }, () => { + handledBy = 2; + return {}; + }); + }).toThrow(/already registered/); + + await pair.server.request( + { method: "test/method", params: {} }, + z.object({}), + ); + expect(handledBy).toBe(1); + }); + + it("direct setRequestHandler throws for methods owned by the App itself", async () => { + const pair = await connectPair(); + connected.push(pair); + expect(() => { + pair.app.setRequestHandler("ping", () => ({})); + }).toThrow(/already registered/); + }); + + it("removeRequestHandler releases the method for re-registration", async () => { + const pair = await connectPair(); + connected.push(pair); + const params = z.object({}); + let handledBy = 0; + + pair.app.setRequestHandler("test/method", { params }, () => { + handledBy = 1; + return {}; + }); + pair.app.removeRequestHandler("test/method"); + pair.app.setRequestHandler("test/method", { params }, () => { handledBy = 2; return {}; }); - await pair.server.request({ method: "ping", params: {} }); + await pair.server.request( + { method: "test/method", params: {} }, + z.object({}), + ); expect(handledBy).toBe(2); }); - it("uses base MCP SDK replacement semantics for non-event notifications", async () => { + it("direct setNotificationHandler throws when a handler is already registered", async () => { const pair = await connectPair(); connected.push(pair); const calls: number[] = []; @@ -275,14 +312,70 @@ describe("App base MCP SDK v2 Protocol migration", () => { pair.app.setNotificationHandler("test/notification", { params }, () => { calls.push(1); }); - pair.app.setNotificationHandler("test/notification", { params }, () => { - calls.push(2); - }); + expect(() => { + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(2); + }); + }).toThrow(/already registered/); await pair.server.notification({ method: "test/notification", params: {}, }); - expect(calls).toEqual([2]); + expect(calls).toEqual([1]); + }); + + it("direct setNotificationHandler throws for event-mapped methods (listener first)", () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false }, + ); + app.addEventListener("toolinput", () => {}); + expect(() => { + app.setNotificationHandler( + "ui/notifications/tool-input", + { params: z.object({}) }, + () => {}, + ); + }).toThrow(/already registered/); + }); + + it("event registration throws when a direct handler already owns the method", () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false }, + ); + app.setNotificationHandler( + "ui/notifications/tool-input", + { params: z.object({}) }, + () => {}, + ); + expect(() => { + app.ontoolinput = () => {}; + }).toThrow(/already registered/); + expect(() => { + app.addEventListener("toolinput", () => {}); + }).toThrow(/already registered/); + }); + + it("on* request setters keep replace semantics", async () => { + const pair = await connectPair(); + connected.push(pair); + let handledBy = 0; + pair.app.onteardown = async () => { + handledBy = 1; + return {}; + }; + pair.app.onteardown = async () => { + handledBy = 2; + return {}; + }; + await pair.server.request( + { method: "ui/resource-teardown", params: {} }, + z.object({}), + ); + expect(handledBy).toBe(2); }); }); diff --git a/src/app.ts b/src/app.ts index 3fe84d6..fd6bbc7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -23,8 +23,10 @@ import { } from "@modelcontextprotocol/client"; import { EmptyResultSchema } from "@modelcontextprotocol/core"; export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; -import { EventDispatcher } from "./events.js"; +import { EventDispatcher, MethodRegistry } from "./events.js"; export { EventDispatcher } from "./events.js"; + +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; import { PostMessageTransport } from "./message-transport.js"; import { LATEST_PROTOCOL_VERSION, @@ -103,71 +105,6 @@ export { applyDocumentTheme, } from "./styles.js"; -/** - * Metadata key for associating a UI resource URI with a tool. - * - * MCP servers include this key in tool definition metadata (via `tools/list`) - * to indicate which UI resource should be displayed when the tool is called. - * When hosts see a tool with this metadata, they fetch and render the - * corresponding {@link App `App`}. - * - * **Note**: This constant is provided for reference and backwards compatibility. - * Server developers should use {@link server-helpers!registerAppTool `registerAppTool`} - * with the `_meta.ui.resourceUri` format instead. Host developers must check both - * formats for compatibility. - * - * @example Modern format (server-side, not in Apps) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_modernFormat" - * // Preferred: Use registerAppTool with nested ui.resourceUri - * registerAppTool( - * server, - * "weather", - * { - * description: "Get weather forecast", - * _meta: { - * ui: { resourceUri: "ui://weather/forecast" }, - * }, - * }, - * handler, - * ); - * ``` - * - * @example Legacy format (deprecated, for backwards compatibility) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_legacyFormat" - * // Deprecated: Direct use of RESOURCE_URI_META_KEY - * server.registerTool( - * "weather", - * { - * description: "Get weather forecast", - * _meta: { - * [RESOURCE_URI_META_KEY]: "ui://weather/forecast", - * }, - * }, - * handler, - * ); - * ``` - * - * @example How hosts check for this metadata (must support both formats) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide" - * // Hosts should check both modern and legacy formats - * const meta = tool._meta; - * const uiMeta = meta?.ui as McpUiToolMeta | undefined; - * const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined; - * const uiUri = uiMeta?.resourceUri ?? legacyUri; - * if (typeof uiUri === "string" && uiUri.startsWith("ui://")) { - * // Fetch the resource and display the UI - * } - * ``` - */ - -/** - * MIME type for MCP UI resources. - * - * Identifies HTML content as an MCP App UI resource. - * - * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources. - */ - /** * Options for configuring {@link App `App`} behavior. * @@ -359,6 +296,80 @@ export class App extends Protocol { private _initializedSent = false; private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); + private readonly _methods = new MethodRegistry(); + + // ── Handler registration with double-set protection ───────────────── + // + // The base SDK `Protocol` silently replaces an existing handler. The four + // overrides below restore the v1 behaviour: a direct `setRequestHandler` / + // `setNotificationHandler` for a method that already has a handler throws, + // so a stray registration cannot silently disconnect `on*` handlers or + // `addEventListener` listeners. They are arrow-function class fields rather + // than prototype methods so that `Protocol`'s constructor — which registers + // its own ping/cancelled/progress handlers before our fields initialize — + // hits the base implementation and skips tracking. + + /** + * Registers a request handler. Throws if a handler for the same method has + * already been registered — use the `on*` setter for replace semantics. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setRequestHandler: Protocol["setRequestHandler"] = ( + method: string, + ...rest: unknown[] + ) => { + this._methods.claim(method, "setRequestHandler"); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + /** + * Registers a notification handler. Throws if a handler for the same method + * has already been registered — use the `on*` setter (replace semantics) or + * `addEventListener` (multi-listener) for mapped events. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setNotificationHandler: Protocol["setNotificationHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.claim(method, "setNotificationHandler"); + (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + override removeRequestHandler: Protocol["removeRequestHandler"] = + (method: string) => { + this._methods.release(method); + super.removeRequestHandler(method); + }; + + override removeNotificationHandler: Protocol["removeNotificationHandler"] = + (method: string) => { + this._methods.release(method); + super.removeNotificationHandler(method); + }; + + /** + * Register a request handler with replace semantics, bypassing the + * double-set protection of {@link setRequestHandler `setRequestHandler`}. + * Used by the `on*` request-handler setters. + */ + protected replaceRequestHandler: Protocol["setRequestHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.replace(method); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; /** * Warn if a host-bound method is called before {@link connect `connect`} has @@ -524,7 +535,7 @@ export class App extends Protocol { z.config({ jitless: true }); } - this.setRequestHandler("ping", (request) => { + this.replaceRequestHandler("ping", (request) => { console.log("Received ping:", request.params); return {}; }); @@ -1056,7 +1067,7 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); this._onteardown = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/resource-teardown", { params: McpUiResourceTeardownRequestSchema.shape.params, @@ -1114,7 +1125,7 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.setRequestHandler("tools/call", (request, extra) => { + this.replaceRequestHandler("tools/call", (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1185,7 +1196,7 @@ export class App extends Protocol { callback, ); this._onlisttools = callback; - this.setRequestHandler("tools/list", (request, extra) => { + this.replaceRequestHandler("tools/list", (request, extra) => { if (!this._onlisttools) throw new Error("No onlisttools handler set"); return this._onlisttools(request.params, extra); }); @@ -1221,11 +1232,11 @@ export class App extends Protocol { ); } return; - case "ping": - case "ui/resource-teardown": - return; default: - throw new Error(`No handler for method ${method} registered`); + // `ping`, `ui/*`, and custom (vendor-prefixed) methods need no + // declared capability. The base SDK explicitly supports custom + // request handlers via the `{ params, result }` form. + return; } } diff --git a/src/constants.ts b/src/constants.ts index f69cfa5..c879a03 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,66 @@ -/** Legacy metadata key associating an MCP tool with its App resource. */ +/** + * Metadata key for associating a UI resource URI with a tool. + * + * MCP servers include this key in tool definition metadata (via `tools/list`) + * to indicate which UI resource should be displayed when the tool is called. + * When hosts see a tool with this metadata, they fetch and render the + * corresponding {@link app!App `App`}. + * + * **Note**: This constant is provided for reference and backwards compatibility. + * Server developers should use {@link server-helpers!registerAppTool `registerAppTool`} + * with the `_meta.ui.resourceUri` format instead. Host developers must check both + * formats for compatibility. + * + * @example Modern format (server-side, not in Apps) + * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_modernFormat" + * // Preferred: Use registerAppTool with nested ui.resourceUri + * registerAppTool( + * server, + * "weather", + * { + * description: "Get weather forecast", + * _meta: { + * ui: { resourceUri: "ui://weather/forecast" }, + * }, + * }, + * handler, + * ); + * ``` + * + * @example Legacy format (deprecated, for backwards compatibility) + * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_legacyFormat" + * // Deprecated: Direct use of RESOURCE_URI_META_KEY + * server.registerTool( + * "weather", + * { + * description: "Get weather forecast", + * _meta: { + * [RESOURCE_URI_META_KEY]: "ui://weather/forecast", + * }, + * }, + * handler, + * ); + * ``` + * + * @example How hosts check for this metadata (must support both formats) + * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide" + * // Hosts should check both modern and legacy formats + * const meta = tool._meta; + * const uiMeta = meta?.ui as McpUiToolMeta | undefined; + * const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined; + * const uiUri = uiMeta?.resourceUri ?? legacyUri; + * if (typeof uiUri === "string" && uiUri.startsWith("ui://")) { + * // Fetch the resource and display the UI + * } + * ``` + */ export const RESOURCE_URI_META_KEY = "ui/resourceUri"; -/** MIME type for MCP App HTML resources. */ +/** + * MIME type for MCP UI resources. + * + * Identifies HTML content as an MCP App UI resource. + * + * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources. + */ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; diff --git a/src/events.ts b/src/events.ts index aff1446..aa2e862 100644 --- a/src/events.ts +++ b/src/events.ts @@ -89,3 +89,44 @@ export class EventDispatcher> { return slot; } } + +/** + * Tracks which JSON-RPC methods already have a handler registered through the + * owning `App` / `AppBridge`, so a second direct `setRequestHandler` / + * `setNotificationHandler` call for the same method throws instead of + * silently replacing the first handler (the base SDK `Protocol` replaces). + * + * The `on*` setters use {@link replace} for DOM-style replace semantics, and + * `removeRequestHandler` / `removeNotificationHandler` use {@link release}. + * + * @internal + */ +export class MethodRegistry { + private readonly _methods = new Set(); + + /** Claim `method`. Throws if a handler is already registered for it. */ + claim(method: string, via: string): void { + if (this._methods.has(method)) { + throw new Error( + `Handler for "${method}" already registered (via ${via}). ` + + `Use addEventListener() to attach multiple listeners, ` + + `or the on* setter for replace semantics.`, + ); + } + this._methods.add(method); + } + + /** Claim `method` with replace semantics (never throws). */ + replace(method: string): void { + this._methods.add(method); + } + + /** Release `method` so it can be claimed again. */ + release(method: string): void { + this._methods.delete(method); + } + + has(method: string): boolean { + return this._methods.has(method); + } +} From f722753c8fa8d85714390c134877722ee4d6800b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 7 Sep 2026 15:36:30 +0100 Subject: [PATCH 89/97] 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. --- src/app-bridge.test.ts | 35 +++-------------------------------- src/app.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index ea85320..a87b0e7 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1551,40 +1551,9 @@ describe("App <-> AppBridge integration", () => { ).rejects.toThrow(/Invalid input for tool translate/); }); - it("falls back to z.toJSONSchema for zod schemas lacking ~standard.jsonSchema (zod v3.25.x)", async () => { - // zod v3.25 implements ~standard.validate but not ~standard.jsonSchema. - // Simulate by stripping jsonSchema from a real zod schema. - const v4Schema = z.object({ q: z.string() }); - const zod3LikeSchema = Object.assign(Object.create(v4Schema), { - "~standard": { - version: 1 as const, - vendor: "zod", - validate: v4Schema["~standard"].validate, - types: undefined as - | undefined - | { - readonly input: { q: string }; - readonly output: { q: string }; - }, - // no jsonSchema - }, - }); - + it("rejects listTools when a tool schema does not implement Standard JSON Schema", async () => { const appCapabilities = { tools: { listChanged: true } }; app = new App(testAppInfo, appCapabilities, { autoResize: false }); - app.registerTool( - "search", - { inputSchema: zod3LikeSchema }, - async ({ q }: { q: string }) => ({ - content: [{ type: "text" as const, text: q }], - }), - ); - await app.connect(appTransport); - - const list = await bridge.listTools({}); - expect(list.tools[0].inputSchema.properties).toHaveProperty("q"); - - // Non-zod schema without jsonSchema → listTools rejects with guidance. app.registerTool( "broken", { @@ -1598,6 +1567,8 @@ describe("App <-> AppBridge integration", () => { }, async () => ({ content: [] }), ); + await app.connect(appTransport); + expect(bridge.listTools({})).rejects.toThrow( /does not implement Standard JSON Schema/, ); diff --git a/src/app.test.ts b/src/app.test.ts index 35a924f..8420e81 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -6,13 +6,13 @@ import { } from "@modelcontextprotocol/server"; import { z } from "zod/v4"; -import { App } from "./app"; +import { App } from "./app.js"; import { LATEST_PROTOCOL_VERSION, McpUiInitializeRequestSchema, McpUiInitializeResultSchema, McpUiInitializedNotificationSchema, -} from "./types"; +} from "./types.js"; type ConnectedPair = { app: App; From 6d083cd69f7b8214f236b75cc0ce52fe18617a7a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 13:09:13 +0100 Subject: [PATCH 90/97] 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. --- README.md | 2 +- docs/migrate-to-2.md | 24 +++++++---- examples/pdf-server/build-mcpb.mjs | 15 +++++++ .../skills/add-app-to-server/SKILL.md | 5 ++- src/app-bridge.ts | 42 ++++++++++--------- src/app.ts | 7 +++- src/wire-compat.test.ts | 4 +- 7 files changed, 65 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4b92f0c..acfc661 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ resources: ## Getting Started Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of -`ext-apps` (`@modelcontextprotocol/core` is pulled in transitively). +`ext-apps` (`@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it for you). For a View or host: diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md index 3f6db7b..2164b83 100644 --- a/docs/migrate-to-2.md +++ b/docs/migrate-to-2.md @@ -27,7 +27,7 @@ host-side deltas are in error responses (see below). | CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) | `@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend -its `Protocol` class); `@modelcontextprotocol/core` is installed transitively; +its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it without you listing it; `@modelcontextprotocol/server` stays optional and is only needed for the `./server` helpers. Node.js 20+ is required. @@ -48,8 +48,10 @@ its `Protocol` class); `@modelcontextprotocol/core` is installed transitively; `extra.mcpReq.id`. - **`setRequestHandler` / `setNotificationHandler` take method names.** `app.setRequestHandler(SomeRequestSchema, handler)` becomes - `app.setRequestHandler("some/method", handler)` (pass the params schema as the - optional extra argument for validation). + `app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)` + for custom methods (the handler receives the parsed params); the two-argument + `setRequestHandler("tools/call", handler)` form exists only for spec-defined + method names. - **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`); local conditions are `SdkError` with a string `code`: request timeout → `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a @@ -74,10 +76,15 @@ The published `./schema.json` export is regenerated from the 2.x core schemas: - `McpUiToolResultNotification.params.structuredContent` is any JSON value (was `type: "object"`). -- Result `_meta` documents `io.modelcontextprotocol/serverInfo` and no longer - lists `progressToken` / `related-task` (both still pass through). -- `McpUiHostContext.toolInfo.tool.inputSchema` is a loose object. -- A recursive `$defs.__schema0` JSON-value definition is added. +- `McpUiToolResultNotification.params._meta` documents + `io.modelcontextprotocol/serverInfo` and no longer lists `progressToken` / + `related-task` (both still pass through). +- `McpUiHostContext.toolInfo.tool.outputSchema` is a loose object (only + `$schema` is documented); `inputSchema.properties` values are now typed as + JSON values. +- A recursive JSON-value definition (`__schema0`) is added under the `$defs` of + `McpUiHostContext`, `McpUiHostContextChangedNotification` and + `McpUiInitializeResult`. ## Checklist @@ -87,7 +94,8 @@ The published `./schema.json` export is regenerated from the 2.x core schemas: `@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` → `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`, `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js` - → `@modelcontextprotocol/client` or `server`). + → `@modelcontextprotocol/client` or `server` for the types, + `@modelcontextprotocol/core` for the zod schemas). 3. Wrap raw zod shapes with `z.object({...})`. 4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed `setRequestHandler` calls. diff --git a/examples/pdf-server/build-mcpb.mjs b/examples/pdf-server/build-mcpb.mjs index c1c3469..2917334 100644 --- a/examples/pdf-server/build-mcpb.mjs +++ b/examples/pdf-server/build-mcpb.mjs @@ -43,6 +43,21 @@ writeFileSync( path.join(stage, "manifest.json"), JSON.stringify(manifest, null, 2), ); +// The bundle must ship the ext-apps build from this checkout, not whatever +// the registry has: pack the repo root and point the staged manifest at it. +const repoRoot = path.resolve(here, "..", ".."); +const packed = JSON.parse( + execSync( + "npm pack --json --ignore-scripts --pack-destination " + + JSON.stringify(stage), + { + cwd: repoRoot, + encoding: "utf8", + }, + ).replace(/^[^[]*/, ""), +); +pkg.dependencies["@modelcontextprotocol/ext-apps"] = + "file:./" + packed[0].filename; writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2)); const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" }); diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md index df7d931..5a473f0 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -94,10 +94,11 @@ handler schemas before adding the App integration: | `sdk/server/streamableHttp.js` (`StreamableHTTPServerTransport`) | `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` | | Express wiring by hand | `createMcpExpressApp` from `@modelcontextprotocol/express` | | `sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` | -| `sdk/types.js` (types, schemas) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `sdk/types.js` types (`CallToolResult`, …) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `sdk/types.js` zod schemas (`CallToolResultSchema`, …) | `@modelcontextprotocol/core` | | Raw zod shapes: `inputSchema: { q: z.string() }` | `inputSchema: z.object({ q: z.string() })` | | `extra.signal` in tool callbacks | `extra.mcpReq.signal` | -| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", handler)` | +| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", { params: ParamsSchema }, handler)` (the 2-arg form is only for spec methods such as `"tools/call"`) | ## Step 3: Set Up the Build Pipeline diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 04ccb4f..3d145d4 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -42,8 +42,6 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; -type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; - import { type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, @@ -93,6 +91,8 @@ import { export * from "./types.js"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js"; import { RESOURCE_URI_META_KEY } from "./constants.js"; + +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; export { PostMessageTransport } from "./message-transport.js"; /** @@ -527,6 +527,8 @@ export class AppBridge extends Protocol { options?: HostOptions, ) { super(options); + this._methods.replace("notifications/cancelled"); + this._methods.replace("notifications/progress"); this._ensureEventSlot("initialized"); @@ -1213,8 +1215,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/client forthe request type - * @see `CallToolResult` from @modelcontextprotocol/client forthe result type + * @see `CallToolRequest` from @modelcontextprotocol/client for the request type + * @see `CallToolResult` from @modelcontextprotocol/client for the result type */ private _oncalltool?: ( params: CallToolRequest["params"], @@ -1267,8 +1269,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/client forthe request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client forresult types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ private _oncreatesamplingmessage?: ( params: CreateMessageRequest["params"], @@ -1314,12 +1316,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/tools/list_changed", () => { * bridge.sendToolListChanged(); * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/client forthe notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1350,8 +1352,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/client forthe request type - * @see `ListResourcesResult` from @modelcontextprotocol/client forthe result type + * @see `ListResourcesRequest` from @modelcontextprotocol/client for the request type + * @see `ListResourcesResult` from @modelcontextprotocol/client for the result type */ private _onlistresources?: ( params: ListResourcesRequest["params"], @@ -1403,8 +1405,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client forthe request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client forthe result type + * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client for the request type + * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client for the result type */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], @@ -1459,8 +1461,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/client forthe request type - * @see `ReadResourceResult` from @modelcontextprotocol/client forthe result type + * @see `ReadResourceRequest` from @modelcontextprotocol/client for the request type + * @see `ReadResourceResult` from @modelcontextprotocol/client for the result type */ private _onreadresource?: ( params: ReadResourceRequest["params"], @@ -1502,12 +1504,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/resources/list_changed", () => { * bridge.sendResourceListChanged(); * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/client forthe notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1540,8 +1542,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/client forthe request type - * @see `ListPromptsResult` from @modelcontextprotocol/client forthe result type + * @see `ListPromptsRequest` from @modelcontextprotocol/client for the request type + * @see `ListPromptsResult` from @modelcontextprotocol/client for the result type */ private _onlistprompts?: ( params: ListPromptsRequest["params"], @@ -1582,12 +1584,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => { * bridge.sendPromptListChanged(); * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/client forthe notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ diff --git a/src/app.ts b/src/app.ts index fd6bbc7..58ea9da 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,7 +26,6 @@ export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; import { EventDispatcher, MethodRegistry } from "./events.js"; export { EventDispatcher } from "./events.js"; -type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; import { PostMessageTransport } from "./message-transport.js"; import { LATEST_PROTOCOL_VERSION, @@ -69,6 +68,8 @@ import { } from "./standard-schema.js"; import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; + type MethodSchema = ZodObject<{ method: ZodLiteral; params: ZodType; @@ -530,6 +531,10 @@ export class App extends Protocol { private options: AppOptions = { autoResize: true }, ) { super(options); + // Claim the handlers the base Protocol constructor installed so a direct + // setNotificationHandler cannot silently replace them either. + this._methods.replace("notifications/cancelled"); + this._methods.replace("notifications/progress"); if (!options.allowUnsafeEval) { z.config({ jitless: true }); diff --git a/src/wire-compat.test.ts b/src/wire-compat.test.ts index 5fdca8b..762ac77 100644 --- a/src/wire-compat.test.ts +++ b/src/wire-compat.test.ts @@ -77,7 +77,7 @@ const hostContext: McpUiHostContext = { locale: "en-US", }; -/** Captured from ext-apps 1.7.x View (sdk 1.x key order, numeric ids from 0). */ +/** Shaped like ext-apps 1.7.x View traffic (sdk-1 key order, 0-based numeric ids); not a recorded capture. */ const v1View = { initialize: { method: "ui/initialize", @@ -120,7 +120,7 @@ const v1View = { }), }; -/** Captured from ext-apps 1.7.x AppBridge (sdk 1.x). */ +/** Shaped like ext-apps 1.7.x AppBridge traffic (sdk 1.x); not a recorded capture. */ const v1Host = { initializeResult: (id: number) => ({ result: { From 728b0bf38ece48aa636a39be4fa3f44deaead010 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 14:24:25 +0100 Subject: [PATCH 91/97] 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. --- docs/migrate-to-2.md | 11 ++- src/app-bridge.ts | 42 +++++++---- src/app.ts | 42 +++++++---- src/legacy-handlers.test.ts | 145 ++++++++++++++++++++++++++++++++++++ src/legacy-handlers.ts | 94 +++++++++++++++++++++++ src/types.ts | 117 +++++++++++++++++++++++++++++ 6 files changed, 420 insertions(+), 31 deletions(-) create mode 100644 src/legacy-handlers.test.ts create mode 100644 src/legacy-handlers.ts diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md index 2164b83..515c363 100644 --- a/docs/migrate-to-2.md +++ b/docs/migrate-to-2.md @@ -41,8 +41,9 @@ its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `cli Valibot. Raw zod shapes (`{ q: z.string() }`) still work with `registerAppTool` but are deprecated; wrap them with `z.object({...})`. - **`App` / `AppBridge` extend `Protocol` from `@modelcontextprotocol/client`.** - `ProtocolWithEvents`, `AppRequest`, `AppNotification` and `AppResult` are - gone; use the SDK's `Protocol`, `Request`, `Notification` and `Result`. + `ProtocolWithEvents` is gone; use the SDK's `Protocol`. The `AppRequest`, + `AppNotification` and `AppResult` unions are kept as deprecated type + aliases (nothing in 2.x consumes them) and will be removed in 3.0. - **Handler context.** Custom handlers receive the SDK 2.x context: `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` → `extra.mcpReq.id`. @@ -51,7 +52,11 @@ its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `cli `app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)` for custom methods (the handler receives the parsed params); the two-argument `setRequestHandler("tools/call", handler)` form exists only for spec-defined - method names. + method names. The 1.x `(Schema, handler)` form still works on `App` and + `AppBridge` as a deprecated overload: it logs a one-time warning, hands the + handler the whole `{ method, params }` message as before, and gives request + handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`). + It will be removed in 3.0. - **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`); local conditions are `SdkError` with a string `code`: request timeout → `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 3d145d4..c61dfa4 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -35,6 +35,12 @@ import { LoggingMessageNotificationSchema, } from "@modelcontextprotocol/core"; import { EventDispatcher, MethodRegistry } from "./events.js"; +import { + toModernArgs, + type LegacyMethodSchema, + type LegacyNotificationHandler, + type LegacyRequestHandler, +} from "./legacy-handlers.js"; import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; type MethodSchema = ZodObject<{ @@ -324,11 +330,14 @@ export class AppBridge extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setRequestHandler: Protocol["setRequestHandler"] = ( - method: string, - ...rest: unknown[] - ) => { - this._methods.claim(method, "setRequestHandler"); + override setRequestHandler: Protocol["setRequestHandler"] & + (( + /** @deprecated Pass the method name and `{ params }` instead. */ + schema: S, + handler: LegacyRequestHandler, + ) => void) = (...args: unknown[]) => { + const [method, ...rest] = toModernArgs("request", args) ?? args; + this._methods.claim(method as string, "setRequestHandler"); (super.setRequestHandler as unknown as UntypedHandlerSetter).call( this, method, @@ -343,15 +352,20 @@ export class AppBridge extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setNotificationHandler: Protocol["setNotificationHandler"] = - (method: string, ...rest: unknown[]) => { - this._methods.claim(method, "setNotificationHandler"); - (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( - this, - method, - ...rest, - ); - }; + override setNotificationHandler: Protocol["setNotificationHandler"] & + (( + /** @deprecated Pass the method name and `{ params }` instead. */ + schema: S, + handler: LegacyNotificationHandler, + ) => void) = (...args: unknown[]) => { + const [method, ...rest] = toModernArgs("notification", args) ?? args; + this._methods.claim(method as string, "setNotificationHandler"); + (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; override removeRequestHandler: Protocol["removeRequestHandler"] = (method: string) => { diff --git a/src/app.ts b/src/app.ts index 58ea9da..da27b43 100644 --- a/src/app.ts +++ b/src/app.ts @@ -24,6 +24,12 @@ import { import { EmptyResultSchema } from "@modelcontextprotocol/core"; export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; import { EventDispatcher, MethodRegistry } from "./events.js"; +import { + toModernArgs, + type LegacyMethodSchema, + type LegacyNotificationHandler, + type LegacyRequestHandler, +} from "./legacy-handlers.js"; export { EventDispatcher } from "./events.js"; import { PostMessageTransport } from "./message-transport.js"; @@ -316,11 +322,14 @@ export class App extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setRequestHandler: Protocol["setRequestHandler"] = ( - method: string, - ...rest: unknown[] - ) => { - this._methods.claim(method, "setRequestHandler"); + override setRequestHandler: Protocol["setRequestHandler"] & + (( + /** @deprecated Pass the method name and `{ params }` instead. */ + schema: S, + handler: LegacyRequestHandler, + ) => void) = (...args: unknown[]) => { + const [method, ...rest] = toModernArgs("request", args) ?? args; + this._methods.claim(method as string, "setRequestHandler"); (super.setRequestHandler as unknown as UntypedHandlerSetter).call( this, method, @@ -335,15 +344,20 @@ export class App extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setNotificationHandler: Protocol["setNotificationHandler"] = - (method: string, ...rest: unknown[]) => { - this._methods.claim(method, "setNotificationHandler"); - (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( - this, - method, - ...rest, - ); - }; + override setNotificationHandler: Protocol["setNotificationHandler"] & + (( + /** @deprecated Pass the method name and `{ params }` instead. */ + schema: S, + handler: LegacyNotificationHandler, + ) => void) = (...args: unknown[]) => { + const [method, ...rest] = toModernArgs("notification", args) ?? args; + this._methods.claim(method as string, "setNotificationHandler"); + (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; override removeRequestHandler: Protocol["removeRequestHandler"] = (method: string) => { diff --git a/src/legacy-handlers.test.ts b/src/legacy-handlers.test.ts new file mode 100644 index 0000000..0ef36d6 --- /dev/null +++ b/src/legacy-handlers.test.ts @@ -0,0 +1,145 @@ +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; +import { InMemoryTransport, Server } from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import { App } from "./app.js"; +import { AppBridge } from "./app-bridge.js"; +import { + McpUiInitializeRequestSchema, + McpUiInitializeResultSchema, + McpUiInitializedNotificationSchema, + McpUiResourceTeardownRequestSchema, + McpUiToolInputNotificationSchema, + McpUiSizeChangedNotificationSchema, + McpUiOpenLinkRequestSchema, +} from "./types.js"; + +const CustomRequestSchema = z.object({ + method: z.literal("test/echo"), + params: z.object({ text: z.string() }), +}); + +const CustomNotificationSchema = z.object({ + method: z.literal("test/ping"), + params: z.object({ n: z.number() }), +}); + +let warn: ReturnType; +beforeEach(() => { + warn = spyOn(console, "warn").mockImplementation(() => {}); +}); +afterEach(() => warn.mockRestore()); + +describe("1.x setRequestHandler(Schema, handler) on App", () => { + it("registers a custom request and hands the handler the whole message plus a 1.x extra", async () => { + const app = new App({ name: "a", version: "1" }, {}, { autoResize: false }); + const server = new Server( + { name: "s", version: "1" }, + { capabilities: {} }, + ); + server.setRequestHandler( + "ui/initialize", + { + params: McpUiInitializeRequestSchema.shape.params, + result: McpUiInitializeResultSchema, + }, + () => ({ + protocolVersion: "2026-01-26", + hostCapabilities: {}, + hostInfo: { name: "h", version: "1" }, + hostContext: {}, + }), + ); + server.setNotificationHandler( + "ui/notifications/initialized", + { params: McpUiInitializedNotificationSchema.shape.params }, + () => {}, + ); + + let seen: unknown; + let seenExtra: { signal: AbortSignal; requestId: unknown } | undefined; + app.setRequestHandler(CustomRequestSchema, (request, extra) => { + seen = request; + seenExtra = extra; + return { echoed: request.params.text }; + }); + + const [at, st] = InMemoryTransport.createLinkedPair(); + await server.connect(st); + await app.connect(at); + + const result = await server.request( + { method: "test/echo", params: { text: "hi" } }, + z.object({ echoed: z.string() }), + ); + + expect(result).toEqual({ echoed: "hi" }); + expect(seen).toEqual({ method: "test/echo", params: { text: "hi" } }); + expect(seenExtra?.signal).toBeInstanceOf(AbortSignal); + expect(seenExtra?.requestId).toBeDefined(); + expect(warn).toHaveBeenCalledTimes(1); + expect(String(warn.mock.calls[0][0])).toContain( + 'setRequestHandler("test/echo", { params: Schema.shape.params }, handler)', + ); + + await app.close(); + await server.close(); + }); + + it("still throws for a method an on* setter already owns", () => { + const app = new App({ name: "a", version: "1" }, {}, { autoResize: false }); + app.onteardown = async () => ({}); + expect(() => + app.setRequestHandler( + McpUiResourceTeardownRequestSchema, + async () => ({}), + ), + ).toThrow(/already registered/); + }); + + it("still throws for an event-mapped notification", () => { + const app = new App({ name: "a", version: "1" }, {}, { autoResize: false }); + app.ontoolinput = () => {}; + expect(() => + app.setNotificationHandler(McpUiToolInputNotificationSchema, () => {}), + ).toThrow(/already registered/); + }); +}); + +describe("1.x setNotificationHandler(Schema, handler) on AppBridge", () => { + it("registers a custom notification and hands the handler the whole message", async () => { + const bridge = new AppBridge(null, { name: "h", version: "1" }, {}); + // A bare Server stands in for the View: unlike Client, connect() sends no + // MCP initialize, matching what a real App does on this channel. + const view = new Server({ name: "v", version: "1" }, { capabilities: {} }); + let seen: unknown; + bridge.setNotificationHandler(CustomNotificationSchema, (n) => { + seen = n; + }); + + const [bt, vt] = InMemoryTransport.createLinkedPair(); + await bridge.connect(bt); + await view.connect(vt); + await view.notification({ method: "test/ping", params: { n: 3 } }); + await new Promise((r) => setTimeout(r, 10)); + + expect(seen).toEqual({ method: "test/ping", params: { n: 3 } }); + await view.close(); + await bridge.close(); + }); + + it("still throws for a method an on* setter already owns", () => { + const bridge = new AppBridge(null, { name: "h", version: "1" }, {}); + bridge.onsizechange = () => {}; + expect(() => + bridge.setNotificationHandler( + McpUiSizeChangedNotificationSchema, + () => {}, + ), + ).toThrow(/already registered/); + bridge.onopenlink = async () => ({}); + expect(() => + bridge.setRequestHandler(McpUiOpenLinkRequestSchema, async () => ({})), + ).toThrow(/already registered/); + }); +}); diff --git a/src/legacy-handlers.ts b/src/legacy-handlers.ts new file mode 100644 index 0000000..b4d1157 --- /dev/null +++ b/src/legacy-handlers.ts @@ -0,0 +1,94 @@ +import type { BaseContext, RequestId } from "@modelcontextprotocol/client"; +import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; + +/** + * A whole-message Zod schema (`method` literal plus `params`), as accepted by + * the 1.x `setRequestHandler(Schema, handler)` forms. + */ +export type LegacyMethodSchema = ZodObject<{ + method: ZodLiteral; + params: ZodType; +}>; + +/** + * The `extra` argument 1.x request handlers received. + * + * @deprecated Use the 2.x `BaseContext` (`extra.mcpReq.signal`, `extra.mcpReq.id`). + */ +export type LegacyRequestHandlerExtra = { + signal: AbortSignal; + requestId: RequestId; + sessionId?: string; + _meta?: BaseContext["mcpReq"]["_meta"]; + sendRequest: BaseContext["mcpReq"]["send"]; + sendNotification: BaseContext["mcpReq"]["notify"]; + authInfo?: NonNullable["authInfo"]; +}; + +/** @deprecated Use `setRequestHandler("method", { params, result }, (params, ctx) => …)`. */ +export type LegacyRequestHandler = ( + request: S["_output"], + extra: LegacyRequestHandlerExtra, +) => unknown; + +/** @deprecated Use `setNotificationHandler("method", { params }, (params) => …)`. */ +export type LegacyNotificationHandler = ( + notification: S["_output"], +) => void | Promise; + +/** + * Arguments for the 2.x three-argument `setRequestHandler` / + * `setNotificationHandler` form. + */ +type ModernArgs = [ + method: string, + schemas: { params: ZodType }, + handler: (params: unknown, ctx: BaseContext) => unknown, +]; + +const warned = new Set(); + +/** + * Translate a 1.x `(Schema, handler)` registration into the 2.x + * `(method, { params }, handler)` form, or return `undefined` when the call + * is already in the 2.x form. The 1.x handler receives the reassembled + * `{ method, params }` message and, for requests, a 1.x-shaped `extra`. + */ +export function toModernArgs( + kind: "request" | "notification", + args: unknown[], +): ModernArgs | undefined { + const [first, handler] = args; + const shape = (first as Partial | undefined)?.shape; + if (typeof shape?.method?.value !== "string") return undefined; + const method = shape.method.value; + const setter = + kind === "request" ? "setRequestHandler" : "setNotificationHandler"; + if (!warned.has(setter)) { + warned.add(setter); + console.warn( + `[ext-apps] ${setter}(Schema, handler) is deprecated; use ` + + `${setter}("${method}", { params: Schema.shape.params }, handler).`, + ); + } + const call = handler as (...a: unknown[]) => unknown; + return [ + method, + { params: shape.params }, + kind === "request" + ? (params, ctx) => call({ method, params }, legacyExtra(ctx)) + : (params) => call({ method, params }), + ]; +} + +function legacyExtra(ctx: BaseContext): LegacyRequestHandlerExtra { + return { + signal: ctx.mcpReq.signal, + requestId: ctx.mcpReq.id, + sessionId: ctx.sessionId, + _meta: ctx.mcpReq._meta, + sendRequest: ctx.mcpReq.send, + sendNotification: ctx.mcpReq.notify, + authInfo: ctx.http?.authInfo, + }; +} diff --git a/src/types.ts b/src/types.ts index 655812b..e4ed629 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,55 @@ * @see `generated/schema.test.ts` for compile-time verification */ +import type { + CallToolRequest, + CallToolResult, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultWithTools, + EmptyResult, + ListPromptsRequest, + ListPromptsResult, + ListResourceTemplatesRequest, + ListResourceTemplatesResult, + ListResourcesRequest, + ListResourcesResult, + ListToolsRequest, + ListToolsResult, + LoggingMessageNotification, + PingRequest, + PromptListChangedNotification, + ReadResourceRequest, + ReadResourceResult, + ResourceListChangedNotification, + ToolListChangedNotification, +} from "@modelcontextprotocol/client"; +import type { + McpUiDownloadFileRequest, + McpUiDownloadFileResult, + McpUiHostContextChangedNotification, + McpUiInitializeRequest, + McpUiInitializeResult, + McpUiInitializedNotification, + McpUiMessageRequest, + McpUiMessageResult, + McpUiOpenLinkRequest, + McpUiOpenLinkResult, + McpUiRequestDisplayModeRequest, + McpUiRequestDisplayModeResult, + McpUiRequestTeardownNotification, + McpUiResourceTeardownRequest, + McpUiResourceTeardownResult, + McpUiSandboxProxyReadyNotification, + McpUiSandboxResourceReadyNotification, + McpUiSizeChangedNotification, + McpUiToolCancelledNotification, + McpUiToolInputNotification, + McpUiToolInputPartialNotification, + McpUiToolResultNotification, + McpUiUpdateModelContextRequest, +} from "./spec.types.js"; + // Re-export all types from spec.types.ts export { LATEST_PROTOCOL_VERSION, @@ -108,3 +157,71 @@ export { McpUiToolVisibilitySchema, McpUiToolMetaSchema, } from "./generated/schema.js"; + +/** + * Union of every request an {@link app!App `App`} may send or receive. + * + * @deprecated 1.x fed this to the SDK's `Protocol` generics; SDK 2.x derives + * request types from the method name, so nothing consumes it anymore. + */ +export type AppRequest = + | McpUiInitializeRequest + | McpUiOpenLinkRequest + | McpUiDownloadFileRequest + | McpUiMessageRequest + | McpUiUpdateModelContextRequest + | McpUiResourceTeardownRequest + | McpUiRequestDisplayModeRequest + | CallToolRequest + | ListToolsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | ListPromptsRequest + | CreateMessageRequest + | PingRequest; + +/** + * Union of every notification an {@link app!App `App`} may send or receive. + * + * @deprecated See {@link AppRequest}. + */ +export type AppNotification = + // Sent to app + | McpUiHostContextChangedNotification + | McpUiToolInputNotification + | McpUiToolInputPartialNotification + | McpUiToolResultNotification + | McpUiToolCancelledNotification + | McpUiSandboxResourceReadyNotification + | ToolListChangedNotification + | ResourceListChangedNotification + | PromptListChangedNotification + // Received from app + | McpUiInitializedNotification + | McpUiSizeChangedNotification + | McpUiSandboxProxyReadyNotification + | McpUiRequestTeardownNotification + | LoggingMessageNotification; + +/** + * Union of every result an {@link app!App `App`} may send or receive. + * + * @deprecated See {@link AppRequest}. + */ +export type AppResult = + | McpUiInitializeResult + | McpUiOpenLinkResult + | McpUiDownloadFileResult + | McpUiMessageResult + | McpUiResourceTeardownResult + | McpUiRequestDisplayModeResult + | CallToolResult + | ListToolsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | CreateMessageResult + | CreateMessageResultWithTools + | EmptyResult; From 4eab52e372e064f4a58dfcb24040df9155564f7d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 15:31:03 +0100 Subject: [PATCH 92/97] 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. --- src/app.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app.ts b/src/app.ts index da27b43..0ab51ac 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,6 +30,12 @@ import { type LegacyNotificationHandler, type LegacyRequestHandler, } from "./legacy-handlers.js"; +export type { + LegacyMethodSchema, + LegacyNotificationHandler, + LegacyRequestHandler, + LegacyRequestHandlerExtra, +} from "./legacy-handlers.js"; export { EventDispatcher } from "./events.js"; import { PostMessageTransport } from "./message-transport.js"; From 5f6346ef8662c17b25f6edec1c9719b19413fd77 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 14:34:47 +0100 Subject: [PATCH 93/97] 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. --- package-lock.json | 198 ++++++++++++++++++++- package.json | 3 + src/interop-v1.test.ts | 379 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 src/interop-v1.test.ts diff --git a/package-lock.json b/package-lock.json index f63b3d8..0189ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@boneskull/typedoc-plugin-mermaid": "^0.2.0", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/sdk": "1.30.0", "@modelcontextprotocol/server": "2.0.0", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", @@ -33,6 +34,7 @@ "electron-to-chromium": "1.5.267", "esbuild": "^0.25.12", "express": "^5.1.0", + "ext-apps-v1": "npm:@modelcontextprotocol/ext-apps@1.7.5", "husky": "^9.1.7", "nodemon": "^3.1.0", "playwright": "1.57.0", @@ -2756,6 +2758,47 @@ "resolved": "examples/quickstart", "link": true }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", @@ -4375,6 +4418,41 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5937,6 +6015,81 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/ext-apps-v1": { + "name": "@modelcontextprotocol/ext-apps", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz", + "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -6262,7 +6415,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -6399,6 +6551,16 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6586,6 +6748,20 @@ "node": ">=6" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7578,6 +7754,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -9716,6 +9902,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index da82a66..bcea649 100644 --- a/package.json +++ b/package.json @@ -76,10 +76,12 @@ "update-lock:docker": "rm -rf node_modules package-lock.json examples/*/node_modules && docker run --rm --platform linux/amd64 -v $(pwd):/work -w /work -e HOME=/tmp node:latest npm i --registry=https://registry.npmjs.org/ --ignore-scripts && rm -rf node_modules examples/*/node_modules && npm i --registry=https://registry.npmjs.org/" }, "author": "Olivier Chafik", + "//interop": "The @modelcontextprotocol/sdk 1.x and ext-apps-v1 devDependencies exist only for src/interop-v1.test.ts, which runs the published 1.7.5 package against this build. The library does not use them.", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/sdk": "1.30.0", "@modelcontextprotocol/server": "2.0.0", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", @@ -95,6 +97,7 @@ "electron-to-chromium": "1.5.267", "esbuild": "^0.25.12", "express": "^5.1.0", + "ext-apps-v1": "npm:@modelcontextprotocol/ext-apps@1.7.5", "husky": "^9.1.7", "nodemon": "^3.1.0", "playwright": "1.57.0", diff --git a/src/interop-v1.test.ts b/src/interop-v1.test.ts new file mode 100644 index 0000000..31dbfa1 --- /dev/null +++ b/src/interop-v1.test.ts @@ -0,0 +1,379 @@ +/** + * Cross-version interop against the *published* 1.x package. + * + * `ext-apps-v1` is an npm alias for `@modelcontextprotocol/ext-apps@1.7.5` + * (built on `@modelcontextprotocol/sdk` 1.x). Each pairing wires a real + * `App` to a real `AppBridge` of the other major over an in-memory duplex, + * with the bridge fronting a real MCP `Client`+`McpServer` of its own SDK + * generation, and asserts the observable outcomes. Complements + * `wire-compat.test.ts`, which replays hand-shaped 1.x JSON instead. + */ +import { describe, it, expect, beforeAll, afterAll } from "bun:test"; +import { readFileSync } from "node:fs"; +import { + Client as ClientV2, + InMemoryTransport as InMemoryTransportV2, + ProtocolError, + type Transport, +} from "@modelcontextprotocol/client"; +import { EmptyResultSchema as EmptyResultSchemaV2 } from "@modelcontextprotocol/core"; +import { McpServer as McpServerV2 } from "@modelcontextprotocol/server"; +import { Client as ClientV1 } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport as InMemoryTransportV1 } from "@modelcontextprotocol/sdk/inMemory.js"; +import { McpServer as McpServerV1 } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Protocol as ProtocolV1 } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import { + EmptyResultSchema as EmptyResultSchemaV1, + McpError, +} from "@modelcontextprotocol/sdk/types.js"; +import { App as AppV1 } from "ext-apps-v1"; +import { AppBridge as AppBridgeV1 } from "ext-apps-v1/app-bridge"; +import { z } from "zod/v4"; + +import { App as AppV2 } from "./app.js"; +import { AppBridge as AppBridgeV2 } from "./app-bridge.js"; + +const flush = () => new Promise((resolve) => setTimeout(resolve, 5)); + +/** In-memory duplex. Structurally a `Transport` for both SDK generations. */ +function createDuplex(): [Transport, Transport] { + const make = () => { + let closed = false; + const t: Transport & { peer?: typeof t } = { + async start() {}, + async send(message) { + // Serialize like a real transport so each side parses plain JSON. + t.peer?.onmessage?.(JSON.parse(JSON.stringify(message))); + }, + async close() { + if (closed) return; + closed = true; + t.onclose?.(); + await t.peer?.close(); + }, + }; + return t; + }; + const [a, b] = [make(), make()]; + a.peer = b; + b.peer = a; + return [a, b]; +} + +/** One SDK generation's MCP pieces, as used by the App side or the host side. */ +const sdkV1 = { + Client: ClientV1, + McpServer: McpServerV1, + InMemoryTransport: InMemoryTransportV1, + EmptyResultSchema: EmptyResultSchemaV1, + error: (code: number, message: string): Error => new McpError(code, message), +}; +const sdkV2 = { + Client: ClientV2, + McpServer: McpServerV2, + InMemoryTransport: InMemoryTransportV2, + EmptyResultSchema: EmptyResultSchemaV2, + error: (code: number, message: string): Error => + new ProtocolError(code, message), +}; + +const pairings = [ + { + name: "2.x App with 1.x AppBridge (sdk 1.x host)", + App: AppV2 as any, + AppBridge: AppBridgeV1 as any, + view: sdkV2, + host: sdkV1, + // sdk 1.x reports a Zod params failure as InternalError. + invalidParamsCode: -32603, + // sdk 1.x forwards a handler-thrown -32002 unchanged. + handlerThrown32002Code: -32002, + // McpServer 1.x answers an unknown tool with an isError result. + unknownToolRejects: false, + }, + { + name: "1.x App with 2.x AppBridge (sdk 2.x host)", + App: AppV1 as any, + AppBridge: AppBridgeV2 as any, + view: sdkV1, + host: sdkV2, + invalidParamsCode: -32602, + // SDK 2.x never emits -32002 on the wire (see wire-compat.test.ts). + handlerThrown32002Code: -32602, + // McpServer 2.x answers an unknown tool with a JSON-RPC error. + unknownToolRejects: true, + }, +]; + +const hostInfo = { name: "TestHost", version: "9.9.9" }; +const appInfo = { name: "TestView", version: "1.2.3" }; +const hostCapabilities = { + openLinks: {}, + serverTools: {}, + serverResources: {}, + logging: {}, + updateModelContext: {}, +}; +const hostContext = { theme: "dark", displayMode: "inline", locale: "en-US" }; + +it("pits 2.x against the published 1.x package, not against itself", () => { + const v1PackageJson = JSON.parse( + readFileSync( + new URL("../../package.json", import.meta.resolve("ext-apps-v1")), + "utf8", + ), + ); + expect(v1PackageJson).toMatchObject({ + name: "@modelcontextprotocol/ext-apps", + version: "1.7.5", + }); + // 1.x classes extend the sdk-1 Protocol; 2.x classes do not. + expect(AppV1.prototype).toBeInstanceOf(ProtocolV1); + expect(AppBridgeV1.prototype).toBeInstanceOf(ProtocolV1); + expect(AppV2.prototype).not.toBeInstanceOf(ProtocolV1); + expect(AppBridgeV2.prototype).not.toBeInstanceOf(ProtocolV1); +}); + +// The tests in each pairing share one connected App/AppBridge fixture and run +// in file order (bun does not randomize); later tests assume earlier ones +// left the pair connected. +describe.each(pairings)("$name", (pairing) => { + const { view, host } = pairing; + let server: any, client: any, bridge: any, app: any; + const events: string[] = []; + const record = (name: string) => (params?: unknown) => { + events.push(`${name}:${JSON.stringify(params ?? null)}`); + }; + const drain = () => events.splice(0); + // Captured inside oninitialized: the bridge must have processed + // ui/initialize before ui/notifications/initialized arrives. + let appVersionAtInitialized: unknown = "not-fired"; + let hostAbortReason: unknown; + + beforeAll(async () => { + server = new host.McpServer({ name: "TestServer", version: "1.0.0" }); + server.registerTool( + "echo", + { inputSchema: z.object({ a: z.number() }) }, + async ({ a }: { a: number }) => ({ + content: [{ type: "text", text: `echo ${a}` }], + }), + ); + server.registerTool("boom", { inputSchema: z.object({}) }, async () => { + throw new Error("tool exploded"); + }); + server.registerResource( + "res", + "test://res", + { mimeType: "text/plain" }, + async (uri: URL) => ({ + contents: [{ uri: uri.href, mimeType: "text/plain", text: "hello" }], + }), + ); + const [clientTransport, serverTransport] = + host.InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + client = new host.Client({ name: "HostOuterClient", version: "1.0.0" }); + await client.connect(clientTransport); + + bridge = new pairing.AppBridge(client, hostInfo, hostCapabilities, { + hostContext, + }); + app = new pairing.App( + appInfo, + { tools: { listChanged: true } }, + { + autoResize: false, + }, + ); + bridge.oninitialized = () => { + appVersionAtInitialized = bridge.getAppVersion(); + }; + for (const name of ["onsizechange", "onloggingmessage"]) { + bridge[name] = record(`bridge.${name}`); + } + for (const name of ["onmessage", "onupdatemodelcontext"]) { + bridge[name] = async (params: unknown) => { + record(`bridge.${name}`)(params); + return {}; + }; + } + bridge.onopenlink = (params: { url: string }, extra: any) => { + record("bridge.onopenlink")(params); + if (params.url !== "https://slow") return {}; + // Hang until the view cancels; sdk 1.x passes `extra.signal`, 2.x `extra.mcpReq.signal`. + const signal: AbortSignal = extra.signal ?? extra.mcpReq.signal; + return new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => { + hostAbortReason = signal.reason; + reject(new Error("aborted")); + }); + }); + }; + for (const name of [ + "ontoolinput", + "ontoolresult", + "ontoolcancelled", + "onhostcontextchanged", + ]) { + app[name] = record(`app.${name}`); + } + app.onteardown = async (params: unknown) => { + record("app.onteardown")(params); + return {}; + }; + }); + + afterAll(async () => { + await app?.close().catch(() => {}); + await bridge?.close().catch(() => {}); + await client?.close().catch(() => {}); + await server?.close().catch(() => {}); + }); + + it("completes ui/initialize and delivers initialized in order", async () => { + const [appTransport, bridgeTransport] = createDuplex(); + // Tap the raw ui/initialize result so an additive key on the 2.x side + // (which the getters below would not surface) still fails here. + let initializeResult: unknown; + const send = bridgeTransport.send.bind(bridgeTransport); + bridgeTransport.send = async (message, options) => { + if ("result" in message && "id" in message && message.id === 0) { + initializeResult = message.result; + } + await send(message, options); + }; + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + await flush(); + + expect(app.getHostContext()).toEqual(hostContext); + expect(app.getHostCapabilities()).toEqual(hostCapabilities); + expect(app.getHostVersion()).toEqual(hostInfo); + expect(bridge.getAppCapabilities()).toEqual({ + tools: { listChanged: true }, + }); + expect(appVersionAtInitialized).toEqual(appInfo); + expect(initializeResult).toEqual({ + protocolVersion: "2026-01-26", + hostCapabilities, + hostInfo, + hostContext, + }); + }); + + it("delivers host-to-view notifications", async () => { + bridge.sendToolInput({ arguments: { location: "NYC" } }); + bridge.sendToolResult({ content: [{ type: "text", text: "sunny" }] }); + bridge.sendToolCancelled({ reason: "user" }); + bridge.setHostContext({ theme: "light" }); + await flush(); + + expect(drain()).toEqual([ + 'app.ontoolinput:{"arguments":{"location":"NYC"}}', + 'app.ontoolresult:{"content":[{"type":"text","text":"sunny"}]}', + 'app.ontoolcancelled:{"reason":"user"}', + 'app.onhostcontextchanged:{"theme":"light"}', + ]); + expect(app.getHostContext()).toEqual({ ...hostContext, theme: "light" }); + }); + + it("round-trips view-to-host requests and notifications", async () => { + const message = { role: "user", content: [{ type: "text", text: "hi" }] }; + expect(await app.sendMessage(message)).toEqual({}); + expect(await app.openLink({ url: "https://example.com" })).toEqual({}); + expect(await app.requestDisplayMode({ mode: "fullscreen" })).toEqual({ + mode: "inline", + }); + bridge.onrequestdisplaymode = async (params: { mode: string }) => params; + expect(await app.requestDisplayMode({ mode: "fullscreen" })).toEqual({ + mode: "fullscreen", + }); + const context = { content: [{ type: "text", text: "ctx" }] }; + expect(await app.updateModelContext(context)).toEqual({}); + await app.sendLog({ level: "info", data: { m: "hello" } }); + await app.sendSizeChanged({ width: 320, height: 240 }); + await flush(); + + expect(drain()).toEqual([ + `bridge.onmessage:${JSON.stringify(message)}`, + 'bridge.onopenlink:{"url":"https://example.com"}', + `bridge.onupdatemodelcontext:${JSON.stringify(context)}`, + 'bridge.onloggingmessage:{"level":"info","data":{"m":"hello"}}', + 'bridge.onsizechange:{"width":320,"height":240}', + ]); + }); + + it("proxies tools/call and resources/read to the real MCP server", async () => { + expect( + await app.callServerTool({ name: "echo", arguments: { a: 1 } }), + ).toEqual({ content: [{ type: "text", text: "echo 1" }] }); + expect( + await app.callServerTool({ name: "boom", arguments: {} }), + ).toMatchObject({ + isError: true, + content: [{ type: "text", text: "tool exploded" }], + }); + const unknown = app.callServerTool({ name: "nope", arguments: {} }); + if (pairing.unknownToolRejects) { + await expect(unknown).rejects.toMatchObject({ code: -32602 }); + } else { + expect(await unknown).toMatchObject({ isError: true }); + } + expect(await app.readServerResource({ uri: "test://res" })).toEqual({ + contents: [{ uri: "test://res", mimeType: "text/plain", text: "hello" }], + }); + }); + + it("propagates JSON-RPC errors with the expected codes", async () => { + const raw = (method: string, params: unknown) => + app.request({ method, params }, view.EmptyResultSchema); + await expect(raw("ui/does-not-exist", {})).rejects.toMatchObject({ + code: -32601, + }); + await expect(raw("ui/open-link", { url: 42 })).rejects.toMatchObject({ + code: pairing.invalidParamsCode, + }); + bridge.onreadresource = async () => { + throw host.error(-32002, "Resource not found (host-thrown -32002)"); + }; + const notFound = app.readServerResource({ uri: "test://missing" }); + await expect(notFound).rejects.toMatchObject({ + code: pairing.handlerThrown32002Code, + message: expect.stringContaining( + "Resource not found (host-thrown -32002)", + ), + }); + }); + + it("cancels an in-flight request via AbortSignal on both ends", async () => { + const controller = new AbortController(); + const pending = app.openLink( + { url: "https://slow" }, + { signal: controller.signal }, + ); + await flush(); + controller.abort("user-cancelled"); + await expect(pending).rejects.toMatchObject({ + message: expect.stringContaining("user-cancelled"), + }); + await flush(); + expect(String(hostAbortReason)).toContain("user-cancelled"); + drain(); + }); + + it("tears down, then closes both ends", async () => { + expect(await bridge.teardownResource({})).toEqual({}); + expect(drain()).toEqual(["app.onteardown:{}"]); + + const closed: string[] = []; + app.onclose = () => closed.push("app"); + bridge.onclose = () => closed.push("bridge"); + await app.close(); + await flush(); + expect(closed.sort()).toEqual(["app", "bridge"]); + await expect(app.openLink({ url: "https://after-close" })).rejects.toThrow( + "Not connected", + ); + }); +}); From 853c9f749c2c04e9b8391919905935dee411300b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 18:01:18 +0100 Subject: [PATCH 94/97] 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. --- README.md | 6 ++--- docs/migrate-to-2.md | 55 ++++++++++++++++++++++++++++++------------ src/app-bridge.ts | 23 ++++++------------ src/app.ts | 25 ++++++++----------- src/legacy-handlers.ts | 27 +++++++++++++++++++++ 5 files changed, 87 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index acfc661..ab4004d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Why · - Quickstart + Quickstart · API Docs · @@ -151,7 +151,7 @@ in a 1.x host and a 2.x host renders 1.x Views. See the when upgrading from 1.x. **New here?** Start with the -[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) +[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html) to build your first MCP App. ## Using the SDK @@ -272,7 +272,7 @@ point your client at a local build: ## Resources -- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) +- [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)) diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md index 515c363..f9341d0 100644 --- a/docs/migrate-to-2.md +++ b/docs/migrate-to-2.md @@ -17,14 +17,22 @@ The `ui/*` messages exchanged over the iframe channel are byte-identical to 1.x. A 2.x View runs in a 1.x host and a 2.x host renders 1.x Views. The only host-side deltas are in error responses (see below). +## What stays the same + +`App` / `AppBridge` constructors, the `on*` setters, `addEventListener`, +`callServerTool`, `readServerResource`, `registerAppResource`, the React hooks +and the `_meta.ui.resourceUri` handling are unchanged. Published `.d.ts` files +now resolve under `moduleResolution: NodeNext` / `Node16` without workarounds +(#705). + ## Peer dependencies by role -| Role | Install | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) | -| Host author | same as View author | -| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; `@modelcontextprotocol/node` and `express` for HTTP transports | -| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) | +| Role | Install | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) | +| Host author | same as View author | +| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; for HTTP transports also `@modelcontextprotocol/node@^2.0.0` (`NodeStreamableHTTPServerTransport`) and `@modelcontextprotocol/express@^2.0.0` (`createMcpExpressApp`, which peers on `express`) | +| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) | `@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it without you listing it; @@ -35,18 +43,23 @@ its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `cli - **Peer packages.** `@modelcontextprotocol/sdk@^1` is replaced by the split 2.x packages above, all at `^2.0.0`. Remove the 1.x package from your - project; the two SDKs do not interoperate. + project: the two SDKs share no classes or types (a 1.x `McpError` or + `Client` cannot be passed to 2.x code), even though they speak the same + wire protocol. - **zod 3 dropped.** The peer range is `zod@^4.2.0`. Tool schemas must - implement Standard JSON Schema (`~standard.jsonSchema`): zod v4, ArkType, - Valibot. Raw zod shapes (`{ q: z.string() }`) still work with + implement Standard JSON Schema (`~standard.jsonSchema`): zod 4.2+, ArkType, + Valibot. zod 4.0 and 4.1 do not expose `~standard.jsonSchema` and are + rejected too. Raw zod shapes (`{ q: z.string() }`) still work with `registerAppTool` but are deprecated; wrap them with `z.object({...})`. - **`App` / `AppBridge` extend `Protocol` from `@modelcontextprotocol/client`.** `ProtocolWithEvents` is gone; use the SDK's `Protocol`. The `AppRequest`, `AppNotification` and `AppResult` unions are kept as deprecated type aliases (nothing in 2.x consumes them) and will be removed in 3.0. -- **Handler context.** Custom handlers receive the SDK 2.x context: - `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` → - `extra.mcpReq.id`. +- **Handler context.** Custom request handlers, `registerAppTool` callbacks + (server) and `app.registerTool` callbacks (View) receive the SDK 2.x + context: `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` → + `extra.mcpReq.id`; `extra.sessionId` is unchanged and `extra.authInfo` + moved to `extra.http?.authInfo`. - **`setRequestHandler` / `setNotificationHandler` take method names.** `app.setRequestHandler(SomeRequestSchema, handler)` becomes `app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)` @@ -55,14 +68,23 @@ its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `cli method names. The 1.x `(Schema, handler)` form still works on `App` and `AppBridge` as a deprecated overload: it logs a one-time warning, hands the handler the whole `{ method, params }` message as before, and gives request - handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`). - It will be removed in 3.0. + handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`, + `sendRequest`, `sendNotification`, `authInfo`; exported as + `LegacyRequestHandlerExtra`). It will be removed in 3.0. +- **Double-registration guard covers the SDK's own handlers.** Registering a + handler for `notifications/progress` or `notifications/cancelled` (which + the base `Protocol` already owns) now throws `already registered`, as + `ping` and the `on*`-owned methods did in 1.x. Use `addEventListener` or + the SDK's progress callbacks instead of replacing these. - **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`); local conditions are `SdkError` with a string `code`: request timeout → `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a request with an `AbortSignal` also rejects with `"REQUEST_TIMEOUT"` (the message is the abort reason). Messages no longer carry the `MCP error N:` prefix. + A resource-not-found reply that carries `data.uri` arrives as + `ResourceNotFoundError` (a `ProtocolError` subclass, `code` `-32602`, with a + `.uri` getter). ## Host-side wire deltas @@ -98,8 +120,9 @@ The published `./schema.json` export is regenerated from the 2.x core schemas: 2. Replace `sdk/...` imports with the split packages (`sdk/server/mcp.js` → `@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` → `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`, - `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js` - → `@modelcontextprotocol/client` or `server` for the types, + `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, hand-written + Express routing → `createMcpExpressApp` from `@modelcontextprotocol/express`, + `sdk/types.js` → `@modelcontextprotocol/client` or `server` for the types, `@modelcontextprotocol/core` for the zod schemas). 3. Wrap raw zod shapes with `z.object({...})`. 4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed diff --git a/src/app-bridge.ts b/src/app-bridge.ts index c61dfa4..1ac4db4 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -37,9 +37,8 @@ import { import { EventDispatcher, MethodRegistry } from "./events.js"; import { toModernArgs, - type LegacyMethodSchema, - type LegacyNotificationHandler, - type LegacyRequestHandler, + type LegacyNotificationHandlerSetter, + type LegacyRequestHandlerSetter, } from "./legacy-handlers.js"; import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; @@ -330,12 +329,9 @@ export class AppBridge extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setRequestHandler: Protocol["setRequestHandler"] & - (( - /** @deprecated Pass the method name and `{ params }` instead. */ - schema: S, - handler: LegacyRequestHandler, - ) => void) = (...args: unknown[]) => { + override setRequestHandler: LegacyRequestHandlerSetter< + Protocol["setRequestHandler"] + > = (...args: unknown[]) => { const [method, ...rest] = toModernArgs("request", args) ?? args; this._methods.claim(method as string, "setRequestHandler"); (super.setRequestHandler as unknown as UntypedHandlerSetter).call( @@ -352,12 +348,9 @@ export class AppBridge extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setNotificationHandler: Protocol["setNotificationHandler"] & - (( - /** @deprecated Pass the method name and `{ params }` instead. */ - schema: S, - handler: LegacyNotificationHandler, - ) => void) = (...args: unknown[]) => { + override setNotificationHandler: LegacyNotificationHandlerSetter< + Protocol["setNotificationHandler"] + > = (...args: unknown[]) => { const [method, ...rest] = toModernArgs("notification", args) ?? args; this._methods.claim(method as string, "setNotificationHandler"); (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( diff --git a/src/app.ts b/src/app.ts index 0ab51ac..89c6c35 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,15 +26,16 @@ export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; import { EventDispatcher, MethodRegistry } from "./events.js"; import { toModernArgs, - type LegacyMethodSchema, - type LegacyNotificationHandler, - type LegacyRequestHandler, + type LegacyNotificationHandlerSetter, + type LegacyRequestHandlerSetter, } from "./legacy-handlers.js"; export type { LegacyMethodSchema, LegacyNotificationHandler, + LegacyNotificationHandlerSetter, LegacyRequestHandler, LegacyRequestHandlerExtra, + LegacyRequestHandlerSetter, } from "./legacy-handlers.js"; export { EventDispatcher } from "./events.js"; @@ -328,12 +329,9 @@ export class App extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setRequestHandler: Protocol["setRequestHandler"] & - (( - /** @deprecated Pass the method name and `{ params }` instead. */ - schema: S, - handler: LegacyRequestHandler, - ) => void) = (...args: unknown[]) => { + override setRequestHandler: LegacyRequestHandlerSetter< + Protocol["setRequestHandler"] + > = (...args: unknown[]) => { const [method, ...rest] = toModernArgs("request", args) ?? args; this._methods.claim(method as string, "setRequestHandler"); (super.setRequestHandler as unknown as UntypedHandlerSetter).call( @@ -350,12 +348,9 @@ export class App extends Protocol { * * @throws {Error} if a handler for this method is already registered. */ - override setNotificationHandler: Protocol["setNotificationHandler"] & - (( - /** @deprecated Pass the method name and `{ params }` instead. */ - schema: S, - handler: LegacyNotificationHandler, - ) => void) = (...args: unknown[]) => { + override setNotificationHandler: LegacyNotificationHandlerSetter< + Protocol["setNotificationHandler"] + > = (...args: unknown[]) => { const [method, ...rest] = toModernArgs("notification", args) ?? args; this._methods.claim(method as string, "setNotificationHandler"); (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( diff --git a/src/legacy-handlers.ts b/src/legacy-handlers.ts index b4d1157..237b8eb 100644 --- a/src/legacy-handlers.ts +++ b/src/legacy-handlers.ts @@ -36,6 +36,33 @@ export type LegacyNotificationHandler = ( notification: S["_output"], ) => void | Promise; +/** + * `setRequestHandler` with the 1.x `(Schema, handler)` form kept as a + * deprecated overload next to the 2.x forms in `Modern`. + */ +export type LegacyRequestHandlerSetter = Modern & { + /** + * @deprecated 1.x form. Pass the method name and `{ params }` instead; + * removed in 3.0. + */ + ( + schema: S, + handler: LegacyRequestHandler, + ): void; +}; + +/** `setNotificationHandler` counterpart of {@link LegacyRequestHandlerSetter}. */ +export type LegacyNotificationHandlerSetter = Modern & { + /** + * @deprecated 1.x form. Pass the method name and `{ params }` instead; + * removed in 3.0. + */ + ( + schema: S, + handler: LegacyNotificationHandler, + ): void; +}; + /** * Arguments for the 2.x three-argument `setRequestHandler` / * `setNotificationHandler` form. From a0dcf168432d52ad1e71cd3a37958ed0c03849b3 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 18:38:58 +0100 Subject: [PATCH 95/97] 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). --- .github/workflows/ci.yml | 9 ++++++++- examples/basic-host/tsconfig.json | 1 + examples/basic-server-preact/tsconfig.json | 1 + examples/basic-server-preact/tsconfig.server.json | 1 + examples/basic-server-react/tsconfig.json | 1 + examples/basic-server-react/tsconfig.server.json | 1 + examples/basic-server-solid/tsconfig.json | 1 + examples/basic-server-solid/tsconfig.server.json | 1 + examples/basic-server-svelte/tsconfig.json | 1 + examples/basic-server-svelte/tsconfig.server.json | 1 + examples/basic-server-vanillajs/tsconfig.json | 1 + examples/basic-server-vanillajs/tsconfig.server.json | 1 + examples/basic-server-vue/tsconfig.json | 1 + examples/basic-server-vue/tsconfig.server.json | 1 + examples/budget-allocator-server/tsconfig.json | 1 + examples/budget-allocator-server/tsconfig.server.json | 1 + examples/cohort-heatmap-server/tsconfig.json | 1 + examples/cohort-heatmap-server/tsconfig.server.json | 1 + examples/customer-segmentation-server/tsconfig.json | 1 + .../customer-segmentation-server/tsconfig.server.json | 1 + examples/debug-server/tsconfig.json | 1 + examples/debug-server/tsconfig.server.json | 1 + examples/integration-server/tsconfig.json | 1 + examples/integration-server/tsconfig.server.json | 1 + examples/lazy-auth-server/tsconfig.json | 1 + examples/lazy-auth-server/tsconfig.server.json | 1 + examples/map-server/tsconfig.json | 1 + examples/map-server/tsconfig.server.json | 1 + examples/pdf-server/tsconfig.json | 1 + examples/pdf-server/tsconfig.server.json | 1 + examples/quickstart/tsconfig.json | 1 + examples/quickstart/tsconfig.server.json | 1 + examples/scenario-modeler-server/tsconfig.json | 1 + examples/scenario-modeler-server/tsconfig.server.json | 1 + examples/shadertoy-server/tsconfig.json | 1 + examples/shadertoy-server/tsconfig.server.json | 1 + examples/sheet-music-server/tsconfig.json | 1 + examples/sheet-music-server/tsconfig.server.json | 1 + examples/system-monitor-server/tsconfig.json | 1 + examples/system-monitor-server/tsconfig.server.json | 1 + examples/threejs-server/tsconfig.json | 1 + examples/threejs-server/tsconfig.server.json | 1 + examples/transcript-server/tsconfig.json | 1 + examples/transcript-server/tsconfig.server.json | 1 + examples/video-resource-server/tsconfig.json | 1 + examples/video-resource-server/tsconfig.server.json | 1 + examples/wiki-explorer-server/tsconfig.json | 1 + examples/wiki-explorer-server/tsconfig.server.json | 1 + tsconfig.json | 1 + 49 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 756f259..6848e4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,8 +147,10 @@ jobs: - 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 @@ -180,9 +182,14 @@ jobs: 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: | + cp -r "$(pwd)" ~/ext-apps && cd ~/ext-apps npm ci npm run build npm run examples:build diff --git a/examples/basic-host/tsconfig.json b/examples/basic-host/tsconfig.json index 9d7c3bc..3c143ca 100644 --- a/examples/basic-host/tsconfig.json +++ b/examples/basic-host/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-preact/tsconfig.json b/examples/basic-server-preact/tsconfig.json index 32182d9..9bf9389 100644 --- a/examples/basic-server-preact/tsconfig.json +++ b/examples/basic-server-preact/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-preact/tsconfig.server.json b/examples/basic-server-preact/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/basic-server-preact/tsconfig.server.json +++ b/examples/basic-server-preact/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/basic-server-react/tsconfig.json b/examples/basic-server-react/tsconfig.json index fc3c210..3ecbdad 100644 --- a/examples/basic-server-react/tsconfig.json +++ b/examples/basic-server-react/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-react/tsconfig.server.json b/examples/basic-server-react/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/basic-server-react/tsconfig.server.json +++ b/examples/basic-server-react/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/basic-server-solid/tsconfig.json b/examples/basic-server-solid/tsconfig.json index 84f0efe..bb13a01 100644 --- a/examples/basic-server-solid/tsconfig.json +++ b/examples/basic-server-solid/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-solid/tsconfig.server.json b/examples/basic-server-solid/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/basic-server-solid/tsconfig.server.json +++ b/examples/basic-server-solid/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/basic-server-svelte/tsconfig.json b/examples/basic-server-svelte/tsconfig.json index 5edb66a..4fd3cd7 100644 --- a/examples/basic-server-svelte/tsconfig.json +++ b/examples/basic-server-svelte/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-svelte/tsconfig.server.json b/examples/basic-server-svelte/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/basic-server-svelte/tsconfig.server.json +++ b/examples/basic-server-svelte/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/basic-server-vanillajs/tsconfig.json b/examples/basic-server-vanillajs/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/basic-server-vanillajs/tsconfig.json +++ b/examples/basic-server-vanillajs/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-vanillajs/tsconfig.server.json b/examples/basic-server-vanillajs/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/basic-server-vanillajs/tsconfig.server.json +++ b/examples/basic-server-vanillajs/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/basic-server-vue/tsconfig.json b/examples/basic-server-vue/tsconfig.json index 5edb66a..4fd3cd7 100644 --- a/examples/basic-server-vue/tsconfig.json +++ b/examples/basic-server-vue/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/basic-server-vue/tsconfig.server.json b/examples/basic-server-vue/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/basic-server-vue/tsconfig.server.json +++ b/examples/basic-server-vue/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/budget-allocator-server/tsconfig.json b/examples/budget-allocator-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/budget-allocator-server/tsconfig.json +++ b/examples/budget-allocator-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/budget-allocator-server/tsconfig.server.json b/examples/budget-allocator-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/budget-allocator-server/tsconfig.server.json +++ b/examples/budget-allocator-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/cohort-heatmap-server/tsconfig.json b/examples/cohort-heatmap-server/tsconfig.json index fc3c210..3ecbdad 100644 --- a/examples/cohort-heatmap-server/tsconfig.json +++ b/examples/cohort-heatmap-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/cohort-heatmap-server/tsconfig.server.json b/examples/cohort-heatmap-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/cohort-heatmap-server/tsconfig.server.json +++ b/examples/cohort-heatmap-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/customer-segmentation-server/tsconfig.json b/examples/customer-segmentation-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/customer-segmentation-server/tsconfig.json +++ b/examples/customer-segmentation-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/customer-segmentation-server/tsconfig.server.json b/examples/customer-segmentation-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/customer-segmentation-server/tsconfig.server.json +++ b/examples/customer-segmentation-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/debug-server/tsconfig.json b/examples/debug-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/debug-server/tsconfig.json +++ b/examples/debug-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/debug-server/tsconfig.server.json b/examples/debug-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/debug-server/tsconfig.server.json +++ b/examples/debug-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/integration-server/tsconfig.json b/examples/integration-server/tsconfig.json index fc3c210..3ecbdad 100644 --- a/examples/integration-server/tsconfig.json +++ b/examples/integration-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/integration-server/tsconfig.server.json b/examples/integration-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/integration-server/tsconfig.server.json +++ b/examples/integration-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/lazy-auth-server/tsconfig.json b/examples/lazy-auth-server/tsconfig.json index 6c553b5..d6a42ce 100644 --- a/examples/lazy-auth-server/tsconfig.json +++ b/examples/lazy-auth-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/lazy-auth-server/tsconfig.server.json b/examples/lazy-auth-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/lazy-auth-server/tsconfig.server.json +++ b/examples/lazy-auth-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/map-server/tsconfig.json b/examples/map-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/map-server/tsconfig.json +++ b/examples/map-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/map-server/tsconfig.server.json b/examples/map-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/map-server/tsconfig.server.json +++ b/examples/map-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/pdf-server/tsconfig.json b/examples/pdf-server/tsconfig.json index 0501827..2fe7e62 100644 --- a/examples/pdf-server/tsconfig.json +++ b/examples/pdf-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client", "bun"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/pdf-server/tsconfig.server.json b/examples/pdf-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/pdf-server/tsconfig.server.json +++ b/examples/pdf-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/quickstart/tsconfig.json b/examples/quickstart/tsconfig.json index 6c553b5..d6a42ce 100644 --- a/examples/quickstart/tsconfig.json +++ b/examples/quickstart/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/quickstart/tsconfig.server.json b/examples/quickstart/tsconfig.server.json index 7e65f5f..72552e2 100644 --- a/examples/quickstart/tsconfig.server.json +++ b/examples/quickstart/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/scenario-modeler-server/tsconfig.json b/examples/scenario-modeler-server/tsconfig.json index fc3c210..3ecbdad 100644 --- a/examples/scenario-modeler-server/tsconfig.json +++ b/examples/scenario-modeler-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/scenario-modeler-server/tsconfig.server.json b/examples/scenario-modeler-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/scenario-modeler-server/tsconfig.server.json +++ b/examples/scenario-modeler-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/shadertoy-server/tsconfig.json b/examples/shadertoy-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/shadertoy-server/tsconfig.json +++ b/examples/shadertoy-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/shadertoy-server/tsconfig.server.json b/examples/shadertoy-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/shadertoy-server/tsconfig.server.json +++ b/examples/shadertoy-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/sheet-music-server/tsconfig.json b/examples/sheet-music-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/sheet-music-server/tsconfig.json +++ b/examples/sheet-music-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/sheet-music-server/tsconfig.server.json b/examples/sheet-music-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/sheet-music-server/tsconfig.server.json +++ b/examples/sheet-music-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/system-monitor-server/tsconfig.json b/examples/system-monitor-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/system-monitor-server/tsconfig.json +++ b/examples/system-monitor-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/system-monitor-server/tsconfig.server.json b/examples/system-monitor-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/system-monitor-server/tsconfig.server.json +++ b/examples/system-monitor-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/threejs-server/tsconfig.json b/examples/threejs-server/tsconfig.json index fc3c210..3ecbdad 100644 --- a/examples/threejs-server/tsconfig.json +++ b/examples/threejs-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/threejs-server/tsconfig.server.json b/examples/threejs-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/threejs-server/tsconfig.server.json +++ b/examples/threejs-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/transcript-server/tsconfig.json b/examples/transcript-server/tsconfig.json index 535267b..00e4801 100644 --- a/examples/transcript-server/tsconfig.json +++ b/examples/transcript-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client", "dom-speech-recognition"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/transcript-server/tsconfig.server.json b/examples/transcript-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/transcript-server/tsconfig.server.json +++ b/examples/transcript-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/video-resource-server/tsconfig.json b/examples/video-resource-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/video-resource-server/tsconfig.json +++ b/examples/video-resource-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/video-resource-server/tsconfig.server.json b/examples/video-resource-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/video-resource-server/tsconfig.server.json +++ b/examples/video-resource-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/examples/wiki-explorer-server/tsconfig.json b/examples/wiki-explorer-server/tsconfig.json index 535267b..b028ff5 100644 --- a/examples/wiki-explorer-server/tsconfig.json +++ b/examples/wiki-explorer-server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/examples/wiki-explorer-server/tsconfig.server.json b/examples/wiki-explorer-server/tsconfig.server.json index 05ddd8e..27eb28f 100644 --- a/examples/wiki-explorer-server/tsconfig.server.json +++ b/examples/wiki-explorer-server/tsconfig.server.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", diff --git a/tsconfig.json b/tsconfig.json index 08ff110..934b950 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["node", "bun"], "target": "ES2020", "module": "ESNext", "lib": ["ES2020", "DOM"], From ae4f7e0ad0607ed46862a26af664a05f94b0246a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 18:39:50 +0100 Subject: [PATCH 96/97] Sync the quickstart tsconfig snippets --- docs/quickstart.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/quickstart.md b/docs/quickstart.md index 2039dc1..a021912 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -59,6 +59,7 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN ```json source="../examples/quickstart/tsconfig.json" { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", @@ -87,6 +88,7 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN ```json source="../examples/quickstart/tsconfig.server.json" { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", From 2be172defbadb4dc2a92b2cb447b534aebb56d41 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 19:37:47 +0100 Subject: [PATCH 97/97] Use the canonical git+https form for repository.url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- examples/customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/lazy-auth-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index c88a306..60aa40d 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -5,7 +5,7 @@ "description": "Basic MCP App Server example using Preact", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-preact" }, "license": "MIT", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 07a9d3b..ea51a8a 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -5,7 +5,7 @@ "description": "Basic MCP App Server example using React", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-react" }, "license": "MIT", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index a459ebc..b3ca909 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -5,7 +5,7 @@ "description": "Basic MCP App Server example using Solid", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-solid" }, "license": "MIT", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 3e0279f..19fe4c9 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -5,7 +5,7 @@ "description": "Basic MCP App Server example using Svelte", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-svelte" }, "license": "MIT", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 1404b65..ebfebbb 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -5,7 +5,7 @@ "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-vanillajs" }, "license": "MIT", diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 32083c6..858dd89 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -5,7 +5,7 @@ "description": "Basic MCP App Server example using Vue", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-vue" }, "license": "MIT", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index e44a147..6e41622 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -5,7 +5,7 @@ "description": "Budget allocator MCP App Server with interactive visualization", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/budget-allocator-server" }, "license": "MIT", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index de20f13..7e2d2e2 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -5,7 +5,7 @@ "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/cohort-heatmap-server" }, "license": "MIT", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 643db17..bac3b89 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -5,7 +5,7 @@ "description": "Customer segmentation MCP App Server with filtering", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/customer-segmentation-server" }, "license": "MIT", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 5be9291..e28f4d3 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -5,7 +5,7 @@ "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/debug-server" }, "license": "MIT", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index b12b29b..dbd4601 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -5,7 +5,7 @@ "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/lazy-auth-server" }, "license": "MIT", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index dc6a37f..cc4585f 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -5,7 +5,7 @@ "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/map-server" }, "license": "MIT", diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index ae0870a..c8d9cf8 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -5,7 +5,7 @@ "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/pdf-server" }, "license": "MIT", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index eecb080..57f8444 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -6,7 +6,7 @@ "description": "Quickstart MCP App Server example", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/quickstart" }, "license": "MIT", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index cff7024..cc12249 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -5,7 +5,7 @@ "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/say-server" }, "license": "MIT", diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index d82424d..e6f37c0 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -5,7 +5,7 @@ "description": "Financial scenario modeling MCP App Server", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/scenario-modeler-server" }, "license": "MIT", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 3e16a92..aec56c6 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -5,7 +5,7 @@ "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/shadertoy-server" }, "license": "MIT", diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 1b233bb..85ca6bb 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -5,7 +5,7 @@ "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/sheet-music-server" }, "license": "MIT", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index f3a5763..2dc3ef1 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -5,7 +5,7 @@ "description": "System monitor MCP App Server with real-time stats", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/system-monitor-server" }, "license": "MIT", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index f8df10f..68864d9 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -5,7 +5,7 @@ "description": "Three.js 3D visualization MCP App Server", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/threejs-server" }, "license": "MIT", diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 405f85f..46a8f98 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -5,7 +5,7 @@ "description": "MCP App Server for live speech transcription", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/transcript-server" }, "license": "MIT", diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index dea8fcb..4a7a61f 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -5,7 +5,7 @@ "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/video-resource-server" }, "license": "MIT", diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 0831670..52304cf 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -5,7 +5,7 @@ "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/wiki-explorer-server" }, "license": "MIT", diff --git a/package.json b/package.json index bcea649..c86efa9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@modelcontextprotocol/ext-apps", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps" + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", "version": "2.0.0",

yE%Wu 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 OTiEw}s+GC7E&l*P; + stats(): RangeServerStats; + resetStats(): void; + /** Resolve any requests currently stalled by ?stallAfterBytes=N. */ + release(): void; + close(): Promise; +} + +const LOREM = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod " + + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + + "commodo consequat. "; + +async function buildNoFormsPdf(): Promise { + const doc = await PDFDocument.create(); + const font = await doc.embedFont(StandardFonts.Helvetica); + // Page 1 is text-only (small) so first paint needs minimal bytes. Pages 2+ + // each reference a large embedded JPEG so the bulk of the file is in image + // streams page 1 doesn't need. The stallAfterBytes test holds those back + // and asserts page 1 still renders. The image is >MAX_CHUNK_BYTES (512KB) + // so rendering page 2 also exercises the viewer's >512KB range path. + const big = await doc.embedJpg(makeRandomJpeg(1_100 * 1024)); + const page1 = doc.addPage([612, 792]); + for (let line = 0; line < 30; line++) { + page1.drawText(`1.${line + 1} ${LOREM}`, { + x: 36, + y: 760 - line * 22, + size: 10, + font, + }); + } + for (let p = 1; p < 20; p++) { + const page = doc.addPage([612, 792]); + page.drawImage(big, { x: 36, y: 200, width: 540, height: 540 }); + page.drawText(`Page ${p + 1}`, { x: 36, y: 760, size: 10, font }); + } + return doc.save(); +} + +/** Minimal valid JPEG with `len` bytes of incompressible scan data. */ +export function makeRandomJpeg(len: number): Uint8Array { + // SOI, APP0 (JFIF), SOF0 (baseline 8x8 1-component), DHT (minimal), + // SOS, , EOI. pdf-lib only needs to parse the headers + // to embed; the scan data is opaque. + const header = Uint8Array.from([ + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x0b, + 0x08, 0x00, 0x08, 0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, + 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, + 0x01, 0x00, 0x00, 0x3f, 0x00, + ]); + const scan = new Uint8Array(len); + for (let i = 0; i < len; i++) scan[i] = (i * 1103515245 + 12345) & 0xff; + // Avoid 0xFF in scan data so we don't accidentally form a marker. + for (let i = 0; i < len; i++) if (scan[i] === 0xff) scan[i] = 0xfe; + const eoi = Uint8Array.from([0xff, 0xd9]); + const out = new Uint8Array(header.length + scan.length + eoi.length); + out.set(header, 0); + out.set(scan, header.length); + out.set(eoi, header.length + scan.length); + return out; +} + +/** + * Two pages, page 1 text-only, page 2 carries one native /Text (sticky-note) + * annotation. Used by the tombstone-preservation e2e: the viewer's lazy + * baseline scan must not have visited page 2 when persistAnnotations runs. + */ +async function buildWithNativeAnnotPdf(): Promise { + const doc = await PDFDocument.create(); + const font = await doc.embedFont(StandardFonts.Helvetica); + const page1 = doc.addPage([612, 792]); + page1.drawText("Page 1 — no native annotations here.", { + x: 36, + y: 740, + size: 12, + font, + }); + const page2 = doc.addPage([612, 792]); + page2.drawText("Page 2 — has one native /Text annot.", { + x: 36, + y: 740, + size: 12, + font, + }); + const annotRef = doc.context.register( + doc.context.obj({ + Type: "Annot", + Subtype: "Text", + Rect: [100, 700, 120, 720], + Contents: PDFString.of("native sticky note"), + Open: false, + Name: "Comment", + }), + ); + page2.node.set(PDFName.of("Annots"), doc.context.obj([annotRef])); + return doc.save(); +} + +async function buildFormsPdf(): Promise { + const doc = await PDFDocument.create(); + const form = doc.getForm(); + for (let p = 0; p < 2; p++) doc.addPage([612, 792]); + const [page1] = doc.getPages(); + const fields = ["name", "email", "phone", "city", "notes"]; + fields.forEach((name, i) => { + const f = form.createTextField(name); + f.addToPage(page1, { x: 100, y: 650 - i * 60, width: 300, height: 24 }); + }); + return doc.save(); +} + +export async function startRangeServer(): Promise { + if (process.env.NODE_ENV === "production") { + throw new Error( + "range-counting-server is a test fixture; refusing to start with NODE_ENV=production", + ); + } + const files: Record = { + "/noforms.pdf": await buildNoFormsPdf(), + "/forms.pdf": await buildFormsPdf(), + "/with-native-annot.pdf": await buildWithNativeAnnotPdf(), + }; + const fileSizes = Object.fromEntries( + Object.entries(files).map(([k, v]) => [k, v.length]), + ); + + // Per-path hit count per byte, for overlap accounting. + const hitCounts: Record = {}; + const initHits = () => { + for (const [k, v] of Object.entries(files)) { + hitCounts[k] = new Uint8Array(v.length); + } + }; + initHits(); + + let totalBytesServed = 0; + let releaseResolve: (() => void) | undefined; + let releasePromise = new Promise((r) => (releaseResolve = r)); + + const server = http.createServer(async (req, res) => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + const body = files[url.pathname]; + if (!body) { + res.writeHead(404).end(); + return; + } + + const stallAfterBytes = url.searchParams.get("stallAfterBytes"); + const total = body.length; + const range = req.headers.range; + + let begin = 0; + let end = total; // exclusive + let status = 200; + if (range) { + const m = /^bytes=(\d+)-(\d*)$/.exec(range); + if (m) { + begin = parseInt(m[1], 10); + end = m[2] ? parseInt(m[2], 10) + 1 : total; + begin = Math.min(begin, total); + end = Math.min(end, total); + status = 206; + } + } + + // Stall once N bytes have already been served — lets pdfjs read the + // header/trailer/xref (scattered across the file) before blocking the + // bulk content streams. + if (stallAfterBytes !== null) { + if (totalBytesServed >= parseInt(stallAfterBytes, 10)) { + await releasePromise; + } + } + + const slice = body.subarray(begin, end); + totalBytesServed += slice.length; + const hits = hitCounts[url.pathname]; + for (let i = begin; i < end; i++) hits[i]++; + + const headers: Record = { + "Content-Type": "application/pdf", + "Accept-Ranges": "bytes", + "Content-Length": String(slice.length), + }; + if (status === 206) { + headers["Content-Range"] = `bytes ${begin}-${end - 1}/${total}`; + } + res.writeHead(status, headers); + res.end(slice); + }); + + await new Promise((resolve) => server.listen(0, resolve)); + const port = (server.address() as AddressInfo).port; + + return { + port, + baseUrl: `http://127.0.0.1:${port}`, + fileSizes, + stats() { + let overlapBytes = 0; + for (const hits of Object.values(hitCounts)) { + for (let i = 0; i < hits.length; i++) if (hits[i] > 1) overlapBytes++; + } + return { totalBytesServed, overlapBytes }; + }, + resetStats() { + totalBytesServed = 0; + initHits(); + // Unblock any handlers parked on the previous stall before re-arming, + // otherwise they hold sockets open forever and close() hangs. + releaseResolve?.(); + releasePromise = new Promise((r) => (releaseResolve = r)); + }, + release() { + releaseResolve?.(); + }, + close() { + releaseResolve?.(); + server.closeAllConnections?.(); + return new Promise((resolve) => server.close(() => resolve())); + }, + }; +} From 0008d3b78f94e37bb970673cbc33baecc481ae18 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 27 Apr 2026 16:16:33 +0100 Subject: [PATCH 44/97] chore: bump ext-apps to 1.7.1 (#643) --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 54 +++++++++---------- package.json | 2 +- 27 files changed, 53 insertions(+), 53 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 8045f36..fa215ec 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index dbe0e07..71eb352 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 63d7656..19094cf 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index b39d041..ed0b35c 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 46c38e6..c8ae3b6 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index d41a72e..52b857e 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 4e8022e..d336dbb 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index d8d0e3c..cd746e7 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index a42ba0b..e8490bd 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 46c8037..940bc5f 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index b0a7d54..93fecef 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index f1173ed..7020168 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.0", + "version": "1.7.1", "private": true, "type": "module", "scripts": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index d39648b..de7a1b9 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 57af047..fef9ad1 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index f74e671..35d6082 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.0", + "version": "1.7.1", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 036b66e..7b324a1 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 283c4d5..2226793 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.0", + "version": "1.7.1", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 2d626cf..1bf80c6 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 4924d63..4964478 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index b5f2370..e405bc8 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 05243db..23554a0 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 0c2d5f5..4ba813b 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index f0daa38..43ad9c6 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 9e70d24..2487313 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index a6a8f19..540da43 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.0", + "version": "1.7.1", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index c0c0c47..719cb27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.0", + "version": "1.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "workspaces": [ "examples/*" @@ -66,7 +66,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.0", + "version": "1.7.1", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -109,7 +109,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -153,7 +153,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -200,7 +200,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -244,7 +244,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -288,7 +288,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -330,7 +330,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -374,7 +374,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -417,7 +417,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -464,7 +464,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -507,7 +507,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -548,7 +548,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.7.0", + "version": "1.7.1", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -593,7 +593,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -635,7 +635,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -679,7 +679,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.0", + "version": "1.7.1", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -687,7 +687,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -719,7 +719,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -728,7 +728,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -776,7 +776,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -818,7 +818,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -861,7 +861,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -931,7 +931,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -980,7 +980,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1023,7 +1023,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1065,7 +1065,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/package.json b/package.json index 5072e5b..47160db 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.0", + "version": "1.7.1", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From 3cf5b588a15dd67b7ceb1f69a2346ba84ad2780d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 8 May 2026 13:01:54 +0100 Subject: [PATCH 45/97] 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. --- examples/pdf-server/package.json | 2 +- examples/pdf-server/server.test.ts | 2 +- .../pdf-server/src/pdf-annotations.test.ts | 26 ++++-- examples/pdf-server/src/pdf-annotations.ts | 2 +- package-lock.json | 92 ++++++++++++++----- tests/helpers/range-counting-server.ts | 7 +- 6 files changed, 94 insertions(+), 37 deletions(-) diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index fef9ad1..f732988 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -25,11 +25,11 @@ "build:mcpb": "npm run build && node build-mcpb.mjs" }, "dependencies": { + "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.0.0", "zod": "^4.1.13" }, diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index 8a0790a..33a03f1 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; -import { PDFDocument } from "pdf-lib"; +import { PDFDocument } from "@cantoo/pdf-lib"; import { makeRandomJpeg } from "../../tests/helpers/range-counting-server"; import { createPdfCache, diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts index 239777a..40e63ff 100644 --- a/examples/pdf-server/src/pdf-annotations.test.ts +++ b/examples/pdf-server/src/pdf-annotations.test.ts @@ -18,7 +18,13 @@ import { type PdfAnnotationDef, type AnnotationDiff, } from "./pdf-annotations"; -import { PDFDocument, PDFDict, PDFName, PDFArray, PDFNumber } from "pdf-lib"; +import { + PDFDocument, + PDFDict, + PDFName, + PDFArray, + PDFNumber, +} from "@cantoo/pdf-lib"; // ============================================================================= // Diff Model @@ -1074,16 +1080,16 @@ describe("buildAnnotatedPdfBytes", () => { // Check /C (stroke color) = [1, 0, 0] for #ff0000 const cArr = annotDict.get(PDFName.of("C")) as PDFArray; expect(cArr).toBeDefined(); - expect((cArr.get(0) as PDFNumber).value()).toBe(1); // r - expect((cArr.get(1) as PDFNumber).value()).toBe(0); // g - expect((cArr.get(2) as PDFNumber).value()).toBe(0); // b + expect((cArr.get(0) as PDFNumber).asNumber()).toBe(1); // r + expect((cArr.get(1) as PDFNumber).asNumber()).toBe(0); // g + expect((cArr.get(2) as PDFNumber).asNumber()).toBe(0); // b // Check /IC (fill color) = [0, 1, 0] for #00ff00 const icArr = annotDict.get(PDFName.of("IC")) as PDFArray; expect(icArr).toBeDefined(); - expect((icArr.get(0) as PDFNumber).value()).toBe(0); // r - expect((icArr.get(1) as PDFNumber).value()).toBe(1); // g - expect((icArr.get(2) as PDFNumber).value()).toBe(0); // b + expect((icArr.get(0) as PDFNumber).asNumber()).toBe(0); // r + expect((icArr.get(1) as PDFNumber).asNumber()).toBe(1); // g + expect((icArr.get(2) as PDFNumber).asNumber()).toBe(0); // b }); it("adds freetext annotation to PDF", async () => { @@ -1159,9 +1165,9 @@ describe("buildAnnotatedPdfBytes", () => { // Check /C color const cArr = annotDict.get(PDFName.of("C")) as PDFArray; expect(cArr).toBeDefined(); - expect((cArr.get(0) as PDFNumber).value()).toBe(0); - expect((cArr.get(1) as PDFNumber).value()).toBe(1); - expect((cArr.get(2) as PDFNumber).value()).toBe(0); + expect((cArr.get(0) as PDFNumber).asNumber()).toBe(0); + expect((cArr.get(1) as PDFNumber).asNumber()).toBe(1); + expect((cArr.get(2) as PDFNumber).asNumber()).toBe(0); // Check appearance stream exists and contains the color const ap = annotDict.get(PDFName.of("AP")) as PDFDict; diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts index 73b9b8d..201fe8d 100644 --- a/examples/pdf-server/src/pdf-annotations.ts +++ b/examples/pdf-server/src/pdf-annotations.ts @@ -24,7 +24,7 @@ import { PDFOptionList, PDFRadioGroup, type PDFForm, -} from "pdf-lib"; +} from "@cantoo/pdf-lib"; // ============================================================================= // Types diff --git a/package-lock.json b/package-lock.json index 719cb27..ffda363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -638,11 +638,11 @@ "version": "1.7.1", "license": "MIT", "dependencies": { + "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.0.0", "zod": "^4.1.13" }, @@ -1489,6 +1489,21 @@ } } }, + "node_modules/@cantoo/pdf-lib": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.5.tgz", + "integrity": "sha512-3eMHEaqKHt/G/q+6QjT06A3lz0S/a8x3+myiSN7FNeL3uWcedO0lpfs6TWofa4C03Z1wz3tWeHoa4CsI7DrTSA==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "color": "^4.2.3", + "crypto-js": "^4.2.0", + "node-html-better-parser": ">=1.4.0", + "pako": "^1.0.11", + "tslib": ">=2" + } + }, "node_modules/@clack/core": { "version": "1.0.0-alpha.4", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0-alpha.4.tgz", @@ -5115,11 +5130,23 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5132,9 +5159,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -5266,6 +5302,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -6342,7 +6384,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true, "license": "MIT" }, "node_modules/htmlparser2": { @@ -6489,6 +6530,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7127,6 +7174,15 @@ "node": ">= 0.6" } }, + "node_modules/node-html-better-parser": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.8.tgz", + "integrity": "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw==", + "license": "MIT", + "dependencies": { + "html-entities": "^2.3.2" + } + }, "node_modules/node-html-parser": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", @@ -7377,24 +7433,6 @@ "node": ">= 14.16" } }, - "node_modules/pdf-lib": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", - "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", - "license": "MIT", - "dependencies": { - "@pdf-lib/standard-fonts": "^1.0.0", - "@pdf-lib/upng": "^1.0.1", - "pako": "^1.0.11", - "tslib": "^1.11.1" - } - }, - "node_modules/pdf-lib/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/pdfjs-dist": { "version": "5.4.530", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.530.tgz", @@ -8067,6 +8105,15 @@ "kolorist": "^1.6.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -8751,7 +8798,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsutils": { diff --git a/tests/helpers/range-counting-server.ts b/tests/helpers/range-counting-server.ts index 529ef33..6f0dcd7 100644 --- a/tests/helpers/range-counting-server.ts +++ b/tests/helpers/range-counting-server.ts @@ -8,7 +8,12 @@ */ import http from "node:http"; import type { AddressInfo } from "node:net"; -import { PDFDocument, PDFName, PDFString, StandardFonts } from "pdf-lib"; +import { + PDFDocument, + PDFName, + PDFString, + StandardFonts, +} from "@cantoo/pdf-lib"; export interface RangeServerStats { /** Total bytes written across all responses (sum of slice lengths). */ From 1d091e21321c0680564f19fea9c82a805a352a9c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 8 May 2026 15:55:06 +0100 Subject: [PATCH 46/97] 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. --- examples/basic-server-solid/package.json | 2 +- package-lock.json | 83 ++++++++++++------------ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index ed0b35c..a98ce34 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -40,7 +40,7 @@ "typescript": "^5.9.3", "vite": "^6.0.0", "vite-plugin-singlefile": "^2.3.0", - "vite-plugin-solid": "^2.0.0" + "vite-plugin-solid": "^2.11.12" }, "types": "dist/server.d.ts", "exports": { diff --git a/package-lock.json b/package-lock.json index ffda363..559992b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -222,7 +222,7 @@ "typescript": "^5.9.3", "vite": "^6.0.0", "vite-plugin-singlefile": "^2.3.0", - "vite-plugin-solid": "^2.0.0" + "vite-plugin-solid": "^2.11.12" } }, "examples/basic-server-solid/node_modules/@types/node": { @@ -2562,35 +2562,21 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.0.tgz", - "integrity": "sha512-d5vGKBhWkRoa3xlKOynF8kd+sTSY2D3QSjTMCs46/ddOUOSn5e/E0SaShixFm7H7mNlj4uY0RuU0jAsPM/0qwA==", - "hasInstallScript": true, + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", + "integrity": "sha512-J3WdG1A4JSSKnSWKyU+895dBVYBV2Utgtf7fUsUK45mlkETm53a/1DR6Pm3hUGKqLLQthZLmpxOg8VPzJi/lyg==", "license": "MIT", "workspaces": [ "examples/*" ], - "optionalDependencies": { - "@oven/bun-darwin-aarch64": "^1.2.21", - "@oven/bun-darwin-x64": "^1.2.21", - "@oven/bun-darwin-x64-baseline": "^1.2.21", - "@oven/bun-linux-aarch64": "^1.2.21", - "@oven/bun-linux-aarch64-musl": "^1.2.21", - "@oven/bun-linux-x64": "^1.2.21", - "@oven/bun-linux-x64-baseline": "^1.2.21", - "@oven/bun-linux-x64-musl": "^1.2.21", - "@oven/bun-linux-x64-musl-baseline": "^1.2.21", - "@oven/bun-windows-x64": "^1.2.21", - "@oven/bun-windows-x64-baseline": "^1.2.21", - "@rollup/rollup-darwin-arm64": "^4.53.3", - "@rollup/rollup-darwin-x64": "^4.53.3", - "@rollup/rollup-linux-arm64-gnu": "^4.53.3", - "@rollup/rollup-linux-x64-gnu": "^4.53.3", - "@rollup/rollup-win32-arm64-msvc": "^4.53.3", - "@rollup/rollup-win32-x64-msvc": "^4.53.3" + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" @@ -3053,6 +3039,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3066,6 +3053,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3079,6 +3067,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3092,6 +3081,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3105,6 +3095,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3118,6 +3109,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3131,6 +3123,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3144,6 +3137,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3157,6 +3151,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3184,6 +3179,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3197,6 +3193,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3356,6 +3353,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3369,6 +3367,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3438,6 +3437,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3563,6 +3563,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3618,6 +3619,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3659,6 +3661,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4538,9 +4541,9 @@ } }, "node_modules/babel-plugin-jsx-dom-expressions": { - "version": "0.40.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz", - "integrity": "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==", + "version": "0.40.6", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.6.tgz", + "integrity": "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4578,17 +4581,17 @@ } }, "node_modules/babel-preset-solid": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz", - "integrity": "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==", + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz", + "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jsx-dom-expressions": "^0.40.3" + "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", - "solid-js": "^1.9.10" + "solid-js": "^1.9.12" }, "peerDependenciesMeta": { "solid-js": { @@ -9626,9 +9629,9 @@ } }, "node_modules/vite-plugin-solid": { - "version": "2.11.10", - "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.10.tgz", - "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==", + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.12.tgz", + "integrity": "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==", "dev": true, "license": "MIT", "dependencies": { @@ -9642,7 +9645,7 @@ "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@testing-library/jest-dom": { @@ -9651,9 +9654,9 @@ } }, "node_modules/vite-prerender-plugin": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.12.tgz", - "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.13.tgz", + "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==", "dev": true, "license": "MIT", "dependencies": { @@ -9665,7 +9668,7 @@ "stack-trace": "^1.0.0-pre2" }, "peerDependencies": { - "vite": "5.x || 6.x || 7.x" + "vite": "5.x || 6.x || 7.x || 8.x" } }, "node_modules/vite/node_modules/fdir": { From 9a37ad71827d076af06978fa7f7f510449687061 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 15 May 2026 19:01:25 +0100 Subject: [PATCH 47/97] 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) --- examples/basic-host/package.json | 2 +- examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- examples/debug-server/package.json | 2 +- examples/integration-server/package.json | 2 +- examples/map-server/package.json | 2 +- examples/pdf-server/package.json | 2 +- examples/qr-server/package.json | 2 +- examples/quickstart/package.json | 2 +- examples/say-server/package.json | 2 +- examples/scenario-modeler-server/package.json | 2 +- examples/shadertoy-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/transcript-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package-lock.json | 54 +++++++++---------- package.json | 2 +- 27 files changed, 53 insertions(+), 53 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index fa215ec..a7939a6 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 71eb352..9d0b4d5 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 19094cf..1be79c5 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Basic MCP App Server example using React", "repository": { diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index a98ce34..ac4c197 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index c8ae3b6..4f124f3 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 52b857e..cfb7c1b 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index d336dbb..4f7d19c 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index cd746e7..ad217c8 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index e8490bd..4c20844 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 940bc5f..fada75e 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 93fecef..5bbbad4 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 7020168..0244bad 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.1", + "version": "1.7.2", "private": true, "type": "module", "scripts": { diff --git a/examples/map-server/package.json b/examples/map-server/package.json index de7a1b9..3050e1e 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index f732988..fc4cdb1 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index 35d6082..3c183d0 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.1", + "version": "1.7.2", "private": true, "scripts": { "start": "uv run server.py", diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 7b324a1..5063a6a 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "private": true, "description": "Quickstart MCP App Server example", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 2226793..aca5e3d 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.1", + "version": "1.7.2", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 1bf80c6..e7e1d28 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 4964478..1aa1fd3 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index e405bc8..ffc5c8a 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 23554a0..5a55817 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 4ba813b..be3b293 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 43ad9c6..d4e4ce9 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 2487313..68c890e 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 540da43..96ce612 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { diff --git a/package-lock.json b/package-lock.json index 559992b..54af40d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.1", + "version": "1.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "workspaces": [ "examples/*" @@ -66,7 +66,7 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.1", + "version": "1.7.2", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -109,7 +109,7 @@ }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -153,7 +153,7 @@ }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -200,7 +200,7 @@ }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -244,7 +244,7 @@ }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -288,7 +288,7 @@ }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -330,7 +330,7 @@ }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -374,7 +374,7 @@ }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -417,7 +417,7 @@ }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -464,7 +464,7 @@ }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -507,7 +507,7 @@ }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -548,7 +548,7 @@ "license": "MIT" }, "examples/integration-server": { - "version": "1.7.1", + "version": "1.7.2", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -593,7 +593,7 @@ }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -635,7 +635,7 @@ }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", @@ -679,7 +679,7 @@ }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.1", + "version": "1.7.2", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.29.0" @@ -687,7 +687,7 @@ }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -719,7 +719,7 @@ }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -728,7 +728,7 @@ }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -776,7 +776,7 @@ }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -818,7 +818,7 @@ }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -861,7 +861,7 @@ }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -931,7 +931,7 @@ }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -980,7 +980,7 @@ }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1023,7 +1023,7 @@ }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", @@ -1065,7 +1065,7 @@ }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", diff --git a/package.json b/package.json index 47160db..2f31c24 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From fcc4b03bde93e56cd66fe234bf644ced59aab3f5 Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Mon, 1 Jun 2026 03:19:53 -0700 Subject: [PATCH 48/97] 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//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 --- examples/lazy-auth-server/README.md | 78 ++ examples/lazy-auth-server/main.ts | 42 + examples/lazy-auth-server/mcp-app.html | 27 + examples/lazy-auth-server/package.json | 51 + examples/lazy-auth-server/secret-app.html | 16 + examples/lazy-auth-server/server.ts | 1068 +++++++++++++++++ examples/lazy-auth-server/src/global.css | 100 ++ examples/lazy-auth-server/src/mcp-app.css | 66 + examples/lazy-auth-server/src/mcp-app.ts | 126 ++ examples/lazy-auth-server/src/secret-app.ts | 57 + examples/lazy-auth-server/tsconfig.json | 19 + .../lazy-auth-server/tsconfig.server.json | 17 + examples/lazy-auth-server/vite.config.ts | 24 + package-lock.json | 46 + 14 files changed, 1737 insertions(+) create mode 100644 examples/lazy-auth-server/README.md create mode 100644 examples/lazy-auth-server/main.ts create mode 100644 examples/lazy-auth-server/mcp-app.html create mode 100644 examples/lazy-auth-server/package.json create mode 100644 examples/lazy-auth-server/secret-app.html create mode 100644 examples/lazy-auth-server/server.ts create mode 100644 examples/lazy-auth-server/src/global.css create mode 100644 examples/lazy-auth-server/src/mcp-app.css create mode 100644 examples/lazy-auth-server/src/mcp-app.ts create mode 100644 examples/lazy-auth-server/src/secret-app.ts create mode 100644 examples/lazy-auth-server/tsconfig.json create mode 100644 examples/lazy-auth-server/tsconfig.server.json create mode 100644 examples/lazy-auth-server/vite.config.ts diff --git a/examples/lazy-auth-server/README.md b/examples/lazy-auth-server/README.md new file mode 100644 index 0000000..769ac18 --- /dev/null +++ b/examples/lazy-auth-server/README.md @@ -0,0 +1,78 @@ +# Example: Lazy Auth Server + +An MCP App example demonstrating **lazy (on-demand) auth**: the server connects and lists tools without any authentication, and only asks for OAuth when a _protected_ tool is actually called — by answering `401` with a `WWW-Authenticate` header. A public MCP App renders an "Auth me" button; clicking it calls a protected tool via [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool). The host sees the 401, runs the OAuth flow, retries, and the result renders inline. + +The embedded OAuth authorization server is a deliberately minimal mock (HS256 JWTs, stateless auth codes, auto-approve consent page) so the whole flow runs from a single process with no external dependencies. It is **not** a production authorization server. + +## Tools + +| Tool | Auth | Description | +| ------------------- | ------------- | ----------------------------------------------------------------------------------------------------- | +| `show_auth_button` | public | Renders buttons: "Auth me" (calls `get_secret`), "Revoke token" (calls `revoke_auth_token`) | +| `get_secret` | **protected** | Returns secret data (requires Bearer token) | +| `revoke_auth_token` | **protected** | Revokes the caller's **entire auth session** (access + refresh token) → forces full re-auth | +| `elicit_url` | public | URL elicitation via `elicitInput` (blocks until the elicitation completes) | +| `elicit_by_error` | public | URL elicitation via the `-32042` (`UrlElicitationRequired`) error; succeeds on retry after completion | + +## Getting Started + +```bash +npm install +npm start +# → MCP endpoint at http://localhost:3097/mcp +``` + +To test with a remote MCP host, expose the server through a public tunnel (see [Testing MCP Apps](../../docs/testing-mcp-apps.md)) and set `PUBLIC_URL` to the tunnel URL so OAuth metadata and callback URLs use it: + +```bash +PUBLIC_URL=https:// npm start +``` + +This example is HTTP-only (no stdio mode): the lazy-auth flow relies on HTTP status codes and OAuth endpoints. + +## Environment Variables + +| Var | Required | Description | +| --------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `JWT_SECRET` | recommended | 32+ byte secret for HS256 signing (`openssl rand -hex 32`). A dev-only default is used if unset. | +| `PORT` | no | Local port (default 3097) | +| `PUBLIC_URL` | non-local | Public base URL of the server (e.g. your tunnel URL). Required for non-localhost deployments; client-supplied `Host` headers are only trusted for loopback hosts | +| `ACCESS_TOKEN_TTL_SECONDS` | no | Default access-token lifetime (default **30**, short on purpose so you can watch the host's refresh flow kick in) | +| `REFRESH_TOKEN_TTL_SECONDS` | no | Default refresh-token lifetime (default **300**). Between the access and refresh TTLs, calls succeed via silent refresh; past it, a full re-auth is required | +| `REACTIVE_AUTH_ONLY` | no | Set `1` to remove auth metadata from the root `/.well-known/oauth-*` paths so hosts can't discover auth preemptively — discovery then only happens via the 401 | + +### Per-connection token lifetimes + +The short defaults are great for watching the refresh flow, but slow or automated clients may want tokens that survive a whole session. Any client can request a different access-token lifetime by connecting to a TTL-scoped MCP endpoint path (capped at 24 hours): + +``` +https:///ttl/3600/mcp ← tokens for this connection live 1 hour +``` + +This works through [RFC 8707 resource indicators](https://www.rfc-editor.org/rfc/rfc8707): MCP hosts send the MCP server URL as the `resource` parameter in OAuth authorization and token requests, and this server issues tokens for that grant with the lifetime encoded in the path (refresh tokens are extended to at least match). The TTL is a _path_ segment rather than a query param because hosts canonicalize resource indicators and strip query strings. Each TTL endpoint also enforces its value as a maximum token age, so connecting to a path with a _lower_ TTL than a token's issued lifetime forces the refresh flow. To exercise the full **re-auth** flow, call the `revoke_auth_token` tool. + +## How It Works + +1. **Connect without auth** — `initialize`, `tools/list`, and public tool calls succeed with no `Authorization` header. +2. **Protected tool → 401** — when `get_secret` or `revoke_auth_token` is called without a (valid) Bearer token, the server responds `401` with `WWW-Authenticate: Bearer resource_metadata="…/auth/prm"`. +3. **Discovery** — the host follows `resource_metadata` to the protected-resource metadata (RFC 9728), which points at the authorization server metadata (RFC 8414). +4. **OAuth flow** — the host runs the authorization-code + PKCE flow against the mock `/authorize` and `/token` endpoints (a small consent page keeps the popup visible). +5. **Retry** — the host retries the tool call with the Bearer token and the secret renders inline in the app. +6. **Refresh + revocation** — access tokens expire after 30 seconds and refresh tokens after 5 minutes by default, so all three states are easy to observe: direct success (<30s), silent refresh (30s–5min), and full re-auth (>5min). Connections can request different lifetimes via the `/ttl//mcp` endpoint path (see [Per-connection token lifetimes](#per-connection-token-lifetimes)), and `revoke_auth_token` invalidates the whole session immediately. + +The two `elicit_*` tools demonstrate the complementary pattern of [URL elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation), where the server asks the user to open a URL (e.g. to complete sign-in) either by blocking inside the tool call (`elicit_url`) or by failing with the `-32042` error and succeeding on retry (`elicit_by_error`). + +## Architecture + +- **Stateless auth codes** — grant details are encoded _inside_ the authorization code as a 5-minute JWT, so nothing needs to be stored between requests. +- **Short-lived tokens** — access tokens default to a **30 second** TTL and refresh tokens to **5 minutes**: first `get_secret` succeeds → wait >30s → next call 401s → host refreshes → retry succeeds → wait >5min → full re-auth. Per-connection overrides via the `/ttl//mcp` endpoint path. +- **HS256** — a single shared secret; no key-pair persistence. +- **Per-request MCP server** — each `/mcp` request gets a fresh `McpServer` + `StreamableHTTPServerTransport` (stateless, no session IDs). +- **Session revocation** — all tokens from one OAuth session share a `sid` claim; `revoke_auth_token` adds the sid to an in-memory revocation list checked by both token verification and the refresh grant. + +## Key Files + +- [`server.ts`](server.ts) - OAuth endpoints, discovery metadata, and the MCP server with public + protected tools +- [`main.ts`](main.ts) - HTTP entry point +- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - Public app with the "Auth me" button +- [`secret-app.html`](secret-app.html) / [`src/secret-app.ts`](src/secret-app.ts) - Protected app rendered for `get_secret` diff --git a/examples/lazy-auth-server/main.ts b/examples/lazy-auth-server/main.ts new file mode 100644 index 0000000..a96f34c --- /dev/null +++ b/examples/lazy-auth-server/main.ts @@ -0,0 +1,42 @@ +/** + * Entry point for running the Lazy Auth demo MCP server. + * Run with: npx mcp-server-lazy-auth + * Or: node dist/index.js + * + * This example is HTTP-only (no stdio mode): the lazy-auth flow it demonstrates + * relies on HTTP status codes (401 + WWW-Authenticate) and OAuth endpoints. + * + * To test with a remote MCP host, expose the server through a public tunnel + * and set PUBLIC_URL to the tunnel URL so OAuth metadata and callback URLs + * use it (see docs/testing-mcp-apps.md in the repository root). + */ +import { createApp, PORT } from "./server.js"; + +async function main() { + const app = createApp(); + + const httpServer = app.listen(PORT, (err) => { + if (err) { + console.error("Failed to start server:", err); + process.exit(1); + } + console.log( + `Lazy Auth demo MCP server listening on http://localhost:${PORT}/mcp`, + ); + console.log( + ` Tools: show_auth_button, get_secret [PROTECTED], revoke_auth_token [PROTECTED], elicit_url, elicit_by_error`, + ); + }); + + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/lazy-auth-server/mcp-app.html b/examples/lazy-auth-server/mcp-app.html new file mode 100644 index 0000000..7040f15 --- /dev/null +++ b/examples/lazy-auth-server/mcp-app.html @@ -0,0 +1,27 @@ + + + + + + + Lazy Auth Demo + + +