From 0f034a3b93185a2b8cf9f4424d7aaa52e0d2885a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 12 Dec 2025 16:48:57 +0000 Subject: [PATCH 01/10] Add setter-based handlers for MCP request/notification forwarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add oncalltool, onlistresources, onlistresourcetemplates, onreadresource, onlistprompts setters for Guest UI → Host requests - Add sendToolListChanged, sendResourceListChanged, sendPromptListChanged methods for Host → Guest UI notifications - Refactor connect() to use new setters with inlined callbacks - Update documentation to clarify optional client parameter behavior - Remove unused forwardRequest/forwardNotification helper methods - Add comprehensive tests for AppBridge without MCP client (manual handlers) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app-bridge.test.ts | 197 +++++++++++++++++- src/app-bridge.ts | 463 +++++++++++++++++++++++++++++++++++------ 2 files changed, 591 insertions(+), 69 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index dc559144..2c9aba8f 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -2,7 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } 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 } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolResultSchema, + EmptyResultSchema, + ListPromptsResultSchema, + ListResourcesResultSchema, + ListResourceTemplatesResultSchema, + PromptListChangedNotificationSchema, + ReadResourceResultSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js"; import { App } from "./app"; import { AppBridge, type McpUiHostCapabilities } from "./app-bridge"; @@ -508,4 +518,189 @@ describe("App <-> AppBridge integration", () => { expect(result).toEqual({}); }); }); + + describe("AppBridge without MCP client (manual handlers)", () => { + let app: App; + let bridge: AppBridge; + let appTransport: InMemoryTransport; + let bridgeTransport: InMemoryTransport; + + beforeEach(() => { + [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + app = new App(testAppInfo, {}, { autoResize: false }); + // Pass null instead of a client - manual handler registration + bridge = new AppBridge(null, testHostInfo, testHostCapabilities); + }); + + afterEach(async () => { + await appTransport.close(); + await bridgeTransport.close(); + }); + + it("connect() works without client", async () => { + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // Initialization should still work + const hostInfo = app.getHostVersion(); + expect(hostInfo).toEqual(testHostInfo); + }); + + it("oncalltool setter registers handler for tools/call requests", async () => { + const receivedCalls: unknown[] = []; + bridge.oncalltool = async (params) => { + receivedCalls.push(params); + return { content: [{ type: "text", text: "result" }] }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // App calls a tool via callServerTool + const result = await app.callServerTool({ + name: "test-tool", + arguments: { arg: "value" }, + }); + + expect(receivedCalls).toHaveLength(1); + expect(receivedCalls[0]).toMatchObject({ + name: "test-tool", + arguments: { arg: "value" }, + }); + expect(result.content).toEqual([{ type: "text", text: "result" }]); + }); + + it("onlistresources setter registers handler for resources/list requests", async () => { + const receivedRequests: unknown[] = []; + bridge.onlistresources = async (params) => { + receivedRequests.push(params); + return { resources: [{ uri: "test://resource", name: "Test" }] }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // App sends resources/list request via the protocol's request method + const result = await app.request( + { method: "resources/list", params: {} }, + ListResourcesResultSchema, + ); + + expect(receivedRequests).toHaveLength(1); + expect(result.resources).toEqual([ + { uri: "test://resource", name: "Test" }, + ]); + }); + + it("onreadresource setter registers handler for resources/read requests", async () => { + const receivedRequests: unknown[] = []; + bridge.onreadresource = async (params) => { + receivedRequests.push(params); + return { contents: [{ uri: params.uri, text: "content" }] }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + const result = await app.request( + { method: "resources/read", params: { uri: "test://resource" } }, + ReadResourceResultSchema, + ); + + expect(receivedRequests).toHaveLength(1); + expect(receivedRequests[0]).toMatchObject({ uri: "test://resource" }); + expect(result.contents).toEqual([ + { uri: "test://resource", text: "content" }, + ]); + }); + + it("onlistresourcetemplates setter registers handler for resources/templates/list requests", async () => { + const receivedRequests: unknown[] = []; + bridge.onlistresourcetemplates = async (params) => { + receivedRequests.push(params); + return { + resourceTemplates: [ + { uriTemplate: "test://{id}", name: "Test Template" }, + ], + }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + const result = await app.request( + { method: "resources/templates/list", params: {} }, + ListResourceTemplatesResultSchema, + ); + + expect(receivedRequests).toHaveLength(1); + expect(result.resourceTemplates).toEqual([ + { uriTemplate: "test://{id}", name: "Test Template" }, + ]); + }); + + it("onlistprompts setter registers handler for prompts/list requests", async () => { + const receivedRequests: unknown[] = []; + bridge.onlistprompts = async (params) => { + receivedRequests.push(params); + return { prompts: [{ name: "test-prompt" }] }; + }; + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + const result = await app.request( + { method: "prompts/list", params: {} }, + ListPromptsResultSchema, + ); + + expect(receivedRequests).toHaveLength(1); + expect(result.prompts).toEqual([{ name: "test-prompt" }]); + }); + + it("sendToolListChanged sends notification to app", async () => { + const receivedNotifications: unknown[] = []; + app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => { + receivedNotifications.push(n.params); + }); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + bridge.sendToolListChanged(); + await flush(); + + expect(receivedNotifications).toHaveLength(1); + }); + + it("sendResourceListChanged sends notification to app", async () => { + const receivedNotifications: unknown[] = []; + app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => { + receivedNotifications.push(n.params); + }); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + bridge.sendResourceListChanged(); + await flush(); + + expect(receivedNotifications).toHaveLength(1); + }); + + it("sendPromptListChanged sends notification to app", async () => { + const receivedNotifications: unknown[] = []; + app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => { + receivedNotifications.push(n.params); + }); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + bridge.sendPromptListChanged(); + await flush(); + + expect(receivedNotifications).toHaveLength(1); + }); + }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 4979ac6f..4c8eb982 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,28 +1,39 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import * as z from "zod/v4/core"; - import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { + CallToolRequest, CallToolRequestSchema, + CallToolResult, CallToolResultSchema, Implementation, + ListPromptsRequest, ListPromptsRequestSchema, + ListPromptsResult, ListPromptsResultSchema, + ListResourcesRequest, ListResourcesRequestSchema, + ListResourcesResult, ListResourcesResultSchema, + ListResourceTemplatesRequest, ListResourceTemplatesRequestSchema, + ListResourceTemplatesResult, ListResourceTemplatesResultSchema, LoggingMessageNotification, LoggingMessageNotificationSchema, Notification, PingRequest, PingRequestSchema, + PromptListChangedNotification, PromptListChangedNotificationSchema, + ReadResourceRequest, ReadResourceRequestSchema, + ReadResourceResult, ReadResourceResultSchema, Request, + ResourceListChangedNotification, ResourceListChangedNotificationSchema, Result, + ToolListChangedNotification, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { @@ -161,12 +172,15 @@ export class AppBridge extends Protocol { /** * Create a new AppBridge instance. * - * @param _client - MCP client connected to the server (for proxying requests) + * @param _client - MCP client connected to the server, or `null`. When provided, + * {@link connect} will automatically set up forwarding of MCP requests/notifications + * between the Guest UI and the server. When `null`, you must register handlers + * manually using the `oncalltool`, `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) * - * @example + * @example With MCP client (automatic forwarding) * ```typescript * const bridge = new AppBridge( * mcpClient, @@ -174,9 +188,19 @@ export class AppBridge extends Protocol { * { openLinks: {}, serverTools: {}, logging: {} } * ); * ``` + * + * @example Without MCP client (manual handlers) + * ```typescript + * const bridge = new AppBridge( + * null, + * { name: "MyHost", version: "1.0.0" }, + * { openLinks: {}, serverTools: {}, logging: {} } + * ); + * bridge.oncalltool = async (params, extra) => { ... }; + * ``` */ constructor( - private _client: Client, + private _client: Client | null, private _hostInfo: Implementation, private _capabilities: McpUiHostCapabilities, options?: HostOptions, @@ -503,6 +527,281 @@ export class AppBridge extends Protocol { ); } + /** + * Register a handler for tool call requests from the Guest UI. + * + * The Guest UI sends `tools/call` requests to execute MCP server tools. This + * handler allows the host to intercept and process these requests, typically + * by forwarding them to the MCP server. + * + * @param callback - Handler that receives tool call params and returns a result + * - params.name - Name of the tool to call + * - params.arguments - Tool arguments + * - extra - Request metadata (abort signal, session info) + * - Returns: Promise with tool execution result + * + * @example + * ```typescript + * bridge.oncalltool = async ({ name, arguments: args }, extra) => { + * return mcpClient.request( + * { method: "tools/call", params: { name, arguments: args } }, + * CallToolResultSchema, + * { signal: extra.signal } + * ); + * }; + * ``` + * + * @see {@link CallToolRequest} for the request type + * @see {@link CallToolResult} for the result type + */ + set oncalltool( + callback: ( + params: CallToolRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + return callback(request.params, extra); + }); + } + + /** + * Notify the Guest UI that the MCP server's tool list has changed. + * + * The host sends `notifications/tools/list_changed` to the Guest UI when it + * receives this notification from the MCP server. This allows the Guest UI + * to refresh its tool cache or UI accordingly. + * + * @param params - Optional notification params (typically empty) + * + * @example + * ```typescript + * // In your MCP client notification handler: + * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => { + * bridge.sendToolListChanged(); + * }); + * ``` + * + * @see {@link ToolListChangedNotification} for the notification type + */ + sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { + return this.notification({ + method: "notifications/tools/list_changed", + params, + } as Notification); + } + + /** + * Register a handler for list resources requests from the Guest UI. + * + * The Guest UI sends `resources/list` requests to enumerate available MCP + * resources. This handler allows the host to intercept and process these + * requests, typically by forwarding them to the MCP server. + * + * @param callback - Handler that receives list params and returns resources + * - params - Request params (may include cursor for pagination) + * - extra - Request metadata (abort signal, session info) + * - Returns: Promise with available resources + * + * @example + * ```typescript + * bridge.onlistresources = async (params, extra) => { + * return mcpClient.request( + * { method: "resources/list", params }, + * ListResourcesResultSchema, + * { signal: extra.signal } + * ); + * }; + * ``` + * + * @see {@link ListResourcesRequest} for the request type + * @see {@link ListResourcesResult} for the result type + */ + set onlistresources( + callback: ( + params: ListResourcesRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler( + ListResourcesRequestSchema, + async (request, extra) => { + return callback(request.params, extra); + }, + ); + } + + /** + * Register a handler for list resource templates requests from the Guest UI. + * + * The Guest UI sends `resources/templates/list` requests to enumerate available + * MCP resource templates. This handler allows the host to intercept and process + * these requests, typically by forwarding them to the MCP server. + * + * @param callback - Handler that receives list params and returns templates + * - params - Request params (may include cursor for pagination) + * - extra - Request metadata (abort signal, session info) + * - Returns: Promise with available templates + * + * @example + * ```typescript + * bridge.onlistresourcetemplates = async (params, extra) => { + * return mcpClient.request( + * { method: "resources/templates/list", params }, + * ListResourceTemplatesResultSchema, + * { signal: extra.signal } + * ); + * }; + * ``` + * + * @see {@link ListResourceTemplatesRequest} for the request type + * @see {@link ListResourceTemplatesResult} for the result type + */ + set onlistresourcetemplates( + callback: ( + params: ListResourceTemplatesRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler( + ListResourceTemplatesRequestSchema, + async (request, extra) => { + return callback(request.params, extra); + }, + ); + } + + /** + * Register a handler for read resource requests from the Guest UI. + * + * The Guest UI sends `resources/read` requests to retrieve the contents of an + * MCP resource. This handler allows the host to intercept and process these + * requests, typically by forwarding them to the MCP server. + * + * @param callback - Handler that receives read params and returns resource content + * - params.uri - URI of the resource to read + * - extra - Request metadata (abort signal, session info) + * - Returns: Promise with resource contents + * + * @example + * ```typescript + * bridge.onreadresource = async ({ uri }, extra) => { + * return mcpClient.request( + * { method: "resources/read", params: { uri } }, + * ReadResourceResultSchema, + * { signal: extra.signal } + * ); + * }; + * ``` + * + * @see {@link ReadResourceRequest} for the request type + * @see {@link ReadResourceResult} for the result type + */ + set onreadresource( + callback: ( + params: ReadResourceRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler( + ReadResourceRequestSchema, + async (request, extra) => { + return callback(request.params, extra); + }, + ); + } + + /** + * Notify the Guest UI that the MCP server's resource list has changed. + * + * The host sends `notifications/resources/list_changed` to the Guest UI when it + * receives this notification from the MCP server. This allows the Guest UI + * to refresh its resource cache or UI accordingly. + * + * @param params - Optional notification params (typically empty) + * + * @example + * ```typescript + * // In your MCP client notification handler: + * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => { + * bridge.sendResourceListChanged(); + * }); + * ``` + * + * @see {@link ResourceListChangedNotification} for the notification type + */ + sendResourceListChanged( + params: ResourceListChangedNotification["params"] = {}, + ) { + return this.notification({ + method: "notifications/resources/list_changed", + params, + } as Notification); + } + + /** + * Register a handler for list prompts requests from the Guest UI. + * + * The Guest UI sends `prompts/list` requests to enumerate available MCP + * prompts. This handler allows the host to intercept and process these + * requests, typically by forwarding them to the MCP server. + * + * @param callback - Handler that receives list params and returns prompts + * - params - Request params (may include cursor for pagination) + * - extra - Request metadata (abort signal, session info) + * - Returns: Promise with available prompts + * + * @example + * ```typescript + * bridge.onlistprompts = async (params, extra) => { + * return mcpClient.request( + * { method: "prompts/list", params }, + * ListPromptsResultSchema, + * { signal: extra.signal } + * ); + * }; + * ``` + * + * @see {@link ListPromptsRequest} for the request type + * @see {@link ListPromptsResult} for the result type + */ + set onlistprompts( + callback: ( + params: ListPromptsRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler(ListPromptsRequestSchema, async (request, extra) => { + return callback(request.params, extra); + }); + } + + /** + * Notify the Guest UI that the MCP server's prompt list has changed. + * + * The host sends `notifications/prompts/list_changed` to the Guest UI when it + * receives this notification from the MCP server. This allows the Guest UI + * to refresh its prompt cache or UI accordingly. + * + * @param params - Optional notification params (typically empty) + * + * @example + * ```typescript + * // In your MCP client notification handler: + * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => { + * bridge.sendPromptListChanged(); + * }); + * ``` + * + * @see {@link PromptListChangedNotification} for the notification type + */ + sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { + return this.notification({ + method: "notifications/prompts/list_changed", + params, + } as Notification); + } + /** * Verify that the guest supports the capability required for the given request method. * @internal @@ -837,39 +1136,19 @@ export class AppBridge extends Protocol { ); } - private forwardRequest< - Req extends z.$ZodObject<{ - method: z.$ZodLiteral; - }>, - Res extends z.$ZodObject<{}>, - >(requestSchema: Req, resultSchema: Res) { - this.setRequestHandler(requestSchema, async (request, extra) => { - console.log(`Forwarding request ${request.method} from MCP UI client`); - return this._client.request(request, resultSchema, { - signal: extra.signal, - }); - }); - } - private forwardNotification< - N extends z.$ZodObject<{ method: z.$ZodLiteral }>, - >(notificationSchema: N) { - this.setNotificationHandler(notificationSchema, async (notification) => { - console.log( - `Forwarding notification ${notification.method} from MCP UI client`, - ); - await this._client.notification(notification); - }); - } - /** - * Connect to the Guest UI via transport and set up message forwarding. + * Connect to the Guest UI via transport and optionally set up message forwarding. + * + * This method establishes the transport connection. If an MCP client was passed + * to the constructor, it also automatically sets up request/notification forwarding + * based on the MCP server's capabilities, proxying the following to the Guest UI: + * - Tools (tools/call, notifications/tools/list_changed) + * - Resources (resources/list, resources/read, resources/templates/list, notifications/resources/list_changed) + * - Prompts (prompts/list, notifications/prompts/list_changed) * - * This method establishes the transport connection and automatically sets up - * request/notification forwarding based on the MCP server's capabilities. - * It proxies the following server capabilities to the Guest UI: - * - Tools (tools/call, tools/list_changed) - * - Resources (resources/list, resources/read, resources/templates/list, resources/list_changed) - * - Prompts (prompts/list, prompts/list_changed) + * If no client was passed to the constructor, no automatic forwarding is set up + * and you must register handlers manually using the `oncalltool`, `onlistresources`, + * etc. setters. * * After calling connect, wait for the `oninitialized` callback before sending * tool input and other data to the Guest UI. @@ -877,12 +1156,12 @@ export class AppBridge extends Protocol { * @param transport - Transport layer (typically PostMessageTransport) * @returns Promise resolving when connection is established * - * @throws {Error} If server capabilities are not available. This occurs when - * connect() is called before the MCP client has completed its initialization - * with the server. Ensure `await client.connect()` completes before calling - * `bridge.connect()`. + * @throws {Error} If a client was passed but server capabilities are not available. + * This occurs when connect() is called before the MCP client has completed its + * initialization with the server. Ensure `await client.connect()` completes + * before calling `bridge.connect()`. * - * @example + * @example With MCP client (automatic forwarding) * ```typescript * const bridge = new AppBridge(mcpClient, hostInfo, capabilities); * const transport = new PostMessageTransport( @@ -897,38 +1176,86 @@ export class AppBridge extends Protocol { * * await bridge.connect(transport); * ``` + * + * @example Without MCP client (manual handlers) + * ```typescript + * const bridge = new AppBridge(null, hostInfo, capabilities); + * + * // Register handlers manually + * bridge.oncalltool = async (params, extra) => { + * // Custom tool call handling + * }; + * + * await bridge.connect(transport); + * ``` */ async connect(transport: Transport) { - // Forward core available MCP features - const serverCapabilities = this._client.getServerCapabilities(); - if (!serverCapabilities) { - throw new Error("Client server capabilities not available"); - } + if (this._client) { + // When a client was passed to the constructor, automatically forward + // MCP requests/notifications between the Guest UI and the server + const serverCapabilities = this._client.getServerCapabilities(); + if (!serverCapabilities) { + throw new Error("Client server capabilities not available"); + } - if (serverCapabilities.tools) { - this.forwardRequest(CallToolRequestSchema, CallToolResultSchema); - if (serverCapabilities.tools.listChanged) { - this.forwardNotification(ToolListChangedNotificationSchema); + if (serverCapabilities.tools) { + this.oncalltool = async (params, extra) => { + return this._client!.request( + { method: "tools/call", params }, + CallToolResultSchema, + { signal: extra.signal }, + ); + }; + if (serverCapabilities.tools.listChanged) { + this._client.setNotificationHandler( + ToolListChangedNotificationSchema, + (n) => this.sendToolListChanged(n.params), + ); + } } - } - if (serverCapabilities.resources) { - this.forwardRequest( - ListResourcesRequestSchema, - ListResourcesResultSchema, - ); - this.forwardRequest( - ListResourceTemplatesRequestSchema, - ListResourceTemplatesResultSchema, - ); - this.forwardRequest(ReadResourceRequestSchema, ReadResourceResultSchema); - if (serverCapabilities.resources.listChanged) { - this.forwardNotification(ResourceListChangedNotificationSchema); + if (serverCapabilities.resources) { + this.onlistresources = async (params, extra) => { + return this._client!.request( + { method: "resources/list", params }, + ListResourcesResultSchema, + { signal: extra.signal }, + ); + }; + this.onlistresourcetemplates = async (params, extra) => { + return this._client!.request( + { method: "resources/templates/list", params }, + ListResourceTemplatesResultSchema, + { signal: extra.signal }, + ); + }; + this.onreadresource = async (params, extra) => { + return this._client!.request( + { method: "resources/read", params }, + ReadResourceResultSchema, + { signal: extra.signal }, + ); + }; + if (serverCapabilities.resources.listChanged) { + this._client.setNotificationHandler( + ResourceListChangedNotificationSchema, + (n) => this.sendResourceListChanged(n.params), + ); + } } - } - if (serverCapabilities.prompts) { - this.forwardRequest(ListPromptsRequestSchema, ListPromptsResultSchema); - if (serverCapabilities.prompts.listChanged) { - this.forwardNotification(PromptListChangedNotificationSchema); + if (serverCapabilities.prompts) { + this.onlistprompts = async (params, extra) => { + return this._client!.request( + { method: "prompts/list", params }, + ListPromptsResultSchema, + { signal: extra.signal }, + ); + }; + if (serverCapabilities.prompts.listChanged) { + this._client.setNotificationHandler( + PromptListChangedNotificationSchema, + (n) => this.sendPromptListChanged(n.params), + ); + } } } From 967a560cd460ac30152d9c2222004b5b9c98eaa2 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 12 Dec 2025 17:36:03 +0000 Subject: [PATCH 02/10] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- src/app-bridge.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 2c9aba8f..e0a65581 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -3,7 +3,6 @@ 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 { - CallToolResultSchema, EmptyResultSchema, ListPromptsResultSchema, ListResourcesResultSchema, From ec887350e7aaf6b06393c0db29cda8f6a5cb3c21 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 12 Dec 2025 18:14:33 +0000 Subject: [PATCH 03/10] fix: move prettier to dependencies for git-based installs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5dd48d7a..b99c39d4 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "express": "^5.1.0", "husky": "^9.1.7", "nodemon": "^3.1.0", - "prettier": "^3.6.2", "ts-to-zod": "^5.1.0", "tsx": "^4.21.0", "typedoc": "^0.28.14", @@ -71,6 +70,7 @@ "zod": "^3.25.0 || ^4.0.0" }, "dependencies": { + "prettier": "^3.6.2", "@modelcontextprotocol/sdk": "^1.24.3", "react": "^19.2.0", "react-dom": "^19.2.0" From fdacebbdec936d8c28a937f596a1ac22baff9454 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 12 Dec 2025 18:33:52 +0000 Subject: [PATCH 04/10] fix: add explicit prettierrc to prevent inheriting parent configs during git-based installs --- .prettierrc.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..a64f001d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2 +} From 10fbccff852deef631d0584b26033571ea22ff72 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 13 Dec 2025 11:29:10 +0000 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: Jonathan Hefner --- src/app-bridge.test.ts | 17 +++++++---------- src/app-bridge.ts | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index e0a65581..bf3e2128 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -546,27 +546,24 @@ describe("App <-> AppBridge integration", () => { }); it("oncalltool setter registers handler for tools/call requests", async () => { + const toolCall = { name: "test-tool", arguments: { arg: "value" } }; + const resultContent = [{ type: "text", text: "result" }]; const receivedCalls: unknown[] = []; + bridge.oncalltool = async (params) => { receivedCalls.push(params); - return { content: [{ type: "text", text: "result" }] }; + return { content: resultContent }; }; await bridge.connect(bridgeTransport); await app.connect(appTransport); // App calls a tool via callServerTool - const result = await app.callServerTool({ - name: "test-tool", - arguments: { arg: "value" }, - }); + const result = await app.callServerTool(toolCall); expect(receivedCalls).toHaveLength(1); - expect(receivedCalls[0]).toMatchObject({ - name: "test-tool", - arguments: { arg: "value" }, - }); - expect(result.content).toEqual([{ type: "text", text: "result" }]); + expect(receivedCalls[0]).toMatchObject(toolCall); + expect(result.content).toEqual(resultContent); }); it("onlistresources setter registers handler for resources/list requests", async () => { diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 4c8eb982..ea79e702 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -586,9 +586,9 @@ export class AppBridge extends Protocol { */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ - method: "notifications/tools/list_changed", + method: "notifications/tools/list_changed" as const, params, - } as Notification); + }); } /** From 915e83697f7020ae25e2f3ce7e3b4f33b0ae7fa9 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 13 Dec 2025 11:26:27 +0000 Subject: [PATCH 06/10] refactor(tests): factor out test values and add missing assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback: - Factor out test value objects (toolCall, resultContent, etc.) for clearer tests - Add missing assertions on receivedRequests[0] for list handlers - Improve test readability by defining expected values at the top of each test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app-bridge.test.ts | 55 ++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index bf3e2128..c82f8b14 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -547,9 +547,9 @@ describe("App <-> AppBridge integration", () => { it("oncalltool setter registers handler for tools/call requests", async () => { const toolCall = { name: "test-tool", arguments: { arg: "value" } }; - const resultContent = [{ type: "text", text: "result" }]; + const resultContent = [{ type: "text" as const, text: "result" }]; const receivedCalls: unknown[] = []; - + bridge.oncalltool = async (params) => { receivedCalls.push(params); return { content: resultContent }; @@ -567,10 +567,13 @@ describe("App <-> AppBridge integration", () => { }); it("onlistresources setter registers handler for resources/list requests", async () => { + const requestParams = {}; + const resources = [{ uri: "test://resource", name: "Test" }]; const receivedRequests: unknown[] = []; + bridge.onlistresources = async (params) => { receivedRequests.push(params); - return { resources: [{ uri: "test://resource", name: "Test" }] }; + return { resources }; }; await bridge.connect(bridgeTransport); @@ -578,18 +581,20 @@ describe("App <-> AppBridge integration", () => { // App sends resources/list request via the protocol's request method const result = await app.request( - { method: "resources/list", params: {} }, + { method: "resources/list", params: requestParams }, ListResourcesResultSchema, ); expect(receivedRequests).toHaveLength(1); - expect(result.resources).toEqual([ - { uri: "test://resource", name: "Test" }, - ]); + expect(receivedRequests[0]).toMatchObject(requestParams); + expect(result.resources).toEqual(resources); }); it("onreadresource setter registers handler for resources/read requests", async () => { + const requestParams = { uri: "test://resource" }; + const contents = [{ uri: "test://resource", text: "content" }]; const receivedRequests: unknown[] = []; + bridge.onreadresource = async (params) => { receivedRequests.push(params); return { contents: [{ uri: params.uri, text: "content" }] }; @@ -599,59 +604,61 @@ describe("App <-> AppBridge integration", () => { await app.connect(appTransport); const result = await app.request( - { method: "resources/read", params: { uri: "test://resource" } }, + { method: "resources/read", params: requestParams }, ReadResourceResultSchema, ); expect(receivedRequests).toHaveLength(1); - expect(receivedRequests[0]).toMatchObject({ uri: "test://resource" }); - expect(result.contents).toEqual([ - { uri: "test://resource", text: "content" }, - ]); + expect(receivedRequests[0]).toMatchObject(requestParams); + expect(result.contents).toEqual(contents); }); it("onlistresourcetemplates setter registers handler for resources/templates/list requests", async () => { + const requestParams = {}; + const resourceTemplates = [ + { uriTemplate: "test://{id}", name: "Test Template" }, + ]; const receivedRequests: unknown[] = []; + bridge.onlistresourcetemplates = async (params) => { receivedRequests.push(params); - return { - resourceTemplates: [ - { uriTemplate: "test://{id}", name: "Test Template" }, - ], - }; + return { resourceTemplates }; }; await bridge.connect(bridgeTransport); await app.connect(appTransport); const result = await app.request( - { method: "resources/templates/list", params: {} }, + { method: "resources/templates/list", params: requestParams }, ListResourceTemplatesResultSchema, ); expect(receivedRequests).toHaveLength(1); - expect(result.resourceTemplates).toEqual([ - { uriTemplate: "test://{id}", name: "Test Template" }, - ]); + expect(receivedRequests[0]).toMatchObject(requestParams); + expect(result.resourceTemplates).toEqual(resourceTemplates); }); it("onlistprompts setter registers handler for prompts/list requests", async () => { + const requestParams = {}; + const prompts = [{ name: "test-prompt" }]; const receivedRequests: unknown[] = []; + bridge.onlistprompts = async (params) => { receivedRequests.push(params); - return { prompts: [{ name: "test-prompt" }] }; + return { prompts }; }; await bridge.connect(bridgeTransport); await app.connect(appTransport); const result = await app.request( - { method: "prompts/list", params: {} }, + { method: "prompts/list", params: requestParams }, ListPromptsResultSchema, ); expect(receivedRequests).toHaveLength(1); - expect(result.prompts).toEqual([{ name: "test-prompt" }]); + expect(receivedRequests[0]).toMatchObject(requestParams); + expect(result.prompts).toEqual(prompts); }); it("sendToolListChanged sends notification to app", async () => { From f5893043bccc53a46138f4aa46b8477e61108a09 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 13 Dec 2025 11:36:34 +0000 Subject: [PATCH 07/10] docs: improve TypeDoc formatting and use `as const` for notification methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use proper TypeDoc syntax with `@param callback.params` and `@param callback.extra` - Add {@link ResultType} references for return types - Replace `as Notification` with `as const` on method literals where possible (sendResourceListChanged, sendPromptListChanged) - Simplify sendHostContextChange cast syntax 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app-bridge.ts | 55 +++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/app-bridge.ts b/src/app-bridge.ts index ea79e702..03d401d3 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -534,11 +534,10 @@ export class AppBridge extends Protocol { * handler allows the host to intercept and process these requests, typically * by forwarding them to the MCP server. * - * @param callback - Handler that receives tool call params and returns a result - * - params.name - Name of the tool to call - * - params.arguments - Tool arguments - * - extra - Request metadata (abort signal, session info) - * - Returns: Promise with tool execution result + * @param callback - Handler that receives tool call params and returns a + * {@link CallToolResult} + * @param callback.params - Tool call parameters (name and arguments) + * @param callback.extra - Request metadata (abort signal, session info) * * @example * ```typescript @@ -598,10 +597,10 @@ export class AppBridge extends Protocol { * resources. This handler allows the host to intercept and process these * requests, typically by forwarding them to the MCP server. * - * @param callback - Handler that receives list params and returns resources - * - params - Request params (may include cursor for pagination) - * - extra - Request metadata (abort signal, session info) - * - Returns: Promise with available resources + * @param callback - Handler that receives list params and returns a + * {@link ListResourcesResult} + * @param callback.params - Request params (may include cursor for pagination) + * @param callback.extra - Request metadata (abort signal, session info) * * @example * ```typescript @@ -638,10 +637,10 @@ export class AppBridge extends Protocol { * MCP resource templates. This handler allows the host to intercept and process * these requests, typically by forwarding them to the MCP server. * - * @param callback - Handler that receives list params and returns templates - * - params - Request params (may include cursor for pagination) - * - extra - Request metadata (abort signal, session info) - * - Returns: Promise with available templates + * @param callback - Handler that receives list params and returns a + * {@link ListResourceTemplatesResult} + * @param callback.params - Request params (may include cursor for pagination) + * @param callback.extra - Request metadata (abort signal, session info) * * @example * ```typescript @@ -678,10 +677,10 @@ export class AppBridge extends Protocol { * MCP resource. This handler allows the host to intercept and process these * requests, typically by forwarding them to the MCP server. * - * @param callback - Handler that receives read params and returns resource content - * - params.uri - URI of the resource to read - * - extra - Request metadata (abort signal, session info) - * - Returns: Promise with resource contents + * @param callback - Handler that receives read params and returns a + * {@link ReadResourceResult} + * @param callback.params - Read parameters including the resource URI + * @param callback.extra - Request metadata (abort signal, session info) * * @example * ```typescript @@ -734,9 +733,9 @@ export class AppBridge extends Protocol { params: ResourceListChangedNotification["params"] = {}, ) { return this.notification({ - method: "notifications/resources/list_changed", + method: "notifications/resources/list_changed" as const, params, - } as Notification); + }); } /** @@ -746,10 +745,10 @@ export class AppBridge extends Protocol { * prompts. This handler allows the host to intercept and process these * requests, typically by forwarding them to the MCP server. * - * @param callback - Handler that receives list params and returns prompts - * - params - Request params (may include cursor for pagination) - * - extra - Request metadata (abort signal, session info) - * - Returns: Promise with available prompts + * @param callback - Handler that receives list params and returns a + * {@link ListPromptsResult} + * @param callback.params - Request params (may include cursor for pagination) + * @param callback.extra - Request metadata (abort signal, session info) * * @example * ```typescript @@ -797,9 +796,9 @@ export class AppBridge extends Protocol { */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ - method: "notifications/prompts/list_changed", + method: "notifications/prompts/list_changed" as const, params, - } as Notification); + }); } /** @@ -937,10 +936,10 @@ export class AppBridge extends Protocol { sendHostContextChange( params: McpUiHostContextChangedNotification["params"], ): Promise | void { - return this.notification(({ - method: "ui/notifications/host-context-changed", + return this.notification({ + method: "ui/notifications/host-context-changed" as const, params, - }) as Notification); + } as Notification); } /** From 16fb2e49cc7e60caf842912609a574a3991ae2af Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 13 Dec 2025 11:41:27 +0000 Subject: [PATCH 08/10] refactor(types): use explicit type unions for AppBridge protocol types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AppBridgeRequest, AppBridgeNotification, AppBridgeResult type unions that explicitly list all valid types for the protocol - Replace generic SDK Request/Notification/Result with our specific types - Remove all type casts by using `as const` on method literals - Add index signature to McpUiHostContext for forward compatibility This improves type safety and documents exactly what the AppBridge supports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app-bridge.ts | 112 +++++++++++++++++++++++++++++++------- src/app.ts | 16 +++--- src/generated/schema.json | 6 +- src/generated/schema.ts | 2 +- src/spec.types.ts | 2 + 5 files changed, 107 insertions(+), 31 deletions(-) diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 03d401d3..dec49be1 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -5,6 +5,7 @@ import { CallToolRequestSchema, CallToolResult, CallToolResultSchema, + EmptyResult, Implementation, ListPromptsRequest, ListPromptsRequestSchema, @@ -18,9 +19,10 @@ import { ListResourceTemplatesRequestSchema, ListResourceTemplatesResult, ListResourceTemplatesResultSchema, + ListToolsRequest, + ListToolsResult, LoggingMessageNotification, LoggingMessageNotificationSchema, - Notification, PingRequest, PingRequestSchema, PromptListChangedNotification, @@ -29,10 +31,8 @@ import { ReadResourceRequestSchema, ReadResourceResult, ReadResourceResultSchema, - Request, ResourceListChangedNotification, ResourceListChangedNotificationSchema, - Result, ToolListChangedNotification, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; @@ -66,6 +66,7 @@ import { McpUiOpenLinkRequestSchema, McpUiOpenLinkResult, McpUiResourceTeardownRequest, + McpUiResourceTeardownResult, McpUiResourceTeardownResultSchema, McpUiSandboxProxyReadyNotification, McpUiSandboxProxyReadyNotificationSchema, @@ -75,6 +76,73 @@ export * from "./types"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; export { PostMessageTransport } from "./message-transport"; +/** + * All request types in the MCP Apps protocol. + * + * Includes: + * - MCP UI requests (initialize, open-link, message, resource-teardown) + * - MCP server requests forwarded from the app (tools/call, resources/*, prompts/list) + * - Protocol requests (ping) + */ +export type AppRequest = + | McpUiInitializeRequest + | McpUiOpenLinkRequest + | McpUiMessageRequest + | McpUiResourceTeardownRequest + | CallToolRequest + | ListToolsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | ListPromptsRequest + | 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 + * - Logging messages + */ +export type AppNotification = + // Sent to app + | McpUiHostContextChangedNotification + | McpUiToolInputNotification + | McpUiToolInputPartialNotification + | McpUiToolResultNotification + | McpUiToolCancelledNotification + | McpUiSandboxResourceReadyNotification + | ToolListChangedNotification + | ResourceListChangedNotification + | PromptListChangedNotification + // Received from app + | McpUiInitializedNotification + | McpUiSizeChangedNotification + | McpUiSandboxProxyReadyNotification + | LoggingMessageNotification; + +/** + * All result types in the MCP Apps protocol. + */ +export type AppResult = + | McpUiInitializeResult + | McpUiOpenLinkResult + | McpUiMessageResult + | McpUiResourceTeardownResult + | CallToolResult + | ListToolsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | EmptyResult; + /** * Options for configuring AppBridge behavior. * @@ -164,7 +232,11 @@ type RequestHandlerExtra = Parameters< * await bridge.connect(transport); * ``` */ -export class AppBridge extends Protocol { +export class AppBridge extends Protocol< + AppRequest, + AppNotification, + AppResult +> { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; @@ -805,7 +877,7 @@ export class AppBridge extends Protocol { * Verify that the guest supports the capability required for the given request method. * @internal */ - assertCapabilityForMethod(method: Request["method"]): void { + assertCapabilityForMethod(method: AppRequest["method"]): void { // TODO } @@ -813,7 +885,7 @@ export class AppBridge extends Protocol { * Verify that a request handler is registered and supported for the given method. * @internal */ - assertRequestHandlerCapability(method: Request["method"]): void { + assertRequestHandlerCapability(method: AppRequest["method"]): void { // TODO } @@ -821,7 +893,7 @@ export class AppBridge extends Protocol { * Verify that the host supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(method: Notification["method"]): void { + assertNotificationCapability(method: AppNotification["method"]): void { // TODO } @@ -939,7 +1011,7 @@ export class AppBridge extends Protocol { return this.notification({ method: "ui/notifications/host-context-changed" as const, params, - } as Notification); + }); } /** @@ -965,8 +1037,8 @@ export class AppBridge extends Protocol { * @see {@link sendToolResult} for sending results after execution */ sendToolInput(params: McpUiToolInputNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-input", + return this.notification({ + method: "ui/notifications/tool-input" as const, params, }); } @@ -999,8 +1071,8 @@ export class AppBridge extends Protocol { * @see {@link sendToolInput} for sending complete arguments */ sendToolInputPartial(params: McpUiToolInputPartialNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-input-partial", + return this.notification({ + method: "ui/notifications/tool-input-partial" as const, params, }); } @@ -1030,8 +1102,8 @@ export class AppBridge extends Protocol { * @see {@link sendToolInput} for sending tool arguments before results */ sendToolResult(params: McpUiToolResultNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-result", + return this.notification({ + method: "ui/notifications/tool-result" as const, params, }); } @@ -1067,8 +1139,8 @@ export class AppBridge extends Protocol { * @see {@link sendToolInput} for sending tool arguments */ sendToolCancelled(params: McpUiToolCancelledNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-cancelled", + return this.notification({ + method: "ui/notifications/tool-cancelled" as const, params, }); } @@ -1091,8 +1163,8 @@ export class AppBridge extends Protocol { sendSandboxResourceReady( params: McpUiSandboxResourceReadyNotification["params"], ) { - return this.notification({ - method: "ui/notifications/sandbox-resource-ready", + return this.notification({ + method: "ui/notifications/sandbox-resource-ready" as const, params, }); } @@ -1126,8 +1198,8 @@ export class AppBridge extends Protocol { options?: RequestOptions, ) { return this.request( - { - method: "ui/resource-teardown", + { + method: "ui/resource-teardown" as const, params, }, McpUiResourceTeardownResultSchema, diff --git a/src/app.ts b/src/app.ts index 51189b97..cee778b3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,11 +13,13 @@ import { ListToolsRequest, ListToolsRequestSchema, LoggingMessageNotification, - Notification, PingRequestSchema, - Request, - Result, } from "@modelcontextprotocol/sdk/types.js"; +import { + AppNotification, + AppRequest, + AppResult, +} from "./app-bridge"; import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, @@ -189,7 +191,7 @@ type RequestHandlerExtra = Parameters< * }); * ``` */ -export class App extends Protocol { +export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; @@ -642,7 +644,7 @@ export class App extends Protocol { * Verify that the host supports the capability required for the given request method. * @internal */ - assertCapabilityForMethod(method: Request["method"]): void { + assertCapabilityForMethod(method: AppRequest["method"]): void { // TODO } @@ -650,7 +652,7 @@ export class App extends Protocol { * Verify that the app declared the capability required for the given request method. * @internal */ - assertRequestHandlerCapability(method: Request["method"]): void { + assertRequestHandlerCapability(method: AppRequest["method"]): void { switch (method) { case "tools/call": case "tools/list": @@ -672,7 +674,7 @@ export class App extends Protocol { * Verify that the app supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(method: Notification["method"]): void { + assertNotificationCapability(method: AppNotification["method"]): void { // TODO } diff --git a/src/generated/schema.json b/src/generated/schema.json index 4d4ae53b..f70b60f9 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -381,7 +381,7 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} } }, "required": ["method", "params"], @@ -667,7 +667,7 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} }, "McpUiInitializeRequest": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -1135,7 +1135,7 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} } }, "required": [ diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 2fa4a13f..baff9658 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -337,7 +337,7 @@ export const McpUiToolResultNotificationSchema = z.object({ /** * @description Rich context about the host environment provided to Guest UIs. */ -export const McpUiHostContextSchema = z.object({ +export const McpUiHostContextSchema = z.looseObject({ /** @description Metadata of the tool call that instantiated this App. */ toolInfo: z .object({ diff --git a/src/spec.types.ts b/src/spec.types.ts index 84713847..01cc4713 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -184,6 +184,8 @@ export interface McpUiToolCancelledNotification { * @description Rich context about the host environment provided to Guest UIs. */ export interface McpUiHostContext { + /** @description Allow additional properties for forward compatibility. */ + [key: string]: unknown; /** @description Metadata of the tool call that instantiated this App. */ toolInfo?: { /** @description JSON-RPC id of the tools/call request. */ From 4653156dd7dd90460ab0875db41f32d6248d0da1 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 13 Dec 2025 11:51:00 +0000 Subject: [PATCH 09/10] refactor: move AppRequest/AppNotification/AppResult to types.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move protocol type unions from app-bridge.ts to types.ts where they belong with other type definitions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app-bridge.ts | 73 ++----------------------- src/app.ts | 6 +-- src/generated/schema.json | 20 +++---- src/types.ts | 110 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 85 deletions(-) diff --git a/src/app-bridge.ts b/src/app-bridge.ts index dec49be1..bed0a75d 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -5,7 +5,6 @@ import { CallToolRequestSchema, CallToolResult, CallToolResultSchema, - EmptyResult, Implementation, ListPromptsRequest, ListPromptsRequestSchema, @@ -19,8 +18,6 @@ import { ListResourceTemplatesRequestSchema, ListResourceTemplatesResult, ListResourceTemplatesResultSchema, - ListToolsRequest, - ListToolsResult, LoggingMessageNotification, LoggingMessageNotificationSchema, PingRequest, @@ -43,6 +40,9 @@ import { } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { + type AppNotification, + type AppRequest, + type AppResult, type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, type McpUiToolCancelledNotification, @@ -76,73 +76,6 @@ export * from "./types"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; export { PostMessageTransport } from "./message-transport"; -/** - * All request types in the MCP Apps protocol. - * - * Includes: - * - MCP UI requests (initialize, open-link, message, resource-teardown) - * - MCP server requests forwarded from the app (tools/call, resources/*, prompts/list) - * - Protocol requests (ping) - */ -export type AppRequest = - | McpUiInitializeRequest - | McpUiOpenLinkRequest - | McpUiMessageRequest - | McpUiResourceTeardownRequest - | CallToolRequest - | ListToolsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | ListPromptsRequest - | 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 - * - Logging messages - */ -export type AppNotification = - // Sent to app - | McpUiHostContextChangedNotification - | McpUiToolInputNotification - | McpUiToolInputPartialNotification - | McpUiToolResultNotification - | McpUiToolCancelledNotification - | McpUiSandboxResourceReadyNotification - | ToolListChangedNotification - | ResourceListChangedNotification - | PromptListChangedNotification - // Received from app - | McpUiInitializedNotification - | McpUiSizeChangedNotification - | McpUiSandboxProxyReadyNotification - | LoggingMessageNotification; - -/** - * All result types in the MCP Apps protocol. - */ -export type AppResult = - | McpUiInitializeResult - | McpUiOpenLinkResult - | McpUiMessageResult - | McpUiResourceTeardownResult - | CallToolResult - | ListToolsResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | EmptyResult; - /** * Options for configuring AppBridge behavior. * diff --git a/src/app.ts b/src/app.ts index cee778b3..c4970fcd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,11 +15,7 @@ import { LoggingMessageNotification, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; -import { - AppNotification, - AppRequest, - AppResult, -} from "./app-bridge"; +import { AppNotification, AppRequest, AppResult } from "./types"; import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, diff --git a/src/generated/schema.json b/src/generated/schema.json index f70b60f9..2b5aaf17 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -1212,7 +1212,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1259,7 +1259,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1306,7 +1306,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1380,7 +1380,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1473,7 +1473,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1871,7 +1871,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1918,7 +1918,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1965,7 +1965,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2039,7 +2039,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2132,7 +2132,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false diff --git a/src/types.ts b/src/types.ts index 5ed81313..8544b27e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,27 @@ export { type McpUiResourceMeta, } from "./spec.types.js"; +// Import types needed for protocol type unions (not re-exported, just used internally) +import type { + McpUiInitializeRequest, + McpUiOpenLinkRequest, + McpUiMessageRequest, + McpUiResourceTeardownRequest, + McpUiHostContextChangedNotification, + McpUiToolInputNotification, + McpUiToolInputPartialNotification, + McpUiToolResultNotification, + McpUiToolCancelledNotification, + McpUiSandboxResourceReadyNotification, + McpUiInitializedNotification, + McpUiSizeChangedNotification, + McpUiSandboxProxyReadyNotification, + McpUiInitializeResult, + McpUiOpenLinkResult, + McpUiMessageResult, + McpUiResourceTeardownResult, +} from "./spec.types.js"; + // Re-export all schemas from generated/schema.ts (already PascalCase) export { McpUiThemeSchema, @@ -65,3 +86,92 @@ export { McpUiResourceCspSchema, McpUiResourceMetaSchema, } from "./generated/schema.js"; + +// Re-export SDK types used in protocol type unions +import { + CallToolRequest, + CallToolResult, + 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) + * - MCP server requests forwarded from the app (tools/call, resources/*, prompts/list) + * - Protocol requests (ping) + */ +export type AppRequest = + | McpUiInitializeRequest + | McpUiOpenLinkRequest + | McpUiMessageRequest + | McpUiResourceTeardownRequest + | CallToolRequest + | ListToolsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | ListPromptsRequest + | 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 + * - Logging messages + */ +export type AppNotification = + // Sent to app + | McpUiHostContextChangedNotification + | McpUiToolInputNotification + | McpUiToolInputPartialNotification + | McpUiToolResultNotification + | McpUiToolCancelledNotification + | McpUiSandboxResourceReadyNotification + | ToolListChangedNotification + | ResourceListChangedNotification + | PromptListChangedNotification + // Received from app + | McpUiInitializedNotification + | McpUiSizeChangedNotification + | McpUiSandboxProxyReadyNotification + | LoggingMessageNotification; + +/** + * All result types in the MCP Apps protocol. + */ +export type AppResult = + | McpUiInitializeResult + | McpUiOpenLinkResult + | McpUiMessageResult + | McpUiResourceTeardownResult + | CallToolResult + | ListToolsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | EmptyResult; From 05cb2a12c664470ee345c1ae5a883079862d2e04 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 13 Dec 2025 12:02:06 +0000 Subject: [PATCH 10/10] chore: regenerate schemas with clean npm install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/generated/schema.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 2b5aaf17..f70b60f9 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -1212,7 +1212,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1259,7 +1259,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1306,7 +1306,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1380,7 +1380,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1473,7 +1473,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1871,7 +1871,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1918,7 +1918,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -1965,7 +1965,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -2039,7 +2039,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -2132,7 +2132,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false