@assistant-ui/react 0.12.28 → 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 (187) 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 -18
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +3 -0
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts.map +1 -1
  10. package/dist/client/SingleThreadList.js +3 -0
  11. package/dist/client/SingleThreadList.js.map +1 -1
  12. package/dist/context/providers/ThreadViewportProvider.d.ts.map +1 -1
  13. package/dist/context/providers/ThreadViewportProvider.js +2 -10
  14. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  15. package/dist/context/stores/ThreadViewport.d.ts +46 -4
  16. package/dist/context/stores/ThreadViewport.d.ts.map +1 -1
  17. package/dist/context/stores/ThreadViewport.js +51 -7
  18. package/dist/context/stores/ThreadViewport.js.map +1 -1
  19. package/dist/index.d.ts +5 -30
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -28
  22. package/dist/index.js.map +1 -1
  23. package/dist/legacy-runtime/cloud/auiV0.d.ts +10 -1
  24. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  25. package/dist/legacy-runtime/cloud/auiV0.js +21 -3
  26. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  27. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +1 -1
  28. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts.map +1 -1
  29. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +1 -1
  30. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js.map +1 -1
  31. package/dist/mcp-apps/McpAppRenderer.d.ts +28 -0
  32. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -0
  33. package/dist/mcp-apps/McpAppRenderer.js +115 -0
  34. package/dist/mcp-apps/McpAppRenderer.js.map +1 -0
  35. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +3 -0
  36. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -0
  37. package/dist/mcp-apps/McpAppsRemoteHost.js +27 -0
  38. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -0
  39. package/dist/mcp-apps/app-frame.d.ts +3 -0
  40. package/dist/mcp-apps/app-frame.d.ts.map +1 -0
  41. package/dist/mcp-apps/app-frame.js +203 -0
  42. package/dist/mcp-apps/app-frame.js.map +1 -0
  43. package/dist/mcp-apps/bridge.d.ts +18 -0
  44. package/dist/mcp-apps/bridge.d.ts.map +1 -0
  45. package/dist/mcp-apps/bridge.js +290 -0
  46. package/dist/mcp-apps/bridge.js.map +1 -0
  47. package/dist/mcp-apps/index.d.ts +4 -0
  48. package/dist/mcp-apps/index.d.ts.map +1 -0
  49. package/dist/mcp-apps/index.js +3 -0
  50. package/dist/mcp-apps/index.js.map +1 -0
  51. package/dist/mcp-apps/types.d.ts +144 -0
  52. package/dist/mcp-apps/types.d.ts.map +1 -0
  53. package/dist/mcp-apps/types.js +3 -0
  54. package/dist/mcp-apps/types.js.map +1 -0
  55. package/dist/mcp-apps/utils.d.ts +5 -0
  56. package/dist/mcp-apps/utils.d.ts.map +1 -0
  57. package/dist/mcp-apps/utils.js +10 -0
  58. package/dist/mcp-apps/utils.js.map +1 -0
  59. package/dist/primitives/composer/ComposerInput.d.ts +6 -0
  60. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  61. package/dist/primitives/composer/ComposerInput.js +28 -6
  62. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  63. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  64. package/dist/primitives/composer/trigger/TriggerPopover.js +17 -1
  65. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  66. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +33 -0
  67. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
  68. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +80 -11
  69. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  70. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  71. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +2 -1
  72. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  73. package/dist/primitives/message/MessageRoot.d.ts +6 -30
  74. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  75. package/dist/primitives/message/MessageRoot.js +68 -25
  76. package/dist/primitives/message/MessageRoot.js.map +1 -1
  77. package/dist/primitives/messagePart/useMessagePartSource.d.ts +22 -3
  78. package/dist/primitives/messagePart/useMessagePartSource.d.ts.map +1 -1
  79. package/dist/primitives/thread/ThreadViewport.d.ts +38 -0
  80. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  81. package/dist/primitives/thread/ThreadViewport.js +53 -5
  82. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  83. package/dist/primitives/thread/ThreadViewportFooter.d.ts +2 -1
  84. package/dist/primitives/thread/ThreadViewportFooter.d.ts.map +1 -1
  85. package/dist/primitives/thread/ThreadViewportFooter.js +2 -1
  86. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  87. package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.d.ts +22 -0
  88. package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.d.ts.map +1 -0
  89. package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.js +53 -0
  90. package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.js.map +1 -0
  91. package/dist/primitives/thread/topAnchor/createReserveObservers.d.ts +5 -0
  92. package/dist/primitives/thread/topAnchor/createReserveObservers.d.ts.map +1 -0
  93. package/dist/primitives/thread/topAnchor/createReserveObservers.js +38 -0
  94. package/dist/primitives/thread/topAnchor/createReserveObservers.js.map +1 -0
  95. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.d.ts +22 -0
  96. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.d.ts.map +1 -0
  97. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js +75 -0
  98. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js.map +1 -0
  99. package/dist/primitives/thread/topAnchor/topAnchorTurn.d.ts +15 -0
  100. package/dist/primitives/thread/topAnchor/topAnchorTurn.d.ts.map +1 -0
  101. package/dist/primitives/thread/topAnchor/topAnchorTurn.js +13 -0
  102. package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -0
  103. package/dist/primitives/thread/topAnchor/topAnchorUtils.d.ts +15 -0
  104. package/dist/primitives/thread/topAnchor/topAnchorUtils.d.ts.map +1 -0
  105. package/dist/primitives/thread/topAnchor/topAnchorUtils.js +51 -0
  106. package/dist/primitives/thread/topAnchor/topAnchorUtils.js.map +1 -0
  107. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.d.ts +7 -0
  108. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.d.ts.map +1 -0
  109. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +18 -0
  110. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -0
  111. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  112. package/dist/primitives/thread/useThreadViewportAutoScroll.js +13 -1
  113. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  114. package/dist/primitives/thread.d.ts +0 -1
  115. package/dist/primitives/thread.d.ts.map +1 -1
  116. package/dist/primitives/thread.js +0 -1
  117. package/dist/primitives/thread.js.map +1 -1
  118. package/dist/primitives/threadList/ThreadListLoadMore.d.ts +13 -0
  119. package/dist/primitives/threadList/ThreadListLoadMore.d.ts.map +1 -0
  120. package/dist/primitives/threadList/ThreadListLoadMore.js +11 -0
  121. package/dist/primitives/threadList/ThreadListLoadMore.js.map +1 -0
  122. package/dist/primitives/threadList.d.ts +1 -0
  123. package/dist/primitives/threadList.d.ts.map +1 -1
  124. package/dist/primitives/threadList.js +1 -0
  125. package/dist/primitives/threadList.js.map +1 -1
  126. package/dist/utils/hooks/useManagedRef.d.ts.map +1 -1
  127. package/dist/utils/hooks/useManagedRef.js +1 -0
  128. package/dist/utils/hooks/useManagedRef.js.map +1 -1
  129. package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
  130. package/dist/utils/hooks/useOnResizeContent.js +1 -2
  131. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  132. package/package.json +13 -13
  133. package/src/client/ExternalThread.ts +32 -19
  134. package/src/client/InMemoryThreadList.ts +3 -0
  135. package/src/client/SingleThreadList.ts +3 -0
  136. package/src/context/providers/ThreadViewportProvider.tsx +2 -12
  137. package/src/context/stores/ThreadViewport.ts +111 -11
  138. package/src/index.ts +20 -34
  139. package/src/legacy-runtime/cloud/auiV0.ts +37 -4
  140. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +1 -5
  141. package/src/mcp-apps/McpAppRenderer.tsx +215 -0
  142. package/src/mcp-apps/McpAppsRemoteHost.ts +52 -0
  143. package/src/mcp-apps/app-frame.tsx +280 -0
  144. package/src/mcp-apps/bridge.test.ts +391 -0
  145. package/src/mcp-apps/bridge.ts +435 -0
  146. package/src/mcp-apps/index.ts +16 -0
  147. package/src/mcp-apps/types.ts +158 -0
  148. package/src/mcp-apps/utils.ts +16 -0
  149. package/src/primitives/composer/ComposerInput.test.tsx +280 -0
  150. package/src/primitives/composer/ComposerInput.tsx +29 -6
  151. package/src/primitives/composer/trigger/TriggerPopover.tsx +21 -1
  152. package/src/primitives/composer/trigger/TriggerPopoverRootContext.test.tsx +152 -0
  153. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +134 -17
  154. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +236 -0
  155. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +2 -1
  156. package/src/primitives/message/MessageRoot.tsx +135 -57
  157. package/src/primitives/thread/ThreadViewport.tsx +95 -4
  158. package/src/primitives/thread/ThreadViewportFooter.tsx +2 -1
  159. package/src/primitives/thread/topAnchor/computeTopAnchorSlack.test.ts +131 -0
  160. package/src/primitives/thread/topAnchor/computeTopAnchorSlack.ts +94 -0
  161. package/src/primitives/thread/topAnchor/createReserveObservers.ts +50 -0
  162. package/src/primitives/thread/topAnchor/mountTopAnchorReserve.test.ts +131 -0
  163. package/src/primitives/thread/topAnchor/mountTopAnchorReserve.ts +127 -0
  164. package/src/primitives/thread/topAnchor/topAnchorTurn.test.ts +46 -0
  165. package/src/primitives/thread/topAnchor/topAnchorTurn.ts +30 -0
  166. package/src/primitives/thread/topAnchor/topAnchorUtils.ts +58 -0
  167. package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +19 -0
  168. package/src/primitives/thread/useThreadViewportAutoScroll.ts +15 -1
  169. package/src/primitives/thread.ts +0 -1
  170. package/src/primitives/threadList/ThreadListLoadMore.tsx +24 -0
  171. package/src/primitives/threadList.ts +1 -0
  172. package/src/tests/BaseComposerRuntimeCore.test.ts +4 -0
  173. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +138 -0
  174. package/src/tests/RemoteThreadListRuntime.deferredProvider.test.tsx +28 -17
  175. package/src/tests/auiV0Encode.test.ts +55 -0
  176. package/src/utils/hooks/useManagedRef.ts +1 -0
  177. package/src/utils/hooks/useOnResizeContent.ts +1 -2
  178. package/dist/legacy-runtime/runtime-cores/external-store/getExternalStoreMessage.d.ts +0 -3
  179. package/dist/legacy-runtime/runtime-cores/external-store/getExternalStoreMessage.d.ts.map +0 -1
  180. package/dist/legacy-runtime/runtime-cores/external-store/getExternalStoreMessage.js +0 -3
  181. package/dist/legacy-runtime/runtime-cores/external-store/getExternalStoreMessage.js.map +0 -1
  182. package/dist/primitives/thread/ThreadViewportSlack.d.ts +0 -20
  183. package/dist/primitives/thread/ThreadViewportSlack.d.ts.map +0 -1
  184. package/dist/primitives/thread/ThreadViewportSlack.js +0 -80
  185. package/dist/primitives/thread/ThreadViewportSlack.js.map +0 -1
  186. package/src/legacy-runtime/runtime-cores/external-store/getExternalStoreMessage.ts +0 -6
  187. package/src/primitives/thread/ThreadViewportSlack.tsx +0 -116
@@ -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
+ });