Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions examples/basic-host/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -22,6 +22,7 @@ export interface ServerInfo {
name: string;
client: Client;
tools: Map<string, Tool>;
resources: Map<string, Resource>;
appHtmlCache: Map<string, string>;
}

Expand All @@ -36,7 +37,12 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
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<Client> {
Expand Down Expand Up @@ -128,14 +134,23 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes

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

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

// Try both _meta (spec) and meta (Python SDK quirk)
// Try both _meta (spec) and meta (Python SDK quirk) for content-level
const contentMeta = (content as any)._meta || (content as any).meta;
const csp = contentMeta?.ui?.csp;
const permissions = contentMeta?.ui?.permissions;

// Get listing-level metadata as fallback
const listingResource = serverInfo.resources.get(uri);
const listingMeta = (listingResource as any)?._meta;
log.info("Resource listing._meta:", listingMeta);

// Content-level takes precedence, fall back to listing-level
const uiMeta = contentMeta?.ui ?? listingMeta?.ui;
const csp = uiMeta?.csp;
const permissions = uiMeta?.permissions;

return { html, csp, permissions };
}
Expand Down
2 changes: 1 addition & 1 deletion examples/map-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function createServer(): McpServer {
);
return {
contents: [
// _meta must be on the content item, not the resource metadata
// CSP metadata on the content item takes precedence over listing-level _meta
{
uri: RESOURCE_URI,
mimeType: RESOURCE_MIME_TYPE,
Expand Down
32 changes: 27 additions & 5 deletions specification/draft/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,17 @@ The resource content is returned via `resources/read`:
}
```

#### Metadata Location

`UIResourceMeta` (CSP, permissions, domain, prefersBorder) may be provided on either or both:

- **`resources/list`:** On the resource entry's `_meta.ui` field. Useful as a static default that hosts can review at connection time.
- **`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.

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.

> **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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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';
Expand Down
28 changes: 24 additions & 4 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import type {
RegisteredTool,
ResourceMetadata,
ToolCallback,
ReadResourceCallback,
ReadResourceCallback as _ReadResourceCallback,
RegisteredResource,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import type {
Expand All @@ -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.
Expand Down Expand Up @@ -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?: {
/**
Expand Down Expand Up @@ -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<McpUiReadResourceResult>;
export type ReadResourceCallback = McpUiReadResourceCallback;

/**
* Register an app resource with the MCP server.
*
Expand Down Expand Up @@ -305,7 +325,7 @@ export function registerAppResource(
name: string,
uri: string,
config: McpUiAppResourceConfig,
readCallback: ReadResourceCallback,
readCallback: McpUiReadResourceCallback,
): RegisteredResource {
return server.registerResource(
name,
Expand Down
Loading