feat(server): add getUiCapability helper for capability negotiation (#313)

* 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

* 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

* 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

* 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.

* 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

* fix: update @see link to getUiCapability

* refactor(server): inline ClientCapabilitiesWithExtensions type in getUiCapability

* refactor: move McpUiClientCapabilities to spec.types.ts
This commit is contained in:
Olivier Chafik 2026-01-23 16:26:49 +00:00 • committed by GitHub
commit cfcbc9605f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 238 additions and 73 deletions

View file

@ -4,6 +4,8 @@ import {
registerAppResource,
RESOURCE_URI_META_KEY,
RESOURCE_MIME_TYPE,
getUiCapability,
EXTENSION_ID,
} from "./index";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@ -318,3 +320,39 @@ describe("registerAppResource", () => {
expect(result).toEqual(expectedResult);
});
});
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 extensions field", () => {
const caps = {
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 = {
extensions: {
"some-other-extension": {
mimeTypes: [MIME_TYPE],
},
},
};
expect(getUiCapability(caps)).toBeUndefined();
});
});

View file

@ -36,6 +36,7 @@ import {
RESOURCE_MIME_TYPE,
McpUiResourceMeta,
McpUiToolMeta,
McpUiClientCapabilities,
} from "../app.js";
import type {
BaseToolCallback,
@ -49,7 +50,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 };
@ -313,3 +317,58 @@ export function registerAppResource(
readCallback,
);
}
/**
* Extension identifier for MCP Apps capability negotiation.
*
* Used as the key in `extensions` to advertise MCP Apps support.
*/
export const EXTENSION_ID = "io.modelcontextprotocol/ui";
/**
* Get MCP Apps capability settings from client capabilities.
*
* 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
*
* @example Check for MCP Apps support in server initialization
* ```typescript
* import { getUiCapability, RESOURCE_MIME_TYPE, registerAppTool } from "@modelcontextprotocol/ext-apps/server";
*
* server.oninitialized = ({ 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" } },
* }, weatherHandler);
* } else {
* // Register text-only fallback
* server.registerTool("weather", {
* description: "Get weather as text",
* }, textWeatherHandler);
* }
* };
* ```
*/
export function getUiCapability(
clientCapabilities:
| (ClientCapabilities & { extensions?: Record<string, unknown> })
| null
| undefined,
): McpUiClientCapabilities | undefined {
if (!clientCapabilities) {
return undefined;
}
return clientCapabilities.extensions?.[EXTENSION_ID] as
| McpUiClientCapabilities
| undefined;
}