diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 434169a9..36ecee0d 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -2,7 +2,7 @@ import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNo import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js"; import { getTheme, onThemeChange } from "./theme"; import { HOST_STYLE_VARIABLES } from "./host-styles"; @@ -22,6 +22,7 @@ export interface ServerInfo { name: string; client: Client; tools: Map; + resources: Map; appHtmlCache: Map; } @@ -36,7 +37,12 @@ export async function connectToServer(serverUrl: URL): Promise { const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool])); log.info("Server tools:", Array.from(tools.keys())); - return { name, client, tools, appHtmlCache: new Map() }; + // Fetch resources for listing-level _meta.ui (fallback for content-level) + const resourcesList = await client.listResources(); + const resources = new Map(resourcesList.resources.map((r) => [r.uri, r])); + log.info("Server resources:", Array.from(resources.keys())); + + return { name, client, tools, resources, appHtmlCache: new Map() }; } async function connectWithFallback(serverUrl: URL): Promise { @@ -128,14 +134,23 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise **Server guidance:** Prefer placing `_meta.ui` on the content item in `resources/read`, especially when metadata is dynamic or varies per-response. Use the listing-level `_meta.ui` (via `registerAppResource` config) when metadata is static and you want hosts to be able to review security configuration at connection time without fetching the resource. + #### Content Requirements: - URI MUST start with `ui://` scheme @@ -287,15 +298,24 @@ The resource content is returned via `resources/read`: Example: ```json -// Resource declaration +// Resource declaration (resources/list) — static defaults for host review { "uri": "ui://weather-server/dashboard-template", "name": "weather_dashboard", "description": "Interactive weather dashboard view", - "mimeType": "text/html;profile=mcp-app" + "mimeType": "text/html;profile=mcp-app", + "_meta": { + "ui": { + "csp": { + "connectDomains": ["https://api.openweathermap.org"], + "resourceDomains": ["https://cdn.jsdelivr.net"] + }, + "prefersBorder": true + } + } } -// Resource content with metadata +// Resource content (resources/read) — takes precedence when present { "contents": [{ "uri": "ui://weather-server/dashboard-template", @@ -1725,8 +1745,10 @@ Hosts MUST enforce Content Security Policies based on resource metadata. **CSP Construction from Metadata:** ```typescript -const csp = resource._meta?.ui?.csp; // `resource` is extracted from the `contents` of the `resources/read` result -const permissions = resource._meta?.ui?.permissions; +// Prefer content-level _meta.ui (resources/read), fall back to listing-level (resources/list) +const uiMeta = resource._meta?.ui ?? listingResource._meta?.ui; +const csp = uiMeta?.csp; +const permissions = uiMeta?.permissions; const cspValue = ` default-src 'none'; diff --git a/src/server/index.ts b/src/server/index.ts index e6a0dc7c..66612365 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -44,7 +44,7 @@ import type { RegisteredTool, ResourceMetadata, ToolCallback, - ReadResourceCallback, + ReadResourceCallback as _ReadResourceCallback, RegisteredResource, } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { @@ -53,12 +53,13 @@ import type { } from "@modelcontextprotocol/sdk/server/zod-compat.js"; import type { ClientCapabilities, + ReadResourceResult, ToolAnnotations, } from "@modelcontextprotocol/sdk/types.js"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; -export type { ResourceMetadata, ToolCallback, ReadResourceCallback }; +export type { ResourceMetadata, ToolCallback }; /** * Base tool configuration matching the standard MCP server tool options. @@ -110,12 +111,19 @@ export interface McpUiAppToolConfig extends ToolConfig { * Extends the base MCP SDK `ResourceMetadata` with optional UI metadata * for configuring security policies and rendering preferences. * + * The `_meta.ui` field here is included in the `resources/list` response and serves as + * a static default for hosts to review at connection time. When the `resources/read` + * content item also includes `_meta.ui`, the content-item value takes precedence. + * * @see {@link registerAppResource `registerAppResource`} for usage */ export interface McpUiAppResourceConfig extends ResourceMetadata { /** * Optional UI metadata for the resource. - * Used to configure security policies (CSP) and rendering preferences. + * + * This appears on the resource entry in `resources/list` and acts as a listing-level + * fallback. Individual content items returned by `resources/read` may include their + * own `_meta.ui` which takes precedence over this value. */ _meta?: { /** @@ -235,6 +243,18 @@ export function registerAppTool< return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } +export type McpUiReadResourceResult = ReadResourceResult & { + _meta?: { + ui?: McpUiResourceMeta; + [key: string]: unknown; + }; +}; +export type McpUiReadResourceCallback = ( + uri: URL, + extra: Parameters<_ReadResourceCallback>[1], +) => McpUiReadResourceResult | Promise; +export type ReadResourceCallback = McpUiReadResourceCallback; + /** * Register an app resource with the MCP server. * @@ -305,7 +325,7 @@ export function registerAppResource( name: string, uri: string, config: McpUiAppResourceConfig, - readCallback: ReadResourceCallback, + readCallback: McpUiReadResourceCallback, ): RegisteredResource { return server.registerResource( name,