Skip to content

Commit 782d780

Browse files
committed
refactor: improve ui/update-model-context API
- Rename sendUpdateModelContext → updateModelContext (avoid double verbs) - Remove role field from params (always user, adds no value) - Add structuredContent support for machine-readable context data - Use EmptyResult instead of {isError?: boolean} for result type - Errors now signaled via JSON-RPC error responses - Update capability to specify supported content types: text, image, audio, resource, resourceLink, structuredContent - Add spec guidance for deferred delivery semantics: - Host MAY defer until next user message - Host MAY dedupe identical updates - Only last update before user message is sent to model
1 parent a809478 commit 782d780

8 files changed

Lines changed: 506 additions & 825 deletions

File tree

specification/draft/apps.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -809,8 +809,8 @@ Guest UI behavior:
809809
id: 3,
810810
method: "ui/update-model-context",
811811
params: {
812-
role: "user",
813-
content: ContentBlock[]
812+
content?: ContentBlock[],
813+
structuredContent?: Record<string, unknown>
814814
}
815815
}
816816

@@ -836,8 +836,11 @@ Guest UI MAY send this request to update the Host's model context. This context
836836
This event serves a different use case from `notifications/message` (logging) and `ui/message` (which also trigger follow-ups).
837837

838838
Host behavior:
839-
- SHOULD store the context snapshot in the conversation context
839+
- SHOULD provide the context to the model in future turns
840840
- SHOULD overwrite the previous model context with the new update
841+
- MAY defer sending the context to the model until the next user message (including `ui/message`)
842+
- MAY dedupe identical `ui/update-model-context` calls
843+
- If multiple updates are received before the next user message, Host SHOULD only send the last update to the model
841844
- MAY display context updates to the user
842845

843846
#### Notifications (Host → UI)

src/app-bridge.test.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -440,37 +440,34 @@ describe("App <-> AppBridge integration", () => {
440440
});
441441
});
442442

443-
it("app.sendUpdateModelContext triggers bridge.onupdatemodelcontext and returns result", async () => {
443+
it("app.updateModelContext triggers bridge.onupdatemodelcontext and returns result", async () => {
444444
const receivedContexts: unknown[] = [];
445445
bridge.onupdatemodelcontext = async (params) => {
446446
receivedContexts.push(params);
447447
return {};
448448
};
449449

450450
await app.connect(appTransport);
451-
const result = await app.sendUpdateModelContext({
452-
role: "user",
451+
const result = await app.updateModelContext({
453452
content: [{ type: "text", text: "User selected 3 items" }],
454453
});
455454

456455
expect(receivedContexts).toHaveLength(1);
457456
expect(receivedContexts[0]).toMatchObject({
458-
role: "user",
459457
content: [{ type: "text", text: "User selected 3 items" }],
460458
});
461459
expect(result).toEqual({});
462460
});
463461

464-
it("app.sendUpdateModelContext works with multiple content blocks", async () => {
462+
it("app.updateModelContext works with multiple content blocks", async () => {
465463
const receivedContexts: unknown[] = [];
466464
bridge.onupdatemodelcontext = async (params) => {
467465
receivedContexts.push(params);
468466
return {};
469467
};
470468

471469
await app.connect(appTransport);
472-
const result = await app.sendUpdateModelContext({
473-
role: "user",
470+
const result = await app.updateModelContext({
474471
content: [
475472
{ type: "text", text: "Filter applied" },
476473
{ type: "text", text: "Category: electronics" },
@@ -479,7 +476,6 @@ describe("App <-> AppBridge integration", () => {
479476

480477
expect(receivedContexts).toHaveLength(1);
481478
expect(receivedContexts[0]).toMatchObject({
482-
role: "user",
483479
content: [
484480
{ type: "text", text: "Filter applied" },
485481
{ type: "text", text: "Category: electronics" },
@@ -488,18 +484,36 @@ describe("App <-> AppBridge integration", () => {
488484
expect(result).toEqual({});
489485
});
490486

491-
it("app.sendUpdateModelContext returns error result when handler indicates error", async () => {
492-
bridge.onupdatemodelcontext = async () => {
493-
return { isError: true };
487+
it("app.updateModelContext works with structuredContent", async () => {
488+
const receivedContexts: unknown[] = [];
489+
bridge.onupdatemodelcontext = async (params) => {
490+
receivedContexts.push(params);
491+
return {};
494492
};
495493

496494
await app.connect(appTransport);
497-
const result = await app.sendUpdateModelContext({
498-
role: "user",
499-
content: [{ type: "text", text: "Test" }],
495+
const result = await app.updateModelContext({
496+
structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" },
500497
});
501498

502-
expect(result.isError).toBe(true);
499+
expect(receivedContexts).toHaveLength(1);
500+
expect(receivedContexts[0]).toMatchObject({
501+
structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" },
502+
});
503+
expect(result).toEqual({});
504+
});
505+
506+
it("app.updateModelContext throws when handler throws", async () => {
507+
bridge.onupdatemodelcontext = async () => {
508+
throw new Error("Context update failed");
509+
};
510+
511+
await app.connect(appTransport);
512+
await expect(
513+
app.updateModelContext({
514+
content: [{ type: "text", text: "Test" }],
515+
}),
516+
).rejects.toThrow("Context update failed");
503517
});
504518
});
505519

src/app-bridge.ts

Lines changed: 16 additions & 18 deletions
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,
@@ -53,7 +54,6 @@ import {
5354
McpUiAppCapabilities,
5455
McpUiUpdateModelContextRequest,
5556
McpUiUpdateModelContextRequestSchema,
56-
McpUiUpdateModelContextResult,
5757
McpUiHostCapabilities,
5858
McpUiHostContext,
5959
McpUiHostContextChangedNotification,
@@ -641,35 +641,33 @@ export class AppBridge extends Protocol<
641641
* The Guest UI sends `ui/update-model-context` requests to update the Host's
642642
* model context. Each request overwrites the previous context stored by the Guest UI.
643643
* 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
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.
645649
*
646650
* @example
647651
* ```typescript
648-
* bridge.onupdatemodelcontext = async ({ role, content }, extra) => {
649-
* try {
650-
* // Update the model context with the new snapshot
651-
* modelContext = {
652-
* type: "app_context",
653-
* role,
654-
* content,
655-
* timestamp: Date.now()
656-
* };
657-
* return {};
658-
* } catch (err) {
659-
* // Handle error and signal failure to the app
660-
* return { isError: true };
661-
* }
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 {};
662661
* };
663662
* ```
664663
*
665664
* @see {@link McpUiUpdateModelContextRequest} for the request type
666-
* @see {@link McpUiUpdateModelContextResult} for the result type
667665
*/
668666
set onupdatemodelcontext(
669667
callback: (
670668
params: McpUiUpdateModelContextRequest["params"],
671669
extra: RequestHandlerExtra,
672-
) => Promise<McpUiUpdateModelContextResult>,
670+
) => Promise<EmptyResult>,
673671
) {
674672
this.setRequestHandler(
675673
McpUiUpdateModelContextRequestSchema,

src/app.ts

Lines changed: 20 additions & 8 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,
@@ -21,7 +22,6 @@ import {
2122
LATEST_PROTOCOL_VERSION,
2223
McpUiAppCapabilities,
2324
McpUiUpdateModelContextRequest,
24-
McpUiUpdateModelContextResultSchema,
2525
McpUiHostCapabilities,
2626
McpUiHostContext,
2727
McpUiHostContextChangedNotification,
@@ -812,26 +812,38 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
812812
}
813813

814814
/**
815-
* Send context updates to the host to be included in the agent's context.
815+
* Update the host's model context with app state.
816816
*
817817
* Unlike `sendLog`, which is for debugging/telemetry, context updates
818-
* are inteded to be available to the model in future reasoning,
818+
* are intended to be available to the model in future reasoning,
819819
* without requiring a follow-up action (like `sendMessage`).
820820
*
821-
* @param params - Context role and content (same structure as ui/message)
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
822826
* @param options - Request options (timeout, etc.)
823827
*
828+
* @throws {Error} If the host rejects the context update (e.g., unsupported content type)
829+
*
824830
* @example Update model context with current app state
825831
* ```typescript
826-
* await app.sendUpdateModelContext({
827-
* role: "user",
832+
* await app.updateModelContext({
828833
* content: [{ type: "text", text: "User selected 3 items totaling $150.00" }]
829834
* });
830835
* ```
831836
*
837+
* @example Update with structured content
838+
* ```typescript
839+
* await app.updateModelContext({
840+
* structuredContent: { selectedItems: 3, total: 150.00, currency: "USD" }
841+
* });
842+
* ```
843+
*
832844
* @returns Promise that resolves when the context update is acknowledged
833845
*/
834-
sendUpdateModelContext(
846+
updateModelContext(
835847
params: McpUiUpdateModelContextRequest["params"],
836848
options?: RequestOptions,
837849
) {
@@ -840,7 +852,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
840852
method: "ui/update-model-context",
841853
params,
842854
},
843-
McpUiUpdateModelContextResultSchema,
855+
EmptyResultSchema,
844856
options,
845857
);
846858
}

src/generated/schema.json

Lines changed: 82 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)