@assistant-ui/react-ag-ui 0.0.45 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { isRecord } from "@assistant-ui/core/internal";
2
+ import { isMcpAppUri } from "@assistant-ui/core";
3
+ //#region src/runtime/mcp-tool-result.ts
4
+ const parseMcpToolCallResult = (source, modelContent) => {
5
+ const structuredContent = isRecord(source.structuredContent) ? source.structuredContent : void 0;
6
+ const meta = isRecord(source._meta) ? source._meta : void 0;
7
+ if (!structuredContent && !meta) return void 0;
8
+ return {
9
+ content: modelContent ? [{
10
+ type: "text",
11
+ text: modelContent
12
+ }] : [],
13
+ ...structuredContent ? { structuredContent } : {},
14
+ ...meta ? { _meta: meta } : {},
15
+ ...typeof source.isError === "boolean" ? { isError: source.isError } : {}
16
+ };
17
+ };
18
+ const readMcpAppResourceUri = (meta) => {
19
+ if (!isRecord(meta)) return void 0;
20
+ const uri = meta["ui/resourceUri"];
21
+ return typeof uri === "string" && isMcpAppUri(uri) ? uri : void 0;
22
+ };
23
+ //#endregion
24
+ export { parseMcpToolCallResult, readMcpAppResourceUri };
25
+
26
+ //# sourceMappingURL=mcp-tool-result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-tool-result.js","names":[],"sources":["../../src/runtime/mcp-tool-result.ts"],"sourcesContent":["import { isRecord } from \"@assistant-ui/core/internal\";\nimport { isMcpAppUri } from \"@assistant-ui/core\";\n\nexport type McpToolCallResult = Record<string, unknown> & {\n content: unknown[];\n structuredContent?: Record<string, unknown>;\n _meta?: Record<string, unknown>;\n isError?: boolean;\n};\n\nexport const parseMcpToolCallResult = (\n source: Record<string, unknown>,\n modelContent: string,\n): McpToolCallResult | undefined => {\n const structuredContent = isRecord(source.structuredContent)\n ? source.structuredContent\n : undefined;\n const meta = isRecord(source._meta) ? source._meta : undefined;\n if (!structuredContent && !meta) return undefined;\n\n return {\n content: modelContent ? [{ type: \"text\", text: modelContent }] : [],\n ...(structuredContent ? { structuredContent } : {}),\n ...(meta ? { _meta: meta } : {}),\n ...(typeof source.isError === \"boolean\" ? { isError: source.isError } : {}),\n };\n};\n\n// _meta[\"ui/resourceUri\"] is the MCP-UI pointer react-ai-sdk reads off\n// CallToolResult._meta; only ui:// URIs identify an MCP Apps widget. A\n// structured carrier is deliberately not read until upstream AG-UI settles\n// one (agno-agi/agno#9087).\nexport const readMcpAppResourceUri = (meta: unknown): string | undefined => {\n if (!isRecord(meta)) return undefined;\n const uri = meta[\"ui/resourceUri\"];\n return typeof uri === \"string\" && isMcpAppUri(uri) ? uri : undefined;\n};\n"],"mappings":";;;AAUA,MAAa,0BACX,QACA,iBACkC;CAClC,MAAM,oBAAoB,SAAS,OAAO,iBAAiB,IACvD,OAAO,oBACP,KAAA;CACJ,MAAM,OAAO,SAAS,OAAO,KAAK,IAAI,OAAO,QAAQ,KAAA;CACrD,IAAI,CAAC,qBAAqB,CAAC,MAAM,OAAO,KAAA;CAExC,OAAO;EACL,SAAS,eAAe,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAa,CAAC,IAAI,CAAC;EAClE,GAAI,oBAAoB,EAAE,kBAAkB,IAAI,CAAC;EACjD,GAAI,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;EAC9B,GAAI,OAAO,OAAO,YAAY,YAAY,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;CAC3E;AACF;AAMA,MAAa,yBAAyB,SAAsC;CAC1E,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO,KAAA;CAC5B,MAAM,MAAM,KAAK;CACjB,OAAO,OAAO,QAAQ,YAAY,YAAY,GAAG,IAAI,MAAM,KAAA;AAC7D"}
@@ -1,4 +1,5 @@
1
1
  import { Logger } from "./logger.js";
2
+ import { McpToolCallResult } from "./mcp-tool-result.js";
2
3
  import { AttachmentAdapter, CreateAppendMessage, DictationAdapter, ExternalStoreSharedOptions, ExternalStoreThreadListAdapter, FeedbackAdapter, RealtimeVoiceAdapter, SpeechSynthesisAdapter, ThreadHistoryAdapter, ThreadMessage } from "@assistant-ui/core";
3
4
  import { AbstractAgent } from "@ag-ui/client";
4
5
  import { ReadonlyJSONValue } from "assistant-stream/utils";
@@ -160,6 +161,7 @@ type AgUiEvent = {
160
161
  toolCallId: string;
161
162
  content: string;
162
163
  role?: "tool";
164
+ mcpResult?: McpToolCallResult;
163
165
  } | {
164
166
  type: "ACTIVITY_SNAPSHOT";
165
167
  activityType: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/runtime/types.ts"],"mappings":";;;;;;;;;;;KAsBK;EACH,mBAAmB;EACnB,QAAQ;;;;;;EAMR;;KAGU,2BAA2B,KACrC;EAGA,qBAEM,qBACG,QAAQ,wBAAwB;;KAI/B;EACV,cAAc;EACd,SAAS;EACT,YAAY;EACZ,QAAQ;EACR,WAAW;EACX,UAAU;;;;EAIV,aAAa;;KAGH,wBAAwB;EAClC,OAAO;EACP,SAAS,QAAQ;EACjB;;;;;;;;;;;EAWA;EACA,WAAW,GAAG;EACd;EACA,WAAW;;KAGD;KAMA;EACV;EACA,QAAQ;EACR;EACA;EACA,iBAAiB;EACjB;EACA,WAAW;;KAGD;EACV;EACA;EACA;;KAGU;EACV,qBAAqB;EACrB,2BACE,oBAAoB,sBACjB;EACL,YACE,SAAS,qBACT,qBAAqB,sBAClB;EACL,OAAO;EACP,WACE,MACI,sBACE,MAAM,kCAAkC;;KAItC;EACN;;EACA;EAAmB,YAAY;;KAEzB;EACN;EAAqB;;EAErB;EACA;EACA,UAAU;;EAEV;EAAuB;;EACvB;EAAmB;EAAkB;;EACrC;EAA4B;;EAC5B;EAA8B;EAAoB;;EAClD;EAA0B;;EAC1B;EAA4B;;EAC5B;EAAwB;;EACxB;;EACA;EAAuC;;EACvC;;EACA;;EACA;EAAyB;;EACzB;EAAiC;;EACjC;EAAmC;EAAoB;;EACvD;EAA+B;;EAC/B;EAAuB;;EAEvB;EACA;EACA;EACA;;EAEA;EAAwB;EAAoB;;EAC5C;EAAuB;;EAEvB;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA,SAAS;;EAET;EAAa;EAAY;;EACzB;EAAgB;EAAc;;EAC9B;EAAwB;;EACxB;EAAqB;;EACrB;EAA2B"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/runtime/types.ts"],"mappings":";;;;;;;;;;;;KAuBK;EACH,mBAAmB;EACnB,QAAQ;;;;;;EAMR;;KAGU,2BAA2B,KACrC;EAGA,qBAEM,qBACG,QAAQ,wBAAwB;;KAI/B;EACV,cAAc;EACd,SAAS;EACT,YAAY;EACZ,QAAQ;EACR,WAAW;EACX,UAAU;;;;EAIV,aAAa;;KAGH,wBAAwB;EAClC,OAAO;EACP,SAAS,QAAQ;EACjB;;;;;;;;;;;EAWA;EACA,WAAW,GAAG;EACd;EACA,WAAW;;KAGD;KAMA;EACV;EACA,QAAQ;EACR;EACA;EACA,iBAAiB;EACjB;EACA,WAAW;;KAGD;EACV;EACA;EACA;;KAGU;EACV,qBAAqB;EACrB,2BACE,oBAAoB,sBACjB;EACL,YACE,SAAS,qBACT,qBAAqB,sBAClB;EACL,OAAO;EACP,WACE,MACI,sBACE,MAAM,kCAAkC;;KAItC;EACN;;EACA;EAAmB,YAAY;;KAEzB;EACN;EAAqB;;EAErB;EACA;EACA,UAAU;;EAEV;EAAuB;;EACvB;EAAmB;EAAkB;;EACrC;EAA4B;;EAC5B;EAA8B;EAAoB;;EAClD;EAA0B;;EAC1B;EAA4B;;EAC5B;EAAwB;;EACxB;;EACA;EAAuC;;EACvC;;EACA;;EACA;EAAyB;;EACzB;EAAiC;;EACjC;EAAmC;EAAoB;;EACvD;EAA+B;;EAC/B;EAAuB;;EAEvB;EACA;EACA;EACA;;EAEA;EAAwB;EAAoB;;EAC5C;EAAuB;;EAEvB;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA,YAAY;;EAGZ;EACA;EACA,SAAS;;EAET;EAAa;EAAY;;EACzB;EAAgB;EAAc;;EAC9B;EAAwB;;EACxB;EAAqB;;EACrB;EAA2B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-ag-ui",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "AG-UI protocol adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ag-ui",
@@ -30,9 +30,9 @@
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
32
  "@ag-ui/client": "^0.0.57",
33
- "@assistant-ui/core": "^0.2.21",
34
- "@assistant-ui/store": "^0.2.20",
35
- "assistant-stream": "^0.3.26",
33
+ "@assistant-ui/core": "^0.2.23",
34
+ "@assistant-ui/store": "^0.2.22",
35
+ "assistant-stream": "^0.3.28",
36
36
  "fast-json-patch": "^3.1.1"
37
37
  },
38
38
  "peerDependencies": {
@@ -46,10 +46,10 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^19.2.17",
49
- "react": "^19.2.7",
49
+ "react": "^19.2.8",
50
50
  "vitest": "^4.1.10",
51
51
  "zod": "^4.4.3",
52
- "@assistant-ui/x-buildutils": "0.0.18"
52
+ "@assistant-ui/x-buildutils": "0.0.20"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public",
@@ -1,6 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { generateId, fromThreadMessageLike } from "@assistant-ui/core";
3
+ import {
4
+ generateId,
5
+ fromThreadMessageLike,
6
+ isMcpAppUri,
7
+ } from "@assistant-ui/core";
4
8
  import type {
5
9
  AddToolResultOptions,
6
10
  AppendMessage,
@@ -19,11 +23,14 @@ import { MessageRepository } from "@assistant-ui/core/internal";
19
23
  import type { AbstractAgent } from "@ag-ui/client";
20
24
  import jsonpatch, { type Operation } from "fast-json-patch";
21
25
  import type { Logger } from "./logger";
26
+ import { readMcpAppResourceUri } from "./mcp-tool-result";
22
27
  import type { AgUiEvent, AgUiInterrupt, AgUiResumeEntry } from "./types";
23
28
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
24
29
  import {
25
30
  AG_UI_METADATA_NAMESPACE,
26
31
  type AgUiCustomMetadata,
32
+ isPlainObject,
33
+ MCP_APPS_ACTIVITY_TYPE,
27
34
  RunAggregator,
28
35
  tryParseJSON,
29
36
  } from "./adapter/run-aggregator";
@@ -425,25 +432,11 @@ export class AgUiThreadRuntimeCore {
425
432
  );
426
433
  }
427
434
 
428
- const responsesById = new Map<string, AgUiResumeEntry>();
429
- for (const entry of responses) {
430
- if (!entry || typeof entry.interruptId !== "string") {
431
- throw new Error(
432
- "[agui] submitInterruptResponses: every entry must have an interruptId",
433
- );
434
- }
435
- if (entry.status !== "resolved" && entry.status !== "cancelled") {
436
- throw new Error(
437
- `[agui] submitInterruptResponses: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
438
- );
439
- }
440
- if (responsesById.has(entry.interruptId)) {
441
- throw new Error(
442
- `[agui] submitInterruptResponses: duplicate response for interrupt ${entry.interruptId}`,
443
- );
444
- }
445
- responsesById.set(entry.interruptId, entry);
446
- }
435
+ const responsesById = this.collectInterruptResponses(
436
+ "submitInterruptResponses",
437
+ pending.interrupts,
438
+ responses,
439
+ );
447
440
 
448
441
  const openIds = pending.interrupts.map((i) => i.id);
449
442
  const missing = openIds.filter((id) => !responsesById.has(id));
@@ -452,13 +445,6 @@ export class AgUiThreadRuntimeCore {
452
445
  `[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`,
453
446
  );
454
447
  }
455
- const known = new Set(openIds);
456
- const unknownIds = [...responsesById.keys()].filter((id) => !known.has(id));
457
- if (unknownIds.length > 0) {
458
- throw new Error(
459
- `[agui] submitInterruptResponses: unknown interrupt ids: ${unknownIds.join(", ")}`,
460
- );
461
- }
462
448
 
463
449
  const now = Date.now();
464
450
  for (const interrupt of pending.interrupts) {
@@ -538,35 +524,51 @@ export class AgUiThreadRuntimeCore {
538
524
  interrupts: readonly AgUiInterrupt[],
539
525
  responses: readonly AgUiResumeEntry[] | undefined,
540
526
  ): AgUiResumeEntry[] {
541
- const openIds = interrupts.map((interrupt) => interrupt.id);
542
- const known = new Set(openIds);
527
+ const responsesById = this.collectInterruptResponses(
528
+ "steerAway",
529
+ interrupts,
530
+ responses ?? [],
531
+ );
532
+ return interrupts.map(
533
+ (interrupt) =>
534
+ responsesById.get(interrupt.id) ?? {
535
+ interruptId: interrupt.id,
536
+ status: "cancelled",
537
+ },
538
+ );
539
+ }
540
+
541
+ private collectInterruptResponses(
542
+ method: "submitInterruptResponses" | "steerAway",
543
+ interrupts: readonly AgUiInterrupt[],
544
+ responses: readonly AgUiResumeEntry[],
545
+ ): Map<string, AgUiResumeEntry> {
546
+ const known = new Set(interrupts.map((interrupt) => interrupt.id));
543
547
  const responsesById = new Map<string, AgUiResumeEntry>();
544
- for (const entry of responses ?? []) {
548
+ for (const entry of responses) {
545
549
  if (!entry || typeof entry.interruptId !== "string") {
546
550
  throw new Error(
547
- "[agui] steerAway: every response must have an interruptId",
551
+ `[agui] ${method}: every entry must have an interruptId`,
548
552
  );
549
553
  }
550
554
  if (entry.status !== "resolved" && entry.status !== "cancelled") {
551
555
  throw new Error(
552
- `[agui] steerAway: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
556
+ `[agui] ${method}: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
553
557
  );
554
558
  }
555
559
  if (!known.has(entry.interruptId)) {
556
560
  throw new Error(
557
- `[agui] steerAway: unknown interrupt id ${entry.interruptId}`,
561
+ `[agui] ${method}: unknown interrupt id ${entry.interruptId}`,
558
562
  );
559
563
  }
560
564
  if (responsesById.has(entry.interruptId)) {
561
565
  throw new Error(
562
- `[agui] steerAway: duplicate response for interrupt ${entry.interruptId}`,
566
+ `[agui] ${method}: duplicate response for interrupt ${entry.interruptId}`,
563
567
  );
564
568
  }
565
569
  responsesById.set(entry.interruptId, entry);
566
570
  }
567
- return openIds.map(
568
- (id) => responsesById.get(id) ?? { interruptId: id, status: "cancelled" },
569
- );
571
+ return responsesById;
570
572
  }
571
573
 
572
574
  private toAppendMessage(message: CreateAppendMessage): AppendMessage {
@@ -1344,6 +1346,22 @@ export class AgUiThreadRuntimeCore {
1344
1346
  aggregator.handle(event);
1345
1347
  return;
1346
1348
  }
1349
+ case "ACTIVITY_SNAPSHOT": {
1350
+ const toolCallId = event.content["toolCallId"];
1351
+ if (
1352
+ event.activityType === MCP_APPS_ACTIVITY_TYPE &&
1353
+ typeof toolCallId === "string" &&
1354
+ !aggregator.hasToolCall(toolCallId)
1355
+ ) {
1356
+ const messageId = this.findMessageIdForToolCall(toolCallId);
1357
+ if (messageId !== undefined) {
1358
+ this.applyCrossRunActivitySnapshot(messageId, toolCallId, event);
1359
+ return;
1360
+ }
1361
+ }
1362
+ aggregator.handle(event);
1363
+ return;
1364
+ }
1347
1365
  default:
1348
1366
  aggregator.handle(event);
1349
1367
  }
@@ -1356,15 +1374,58 @@ export class AgUiThreadRuntimeCore {
1356
1374
  const updated = this.updateMessage(messageId, (message) => {
1357
1375
  if (message.role !== "assistant") return message;
1358
1376
  const assistant = message as ThreadAssistantMessage;
1377
+ const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
1359
1378
  let matchedToolCall = false;
1360
1379
  const content = assistant.content.map((part) => {
1361
1380
  if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId)
1362
1381
  return part;
1363
1382
  matchedToolCall = true;
1383
+ // An applied activity snapshot owns part.result; a later result only
1384
+ // fills what is missing, mirroring the aggregator's finishToolCall.
1385
+ // The aggregator's flag is set only when the snapshot carried a
1386
+ // result, so app presence alone is not enough: the current result
1387
+ // must be CallToolResult-shaped (a required content array).
1388
+ const snapshotResultApplied =
1389
+ part.mcp?.app !== undefined &&
1390
+ isPlainObject(part.result) &&
1391
+ Array.isArray((part.result as Record<string, unknown>)["content"]);
1392
+ if (snapshotResultApplied) {
1393
+ return {
1394
+ ...part,
1395
+ ...(part.modelContent === undefined && event.content
1396
+ ? {
1397
+ modelContent: [
1398
+ { type: "text" as const, text: event.content },
1399
+ ],
1400
+ }
1401
+ : {}),
1402
+ ...(part.isError === undefined
1403
+ ? typeof event.mcpResult?.isError === "boolean"
1404
+ ? { isError: event.mcpResult.isError }
1405
+ : event.role === "tool"
1406
+ ? { isError: false }
1407
+ : {}
1408
+ : {}),
1409
+ ...(event.messageId
1410
+ ? { unstable_toolMessageId: event.messageId }
1411
+ : {}),
1412
+ };
1413
+ }
1364
1414
  return {
1365
1415
  ...part,
1366
- result: tryParseJSON(event.content ?? "") as ReadonlyJSONValue,
1367
- ...(event.role === "tool" ? { isError: false } : {}),
1416
+ result: (event.mcpResult ??
1417
+ tryParseJSON(event.content ?? "")) as ReadonlyJSONValue,
1418
+ ...(event.mcpResult !== undefined
1419
+ ? { modelContent: [{ type: "text" as const, text: event.content }] }
1420
+ : {}),
1421
+ ...(typeof event.mcpResult?.isError === "boolean"
1422
+ ? { isError: event.mcpResult.isError }
1423
+ : event.role === "tool"
1424
+ ? { isError: false }
1425
+ : {}),
1426
+ ...(part.mcp === undefined && mcpAppUri !== undefined
1427
+ ? { mcp: { app: { resourceUri: mcpAppUri } } }
1428
+ : {}),
1368
1429
  ...(event.messageId
1369
1430
  ? { unstable_toolMessageId: event.messageId }
1370
1431
  : {}),
@@ -1381,6 +1442,68 @@ export class AgUiThreadRuntimeCore {
1381
1442
  this.maybeCompleteAfterToolResults(messageId);
1382
1443
  }
1383
1444
 
1445
+ private applyCrossRunActivitySnapshot(
1446
+ messageId: string,
1447
+ toolCallId: string,
1448
+ event: Extract<AgUiEvent, { type: "ACTIVITY_SNAPSHOT" }>,
1449
+ ): void {
1450
+ const resourceUri = event.content["resourceUri"];
1451
+ if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) return;
1452
+ const serverId = event.content["serverId"];
1453
+ const serverHash = event.content["serverHash"];
1454
+ const appServerId =
1455
+ typeof serverId === "string" && serverId.length > 0
1456
+ ? serverId
1457
+ : typeof serverHash === "string" && serverHash.length > 0
1458
+ ? serverHash
1459
+ : undefined;
1460
+ const result = event.content["result"];
1461
+ const updated = this.updateMessage(messageId, (message) => {
1462
+ if (message.role !== "assistant") return message;
1463
+ const assistant = message as ThreadAssistantMessage;
1464
+ let matchedToolCall = false;
1465
+ const content = assistant.content.map((part) => {
1466
+ if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
1467
+ return part;
1468
+ matchedToolCall = true;
1469
+ return {
1470
+ ...part,
1471
+ mcp: {
1472
+ app: {
1473
+ resourceUri,
1474
+ ...(appServerId ? { serverId: appServerId } : {}),
1475
+ },
1476
+ },
1477
+ ...(isPlainObject(result)
1478
+ ? {
1479
+ ...(part.result !== undefined && part.modelContent === undefined
1480
+ ? {
1481
+ modelContent: [
1482
+ {
1483
+ type: "text" as const,
1484
+ text:
1485
+ typeof part.result === "string"
1486
+ ? part.result
1487
+ : JSON.stringify(part.result),
1488
+ },
1489
+ ],
1490
+ }
1491
+ : {}),
1492
+ result: result as ReadonlyJSONValue,
1493
+ isError: result["isError"] === true,
1494
+ }
1495
+ : {}),
1496
+ };
1497
+ });
1498
+ if (!matchedToolCall) return message;
1499
+ return { ...assistant, content };
1500
+ });
1501
+
1502
+ if (!updated) return;
1503
+ this.notifyUpdate();
1504
+ this.maybeCompleteAfterToolResults(messageId);
1505
+ }
1506
+
1384
1507
  private importMessagesSnapshot(rawMessages: readonly unknown[]) {
1385
1508
  try {
1386
1509
  const normalized = fromAgUiMessages(rawMessages, {
@@ -3,9 +3,14 @@
3
3
  import type { InputContent } from "@ag-ui/client";
4
4
  import type {
5
5
  ThreadMessageLike as CoreThreadMessageLike,
6
+ ToolCallMessagePartMcpMetadata,
6
7
  ToolModelContentPart,
7
8
  } from "@assistant-ui/core";
8
- import { getAutoStatus } from "@assistant-ui/core/internal";
9
+ import {
10
+ getAutoStatus,
11
+ httpUrlPattern,
12
+ parseDataUrl,
13
+ } from "@assistant-ui/core/internal";
9
14
  import { type Tool, toToolsJSONSchema } from "assistant-stream";
10
15
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
11
16
  import {
@@ -13,6 +18,10 @@ import {
13
18
  type AgUiCustomMetadata,
14
19
  } from "./run-aggregator";
15
20
  import type { AgUiInterrupt } from "../types";
21
+ import {
22
+ parseMcpToolCallResult,
23
+ readMcpAppResourceUri,
24
+ } from "../mcp-tool-result";
16
25
 
17
26
  export type { InputContent };
18
27
 
@@ -64,6 +73,7 @@ type ToolCallPart = {
64
73
  isError?: boolean;
65
74
  modelContent?: readonly ToolModelContentPart[];
66
75
  unstable_toolMessageId?: string;
76
+ mcp?: ToolCallMessagePartMcpMetadata;
67
77
  };
68
78
 
69
79
  const isObject = (value: unknown): value is Record<string, unknown> =>
@@ -129,16 +139,6 @@ function extractText(content: unknown): string {
129
139
  .join("\n");
130
140
  }
131
141
 
132
- function parseDataUrl(
133
- value: string,
134
- ): { mimeType: string; data: string } | null {
135
- const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.+)$/);
136
- if (!match) return null;
137
- return { mimeType: match[1]!, data: match[2]! };
138
- }
139
-
140
- const httpUrlPattern = /^https?:\/\//i;
141
-
142
142
  type InputContentSource =
143
143
  | { type: "data"; value: string; mimeType: string }
144
144
  | { type: "url"; value: string; mimeType?: string };
@@ -540,12 +540,18 @@ export function fromAgUiMessages(
540
540
  if (role === "tool") {
541
541
  const toolCallId = getToolCallId(rawMessage) ?? `tool-${generateId()}`;
542
542
  const toolMessageId = getString(rawMessage, "id");
543
+ const modelContent = extractText(rawMessage.content);
544
+ const mcpResult = parseMcpToolCallResult(rawMessage, modelContent);
545
+ const mcpModelContent = mcpResult
546
+ ? [{ type: "text" as const, text: modelContent }]
547
+ : undefined;
543
548
  const result =
544
- rawMessage.result !== undefined
549
+ mcpResult ??
550
+ (rawMessage.result !== undefined
545
551
  ? rawMessage.result
546
552
  : typeof rawMessage.content === "string"
547
553
  ? parseJSONText(rawMessage.content)
548
- : rawMessage.content;
554
+ : rawMessage.content);
549
555
  const isError =
550
556
  typeof rawMessage.error === "string" ||
551
557
  rawMessage.isError === true ||
@@ -554,6 +560,9 @@ export function fromAgUiMessages(
554
560
  : rawMessage.isError === false
555
561
  ? false
556
562
  : undefined;
563
+ const mcpAppUri = readMcpAppResourceUri(mcpResult?._meta);
564
+ const mcpApp =
565
+ mcpAppUri !== undefined ? { resourceUri: mcpAppUri } : undefined;
557
566
 
558
567
  let updated = false;
559
568
  for (
@@ -581,10 +590,12 @@ export function fromAgUiMessages(
581
590
  const updatedPart: ToolCallPart = {
582
591
  ...(part as ToolCallPart),
583
592
  result,
593
+ ...(mcpModelContent ? { modelContent: mcpModelContent } : {}),
584
594
  ...(isError !== undefined ? { isError } : {}),
585
595
  ...(toolMessageId !== undefined
586
596
  ? { unstable_toolMessageId: toolMessageId }
587
597
  : {}),
598
+ ...(mcpApp ? { mcp: { app: mcpApp } } : {}),
588
599
  };
589
600
  const updatedContent = message.content.map((contentPart, index) =>
590
601
  index === partIndex ? updatedPart : contentPart,
@@ -615,10 +626,12 @@ export function fromAgUiMessages(
615
626
  args: {},
616
627
  argsText: "{}",
617
628
  result,
629
+ ...(mcpModelContent ? { modelContent: mcpModelContent } : {}),
618
630
  ...(isError !== undefined ? { isError } : {}),
619
631
  ...(toolMessageId !== undefined
620
632
  ? { unstable_toolMessageId: toolMessageId }
621
633
  : {}),
634
+ ...(mcpApp ? { mcp: { app: mcpApp } } : {}),
622
635
  },
623
636
  ],
624
637
  });
@@ -719,10 +732,9 @@ function convertAssistantMessage(
719
732
  for (const { id: toolCallId, part } of toolCalls) {
720
733
  if (part.result === undefined) continue;
721
734
 
722
- const modelText = extractText(part.modelContent);
723
735
  const resultContent =
724
- modelText.length > 0
725
- ? modelText
736
+ part.modelContent !== undefined
737
+ ? extractText(part.modelContent)
726
738
  : typeof part.result === "string"
727
739
  ? part.result
728
740
  : JSON.stringify(part.result);
@@ -8,6 +8,7 @@ import {
8
8
  type ToolCallMessagePart,
9
9
  type ToolModelContentPart,
10
10
  } from "@assistant-ui/core";
11
+ import { readMcpAppResourceUri } from "../mcp-tool-result";
11
12
  import type { AgUiEvent, AgUiInterrupt } from "../types";
12
13
  import type { Logger } from "../logger";
13
14
 
@@ -34,9 +35,11 @@ type ToolCallState = {
34
35
  snapshotResultApplied: boolean;
35
36
  };
36
37
 
37
- const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
38
+ export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
38
39
 
39
- const isPlainObject = (value: unknown): value is Record<string, unknown> =>
40
+ export const isPlainObject = (
41
+ value: unknown,
42
+ ): value is Record<string, unknown> =>
40
43
  !!value && typeof value === "object" && !Array.isArray(value);
41
44
 
42
45
  export type RunAggregatorOptions = {
@@ -235,8 +238,13 @@ export class RunAggregator {
235
238
  this.finishToolCall(
236
239
  event.toolCallId,
237
240
  event.content ?? "",
238
- event.role === "tool" ? false : undefined,
241
+ typeof event.mcpResult?.isError === "boolean"
242
+ ? event.mcpResult.isError
243
+ : event.role === "tool"
244
+ ? false
245
+ : undefined,
239
246
  event.messageId,
247
+ event.mcpResult,
240
248
  );
241
249
  this.emit();
242
250
  break;
@@ -442,6 +450,7 @@ export class RunAggregator {
442
450
  content: string,
443
451
  isError?: boolean,
444
452
  toolMessageId?: string,
453
+ mcpResult?: Record<string, unknown>,
445
454
  ) {
446
455
  if (!id) return;
447
456
  let entry = this.toolCalls.get(id);
@@ -464,7 +473,16 @@ export class RunAggregator {
464
473
  ) {
465
474
  this.partOrder.push({ kind: "tool-call", toolCallId: id });
466
475
  }
467
- if (entry.snapshotResultApplied) {
476
+ if (mcpResult !== undefined && !entry.snapshotResultApplied) {
477
+ entry.result = mcpResult;
478
+ entry.modelContent = [{ type: "text", text: content }];
479
+ entry.snapshotResultApplied = true;
480
+ entry.isError = isError;
481
+ if (entry.mcpAppResourceUri === undefined) {
482
+ const uri = readMcpAppResourceUri(mcpResult._meta);
483
+ if (uri !== undefined) entry.mcpAppResourceUri = uri;
484
+ }
485
+ } else if (entry.snapshotResultApplied) {
468
486
  if (entry.modelContent === undefined) {
469
487
  entry.modelContent = [{ type: "text", text: content }];
470
488
  }
@@ -1,5 +1,6 @@
1
1
  import type { AgUiEvent, AgUiInterrupt, AgUiRunFinishedOutcome } from "./types";
2
2
  import type { Logger } from "./logger";
3
+ import { parseMcpToolCallResult } from "./mcp-tool-result";
3
4
 
4
5
  export type ParseAgUiEventOptions = {
5
6
  logger?: Logger;
@@ -209,15 +210,17 @@ export const parseAgUiEvent = (
209
210
  case "TOOL_CALL_RESULT": {
210
211
  const toolCallId = getString("toolCallId");
211
212
  if (!toolCallId) return null;
213
+ const content = getString("content") ?? "";
212
214
  return withOptional(
213
215
  {
214
216
  type: "TOOL_CALL_RESULT" as const,
215
217
  toolCallId,
216
- content: getString("content") ?? "",
218
+ content,
217
219
  },
218
220
  {
219
221
  messageId: getString("messageId"),
220
222
  role: payload.role === "tool" ? "tool" : undefined,
223
+ mcpResult: parseMcpToolCallResult(payload, content),
221
224
  },
222
225
  );
223
226
  }
@@ -0,0 +1,37 @@
1
+ import { isRecord } from "@assistant-ui/core/internal";
2
+ import { isMcpAppUri } from "@assistant-ui/core";
3
+
4
+ export type McpToolCallResult = Record<string, unknown> & {
5
+ content: unknown[];
6
+ structuredContent?: Record<string, unknown>;
7
+ _meta?: Record<string, unknown>;
8
+ isError?: boolean;
9
+ };
10
+
11
+ export const parseMcpToolCallResult = (
12
+ source: Record<string, unknown>,
13
+ modelContent: string,
14
+ ): McpToolCallResult | undefined => {
15
+ const structuredContent = isRecord(source.structuredContent)
16
+ ? source.structuredContent
17
+ : undefined;
18
+ const meta = isRecord(source._meta) ? source._meta : undefined;
19
+ if (!structuredContent && !meta) return undefined;
20
+
21
+ return {
22
+ content: modelContent ? [{ type: "text", text: modelContent }] : [],
23
+ ...(structuredContent ? { structuredContent } : {}),
24
+ ...(meta ? { _meta: meta } : {}),
25
+ ...(typeof source.isError === "boolean" ? { isError: source.isError } : {}),
26
+ };
27
+ };
28
+
29
+ // _meta["ui/resourceUri"] is the MCP-UI pointer react-ai-sdk reads off
30
+ // CallToolResult._meta; only ui:// URIs identify an MCP Apps widget. A
31
+ // structured carrier is deliberately not read until upstream AG-UI settles
32
+ // one (agno-agi/agno#9087).
33
+ export const readMcpAppResourceUri = (meta: unknown): string | undefined => {
34
+ if (!isRecord(meta)) return undefined;
35
+ const uri = meta["ui/resourceUri"];
36
+ return typeof uri === "string" && isMcpAppUri(uri) ? uri : undefined;
37
+ };
@@ -13,6 +13,7 @@ import type {
13
13
  import type { AbstractAgent } from "@ag-ui/client";
14
14
  import type { Logger } from "./logger";
15
15
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
16
+ import type { McpToolCallResult } from "./mcp-tool-result";
16
17
 
17
18
  /**
18
19
  * @experimental This API is still under active development and might change without notice.
@@ -162,6 +163,7 @@ export type AgUiEvent =
162
163
  toolCallId: string;
163
164
  content: string;
164
165
  role?: "tool";
166
+ mcpResult?: McpToolCallResult;
165
167
  }
166
168
  | {
167
169
  type: "ACTIVITY_SNAPSHOT";