@assistant-ui/react 0.14.0 → 0.14.2

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.
Files changed (78) hide show
  1. package/README.md +58 -42
  2. package/dist/client/ExternalThread.d.ts +7 -0
  3. package/dist/client/ExternalThread.d.ts.map +1 -1
  4. package/dist/client/ExternalThread.js +24 -16
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/legacy-runtime/cloud/auiV0.d.ts +10 -1
  11. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  12. package/dist/legacy-runtime/cloud/auiV0.js +21 -3
  13. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  14. package/dist/mcp-apps/McpAppRenderer.d.ts +28 -0
  15. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -0
  16. package/dist/mcp-apps/McpAppRenderer.js +115 -0
  17. package/dist/mcp-apps/McpAppRenderer.js.map +1 -0
  18. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +3 -0
  19. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -0
  20. package/dist/mcp-apps/McpAppsRemoteHost.js +27 -0
  21. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -0
  22. package/dist/mcp-apps/app-frame.d.ts +3 -0
  23. package/dist/mcp-apps/app-frame.d.ts.map +1 -0
  24. package/dist/mcp-apps/app-frame.js +203 -0
  25. package/dist/mcp-apps/app-frame.js.map +1 -0
  26. package/dist/mcp-apps/bridge.d.ts +18 -0
  27. package/dist/mcp-apps/bridge.d.ts.map +1 -0
  28. package/dist/mcp-apps/bridge.js +290 -0
  29. package/dist/mcp-apps/bridge.js.map +1 -0
  30. package/dist/mcp-apps/index.d.ts +4 -0
  31. package/dist/mcp-apps/index.d.ts.map +1 -0
  32. package/dist/mcp-apps/index.js +3 -0
  33. package/dist/mcp-apps/index.js.map +1 -0
  34. package/dist/mcp-apps/types.d.ts +144 -0
  35. package/dist/mcp-apps/types.d.ts.map +1 -0
  36. package/dist/mcp-apps/types.js +3 -0
  37. package/dist/mcp-apps/types.js.map +1 -0
  38. package/dist/mcp-apps/utils.d.ts +5 -0
  39. package/dist/mcp-apps/utils.d.ts.map +1 -0
  40. package/dist/mcp-apps/utils.js +10 -0
  41. package/dist/mcp-apps/utils.js.map +1 -0
  42. package/dist/primitives/composer/ComposerInput.d.ts +6 -0
  43. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  44. package/dist/primitives/composer/ComposerInput.js +19 -2
  45. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  46. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  47. package/dist/primitives/composer/trigger/TriggerPopover.js +17 -1
  48. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  49. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +33 -0
  50. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
  51. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +80 -11
  52. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  53. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  54. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +2 -1
  55. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  56. package/dist/primitives/messagePart/useMessagePartSource.d.ts +22 -3
  57. package/dist/primitives/messagePart/useMessagePartSource.d.ts.map +1 -1
  58. package/package.json +6 -6
  59. package/src/client/ExternalThread.ts +32 -17
  60. package/src/index.ts +21 -0
  61. package/src/legacy-runtime/cloud/auiV0.ts +37 -4
  62. package/src/mcp-apps/McpAppRenderer.tsx +215 -0
  63. package/src/mcp-apps/McpAppsRemoteHost.ts +52 -0
  64. package/src/mcp-apps/app-frame.tsx +280 -0
  65. package/src/mcp-apps/bridge.test.ts +391 -0
  66. package/src/mcp-apps/bridge.ts +435 -0
  67. package/src/mcp-apps/index.ts +16 -0
  68. package/src/mcp-apps/types.ts +158 -0
  69. package/src/mcp-apps/utils.ts +16 -0
  70. package/src/primitives/composer/ComposerInput.test.tsx +48 -0
  71. package/src/primitives/composer/ComposerInput.tsx +20 -2
  72. package/src/primitives/composer/trigger/TriggerPopover.tsx +21 -1
  73. package/src/primitives/composer/trigger/TriggerPopoverRootContext.test.tsx +152 -0
  74. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +134 -17
  75. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +236 -0
  76. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +2 -1
  77. package/src/tests/BaseComposerRuntimeCore.test.ts +4 -0
  78. package/src/tests/auiV0Encode.test.ts +55 -0
@@ -0,0 +1,391 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { createMcpAppBridge, type McpAppBridgeFrame } from "./bridge";
4
+ import type {
5
+ McpAppJsonRpcMessage,
6
+ McpAppJsonRpcRequest,
7
+ McpAppJsonRpcResponse,
8
+ } from "./types";
9
+ import { MCP_APP_PROTOCOL_VERSION } from "./types";
10
+
11
+ type Captured = McpAppJsonRpcMessage;
12
+
13
+ function makeFrame() {
14
+ const captured: Captured[] = [];
15
+ const iframe = document.createElement("iframe");
16
+ document.body.appendChild(iframe);
17
+ const frame: McpAppBridgeFrame = {
18
+ iframe,
19
+ origin: "https://app.example",
20
+ sendMessage: (data) => {
21
+ captured.push(data as Captured);
22
+ },
23
+ };
24
+ return { frame, captured };
25
+ }
26
+
27
+ function dispatch(frame: McpAppBridgeFrame, message: McpAppJsonRpcMessage) {
28
+ const event = new MessageEvent("message", {
29
+ data: message,
30
+ origin: frame.origin,
31
+ source: frame.iframe.contentWindow,
32
+ });
33
+ window.dispatchEvent(event);
34
+ }
35
+
36
+ async function flush() {
37
+ await new Promise((r) => setTimeout(r, 0));
38
+ }
39
+
40
+ describe("createMcpAppBridge", () => {
41
+ it("responds to ui/initialize with host info, version, and capabilities", async () => {
42
+ const { frame, captured } = makeFrame();
43
+ const bridge = createMcpAppBridge({
44
+ frame,
45
+ hostInfo: { name: "test-host", version: "9.9.9" },
46
+ hostContext: { theme: "dark" },
47
+ handlers: {
48
+ callTool: vi.fn(),
49
+ sendMessage: vi.fn(),
50
+ },
51
+ });
52
+
53
+ const req: McpAppJsonRpcRequest = {
54
+ jsonrpc: "2.0",
55
+ id: 1,
56
+ method: "ui/initialize",
57
+ };
58
+ dispatch(frame, req);
59
+ await flush();
60
+
61
+ expect(captured).toHaveLength(1);
62
+ const res = captured[0] as McpAppJsonRpcResponse;
63
+ expect(res.id).toBe(1);
64
+ const result = res.result as Record<string, any>;
65
+ expect(result["protocolVersion"]).toBe(MCP_APP_PROTOCOL_VERSION);
66
+ expect(result["host"]).toEqual({ name: "test-host", version: "9.9.9" });
67
+ expect(result["hostContext"]).toEqual({ theme: "dark" });
68
+ expect(result["capabilities"]["tools"]).toBeDefined();
69
+ expect(result["capabilities"]["ui"]["sendMessage"]).toBe(true);
70
+ expect(result["capabilities"]["ui"]["openLink"]).toBe(false);
71
+
72
+ bridge.dispose();
73
+ });
74
+
75
+ it("routes tools/call to handler", async () => {
76
+ const { frame, captured } = makeFrame();
77
+ const callTool = vi.fn().mockResolvedValue({ ok: true });
78
+ const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
79
+
80
+ dispatch(frame, {
81
+ jsonrpc: "2.0",
82
+ id: 7,
83
+ method: "tools/call",
84
+ params: { name: "search", arguments: { q: "hi" } },
85
+ });
86
+ await flush();
87
+
88
+ expect(callTool).toHaveBeenCalledWith({
89
+ name: "search",
90
+ arguments: { q: "hi" },
91
+ });
92
+ expect(captured[0]).toEqual({
93
+ jsonrpc: "2.0",
94
+ id: 7,
95
+ result: { ok: true },
96
+ });
97
+ bridge.dispose();
98
+ });
99
+
100
+ it("rejects tools/call for disallowed tool with -32602", async () => {
101
+ const { frame, captured } = makeFrame();
102
+ const callTool = vi.fn();
103
+ const bridge = createMcpAppBridge({
104
+ frame,
105
+ handlers: { callTool, allowedTools: ["search"] },
106
+ });
107
+
108
+ dispatch(frame, {
109
+ jsonrpc: "2.0",
110
+ id: 2,
111
+ method: "tools/call",
112
+ params: { name: "delete_everything" },
113
+ });
114
+ await flush();
115
+
116
+ expect(callTool).not.toHaveBeenCalled();
117
+ const res = captured[0] as McpAppJsonRpcResponse;
118
+ expect(res.error?.code).toBe(-32602);
119
+ bridge.dispose();
120
+ });
121
+
122
+ it("returns -32601 when no callTool handler", async () => {
123
+ const { frame, captured } = makeFrame();
124
+ const bridge = createMcpAppBridge({ frame });
125
+
126
+ dispatch(frame, {
127
+ jsonrpc: "2.0",
128
+ id: 3,
129
+ method: "tools/call",
130
+ params: { name: "x" },
131
+ });
132
+ await flush();
133
+
134
+ const res = captured[0] as McpAppJsonRpcResponse;
135
+ expect(res.error?.code).toBe(-32601);
136
+ bridge.dispose();
137
+ });
138
+
139
+ it("rejects tools/call with non-object arguments via -32602", async () => {
140
+ const { frame, captured } = makeFrame();
141
+ const callTool = vi.fn();
142
+ const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
143
+
144
+ dispatch(frame, {
145
+ jsonrpc: "2.0",
146
+ id: 11,
147
+ method: "tools/call",
148
+ params: { name: "x", arguments: "not-an-object" },
149
+ });
150
+ await flush();
151
+
152
+ expect(callTool).not.toHaveBeenCalled();
153
+ expect((captured[0] as McpAppJsonRpcResponse).error?.code).toBe(-32602);
154
+ bridge.dispose();
155
+ });
156
+
157
+ it("rejects requestDisplayMode with unknown mode via -32602", async () => {
158
+ const { frame, captured } = makeFrame();
159
+ const requestDisplayMode = vi.fn();
160
+ const bridge = createMcpAppBridge({
161
+ frame,
162
+ handlers: { requestDisplayMode },
163
+ });
164
+
165
+ dispatch(frame, {
166
+ jsonrpc: "2.0",
167
+ id: 13,
168
+ method: "requestDisplayMode",
169
+ params: { mode: "sidebar" },
170
+ });
171
+ await flush();
172
+
173
+ expect(requestDisplayMode).not.toHaveBeenCalled();
174
+ expect((captured[0] as McpAppJsonRpcResponse).error?.code).toBe(-32602);
175
+ bridge.dispose();
176
+ });
177
+
178
+ it("rejects openLink for non-http(s) schemes via -32602", async () => {
179
+ const { frame, captured } = makeFrame();
180
+ const openLink = vi.fn();
181
+ const bridge = createMcpAppBridge({ frame, handlers: { openLink } });
182
+
183
+ dispatch(frame, {
184
+ jsonrpc: "2.0",
185
+ id: 12,
186
+ method: "openLink",
187
+ params: { url: "javascript:alert(1)" },
188
+ });
189
+ await flush();
190
+
191
+ expect(openLink).not.toHaveBeenCalled();
192
+ expect((captured[0] as McpAppJsonRpcResponse).error?.code).toBe(-32602);
193
+ bridge.dispose();
194
+ });
195
+
196
+ it("invokes onSizeChange / onInitialized for notifications", () => {
197
+ const { frame } = makeFrame();
198
+ const onSizeChange = vi.fn();
199
+ const onInitialized = vi.fn();
200
+ const bridge = createMcpAppBridge({
201
+ frame,
202
+ handlers: { onSizeChange, onInitialized },
203
+ });
204
+
205
+ dispatch(frame, {
206
+ jsonrpc: "2.0",
207
+ method: "notifications/size_changed",
208
+ params: { width: 320, height: 240 },
209
+ });
210
+ dispatch(frame, {
211
+ jsonrpc: "2.0",
212
+ method: "notifications/initialized",
213
+ });
214
+
215
+ expect(onSizeChange).toHaveBeenCalledWith({ width: 320, height: 240 });
216
+ expect(onInitialized).toHaveBeenCalled();
217
+ bridge.dispose();
218
+ });
219
+
220
+ it("notifyToolInput / notifyToolResult / notifyHostContextChanged post correct notifications", () => {
221
+ const { frame, captured } = makeFrame();
222
+ const bridge = createMcpAppBridge({ frame });
223
+
224
+ bridge.notifyToolInput({ a: 1 });
225
+ bridge.notifyToolResult({ ok: 1 });
226
+ bridge.notifyHostContextChanged({ theme: "light" });
227
+
228
+ expect(captured).toEqual([
229
+ {
230
+ jsonrpc: "2.0",
231
+ method: "notifications/tools/call/input",
232
+ params: { input: { a: 1 } },
233
+ },
234
+ {
235
+ jsonrpc: "2.0",
236
+ method: "notifications/tools/call/result",
237
+ params: { result: { ok: 1 } },
238
+ },
239
+ {
240
+ jsonrpc: "2.0",
241
+ method: "notifications/host_context/changed",
242
+ params: { theme: "light" },
243
+ },
244
+ ]);
245
+ bridge.dispose();
246
+ });
247
+
248
+ it("routes resources/read and resources/list to handlers", async () => {
249
+ const { frame, captured } = makeFrame();
250
+ const readResource = vi.fn().mockResolvedValue({ contents: [] });
251
+ const listResources = vi.fn().mockResolvedValue({ resources: [] });
252
+ const bridge = createMcpAppBridge({
253
+ frame,
254
+ handlers: { readResource, listResources },
255
+ });
256
+
257
+ dispatch(frame, {
258
+ jsonrpc: "2.0",
259
+ id: 20,
260
+ method: "resources/read",
261
+ params: { uri: "ui://app/x" },
262
+ });
263
+ dispatch(frame, {
264
+ jsonrpc: "2.0",
265
+ id: 21,
266
+ method: "resources/list",
267
+ });
268
+ await flush();
269
+
270
+ expect(readResource).toHaveBeenCalledWith({ uri: "ui://app/x" });
271
+ expect(listResources).toHaveBeenCalled();
272
+ expect(captured.map((c) => (c as McpAppJsonRpcResponse).id)).toEqual([
273
+ 20, 21,
274
+ ]);
275
+ bridge.dispose();
276
+ });
277
+
278
+ it("returns -32601 for resources/read and resources/list when no handler", async () => {
279
+ const { frame, captured } = makeFrame();
280
+ const bridge = createMcpAppBridge({ frame });
281
+
282
+ dispatch(frame, {
283
+ jsonrpc: "2.0",
284
+ id: 22,
285
+ method: "resources/read",
286
+ params: { uri: "ui://x" },
287
+ });
288
+ dispatch(frame, { jsonrpc: "2.0", id: 23, method: "resources/list" });
289
+ await flush();
290
+
291
+ expect((captured[0] as McpAppJsonRpcResponse).error?.code).toBe(-32601);
292
+ expect((captured[1] as McpAppJsonRpcResponse).error?.code).toBe(-32601);
293
+ bridge.dispose();
294
+ });
295
+
296
+ it("routes sendMessage and updateModelContext to handlers", async () => {
297
+ const { frame, captured } = makeFrame();
298
+ const sendMessage = vi.fn().mockResolvedValue({ ok: true });
299
+ const updateModelContext = vi.fn().mockResolvedValue({ ok: true });
300
+ const bridge = createMcpAppBridge({
301
+ frame,
302
+ handlers: { sendMessage, updateModelContext },
303
+ });
304
+
305
+ dispatch(frame, {
306
+ jsonrpc: "2.0",
307
+ id: 30,
308
+ method: "sendMessage",
309
+ params: { text: "hi" },
310
+ });
311
+ dispatch(frame, {
312
+ jsonrpc: "2.0",
313
+ id: 31,
314
+ method: "updateModelContext",
315
+ params: { foo: "bar" },
316
+ });
317
+ await flush();
318
+
319
+ expect(sendMessage).toHaveBeenCalledWith({ text: "hi" });
320
+ expect(updateModelContext).toHaveBeenCalledWith({ foo: "bar" });
321
+ expect(captured.map((c) => (c as McpAppJsonRpcResponse).id)).toEqual([
322
+ 30, 31,
323
+ ]);
324
+ bridge.dispose();
325
+ });
326
+
327
+ it("invokes onLog / onError / onRequestTeardown for notifications", () => {
328
+ const { frame } = makeFrame();
329
+ const onLog = vi.fn();
330
+ const onError = vi.fn();
331
+ const onRequestTeardown = vi.fn();
332
+ const bridge = createMcpAppBridge({
333
+ frame,
334
+ handlers: { onLog, onError, onRequestTeardown },
335
+ });
336
+
337
+ dispatch(frame, {
338
+ jsonrpc: "2.0",
339
+ method: "notifications/log",
340
+ params: { level: "info", message: "hello" },
341
+ });
342
+ dispatch(frame, {
343
+ jsonrpc: "2.0",
344
+ method: "notifications/error",
345
+ params: { message: "kaboom" },
346
+ });
347
+ dispatch(frame, {
348
+ jsonrpc: "2.0",
349
+ method: "notifications/request_teardown",
350
+ params: { reason: "done" },
351
+ });
352
+
353
+ expect(onLog).toHaveBeenCalledWith({ level: "info", message: "hello" });
354
+ expect(onError).toHaveBeenCalled();
355
+ expect(onRequestTeardown).toHaveBeenCalledWith({ reason: "done" });
356
+ bridge.dispose();
357
+ });
358
+
359
+ it("ignores messages from wrong origin or wrong source", async () => {
360
+ const { frame, captured } = makeFrame();
361
+ const callTool = vi.fn();
362
+ const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
363
+
364
+ const msg: McpAppJsonRpcMessage = {
365
+ jsonrpc: "2.0",
366
+ id: 1,
367
+ method: "tools/call",
368
+ params: { name: "search" },
369
+ };
370
+
371
+ window.dispatchEvent(
372
+ new MessageEvent("message", {
373
+ data: msg,
374
+ origin: "https://attacker.example",
375
+ source: frame.iframe.contentWindow,
376
+ }),
377
+ );
378
+ window.dispatchEvent(
379
+ new MessageEvent("message", {
380
+ data: msg,
381
+ origin: frame.origin,
382
+ source: window,
383
+ }),
384
+ );
385
+ await flush();
386
+
387
+ expect(callTool).not.toHaveBeenCalled();
388
+ expect(captured).toHaveLength(0);
389
+ bridge.dispose();
390
+ });
391
+ });