From d9a14eddc4a6f50d5e03a91556d9c12c423c6504 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 20 Jan 2026 13:57:41 +0000 Subject: [PATCH 1/8] feat(server): add hasUiSupport/getUiCapability for experimental+extensions Support double-tagging for MCP Apps capability negotiation: - Check both experimental and extensions fields in client capabilities - Add hasUiSupport() to easily check if client supports MCP Apps - Add getUiCapability() to retrieve the capability settings - Update spec to document both capability locations This enables forward compatibility as MCP transitions from experimental to the extensions field (SEP-1724). Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 0 Claude-Permission-Prompts: 0 Claude-Escapes: 0 --- specification/draft/apps.mdx | 43 ++++++++-- src/server/index.test.ts | 162 +++++++++++++++++++++++++++++++++++ src/server/index.ts | 139 ++++++++++++++++++++++++++++++ 3 files changed, 338 insertions(+), 6 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 272ba3f4d..a59fd5075 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1412,11 +1412,43 @@ Note: Tools with `visibility: ["app"]` are hidden from the agent but remain call ### Client\<\>Server Capability Negotiation -Clients and servers negotiate MCP Apps support through the standard MCP extensions capability mechanism (defined in SEP-1724). +Clients and servers negotiate MCP Apps support using the extension identifier `io.modelcontextprotocol/ui`. + +#### Capability Location + +The MCP Apps capability can be advertised in either of two locations within `ClientCapabilities`: + +1. **`experimental`** (currently preferred): The `experimental` field is part of the current MCP schema and allows arbitrary extension data. Use this for maximum compatibility. + +2. **`extensions`** (future): Once SEP-1724 is accepted and deployed, `extensions` will be the canonical location. Servers SHOULD check both locations for forward compatibility. #### Client (Host) Capabilities -Clients advertise MCP Apps support in the initialize request using the extension identifier `io.modelcontextprotocol/ui`: +Clients advertise MCP Apps support in the initialize request: + +**Using `experimental` (recommended for current deployments):** + +```json +{ + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": { + "experimental": { + "io.modelcontextprotocol/ui": { + "mimeTypes": ["text/html;profile=mcp-app"] + } + } + }, + "clientInfo": { + "name": "claude-desktop", + "version": "1.0.0" + } + } +} +``` + +**Using `extensions` (once SEP-1724 is accepted):** ```json { @@ -1449,13 +1481,12 @@ Future versions may add additional settings: #### Server Behavior -Servers SHOULD check client (host would-be) capabilities before registering UI-enabled tools: +Servers SHOULD check both `experimental` and `extensions` before registering UI-enabled tools. The SDK provides the `hasUiSupport` helper for this: ```typescript -const hasUISupport = - clientCapabilities?.extensions?.["io.modelcontextprotocol/ui"]?.mimeTypes?.includes("text/html;profile=mcp-app"); +import { hasUiSupport } from "@modelcontextprotocol/ext-apps/server"; -if (hasUISupport) { +if (hasUiSupport(clientCapabilities)) { // Register tools with UI templates server.registerTool("get_weather", { description: "Get weather with interactive dashboard", diff --git a/src/server/index.test.ts b/src/server/index.test.ts index d5e0a80ac..27a2f37cf 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -4,6 +4,9 @@ import { registerAppResource, RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, + hasUiSupport, + getUiCapability, + EXTENSION_ID, } from "./index"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -318,3 +321,162 @@ describe("registerAppResource", () => { expect(result).toEqual(expectedResult); }); }); + +describe("hasUiSupport", () => { + const MIME_TYPE = "text/html;profile=mcp-app"; + + it("should return false for null/undefined capabilities", () => { + expect(hasUiSupport(null)).toBe(false); + expect(hasUiSupport(undefined)).toBe(false); + }); + + it("should return false for empty capabilities", () => { + expect(hasUiSupport({})).toBe(false); + }); + + it("should detect support in experimental field", () => { + const caps = { + experimental: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + }; + expect(hasUiSupport(caps)).toBe(true); + }); + + it("should detect support in extensions field", () => { + const caps = { + extensions: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + }; + expect(hasUiSupport(caps)).toBe(true); + }); + + it("should detect support when both fields are present", () => { + const caps = { + experimental: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + extensions: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + }; + expect(hasUiSupport(caps)).toBe(true); + }); + + it("should return false if MIME type is not in the list", () => { + const caps = { + experimental: { + [EXTENSION_ID]: { + mimeTypes: ["text/plain"], + }, + }, + }; + expect(hasUiSupport(caps)).toBe(false); + }); + + it("should check for custom MIME type when specified", () => { + const caps = { + experimental: { + [EXTENSION_ID]: { + mimeTypes: ["application/x-custom"], + }, + }, + }; + expect(hasUiSupport(caps, "application/x-custom")).toBe(true); + expect(hasUiSupport(caps, MIME_TYPE)).toBe(false); + }); + + it("should return false when extension ID is missing", () => { + const caps = { + experimental: { + "some-other-extension": { + mimeTypes: [MIME_TYPE], + }, + }, + }; + expect(hasUiSupport(caps)).toBe(false); + }); + + it("should return false when mimeTypes is missing", () => { + const caps = { + experimental: { + [EXTENSION_ID]: {}, + }, + }; + expect(hasUiSupport(caps)).toBe(false); + }); +}); + +describe("getUiCapability", () => { + const MIME_TYPE = "text/html;profile=mcp-app"; + + it("should return undefined for null/undefined capabilities", () => { + expect(getUiCapability(null)).toBeUndefined(); + expect(getUiCapability(undefined)).toBeUndefined(); + }); + + it("should return undefined for empty capabilities", () => { + expect(getUiCapability({})).toBeUndefined(); + }); + + it("should return capability from experimental field", () => { + const caps = { + experimental: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + }; + const result = getUiCapability(caps); + expect(result).toEqual({ mimeTypes: [MIME_TYPE] }); + }); + + it("should return capability from extensions field", () => { + const caps = { + extensions: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + }; + const result = getUiCapability(caps); + expect(result).toEqual({ mimeTypes: [MIME_TYPE] }); + }); + + it("should prefer extensions over experimental when both are present", () => { + const caps = { + experimental: { + [EXTENSION_ID]: { + mimeTypes: ["text/plain"], + }, + }, + extensions: { + [EXTENSION_ID]: { + mimeTypes: [MIME_TYPE], + }, + }, + }; + const result = getUiCapability(caps); + expect(result).toEqual({ mimeTypes: [MIME_TYPE] }); + }); + + it("should return undefined when extension ID is missing", () => { + const caps = { + experimental: { + "some-other-extension": { + mimeTypes: [MIME_TYPE], + }, + }, + }; + expect(getUiCapability(caps)).toBeUndefined(); + }); +}); diff --git a/src/server/index.ts b/src/server/index.ts index 6ef076691..734af5ad2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -281,3 +281,142 @@ export function registerAppResource( readCallback, ); } + +/** + * Extension identifier for MCP Apps capability negotiation. + * + * Used as the key in `experimental` or `extensions` to advertise MCP Apps support. + */ +export const EXTENSION_ID = "io.modelcontextprotocol/ui"; + +/** + * MCP Apps capability settings advertised by clients. + * + * @see {@link hasUiSupport} for checking client support + */ +export interface McpUiClientCapability { + /** + * Array of supported MIME types for UI resources. + * Must include `"text/html;profile=mcp-app"` for MCP Apps support. + */ + mimeTypes?: string[]; +} + +/** + * Check if client capabilities indicate MCP Apps support. + * + * This helper checks both `experimental` and `extensions` fields for the + * MCP Apps capability, providing forward compatibility as the MCP specification + * evolves. Currently, `experimental` is preferred (it's part of the existing + * MCP schema); once SEP-1724 is accepted, `extensions` will be the canonical + * location. + * + * @param clientCapabilities - The client capabilities from the initialize response + * @param mimeType - MIME type to check for (defaults to `"text/html;profile=mcp-app"`) + * @returns `true` if the client supports MCP Apps with the specified MIME type + * + * @example Basic usage in server initialization + * ```typescript + * import { hasUiSupport, registerAppTool } from "@modelcontextprotocol/ext-apps/server"; + * + * server.oninitialized = ({ clientCapabilities }) => { + * if (hasUiSupport(clientCapabilities)) { + * registerAppTool(server, "weather", { + * description: "Get weather with interactive dashboard", + * _meta: { ui: { resourceUri: "ui://weather/dashboard" } }, + * }, weatherHandler); + * } else { + * // Register text-only fallback + * server.registerTool("weather", { + * description: "Get weather as text", + * }, textWeatherHandler); + * } + * }; + * ``` + * + * @example Checking for specific MIME type + * ```typescript + * if (hasUiSupport(clientCapabilities, "application/x-custom-widget")) { + * // Client supports custom widget MIME type + * } + * ``` + */ +export function hasUiSupport( + clientCapabilities: + | { + experimental?: Record; + extensions?: Record; + } + | null + | undefined, + mimeType: string = RESOURCE_MIME_TYPE, +): boolean { + if (!clientCapabilities) { + return false; + } + + // Check experimental field (current MCP schema) + const experimentalCap = clientCapabilities.experimental?.[ + EXTENSION_ID + ] as McpUiClientCapability | undefined; + if (experimentalCap?.mimeTypes?.includes(mimeType)) { + return true; + } + + // Check extensions field (future SEP-1724) + const extensionsCap = clientCapabilities.extensions?.[ + EXTENSION_ID + ] as McpUiClientCapability | undefined; + if (extensionsCap?.mimeTypes?.includes(mimeType)) { + return true; + } + + return false; +} + +/** + * Get MCP Apps capability settings from client capabilities. + * + * This helper retrieves the capability object from either `experimental` or + * `extensions`, preferring `extensions` when both are present (for forward + * compatibility with SEP-1724). + * + * @param clientCapabilities - The client capabilities from the initialize response + * @returns The MCP Apps capability settings, or `undefined` if not supported + * + * @example + * ```typescript + * import { getUiCapability } from "@modelcontextprotocol/ext-apps/server"; + * + * const uiCap = getUiCapability(clientCapabilities); + * if (uiCap?.mimeTypes?.includes("text/html;profile=mcp-app")) { + * // Client supports MCP Apps + * } + * ``` + */ +export function getUiCapability( + clientCapabilities: + | { + experimental?: Record; + extensions?: Record; + } + | null + | undefined, +): McpUiClientCapability | undefined { + if (!clientCapabilities) { + return undefined; + } + + // Prefer extensions when available (forward compatibility with SEP-1724) + const extensionsCap = clientCapabilities.extensions?.[ + EXTENSION_ID + ] as McpUiClientCapability | undefined; + if (extensionsCap) { + return extensionsCap; + } + + // Fall back to experimental (current MCP schema) + return clientCapabilities.experimental?.[ + EXTENSION_ID + ] as McpUiClientCapability | undefined; +} From 8451dabad37bc21f62187f062d6add11da5a92bd Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 20 Jan 2026 13:58:59 +0000 Subject: [PATCH 2/8] fix: update @types/node version in package-lock.json Replace invalid 22.19.5/22.19.6/22.19.7 versions with valid 22.19.3 --- package-lock.json | 75 +++++++++++++-------------------------------- src/server/index.ts | 24 +++++++-------- 2 files changed, 33 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index b82b93a41..be7db8578 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,7 +96,7 @@ } }, "examples/basic-host/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -138,7 +138,7 @@ } }, "examples/basic-server-preact/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -183,7 +183,7 @@ } }, "examples/basic-server-react/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -225,7 +225,7 @@ } }, "examples/basic-server-solid/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -267,7 +267,7 @@ } }, "examples/basic-server-svelte/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -307,7 +307,7 @@ } }, "examples/basic-server-vanillajs/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -349,7 +349,7 @@ } }, "examples/basic-server-vue/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -390,7 +390,7 @@ } }, "examples/budget-allocator-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -435,7 +435,7 @@ } }, "examples/cohort-heatmap-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -476,7 +476,7 @@ } }, "examples/customer-segmentation-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -518,7 +518,7 @@ } }, "examples/integration-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -558,7 +558,7 @@ } }, "examples/map-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -599,7 +599,7 @@ } }, "examples/pdf-server/node_modules/@types/node": { - "version": "22.19.6", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -620,39 +620,6 @@ "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "0.4.1", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.4.1", - "react": "^19.2.0", - "react-dom": "^19.2.0" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^4.3.4", - "concurrently": "^9.2.1", - "cross-env": "^10.1.0", - "typescript": "^5.9.3", - "vite": "^6.0.0", - "vite-plugin-singlefile": "^2.3.0" - } - }, - "examples/say-server/node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "examples/say-server/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "examples/scenario-modeler-server": { @@ -687,7 +654,7 @@ } }, "examples/scenario-modeler-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -727,7 +694,7 @@ } }, "examples/shadertoy-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -768,7 +735,7 @@ } }, "examples/sheet-music-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -810,7 +777,7 @@ } }, "examples/system-monitor-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -857,7 +824,7 @@ } }, "examples/threejs-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -898,7 +865,7 @@ } }, "examples/transcript-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -947,7 +914,7 @@ } }, "examples/video-resource-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { @@ -989,7 +956,7 @@ } }, "examples/wiki-explorer-server/node_modules/@types/node": { - "version": "22.19.5", + "version": "22.19.3", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/server/index.ts b/src/server/index.ts index 734af5ad2..8df644f48 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -356,17 +356,17 @@ export function hasUiSupport( } // Check experimental field (current MCP schema) - const experimentalCap = clientCapabilities.experimental?.[ - EXTENSION_ID - ] as McpUiClientCapability | undefined; + const experimentalCap = clientCapabilities.experimental?.[EXTENSION_ID] as + | McpUiClientCapability + | undefined; if (experimentalCap?.mimeTypes?.includes(mimeType)) { return true; } // Check extensions field (future SEP-1724) - const extensionsCap = clientCapabilities.extensions?.[ - EXTENSION_ID - ] as McpUiClientCapability | undefined; + const extensionsCap = clientCapabilities.extensions?.[EXTENSION_ID] as + | McpUiClientCapability + | undefined; if (extensionsCap?.mimeTypes?.includes(mimeType)) { return true; } @@ -408,15 +408,15 @@ export function getUiCapability( } // Prefer extensions when available (forward compatibility with SEP-1724) - const extensionsCap = clientCapabilities.extensions?.[ - EXTENSION_ID - ] as McpUiClientCapability | undefined; + const extensionsCap = clientCapabilities.extensions?.[EXTENSION_ID] as + | McpUiClientCapability + | undefined; if (extensionsCap) { return extensionsCap; } // Fall back to experimental (current MCP schema) - return clientCapabilities.experimental?.[ - EXTENSION_ID - ] as McpUiClientCapability | undefined; + return clientCapabilities.experimental?.[EXTENSION_ID] as + | McpUiClientCapability + | undefined; } From 730c4c2da94a6f27de6765194e125f82f195a347 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 20 Jan 2026 14:12:50 +0000 Subject: [PATCH 3/8] docs(spec): clarify hosts SHOULD double-tag, servers SHOULD check both - Hosts SHOULD advertise capabilities in both experimental and extensions - Servers SHOULD check both locations, preferring extensions when present - Updated example to show double-tagging pattern --- specification/draft/apps.mdx | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index a59fd5075..6a364aa7c 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1416,17 +1416,19 @@ Clients and servers negotiate MCP Apps support using the extension identifier `i #### Capability Location -The MCP Apps capability can be advertised in either of two locations within `ClientCapabilities`: +The MCP Apps capability can be advertised in two locations within `ClientCapabilities`: -1. **`experimental`** (currently preferred): The `experimental` field is part of the current MCP schema and allows arbitrary extension data. Use this for maximum compatibility. +1. **`experimental`**: The `experimental` field is part of the current MCP schema and allows arbitrary extension data. This is required for compatibility with existing SDKs. -2. **`extensions`** (future): Once SEP-1724 is accepted and deployed, `extensions` will be the canonical location. Servers SHOULD check both locations for forward compatibility. +2. **`extensions`**: Once SEP-1724 is accepted and deployed, `extensions` will be the canonical location for extension capabilities. -#### Client (Host) Capabilities +**Clients (hosts) SHOULD advertise in both locations** ("double-tag") to ensure compatibility with all servers during the transition period. + +**Servers SHOULD check both locations** when determining client support, preferring `extensions` when present. -Clients advertise MCP Apps support in the initialize request: +#### Client (Host) Capabilities -**Using `experimental` (recommended for current deployments):** +Clients SHOULD advertise MCP Apps support in both fields: ```json { @@ -1438,24 +1440,7 @@ Clients advertise MCP Apps support in the initialize request: "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] } - } - }, - "clientInfo": { - "name": "claude-desktop", - "version": "1.0.0" - } - } -} -``` - -**Using `extensions` (once SEP-1724 is accepted):** - -```json -{ - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": { + }, "extensions": { "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] From ce43e791ffe0beada626c936ac7fceba1cfc8ce4 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 21 Jan 2026 19:13:50 +0000 Subject: [PATCH 4/8] chore: remove double-tagging recommendation, keep hasUiSupport helper Reverts the spec changes that recommended clients double-tag capabilities in both experimental and extensions fields. The helper functions remain as they're useful for checking capability in either location. --- specification/draft/apps.mdx | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 12af81f33..2e08aa71f 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1420,23 +1420,11 @@ Note: Tools with `visibility: ["app"]` are hidden from the agent but remain call ### Client\<\>Server Capability Negotiation -Clients and servers negotiate MCP Apps support using the extension identifier `io.modelcontextprotocol/ui`. - -#### Capability Location - -The MCP Apps capability can be advertised in two locations within `ClientCapabilities`: - -1. **`experimental`**: The `experimental` field is part of the current MCP schema and allows arbitrary extension data. This is required for compatibility with existing SDKs. - -2. **`extensions`**: Once SEP-1724 is accepted and deployed, `extensions` will be the canonical location for extension capabilities. - -**Clients (hosts) SHOULD advertise in both locations** ("double-tag") to ensure compatibility with all servers during the transition period. - -**Servers SHOULD check both locations** when determining client support, preferring `extensions` when present. +Clients and servers negotiate MCP Apps support through the standard MCP extensions capability mechanism (defined in SEP-1724). #### Client (Host) Capabilities -Clients SHOULD advertise MCP Apps support in both fields: +Clients advertise MCP Apps support in the initialize request using the extension identifier `io.modelcontextprotocol/ui`: ```json { @@ -1444,11 +1432,6 @@ Clients SHOULD advertise MCP Apps support in both fields: "params": { "protocolVersion": "2024-11-05", "capabilities": { - "experimental": { - "io.modelcontextprotocol/ui": { - "mimeTypes": ["text/html;profile=mcp-app"] - } - }, "extensions": { "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] @@ -1474,7 +1457,7 @@ Future versions may add additional settings: #### Server Behavior -Servers SHOULD check both `experimental` and `extensions` before registering UI-enabled tools. The SDK provides the `hasUiSupport` helper for this: +Servers SHOULD check client capabilities before registering UI-enabled tools. The SDK provides the `hasUiSupport` helper for this: ```typescript import { hasUiSupport } from "@modelcontextprotocol/ext-apps/server"; From 0f8176e1f9b09f7dd2d4375565bfa4307395a943 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 22 Jan 2026 10:00:54 +0000 Subject: [PATCH 5/8] refactor(server): simplify to only getUiCapability, remove hasUiSupport - Remove hasUiSupport function (use getUiCapability directly) - Remove double-tagging logic (only check extensions field) - Add ClientCapabilitiesWithExtensions type using SDK's ClientCapabilities - Update spec to use getUiCapability - Simplify tests --- specification/draft/apps.mdx | 7 +- src/server/index.test.ts | 126 +---------------------------------- src/server/index.ts | 116 ++++++++------------------------ 3 files changed, 31 insertions(+), 218 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 2e08aa71f..29c3a434f 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1457,12 +1457,13 @@ Future versions may add additional settings: #### Server Behavior -Servers SHOULD check client capabilities before registering UI-enabled tools. The SDK provides the `hasUiSupport` helper for this: +Servers SHOULD check client capabilities before registering UI-enabled tools. The SDK provides the `getUiCapability` helper for this: ```typescript -import { hasUiSupport } from "@modelcontextprotocol/ext-apps/server"; +import { getUiCapability, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -if (hasUiSupport(clientCapabilities)) { +const uiCap = getUiCapability(clientCapabilities); +if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) { // Register tools with UI templates server.registerTool("get_weather", { description: "Get weather with interactive dashboard", diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 27a2f37cf..e6037a7c2 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -4,7 +4,6 @@ import { registerAppResource, RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, - hasUiSupport, getUiCapability, EXTENSION_ID, } from "./index"; @@ -322,100 +321,6 @@ describe("registerAppResource", () => { }); }); -describe("hasUiSupport", () => { - const MIME_TYPE = "text/html;profile=mcp-app"; - - it("should return false for null/undefined capabilities", () => { - expect(hasUiSupport(null)).toBe(false); - expect(hasUiSupport(undefined)).toBe(false); - }); - - it("should return false for empty capabilities", () => { - expect(hasUiSupport({})).toBe(false); - }); - - it("should detect support in experimental field", () => { - const caps = { - experimental: { - [EXTENSION_ID]: { - mimeTypes: [MIME_TYPE], - }, - }, - }; - expect(hasUiSupport(caps)).toBe(true); - }); - - it("should detect support in extensions field", () => { - const caps = { - extensions: { - [EXTENSION_ID]: { - mimeTypes: [MIME_TYPE], - }, - }, - }; - expect(hasUiSupport(caps)).toBe(true); - }); - - it("should detect support when both fields are present", () => { - const caps = { - experimental: { - [EXTENSION_ID]: { - mimeTypes: [MIME_TYPE], - }, - }, - extensions: { - [EXTENSION_ID]: { - mimeTypes: [MIME_TYPE], - }, - }, - }; - expect(hasUiSupport(caps)).toBe(true); - }); - - it("should return false if MIME type is not in the list", () => { - const caps = { - experimental: { - [EXTENSION_ID]: { - mimeTypes: ["text/plain"], - }, - }, - }; - expect(hasUiSupport(caps)).toBe(false); - }); - - it("should check for custom MIME type when specified", () => { - const caps = { - experimental: { - [EXTENSION_ID]: { - mimeTypes: ["application/x-custom"], - }, - }, - }; - expect(hasUiSupport(caps, "application/x-custom")).toBe(true); - expect(hasUiSupport(caps, MIME_TYPE)).toBe(false); - }); - - it("should return false when extension ID is missing", () => { - const caps = { - experimental: { - "some-other-extension": { - mimeTypes: [MIME_TYPE], - }, - }, - }; - expect(hasUiSupport(caps)).toBe(false); - }); - - it("should return false when mimeTypes is missing", () => { - const caps = { - experimental: { - [EXTENSION_ID]: {}, - }, - }; - expect(hasUiSupport(caps)).toBe(false); - }); -}); - describe("getUiCapability", () => { const MIME_TYPE = "text/html;profile=mcp-app"; @@ -428,18 +333,6 @@ describe("getUiCapability", () => { expect(getUiCapability({})).toBeUndefined(); }); - it("should return capability from experimental field", () => { - const caps = { - experimental: { - [EXTENSION_ID]: { - mimeTypes: [MIME_TYPE], - }, - }, - }; - const result = getUiCapability(caps); - expect(result).toEqual({ mimeTypes: [MIME_TYPE] }); - }); - it("should return capability from extensions field", () => { const caps = { extensions: { @@ -452,26 +345,9 @@ describe("getUiCapability", () => { expect(result).toEqual({ mimeTypes: [MIME_TYPE] }); }); - it("should prefer extensions over experimental when both are present", () => { - const caps = { - experimental: { - [EXTENSION_ID]: { - mimeTypes: ["text/plain"], - }, - }, - extensions: { - [EXTENSION_ID]: { - mimeTypes: [MIME_TYPE], - }, - }, - }; - const result = getUiCapability(caps); - expect(result).toEqual({ mimeTypes: [MIME_TYPE] }); - }); - it("should return undefined when extension ID is missing", () => { const caps = { - experimental: { + extensions: { "some-other-extension": { mimeTypes: [MIME_TYPE], }, diff --git a/src/server/index.ts b/src/server/index.ts index 5c05de734..36775d19f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -29,7 +29,10 @@ import type { AnySchema, ZodRawShapeCompat, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; -import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js"; +import type { + ClientCapabilities, + ToolAnnotations, +} from "@modelcontextprotocol/sdk/types.js"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; @@ -199,10 +202,21 @@ export function registerAppResource( /** * Extension identifier for MCP Apps capability negotiation. * - * Used as the key in `experimental` or `extensions` to advertise MCP Apps support. + * Used as the key in `extensions` to advertise MCP Apps support. */ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; +/** + * Client capabilities with extensions field. + * + * This extends the SDK's `ClientCapabilities` type with the `extensions` field + * (pending SEP-1724). Once `extensions` is added to the SDK, this type can be + * replaced with `ClientCapabilities` directly. + */ +export type ClientCapabilitiesWithExtensions = ClientCapabilities & { + extensions?: Record; +}; + /** * MCP Apps capability settings advertised by clients. * @@ -217,24 +231,21 @@ export interface McpUiClientCapability { } /** - * Check if client capabilities indicate MCP Apps support. + * Get MCP Apps capability settings from client capabilities. * - * This helper checks both `experimental` and `extensions` fields for the - * MCP Apps capability, providing forward compatibility as the MCP specification - * evolves. Currently, `experimental` is preferred (it's part of the existing - * MCP schema); once SEP-1724 is accepted, `extensions` will be the canonical - * location. + * This helper retrieves the capability object from the `extensions` field + * where MCP Apps advertises its support. * * @param clientCapabilities - The client capabilities from the initialize response - * @param mimeType - MIME type to check for (defaults to `"text/html;profile=mcp-app"`) - * @returns `true` if the client supports MCP Apps with the specified MIME type + * @returns The MCP Apps capability settings, or `undefined` if not supported * - * @example Basic usage in server initialization + * @example Check for MCP Apps support in server initialization * ```typescript - * import { hasUiSupport, registerAppTool } from "@modelcontextprotocol/ext-apps/server"; + * import { getUiCapability, RESOURCE_MIME_TYPE, registerAppTool } from "@modelcontextprotocol/ext-apps/server"; * * server.oninitialized = ({ clientCapabilities }) => { - * if (hasUiSupport(clientCapabilities)) { + * const uiCap = getUiCapability(clientCapabilities); + * if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) { * registerAppTool(server, "weather", { * description: "Get weather with interactive dashboard", * _meta: { ui: { resourceUri: "ui://weather/dashboard" } }, @@ -247,90 +258,15 @@ export interface McpUiClientCapability { * } * }; * ``` - * - * @example Checking for specific MIME type - * ```typescript - * if (hasUiSupport(clientCapabilities, "application/x-custom-widget")) { - * // Client supports custom widget MIME type - * } - * ``` - */ -export function hasUiSupport( - clientCapabilities: - | { - experimental?: Record; - extensions?: Record; - } - | null - | undefined, - mimeType: string = RESOURCE_MIME_TYPE, -): boolean { - if (!clientCapabilities) { - return false; - } - - // Check experimental field (current MCP schema) - const experimentalCap = clientCapabilities.experimental?.[EXTENSION_ID] as - | McpUiClientCapability - | undefined; - if (experimentalCap?.mimeTypes?.includes(mimeType)) { - return true; - } - - // Check extensions field (future SEP-1724) - const extensionsCap = clientCapabilities.extensions?.[EXTENSION_ID] as - | McpUiClientCapability - | undefined; - if (extensionsCap?.mimeTypes?.includes(mimeType)) { - return true; - } - - return false; -} - -/** - * Get MCP Apps capability settings from client capabilities. - * - * This helper retrieves the capability object from either `experimental` or - * `extensions`, preferring `extensions` when both are present (for forward - * compatibility with SEP-1724). - * - * @param clientCapabilities - The client capabilities from the initialize response - * @returns The MCP Apps capability settings, or `undefined` if not supported - * - * @example - * ```typescript - * import { getUiCapability } from "@modelcontextprotocol/ext-apps/server"; - * - * const uiCap = getUiCapability(clientCapabilities); - * if (uiCap?.mimeTypes?.includes("text/html;profile=mcp-app")) { - * // Client supports MCP Apps - * } - * ``` */ export function getUiCapability( - clientCapabilities: - | { - experimental?: Record; - extensions?: Record; - } - | null - | undefined, + clientCapabilities: ClientCapabilitiesWithExtensions | null | undefined, ): McpUiClientCapability | undefined { if (!clientCapabilities) { return undefined; } - // Prefer extensions when available (forward compatibility with SEP-1724) - const extensionsCap = clientCapabilities.extensions?.[EXTENSION_ID] as - | McpUiClientCapability - | undefined; - if (extensionsCap) { - return extensionsCap; - } - - // Fall back to experimental (current MCP schema) - return clientCapabilities.experimental?.[EXTENSION_ID] as + return clientCapabilities.extensions?.[EXTENSION_ID] as | McpUiClientCapability | undefined; } From cac9b09f0116b63da6298fabae74d7874b2a8095 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 22 Jan 2026 15:06:49 +0000 Subject: [PATCH 6/8] fix: update @see link to getUiCapability --- src/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/index.ts b/src/server/index.ts index 36775d19f..3612237a7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -220,7 +220,7 @@ export type ClientCapabilitiesWithExtensions = ClientCapabilities & { /** * MCP Apps capability settings advertised by clients. * - * @see {@link hasUiSupport} for checking client support + * @see {@link getUiCapability} for checking client support */ export interface McpUiClientCapability { /** From 6b9e5de6b960c7fce36732417f543e98d48b7a93 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 22 Jan 2026 20:47:27 +0000 Subject: [PATCH 7/8] refactor(server): inline ClientCapabilitiesWithExtensions type in getUiCapability --- src/server/index.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 3612237a7..24a805cc7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -206,17 +206,6 @@ export function registerAppResource( */ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; -/** - * Client capabilities with extensions field. - * - * This extends the SDK's `ClientCapabilities` type with the `extensions` field - * (pending SEP-1724). Once `extensions` is added to the SDK, this type can be - * replaced with `ClientCapabilities` directly. - */ -export type ClientCapabilitiesWithExtensions = ClientCapabilities & { - extensions?: Record; -}; - /** * MCP Apps capability settings advertised by clients. * @@ -236,6 +225,10 @@ export interface McpUiClientCapability { * 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 * @@ -260,7 +253,10 @@ export interface McpUiClientCapability { * ``` */ export function getUiCapability( - clientCapabilities: ClientCapabilitiesWithExtensions | null | undefined, + clientCapabilities: + | (ClientCapabilities & { extensions?: Record }) + | null + | undefined, ): McpUiClientCapability | undefined { if (!clientCapabilities) { return undefined; From fcb5e54fea01a5b49ef1a4ce7563035105cf8e84 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 22 Jan 2026 21:06:26 +0000 Subject: [PATCH 8/8] refactor: move McpUiClientCapabilities to spec.types.ts --- src/generated/schema.json | 14 ++++++++++++++ src/generated/schema.test.ts | 10 ++++++++++ src/generated/schema.ts | 20 ++++++++++++++++++++ src/server/index.ts | 18 +++--------------- src/spec.types.ts | 15 +++++++++++++++ src/types.ts | 1 + 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index e17767d99..6e8b6ef5c 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -28,6 +28,20 @@ }, "additionalProperties": false }, + "McpUiClientCapabilities": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "mimeTypes": { + "description": "Array of supported MIME types for UI resources.\nMust include `\"text/html;profile=mcp-app\"` for MCP Apps support.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "McpUiDisplayMode": { "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 727c28d29..95ec2f216 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -119,6 +119,10 @@ export type McpUiToolMetaSchemaInferredType = z.infer< typeof generated.McpUiToolMetaSchema >; +export type McpUiClientCapabilitiesSchemaInferredType = z.infer< + typeof generated.McpUiClientCapabilitiesSchema +>; + export type McpUiMessageRequestSchemaInferredType = z.infer< typeof generated.McpUiMessageRequestSchema >; @@ -277,6 +281,12 @@ expectType( ); expectType({} as McpUiToolMetaSchemaInferredType); expectType({} as spec.McpUiToolMeta); +expectType( + {} as McpUiClientCapabilitiesSchemaInferredType, +); +expectType( + {} as spec.McpUiClientCapabilities, +); expectType( {} as McpUiMessageRequestSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 32277d23d..4ae5b2ef3 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -565,6 +565,26 @@ export const McpUiToolMetaSchema = z.object({ ), }); +/** + * @description MCP Apps capability settings advertised by clients to servers. + * + * Clients advertise these capabilities via the `extensions` field in their + * capabilities during MCP initialization. Servers can check for MCP Apps + * support using {@link server-helpers!getUiCapability}. + */ +export const McpUiClientCapabilitiesSchema = z.object({ + /** + * @description Array of supported MIME types for UI resources. + * Must include `"text/html;profile=mcp-app"` for MCP Apps support. + */ + mimeTypes: z + .array(z.string()) + .optional() + .describe( + 'Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.', + ), +}); + /** * @description Request to send a message to the host's chat interface. * @see {@link app!App.sendMessage} for the method that sends this request diff --git a/src/server/index.ts b/src/server/index.ts index 24a805cc7..2626c6a7d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -16,6 +16,7 @@ import { RESOURCE_MIME_TYPE, McpUiResourceMeta, McpUiToolMeta, + McpUiClientCapabilities, } from "../app.js"; import type { BaseToolCallback, @@ -206,19 +207,6 @@ export function registerAppResource( */ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; -/** - * MCP Apps capability settings advertised by clients. - * - * @see {@link getUiCapability} for checking client support - */ -export interface McpUiClientCapability { - /** - * Array of supported MIME types for UI resources. - * Must include `"text/html;profile=mcp-app"` for MCP Apps support. - */ - mimeTypes?: string[]; -} - /** * Get MCP Apps capability settings from client capabilities. * @@ -257,12 +245,12 @@ export function getUiCapability( | (ClientCapabilities & { extensions?: Record }) | null | undefined, -): McpUiClientCapability | undefined { +): McpUiClientCapabilities | undefined { if (!clientCapabilities) { return undefined; } return clientCapabilities.extensions?.[EXTENSION_ID] as - | McpUiClientCapability + | McpUiClientCapabilities | undefined; } diff --git a/src/spec.types.ts b/src/spec.types.ts index cb6af1f78..c71b9effe 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -672,3 +672,18 @@ export const INITIALIZED_METHOD: McpUiInitializedNotification["method"] = "ui/notifications/initialized"; export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method"] = "ui/request-display-mode"; + +/** + * @description MCP Apps capability settings advertised by clients to servers. + * + * Clients advertise these capabilities via the `extensions` field in their + * capabilities during MCP initialization. Servers can check for MCP Apps + * support using {@link server-helpers!getUiCapability}. + */ +export interface McpUiClientCapabilities { + /** + * @description Array of supported MIME types for UI resources. + * Must include `"text/html;profile=mcp-app"` for MCP Apps support. + */ + mimeTypes?: string[]; +} diff --git a/src/types.ts b/src/types.ts index 77563dc85..a4770fdaf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,7 @@ export { type McpUiRequestDisplayModeResult, type McpUiToolVisibility, type McpUiToolMeta, + type McpUiClientCapabilities, } from "./spec.types.js"; // Import types needed for protocol type unions (not re-exported, just used internally)