Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
50 changes: 47 additions & 3 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 @@ -379,7 +379,7 @@ UI iframes can use the following subset of standard MCP protocol messages:

**Notifications:**

- `notifications/message` - Log messages to host
- `notifications/message` - Log messages to host (for logging)

**Lifecycle:**

Expand Down Expand Up @@ -533,6 +533,45 @@ Host SHOULD open the URL in the user's default browser or a new tab.

Host SHOULD add the message to the conversation thread, preserving the specified role.

`ui/update-context` - Update the agent's conversation context

```typescript
// Request
{
jsonrpc: "2.0",
id: 3,
method: "ui/update-context",
params: {
role: "user",
content: ContentBlock[]
}
}

// 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 inform the agent about app state changes that should be stored in the conversation context for future reasoning. This event serves a different use case from `notifications/message` (logging) and `ui/message` (which also trigger followups).

Host behavior:
- SHOULD store the context update in the conversation context
- MAY display context updates to the user
- MAY filter or aggregate context updates

#### 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 @@ -764,10 +803,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-context
H ->> H: Store in conversation context
H -->> UI: ui/update-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
46 changes: 46 additions & 0 deletions src/app-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,52 @@ describe("App <-> AppBridge integration", () => {
logger: "TestApp",
});
});

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

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

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

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

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

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

describe("App -> Host requests", () => {
Expand Down
36 changes: 36 additions & 0 deletions src/app-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
type McpUiToolResultNotification,
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
McpUiUpdateContextRequest,
McpUiUpdateContextRequestSchema,
McpUiHostCapabilities,
McpUiHostContext,
McpUiHostContextChangedNotification,
Expand Down Expand Up @@ -502,6 +504,40 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
);
}

/**
* Register a handler for context updates from the Guest UI.
*
* The Guest UI sends `ui/update-context` requests to inform the agent
* about app state changes that should be stored in the conversation context for
* future reasoning. Unlike logging messages, context updates are intended to be
* available to the agent for decision making.
*
* @param callback - Handler that receives context update params
* - params.role - Message role (currently only "user")
* - params.content - Content blocks (text, image, etc.)
*
* @example
* ```typescript
* bridge.oncontext = ({ role, content }) => {
* // Store context update for agent reasoning
* conversationContext.push({
* type: "app_context",
* role,
* content,
* timestamp: Date.now()
* });
* };
* ```
*/
set oncontext(
callback: (params: McpUiUpdateContextRequest["params"]) => void,
) {
this.setRequestHandler(McpUiUpdateContextRequestSchema, async (request) => {
callback(request.params);
return {};
});
}

/**
* Verify that the guest supports the capability required for the given request method.
* @internal
Expand Down
36 changes: 36 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import {
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
McpUiUpdateContextRequest,
McpUiUpdateContextResultSchema,
McpUiHostCapabilities,
McpUiHostContextChangedNotification,
McpUiHostContextChangedNotificationSchema,
Expand Down Expand Up @@ -673,6 +675,40 @@ export class App extends Protocol<Request, Notification, Result> {
});
}

/**
* Send context updates to the host for storage in the agent's conversation context.
*
* Unlike `sendLog` which is for debugging/telemetry, context updates are intended
* to inform the agent about app state changes that should be available for future
* reasoning without requiring a follow-up action (i.e., a prompt).
*
* @param params - Context role and content (same structure as ui/message)
* @param options - Request options (timeout, etc.)
*
* @example Notify agent of significant state change
* ```typescript
* await app.sendContext({
* role: "user",
* content: [{ type: "text", text: "User selected 3 items totaling $150.00" }]
* });
* ```
*
* @returns Promise that resolves when the context update is acknowledged
*/
sendContext(
params: McpUiUpdateContextRequest["params"],
options?: RequestOptions,
) {
return this.request(
<McpUiUpdateContextRequest>{
method: "ui/update-context",
params,
},
McpUiUpdateContextResultSchema,
options,
);
}

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