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:
parent
1003c17c2c
commit
cfcbc9605f
2 changed files with 238 additions and 73 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue