Skip to content

Commit b273279

Browse files
idosalochafik
andauthored
feat: add ui/update-model-context (#125)
--------- Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
1 parent 9acc52c commit b273279

9 files changed

Lines changed: 918 additions & 3 deletions

File tree

specification/draft/apps.mdx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,49 @@ Guest UI behavior:
979979
* Guest UI SHOULD check `availableDisplayModes` in host context before requesting a mode change.
980980
* Guest UI MUST handle the response mode differing from the requested mode.
981981

982+
`ui/update-model-context` - Update the model context
983+
984+
```typescript
985+
// Request
986+
{
987+
jsonrpc: "2.0",
988+
id: 3,
989+
method: "ui/update-model-context",
990+
params: {
991+
content?: ContentBlock[],
992+
structuredContent?: Record<string, unknown>
993+
}
994+
}
995+
996+
// Success Response
997+
{
998+
jsonrpc: "2.0",
999+
id: 3,
1000+
result: {} // Empty result on success
1001+
}
1002+
1003+
// Error Response (if denied or failed)
1004+
{
1005+
jsonrpc: "2.0",
1006+
id: 3,
1007+
error: {
1008+
code: -32000, // Implementation-defined error
1009+
message: "Context update denied" | "Invalid content format"
1010+
}
1011+
}
1012+
```
1013+
1014+
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.
1015+
This event serves a different use case from `notifications/message` (logging) and `ui/message` (which also trigger follow-ups).
1016+
1017+
Host behavior:
1018+
- SHOULD provide the context to the model in future turns
1019+
- MAY overwrite the previous model context with the new update
1020+
- MAY defer sending the context to the model until the next user message (including `ui/message`)
1021+
- MAY dedupe identical `ui/update-model-context` calls
1022+
- If multiple updates are received before the next user message, Host SHOULD only send the last update to the model
1023+
- MAY display context updates to the user
1024+
9821025
#### Notifications (Host → UI)
9831026

9841027
`ui/notifications/tool-input` - Host MUST send this notification with the complete tool arguments after the Guest UI's initialize request completes.
@@ -1222,10 +1265,15 @@ sequenceDiagram
12221265
H-->>UI: ui/notifications/tool-result
12231266
else Message
12241267
UI ->> H: ui/message
1268+
H -->> UI: ui/message response
12251269
H -->> H: Process message and follow up
1226-
else Notify
1270+
else Context update
1271+
UI ->> H: ui/update-model-context
1272+
H ->> H: Store model context (overwrite existing)
1273+
H -->> UI: ui/update-model-context response
1274+
else Log
12271275
UI ->> H: notifications/message
1228-
H ->> H: Process notification and store in context
1276+
H ->> H: Record log for debugging/telemetry
12291277
else Resource read
12301278
UI ->> H: resources/read
12311279
H ->> S: resources/read

src/app-bridge.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,82 @@ describe("App <-> AppBridge integration", () => {
442442
logger: "TestApp",
443443
});
444444
});
445+
446+
it("app.updateModelContext triggers bridge.onupdatemodelcontext and returns result", async () => {
447+
const receivedContexts: unknown[] = [];
448+
bridge.onupdatemodelcontext = async (params) => {
449+
receivedContexts.push(params);
450+
return {};
451+
};
452+
453+
await app.connect(appTransport);
454+
const result = await app.updateModelContext({
455+
content: [{ type: "text", text: "User selected 3 items" }],
456+
});
457+
458+
expect(receivedContexts).toHaveLength(1);
459+
expect(receivedContexts[0]).toMatchObject({
460+
content: [{ type: "text", text: "User selected 3 items" }],
461+
});
462+
expect(result).toEqual({});
463+
});
464+
465+
it("app.updateModelContext works with multiple content blocks", async () => {
466+
const receivedContexts: unknown[] = [];
467+
bridge.onupdatemodelcontext = async (params) => {
468+
receivedContexts.push(params);
469+
return {};
470+
};
471+
472+
await app.connect(appTransport);
473+
const result = await app.updateModelContext({
474+
content: [
475+
{ type: "text", text: "Filter applied" },
476+
{ type: "text", text: "Category: electronics" },
477+
],
478+
});
479+
480+
expect(receivedContexts).toHaveLength(1);
481+
expect(receivedContexts[0]).toMatchObject({
482+
content: [
483+
{ type: "text", text: "Filter applied" },
484+
{ type: "text", text: "Category: electronics" },
485+
],
486+
});
487+
expect(result).toEqual({});
488+
});
489+
490+
it("app.updateModelContext works with structuredContent", async () => {
491+
const receivedContexts: unknown[] = [];
492+
bridge.onupdatemodelcontext = async (params) => {
493+
receivedContexts.push(params);
494+
return {};
495+
};
496+
497+
await app.connect(appTransport);
498+
const result = await app.updateModelContext({
499+
structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" },
500+
});
501+
502+
expect(receivedContexts).toHaveLength(1);
503+
expect(receivedContexts[0]).toMatchObject({
504+
structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" },
505+
});
506+
expect(result).toEqual({});
507+
});
508+
509+
it("app.updateModelContext throws when handler throws", async () => {
510+
bridge.onupdatemodelcontext = async () => {
511+
throw new Error("Context update failed");
512+
};
513+
514+
await app.connect(appTransport);
515+
await expect(
516+
app.updateModelContext({
517+
content: [{ type: "text", text: "Test" }],
518+
}),
519+
).rejects.toThrow("Context update failed");
520+
});
445521
});
446522

447523
describe("App -> Host requests", () => {

src/app-bridge.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CallToolRequestSchema,
66
CallToolResult,
77
CallToolResultSchema,
8+
EmptyResult,
89
Implementation,
910
ListPromptsRequest,
1011
ListPromptsRequestSchema,
@@ -51,6 +52,8 @@ import {
5152
type McpUiToolResultNotification,
5253
LATEST_PROTOCOL_VERSION,
5354
McpUiAppCapabilities,
55+
McpUiUpdateModelContextRequest,
56+
McpUiUpdateModelContextRequestSchema,
5457
McpUiHostCapabilities,
5558
McpUiHostContext,
5659
McpUiHostContextChangedNotification,
@@ -66,7 +69,6 @@ import {
6669
McpUiOpenLinkRequestSchema,
6770
McpUiOpenLinkResult,
6871
McpUiResourceTeardownRequest,
69-
McpUiResourceTeardownResult,
7072
McpUiResourceTeardownResultSchema,
7173
McpUiSandboxProxyReadyNotification,
7274
McpUiSandboxProxyReadyNotificationSchema,
@@ -633,6 +635,48 @@ export class AppBridge extends Protocol<
633635
);
634636
}
635637

638+
/**
639+
* Register a handler for model context updates from the Guest UI.
640+
*
641+
* The Guest UI sends `ui/update-model-context` requests to update the Host's
642+
* model context. Each request overwrites the previous context stored by the Guest UI.
643+
* Unlike logging messages, context updates are intended to be available to
644+
* the model in future turns. Unlike messages, context updates do not trigger follow-ups.
645+
*
646+
* The host will typically defer sending the context to the model until the
647+
* next user message (including `ui/message`), and will only send the last
648+
* update received.
649+
*
650+
* @example
651+
* ```typescript
652+
* bridge.onupdatemodelcontext = async ({ content, structuredContent }, extra) => {
653+
* // Update the model context with the new snapshot
654+
* modelContext = {
655+
* type: "app_context",
656+
* content,
657+
* structuredContent,
658+
* timestamp: Date.now()
659+
* };
660+
* return {};
661+
* };
662+
* ```
663+
*
664+
* @see {@link McpUiUpdateModelContextRequest} for the request type
665+
*/
666+
set onupdatemodelcontext(
667+
callback: (
668+
params: McpUiUpdateModelContextRequest["params"],
669+
extra: RequestHandlerExtra,
670+
) => Promise<EmptyResult>,
671+
) {
672+
this.setRequestHandler(
673+
McpUiUpdateModelContextRequestSchema,
674+
async (request, extra) => {
675+
return callback(request.params, extra);
676+
},
677+
);
678+
}
679+
636680
/**
637681
* Register a handler for tool call requests from the Guest UI.
638682
*

src/app.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CallToolRequestSchema,
1010
CallToolResult,
1111
CallToolResultSchema,
12+
EmptyResultSchema,
1213
Implementation,
1314
ListToolsRequest,
1415
ListToolsRequestSchema,
@@ -20,6 +21,7 @@ import { PostMessageTransport } from "./message-transport";
2021
import {
2122
LATEST_PROTOCOL_VERSION,
2223
McpUiAppCapabilities,
24+
McpUiUpdateModelContextRequest,
2325
McpUiHostCapabilities,
2426
McpUiHostContext,
2527
McpUiHostContextChangedNotification,
@@ -809,6 +811,52 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
809811
});
810812
}
811813

814+
/**
815+
* Update the host's model context with app state.
816+
*
817+
* Unlike `sendLog`, which is for debugging/telemetry, context updates
818+
* are intended to be available to the model in future reasoning,
819+
* without requiring a follow-up action (like `sendMessage`).
820+
*
821+
* The host will typically defer sending the context to the model until the
822+
* next user message (including `ui/message`), and will only send the last
823+
* update received. Each call overwrites any previous context update.
824+
*
825+
* @param params - Context content and/or structured content
826+
* @param options - Request options (timeout, etc.)
827+
*
828+
* @throws {Error} If the host rejects the context update (e.g., unsupported content type)
829+
*
830+
* @example Update model context with current app state
831+
* ```typescript
832+
* await app.updateModelContext({
833+
* content: [{ type: "text", text: "User selected 3 items totaling $150.00" }]
834+
* });
835+
* ```
836+
*
837+
* @example Update with structured content
838+
* ```typescript
839+
* await app.updateModelContext({
840+
* structuredContent: { selectedItems: 3, total: 150.00, currency: "USD" }
841+
* });
842+
* ```
843+
*
844+
* @returns Promise that resolves when the context update is acknowledged
845+
*/
846+
updateModelContext(
847+
params: McpUiUpdateModelContextRequest["params"],
848+
options?: RequestOptions,
849+
) {
850+
return this.request(
851+
<McpUiUpdateModelContextRequest>{
852+
method: "ui/update-model-context",
853+
params,
854+
},
855+
EmptyResultSchema,
856+
options,
857+
);
858+
}
859+
812860
/**
813861
* Request the host to open an external URL in the default browser.
814862
*

0 commit comments

Comments
 (0)