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
57 changes: 57 additions & 0 deletions specification/draft/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,13 @@ Example:
Tools are associated with UI resources through the `_meta.ui` field:

```typescript
type McpUiToolPreload = "optional" | "disabled";

interface McpUiToolMeta {
/** URI of UI resource for rendering tool results */
resourceUri?: string;
/** Whether the Host may preload the UI resource. Default: "optional" */
preload?: McpUiToolPreload;
/**
* Who can access this tool. Default: ["model", "app"]
* - "model": Tool visible to and callable by the agent
Expand Down Expand Up @@ -402,6 +406,22 @@ Example (app-only tool, hidden from model):
}
```

Example (defer loading the View until the tool result is available):

```json
{
"name": "search_orders",
"description": "Search for matching orders",
"inputSchema": { "type": "object" },
"_meta": {
"ui": {
"resourceUri": "ui://orders/search-results",
"preload": "disabled"
}
}
}
```

#### Behavior:

- If `ui.resourceUri` is present and host supports MCP Apps, host renders tool results using the specified UI resource
Expand All @@ -411,6 +431,43 @@ Example (app-only tool, hidden from model):
- Host MAY prefetch and cache UI resource content for performance optimization
- Since UI resources are primarily discovered through tool metadata, Servers MAY omit UI-only resources from `resources/list` and `notifications/resources/list_changed`

#### Preloading:

Hosts that recognize the `preload` field apply the following behavior:

- `preload` defaults to `"optional"` if omitted
- `"optional"`: Host MAY fetch the UI resource and initialize or display its View before tool execution completes
- `"disabled"`: Host MUST wait for the tool result before fetching the UI resource for that invocation or initializing or displaying its View
- `"disabled"` does not require a Host to evict a UI resource that is already cached from another invocation
- After receiving a result without `_meta["ui/close"]: true`, Host MAY load and display the View normally
- Hosts that do not recognize `preload` MAY ignore it and preserve their existing behavior

#### Result-driven View closure:

A Server MAY indicate that the View declared for a tool is not needed for a particular invocation by setting `_meta["ui/close"]` on its `CallToolResult`:

```json
{
"content": [
{
"type": "text",
"text": "Found one matching order."
}
],
"_meta": {
"ui/close": true
}
}
```

Hosts that recognize the `ui/close` signal apply the following behavior:

- The signal applies only to the invocation that produced the result
- If its View has not been initialized or displayed, Host MUST suppress it
- If its View has been initialized or displayed, Host MUST initiate graceful teardown using `ui/resource-teardown` and SHOULD wait for a response before unmounting it
- The signal has no effect when it is omitted, set to `false`, or no View is associated with the invocation
- Hosts that do not recognize `ui/close` MAY ignore it; Servers SHOULD provide meaningful non-UI content for those Hosts

#### Visibility:

- `visibility` defaults to `["model", "app"]` if omitted
Expand Down
46 changes: 46 additions & 0 deletions src/app-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ import { LATEST_PROTOCOL_VERSION } from "./types";
import {
AppBridge,
buildAllowAttribute,
getToolUiPreload,
getToolUiResourceUri,
isToolVisibilityModelOnly,
isToolVisibilityAppOnly,
shouldCloseToolUi,
UI_CLOSE_META_KEY,
type McpUiHostCapabilities,
} from "./app-bridge";

Expand Down Expand Up @@ -2470,6 +2473,49 @@ describe("getToolUiResourceUri", () => {
});
});

describe("getToolUiPreload", () => {
it("defaults to optional when preload is omitted", () => {
expect(getToolUiPreload({})).toBe("optional");
expect(getToolUiPreload({ _meta: { ui: {} } })).toBe("optional");
});

it("returns an explicitly declared mode", () => {
expect(getToolUiPreload({ _meta: { ui: { preload: "optional" } } })).toBe(
"optional",
);
expect(getToolUiPreload({ _meta: { ui: { preload: "disabled" } } })).toBe(
"disabled",
);
});

it("ignores an unrecognized mode for forward compatibility", () => {
expect(getToolUiPreload({ _meta: { ui: { preload: "required" } } })).toBe(
"optional",
);
});
});

describe("shouldCloseToolUi", () => {
it("returns true for the ui/close signal", () => {
expect(shouldCloseToolUi({ _meta: { [UI_CLOSE_META_KEY]: true } })).toBe(
true,
);
});

it("returns false when the signal is absent or false", () => {
expect(shouldCloseToolUi({})).toBe(false);
expect(shouldCloseToolUi({ _meta: { [UI_CLOSE_META_KEY]: false } })).toBe(
false,
);
});

it("ignores non-boolean truthy values", () => {
expect(shouldCloseToolUi({ _meta: { [UI_CLOSE_META_KEY]: "true" } })).toBe(
false,
);
});
});

describe("isToolVisibilityModelOnly", () => {
describe("returns true", () => {
it("when visibility is exactly ['model']", () => {
Expand Down
37 changes: 35 additions & 2 deletions src/app-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,17 @@ import {
McpUiRequestDisplayModeRequestSchema,
McpUiRequestDisplayModeResult,
McpUiResourcePermissions,
McpUiToolPreload,
McpUiToolMeta,
McpUiToolResultMeta,
} from "./types";
export * from "./types";
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app";
import { RESOURCE_URI_META_KEY } from "./app";
export {
RESOURCE_URI_META_KEY,
RESOURCE_MIME_TYPE,
UI_CLOSE_META_KEY,
} from "./app";
import { RESOURCE_URI_META_KEY, UI_CLOSE_META_KEY } from "./app";
export { PostMessageTransport } from "./message-transport";

/**
Expand Down Expand Up @@ -140,6 +146,33 @@ export function getToolUiResourceUri(tool: Partial<Tool>): string | undefined {
return undefined;
}

/**
* Get a tool's UI preload mode.
*
* @param tool - Tool object with optional UI metadata
* @returns The declared preload mode, or `"optional"` when omitted or unrecognized
*/
export function getToolUiPreload(tool: Partial<Tool>): McpUiToolPreload {
const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined;
return uiMeta?.preload === "disabled" ? "disabled" : "optional";
}

/**
* Check whether a tool result requests closure of its associated View.
*
* The signal is scoped to the invocation that produced the result and is only
* active when `_meta["ui/close"]` is the literal boolean `true`.
*
* @param result - MCP tool execution result
* @returns True when the associated View should be suppressed or closed
*/
export function shouldCloseToolUi(
result: Pick<CallToolResult, "_meta">,
): boolean {
const meta = result._meta as McpUiToolResultMeta | undefined;
return meta?.[UI_CLOSE_META_KEY] === true;
}

/**
* Check if a tool is visible to the model only.
*
Expand Down
11 changes: 11 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ export {
*/
export const RESOURCE_URI_META_KEY = "ui/resourceUri";

/**
* Metadata key for suppressing or closing the View associated with a tool
* invocation.
*
* A server sets this key to `true` in `CallToolResult._meta` when the result can
* be presented without the tool's declared UI resource. Hosts should suppress
* a pending View or gracefully tear down an initialized View for that specific
* invocation.
*/
export const UI_CLOSE_META_KEY = "ui/close";

/**
* MIME type for MCP UI resources.
*
Expand Down
38 changes: 38 additions & 0 deletions src/generated/schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/generated/schema.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions src/generated/schema.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import {
RESOURCE_URI_META_KEY,
RESOURCE_MIME_TYPE,
UI_CLOSE_META_KEY,
McpUiResourceCsp,
McpUiResourceMeta,
McpUiToolMeta,
Expand All @@ -60,7 +61,7 @@ import type {
} from "@modelcontextprotocol/sdk/types.js";

// Re-exports for convenience
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, UI_CLOSE_META_KEY };
export type { ResourceMetadata, ToolCallback };

/**
Expand Down
Loading