Skip to content

Commit bcfffb6

Browse files
ochafikclaude
andauthored
Clarify spec and SDK: accept UIResourceMeta in both resources/list and resources/read (modelcontextprotocol#410)
* Clarify spec and SDK: accept UIResourceMeta in both resources/list and resources/read The spec defines UIResource with _meta.ui, implicitly as an extension of the Resource type (resources/list), but examples and practice place _meta.ui on TextResourceContents (resources/read). The registerAppResource helper accepts _meta.ui in its config (which ends up in resources/list), but examples only return said _meta.ui from resources/read — and current hosts might be just ignoring it. This change: - Adds a 'Metadata Location' subsection to the draft spec explicitly stating that _meta.ui is valid on both the resource listing entry AND the content item, with content-item taking precedence. - Updates the spec example to show _meta on the resource declaration (resources/list). - Updates the CSP construction example to reference both locations. - Clarifies McpUiAppResourceConfig JSDoc: _meta here appears in resources/list and serves as a listing-level fallback for when content items omit _meta.ui. - Fixes the map-server example comment that incorrectly stated _meta must only be on the content item. * Add McpUiReadResourceResult type for UI metadata in resources/read * basic-host: support UIResourceMeta fallback from resources/list --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2dd56c3 commit bcfffb6

4 files changed

Lines changed: 73 additions & 16 deletions

File tree

examples/basic-host/src/implementation.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNo
22
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
33
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
44
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5-
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
5+
import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
66
import { getTheme, onThemeChange } from "./theme";
77
import { HOST_STYLE_VARIABLES } from "./host-styles";
88

@@ -22,6 +22,7 @@ export interface ServerInfo {
2222
name: string;
2323
client: Client;
2424
tools: Map<string, Tool>;
25+
resources: Map<string, Resource>;
2526
appHtmlCache: Map<string, string>;
2627
}
2728

@@ -36,7 +37,12 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
3637
const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool]));
3738
log.info("Server tools:", Array.from(tools.keys()));
3839

39-
return { name, client, tools, appHtmlCache: new Map() };
40+
// Fetch resources for listing-level _meta.ui (fallback for content-level)
41+
const resourcesList = await client.listResources();
42+
const resources = new Map(resourcesList.resources.map((r) => [r.uri, r]));
43+
log.info("Server resources:", Array.from(resources.keys()));
44+
45+
return { name, client, tools, resources, appHtmlCache: new Map() };
4046
}
4147

4248
async function connectWithFallback(serverUrl: URL): Promise<Client> {
@@ -128,14 +134,23 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes
128134

129135
const html = "blob" in content ? atob(content.blob) : content.text;
130136

131-
// Extract CSP and permissions metadata from resource content._meta.ui (or content.meta for Python SDK)
137+
// Extract CSP and permissions metadata, preferring content-level (resources/read)
138+
// and falling back to listing-level (resources/list) per the spec
132139
log.info("Resource content keys:", Object.keys(content));
133140
log.info("Resource content._meta:", (content as any)._meta);
134141

135-
// Try both _meta (spec) and meta (Python SDK quirk)
142+
// Try both _meta (spec) and meta (Python SDK quirk) for content-level
136143
const contentMeta = (content as any)._meta || (content as any).meta;
137-
const csp = contentMeta?.ui?.csp;
138-
const permissions = contentMeta?.ui?.permissions;
144+
145+
// Get listing-level metadata as fallback
146+
const listingResource = serverInfo.resources.get(uri);
147+
const listingMeta = (listingResource as any)?._meta;
148+
log.info("Resource listing._meta:", listingMeta);
149+
150+
// Content-level takes precedence, fall back to listing-level
151+
const uiMeta = contentMeta?.ui ?? listingMeta?.ui;
152+
const csp = uiMeta?.csp;
153+
const permissions = uiMeta?.permissions;
139154

140155
return { html, csp, permissions };
141156
}

examples/map-server/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function createServer(): McpServer {
128128
);
129129
return {
130130
contents: [
131-
// _meta must be on the content item, not the resource metadata
131+
// CSP metadata on the content item takes precedence over listing-level _meta
132132
{
133133
uri: RESOURCE_URI,
134134
mimeType: RESOURCE_MIME_TYPE,

specification/draft/apps.mdx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,17 @@ The resource content is returned via `resources/read`:
260260
}
261261
```
262262

263+
#### Metadata Location
264+
265+
`UIResourceMeta` (CSP, permissions, domain, prefersBorder) may be provided on either or both:
266+
267+
- **`resources/list`:** On the resource entry's `_meta.ui` field. Useful as a static default that hosts can review at connection time.
268+
- **`resources/read`:** On each content item's `_meta.ui` field. Useful for per-response overrides or dynamic metadata that is only known at read time.
269+
270+
When `_meta.ui` is present on **both**, the content-item value takes precedence. Hosts MUST check both locations, preferring the content item and falling back to the listing entry.
271+
272+
> **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.
273+
263274
#### Content Requirements:
264275

265276
- URI MUST start with `ui://` scheme
@@ -287,15 +298,24 @@ The resource content is returned via `resources/read`:
287298
Example:
288299

289300
```json
290-
// Resource declaration
301+
// Resource declaration (resources/list) — static defaults for host review
291302
{
292303
"uri": "ui://weather-server/dashboard-template",
293304
"name": "weather_dashboard",
294305
"description": "Interactive weather dashboard view",
295-
"mimeType": "text/html;profile=mcp-app"
306+
"mimeType": "text/html;profile=mcp-app",
307+
"_meta": {
308+
"ui": {
309+
"csp": {
310+
"connectDomains": ["https://api.openweathermap.org"],
311+
"resourceDomains": ["https://cdn.jsdelivr.net"]
312+
},
313+
"prefersBorder": true
314+
}
315+
}
296316
}
297317

298-
// Resource content with metadata
318+
// Resource content (resources/read) — takes precedence when present
299319
{
300320
"contents": [{
301321
"uri": "ui://weather-server/dashboard-template",
@@ -1725,8 +1745,10 @@ Hosts MUST enforce Content Security Policies based on resource metadata.
17251745
**CSP Construction from Metadata:**
17261746

17271747
```typescript
1728-
const csp = resource._meta?.ui?.csp; // `resource` is extracted from the `contents` of the `resources/read` result
1729-
const permissions = resource._meta?.ui?.permissions;
1748+
// Prefer content-level _meta.ui (resources/read), fall back to listing-level (resources/list)
1749+
const uiMeta = resource._meta?.ui ?? listingResource._meta?.ui;
1750+
const csp = uiMeta?.csp;
1751+
const permissions = uiMeta?.permissions;
17301752

17311753
const cspValue = `
17321754
default-src 'none';

src/server/index.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import type {
4545
RegisteredTool,
4646
ResourceMetadata,
4747
ToolCallback,
48-
ReadResourceCallback,
48+
ReadResourceCallback as _ReadResourceCallback,
4949
RegisteredResource,
5050
} from "@modelcontextprotocol/sdk/server/mcp.js";
5151
import type {
@@ -54,12 +54,13 @@ import type {
5454
} from "@modelcontextprotocol/sdk/server/zod-compat.js";
5555
import type {
5656
ClientCapabilities,
57+
ReadResourceResult,
5758
ToolAnnotations,
5859
} from "@modelcontextprotocol/sdk/types.js";
5960

6061
// Re-exports for convenience
6162
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
62-
export type { ResourceMetadata, ToolCallback, ReadResourceCallback };
63+
export type { ResourceMetadata, ToolCallback };
6364

6465
/**
6566
* Base tool configuration matching the standard MCP server tool options.
@@ -111,12 +112,19 @@ export interface McpUiAppToolConfig extends ToolConfig {
111112
* Extends the base MCP SDK `ResourceMetadata` with optional UI metadata
112113
* for configuring security policies and rendering preferences.
113114
*
115+
* The `_meta.ui` field here is included in the `resources/list` response and serves as
116+
* a static default for hosts to review at connection time. When the `resources/read`
117+
* content item also includes `_meta.ui`, the content-item value takes precedence.
118+
*
114119
* @see {@link registerAppResource `registerAppResource`} for usage
115120
*/
116121
export interface McpUiAppResourceConfig extends ResourceMetadata {
117122
/**
118123
* Optional UI metadata for the resource.
119-
* Used to configure security policies (CSP) and rendering preferences.
124+
*
125+
* This appears on the resource entry in `resources/list` and acts as a listing-level
126+
* fallback. Individual content items returned by `resources/read` may include their
127+
* own `_meta.ui` which takes precedence over this value.
120128
*/
121129
_meta?: {
122130
/**
@@ -236,6 +244,18 @@ export function registerAppTool<
236244
return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb);
237245
}
238246

247+
export type McpUiReadResourceResult = ReadResourceResult & {
248+
_meta?: {
249+
ui?: McpUiResourceMeta;
250+
[key: string]: unknown;
251+
};
252+
};
253+
export type McpUiReadResourceCallback = (
254+
uri: URL,
255+
extra: Parameters<_ReadResourceCallback>[1],
256+
) => McpUiReadResourceResult | Promise<McpUiReadResourceResult>;
257+
export type ReadResourceCallback = McpUiReadResourceCallback;
258+
239259
/**
240260
* Register an app resource with the MCP server.
241261
*
@@ -354,7 +374,7 @@ export function registerAppResource(
354374
name: string,
355375
uri: string,
356376
config: McpUiAppResourceConfig,
357-
readCallback: ReadResourceCallback,
377+
readCallback: McpUiReadResourceCallback,
358378
): RegisteredResource {
359379
return server.registerResource(
360380
name,

0 commit comments

Comments
 (0)