Skip to content
Merged
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
52 changes: 50 additions & 2 deletions specification/draft/apps.mdx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably also need to mention that the host MAY defer sending the context to the model, and it MAY dedupe identical ui/update-context calls.

Potentially we could add a boolean that says it replaces / purges any previously pending

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. How about SHOULD provide the context to the model in future turns?
  2. We always replace now

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And should this be ui/update-semantic-state?

Copy link
Collaborator Author

@idosal idosal Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think semantic-state isn't as self-documenting as model-context

Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,49 @@ Guest UI behavior:
* Guest UI SHOULD check `availableDisplayModes` in host context before requesting a mode change.
* Guest UI MUST handle the response mode differing from the requested mode.

`ui/update-model-context` - Update the model context

```typescript
// Request
{
jsonrpc: "2.0",
id: 3,
method: "ui/update-model-context",
params: {
content?: ContentBlock[],
structuredContent?: Record<string, unknown>
}
}

// Success Response
{
jsonrpc: "2.0",
id: 3,
result: {} // Empty result on success
}

// Error Response (if denied or failed)
{
jsonrpc: "2.0",
id: 3,
error: {
code: -32000, // Implementation-defined error
message: "Context update denied" | "Invalid content format"
}
}
```

Guest UI MAY send this request to update the Host's model context. This context will be used in future turns. Each request overwrites the previous context sent by the Guest UI.
This event serves a different use case from `notifications/message` (logging) and `ui/message` (which also trigger follow-ups).

Host behavior:
- SHOULD provide the context to the model in future turns
- MAY overwrite the previous model context with the new update
- MAY defer sending the context to the model until the next user message (including `ui/message`)
- MAY dedupe identical `ui/update-model-context` calls
- If multiple updates are received before the next user message, Host SHOULD only send the last update to the model
- MAY display context updates to the user

#### Notifications (Host → UI)

`ui/notifications/tool-input` - Host MUST send this notification with the complete tool arguments after the Guest UI's initialize request completes.
Expand Down Expand Up @@ -1222,10 +1265,15 @@ sequenceDiagram
H-->>UI: ui/notifications/tool-result
else Message
UI ->> H: ui/message
H -->> UI: ui/message response
H -->> H: Process message and follow up
else Notify
else Context update
UI ->> H: ui/update-model-context
H ->> H: Store model context (overwrite existing)
H -->> UI: ui/update-model-context response
else Log
UI ->> H: notifications/message
H ->> H: Process notification and store in context
H ->> H: Record log for debugging/telemetry
else Resource read
UI ->> H: resources/read
H ->> S: resources/read
Expand Down
76 changes: 76 additions & 0 deletions src/app-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,82 @@ describe("App <-> AppBridge integration", () => {
logger: "TestApp",
});
});

it("app.updateModelContext triggers bridge.onupdatemodelcontext and returns result", async () => {
const receivedContexts: unknown[] = [];
bridge.onupdatemodelcontext = async (params) => {
receivedContexts.push(params);
return {};
};

await app.connect(appTransport);
const result = await app.updateModelContext({
content: [{ type: "text", text: "User selected 3 items" }],
});

expect(receivedContexts).toHaveLength(1);
expect(receivedContexts[0]).toMatchObject({
content: [{ type: "text", text: "User selected 3 items" }],
});
expect(result).toEqual({});
});

it("app.updateModelContext works with multiple content blocks", async () => {
const receivedContexts: unknown[] = [];
bridge.onupdatemodelcontext = async (params) => {
receivedContexts.push(params);
return {};
};

await app.connect(appTransport);
const result = await app.updateModelContext({
content: [
{ type: "text", text: "Filter applied" },
{ type: "text", text: "Category: electronics" },
],
});

expect(receivedContexts).toHaveLength(1);
expect(receivedContexts[0]).toMatchObject({
content: [
{ type: "text", text: "Filter applied" },
{ type: "text", text: "Category: electronics" },
],
});
expect(result).toEqual({});
});

it("app.updateModelContext works with structuredContent", async () => {
const receivedContexts: unknown[] = [];
bridge.onupdatemodelcontext = async (params) => {
receivedContexts.push(params);
return {};
};

await app.connect(appTransport);
const result = await app.updateModelContext({
structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" },
});

expect(receivedContexts).toHaveLength(1);
expect(receivedContexts[0]).toMatchObject({
structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" },
});
expect(result).toEqual({});
});

it("app.updateModelContext throws when handler throws", async () => {
bridge.onupdatemodelcontext = async () => {
throw new Error("Context update failed");
};

await app.connect(appTransport);
await expect(
app.updateModelContext({
content: [{ type: "text", text: "Test" }],
}),
).rejects.toThrow("Context update failed");
});
});

describe("App -> Host requests", () => {
Expand Down
46 changes: 45 additions & 1 deletion src/app-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CallToolRequestSchema,
CallToolResult,
CallToolResultSchema,
EmptyResult,
Implementation,
ListPromptsRequest,
ListPromptsRequestSchema,
Expand Down Expand Up @@ -51,6 +52,8 @@ import {
type McpUiToolResultNotification,
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
McpUiUpdateModelContextRequest,
McpUiUpdateModelContextRequestSchema,
McpUiHostCapabilities,
McpUiHostContext,
McpUiHostContextChangedNotification,
Expand All @@ -66,7 +69,6 @@ import {
McpUiOpenLinkRequestSchema,
McpUiOpenLinkResult,
McpUiResourceTeardownRequest,
McpUiResourceTeardownResult,
McpUiResourceTeardownResultSchema,
McpUiSandboxProxyReadyNotification,
McpUiSandboxProxyReadyNotificationSchema,
Expand Down Expand Up @@ -633,6 +635,48 @@ export class AppBridge extends Protocol<
);
}

/**
* Register a handler for model context updates from the Guest UI.
*
* The Guest UI sends `ui/update-model-context` requests to update the Host's
* model context. Each request overwrites the previous context stored by the Guest UI.
* Unlike logging messages, context updates are intended to be available to
* the model in future turns. Unlike messages, context updates do not trigger follow-ups.
*
* The host will typically defer sending the context to the model until the
* next user message (including `ui/message`), and will only send the last
* update received.
*
* @example
* ```typescript
* bridge.onupdatemodelcontext = async ({ content, structuredContent }, extra) => {
* // Update the model context with the new snapshot
* modelContext = {
* type: "app_context",
* content,
* structuredContent,
* timestamp: Date.now()
* };
* return {};
* };
* ```
*
* @see {@link McpUiUpdateModelContextRequest} for the request type
*/
set onupdatemodelcontext(
callback: (
params: McpUiUpdateModelContextRequest["params"],
extra: RequestHandlerExtra,
) => Promise<EmptyResult>,
) {
this.setRequestHandler(
McpUiUpdateModelContextRequestSchema,
async (request, extra) => {
return callback(request.params, extra);
},
);
}

/**
* Register a handler for tool call requests from the Guest UI.
*
Expand Down
48 changes: 48 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CallToolRequestSchema,
CallToolResult,
CallToolResultSchema,
EmptyResultSchema,
Implementation,
ListToolsRequest,
ListToolsRequestSchema,
Expand All @@ -20,6 +21,7 @@ import { PostMessageTransport } from "./message-transport";
import {
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
McpUiUpdateModelContextRequest,
McpUiHostCapabilities,
McpUiHostContext,
McpUiHostContextChangedNotification,
Expand Down Expand Up @@ -809,6 +811,52 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
});
}

/**
* Update the host's model context with app state.
*
* Unlike `sendLog`, which is for debugging/telemetry, context updates
* are intended to be available to the model in future reasoning,
* without requiring a follow-up action (like `sendMessage`).
*
* The host will typically defer sending the context to the model until the
* next user message (including `ui/message`), and will only send the last
* update received. Each call overwrites any previous context update.
*
* @param params - Context content and/or structured content
* @param options - Request options (timeout, etc.)
*
* @throws {Error} If the host rejects the context update (e.g., unsupported content type)
*
* @example Update model context with current app state
* ```typescript
* await app.updateModelContext({
* content: [{ type: "text", text: "User selected 3 items totaling $150.00" }]
* });
* ```
*
* @example Update with structured content
* ```typescript
* await app.updateModelContext({
* structuredContent: { selectedItems: 3, total: 150.00, currency: "USD" }
* });
* ```
*
* @returns Promise that resolves when the context update is acknowledged
*/
updateModelContext(
params: McpUiUpdateModelContextRequest["params"],
options?: RequestOptions,
) {
return this.request(
<McpUiUpdateModelContextRequest>{
method: "ui/update-model-context",
params,
},
EmptyResultSchema,
options,
);
}

/**
* Request the host to open an external URL in the default browser.
*
Expand Down
Loading
Loading