@apollo/client-ai-apps 0.6.5 → 0.7.0

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 (258) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/CONTRIBUTING.md +195 -0
  3. package/README.md +74 -0
  4. package/dist/core/AbstractApolloClient.d.ts +33 -0
  5. package/dist/core/AbstractApolloClient.d.ts.map +1 -0
  6. package/dist/core/AbstractApolloClient.js +129 -0
  7. package/dist/core/AbstractApolloClient.js.map +1 -0
  8. package/dist/core/ApolloClient.d.ts +3 -7
  9. package/dist/core/ApolloClient.d.ts.map +1 -1
  10. package/dist/core/ApolloClient.js +5 -4
  11. package/dist/core/ApolloClient.js.map +1 -1
  12. package/dist/{mcp/core → core}/McpAppManager.d.ts +14 -10
  13. package/dist/core/McpAppManager.d.ts.map +1 -0
  14. package/dist/core/McpAppManager.js +56 -0
  15. package/dist/core/McpAppManager.js.map +1 -0
  16. package/dist/core/typeRegistration.d.ts +0 -14
  17. package/dist/core/typeRegistration.d.ts.map +1 -1
  18. package/dist/core/typeRegistration.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mcp.d.ts +0 -1
  23. package/dist/index.mcp.d.ts.map +1 -1
  24. package/dist/index.mcp.js +0 -1
  25. package/dist/index.mcp.js.map +1 -1
  26. package/dist/index.openai.d.ts +0 -1
  27. package/dist/index.openai.d.ts.map +1 -1
  28. package/dist/index.openai.js +0 -1
  29. package/dist/index.openai.js.map +1 -1
  30. package/dist/link/ToolCallLink.d.ts +6 -1
  31. package/dist/link/ToolCallLink.d.ts.map +1 -1
  32. package/dist/link/ToolCallLink.js +17 -4
  33. package/dist/link/ToolCallLink.js.map +1 -1
  34. package/dist/link/ToolHydrationLink.d.ts +21 -0
  35. package/dist/link/ToolHydrationLink.d.ts.map +1 -0
  36. package/dist/link/ToolHydrationLink.js +57 -0
  37. package/dist/link/ToolHydrationLink.js.map +1 -0
  38. package/dist/mcp/core/ApolloClient.d.ts +3 -20
  39. package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
  40. package/dist/mcp/core/ApolloClient.js +20 -101
  41. package/dist/mcp/core/ApolloClient.js.map +1 -1
  42. package/dist/mcp/index.d.ts +0 -1
  43. package/dist/mcp/index.d.ts.map +1 -1
  44. package/dist/mcp/index.js +0 -1
  45. package/dist/mcp/index.js.map +1 -1
  46. package/dist/openai/core/ApolloClient.d.ts +3 -20
  47. package/dist/openai/core/ApolloClient.d.ts.map +1 -1
  48. package/dist/openai/core/ApolloClient.js +36 -101
  49. package/dist/openai/core/ApolloClient.js.map +1 -1
  50. package/dist/openai/index.d.ts +0 -1
  51. package/dist/openai/index.d.ts.map +1 -1
  52. package/dist/openai/index.js +0 -1
  53. package/dist/openai/index.js.map +1 -1
  54. package/dist/openai/react/index.d.ts +0 -7
  55. package/dist/openai/react/index.d.ts.map +1 -1
  56. package/dist/openai/react/index.js +0 -7
  57. package/dist/openai/react/index.js.map +1 -1
  58. package/dist/react/ApolloProvider.d.ts.map +1 -1
  59. package/dist/react/ApolloProvider.js +1 -1
  60. package/dist/react/ApolloProvider.js.map +1 -1
  61. package/dist/{mcp/react/hooks → react}/createHydrationUtils.d.ts +1 -1
  62. package/dist/react/createHydrationUtils.d.ts.map +1 -0
  63. package/dist/{mcp/react/hooks → react}/createHydrationUtils.js +7 -9
  64. package/dist/react/createHydrationUtils.js.map +1 -0
  65. package/dist/react/hooks/internal/useApolloClient.d.ts +3 -0
  66. package/dist/react/hooks/internal/useApolloClient.d.ts.map +1 -0
  67. package/dist/{mcp/react/hooks → react/hooks/internal}/useApolloClient.js +3 -3
  68. package/dist/react/hooks/internal/useApolloClient.js.map +1 -0
  69. package/dist/react/hooks/useApp.d.ts.map +1 -0
  70. package/dist/react/hooks/useApp.js +5 -0
  71. package/dist/react/hooks/useApp.js.map +1 -0
  72. package/dist/react/hooks/useHostContext.d.ts.map +1 -0
  73. package/dist/{openai/react → react}/hooks/useHostContext.js +1 -1
  74. package/dist/react/hooks/useHostContext.js.map +1 -0
  75. package/dist/react/hooks/useToolInfo.d.ts +3 -0
  76. package/dist/react/hooks/useToolInfo.d.ts.map +1 -0
  77. package/dist/react/hooks/useToolInfo.js +5 -0
  78. package/dist/react/hooks/useToolInfo.js.map +1 -0
  79. package/dist/react/hooks/useToolMetadata.d.ts +2 -0
  80. package/dist/react/hooks/useToolMetadata.d.ts.map +1 -0
  81. package/dist/react/hooks/useToolMetadata.js +5 -0
  82. package/dist/react/hooks/useToolMetadata.js.map +1 -0
  83. package/dist/react/index.d.ts +5 -16
  84. package/dist/react/index.d.ts.map +1 -1
  85. package/dist/react/index.js +5 -19
  86. package/dist/react/index.js.map +1 -1
  87. package/dist/utilities/connectToHost.d.ts +3 -0
  88. package/dist/utilities/connectToHost.d.ts.map +1 -0
  89. package/dist/utilities/connectToHost.js +11 -0
  90. package/dist/utilities/connectToHost.js.map +1 -0
  91. package/dist/utilities/index.d.ts +1 -0
  92. package/dist/utilities/index.d.ts.map +1 -1
  93. package/dist/utilities/index.js +1 -0
  94. package/dist/utilities/index.js.map +1 -1
  95. package/package.json +5 -22
  96. package/src/core/AbstractApolloClient.ts +217 -0
  97. package/src/core/ApolloClient.ts +8 -10
  98. package/src/core/McpAppManager.ts +106 -0
  99. package/src/core/typeRegistration.ts +0 -15
  100. package/src/index.mcp.ts +0 -1
  101. package/src/index.openai.ts +0 -1
  102. package/src/index.ts +1 -6
  103. package/src/link/ToolCallLink.ts +27 -5
  104. package/src/link/ToolHydrationLink.ts +90 -0
  105. package/src/link/__tests__/ToolCallLink.test.ts +99 -0
  106. package/src/mcp/core/ApolloClient.ts +32 -170
  107. package/src/mcp/core/__tests__/ApolloClient.test.ts +398 -140
  108. package/src/mcp/index.ts +0 -1
  109. package/src/openai/core/ApolloClient.ts +48 -166
  110. package/src/openai/core/__tests__/ApolloClient.test.ts +680 -185
  111. package/src/openai/index.ts +0 -1
  112. package/src/openai/react/index.ts +0 -7
  113. package/src/react/ApolloProvider.tsx +1 -6
  114. package/src/react/__tests__/ApolloProvider/mcp.test.tsx +66 -29
  115. package/src/react/__tests__/ApolloProvider/openai.test.tsx +16 -41
  116. package/src/react/__tests__/createHydrationUtils.test.tsx +1260 -0
  117. package/src/{mcp/react/hooks → react}/createHydrationUtils.ts +7 -10
  118. package/src/react/hooks/__tests__/useApp.test.tsx +46 -0
  119. package/src/react/hooks/__tests__/useHostContext.test.tsx +99 -0
  120. package/src/react/hooks/__tests__/useToolInfo.test.tsx +98 -0
  121. package/src/react/hooks/__tests__/useToolMetadata.test.tsx +58 -0
  122. package/src/{mcp/react/hooks → react/hooks/internal}/useApolloClient.ts +3 -3
  123. package/src/{mcp/react → react}/hooks/useApp.ts +1 -1
  124. package/src/{openai/react → react}/hooks/useHostContext.ts +1 -1
  125. package/src/react/hooks/useToolInfo.ts +6 -0
  126. package/src/react/hooks/useToolMetadata.ts +5 -0
  127. package/src/react/index.ts +5 -36
  128. package/src/testing/internal/graphql/parseManifestOperation.ts +87 -0
  129. package/src/testing/internal/index.ts +3 -0
  130. package/src/testing/internal/matchers/index.ts +1 -0
  131. package/src/testing/internal/matchers/toEmitAnything.ts +43 -0
  132. package/src/testing/internal/matchers/types.ts +1 -0
  133. package/src/testing/internal/mcp/mockMcpHost.ts +25 -4
  134. package/src/testing/internal/tests/eachHostEnv.ts +22 -0
  135. package/src/testing/internal/utilities/createHostEnv.ts +117 -0
  136. package/src/utilities/connectToHost.ts +13 -0
  137. package/src/utilities/index.ts +1 -0
  138. package/tsconfig.vite.json +1 -1
  139. package/vitest.config.ts +13 -0
  140. package/dist/mcp/core/McpAppManager.d.ts.map +0 -1
  141. package/dist/mcp/core/McpAppManager.js +0 -88
  142. package/dist/mcp/core/McpAppManager.js.map +0 -1
  143. package/dist/mcp/link/ToolCallLink.d.ts +0 -28
  144. package/dist/mcp/link/ToolCallLink.d.ts.map +0 -1
  145. package/dist/mcp/link/ToolCallLink.js +0 -35
  146. package/dist/mcp/link/ToolCallLink.js.map +0 -1
  147. package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +0 -1
  148. package/dist/mcp/react/hooks/createHydrationUtils.js.map +0 -1
  149. package/dist/mcp/react/hooks/useApolloClient.d.ts +0 -3
  150. package/dist/mcp/react/hooks/useApolloClient.d.ts.map +0 -1
  151. package/dist/mcp/react/hooks/useApolloClient.js.map +0 -1
  152. package/dist/mcp/react/hooks/useApp.d.ts.map +0 -1
  153. package/dist/mcp/react/hooks/useApp.js +0 -5
  154. package/dist/mcp/react/hooks/useApp.js.map +0 -1
  155. package/dist/mcp/react/hooks/useHostContext.d.ts.map +0 -1
  156. package/dist/mcp/react/hooks/useHostContext.js +0 -7
  157. package/dist/mcp/react/hooks/useHostContext.js.map +0 -1
  158. package/dist/mcp/react/hooks/useToolInfo.d.ts +0 -3
  159. package/dist/mcp/react/hooks/useToolInfo.d.ts.map +0 -1
  160. package/dist/mcp/react/hooks/useToolInfo.js +0 -10
  161. package/dist/mcp/react/hooks/useToolInfo.js.map +0 -1
  162. package/dist/mcp/react/hooks/useToolInput.d.ts +0 -7
  163. package/dist/mcp/react/hooks/useToolInput.d.ts.map +0 -1
  164. package/dist/mcp/react/hooks/useToolInput.js +0 -9
  165. package/dist/mcp/react/hooks/useToolInput.js.map +0 -1
  166. package/dist/mcp/react/hooks/useToolMetadata.d.ts +0 -2
  167. package/dist/mcp/react/hooks/useToolMetadata.d.ts.map +0 -1
  168. package/dist/mcp/react/hooks/useToolMetadata.js +0 -5
  169. package/dist/mcp/react/hooks/useToolMetadata.js.map +0 -1
  170. package/dist/mcp/react/hooks/useToolName.d.ts +0 -7
  171. package/dist/mcp/react/hooks/useToolName.d.ts.map +0 -1
  172. package/dist/mcp/react/hooks/useToolName.js +0 -9
  173. package/dist/mcp/react/hooks/useToolName.js.map +0 -1
  174. package/dist/mcp/react/index.d.ts +0 -8
  175. package/dist/mcp/react/index.d.ts.map +0 -1
  176. package/dist/mcp/react/index.js +0 -8
  177. package/dist/mcp/react/index.js.map +0 -1
  178. package/dist/openai/core/McpAppManager.d.ts +0 -37
  179. package/dist/openai/core/McpAppManager.d.ts.map +0 -1
  180. package/dist/openai/core/McpAppManager.js +0 -97
  181. package/dist/openai/core/McpAppManager.js.map +0 -1
  182. package/dist/openai/link/ToolCallLink.d.ts +0 -28
  183. package/dist/openai/link/ToolCallLink.d.ts.map +0 -1
  184. package/dist/openai/link/ToolCallLink.js +0 -35
  185. package/dist/openai/link/ToolCallLink.js.map +0 -1
  186. package/dist/openai/react/hooks/createHydrationUtils.d.ts +0 -15
  187. package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +0 -1
  188. package/dist/openai/react/hooks/createHydrationUtils.js +0 -113
  189. package/dist/openai/react/hooks/createHydrationUtils.js.map +0 -1
  190. package/dist/openai/react/hooks/useApp.d.ts +0 -2
  191. package/dist/openai/react/hooks/useApp.d.ts.map +0 -1
  192. package/dist/openai/react/hooks/useApp.js +0 -5
  193. package/dist/openai/react/hooks/useApp.js.map +0 -1
  194. package/dist/openai/react/hooks/useHostContext.d.ts +0 -2
  195. package/dist/openai/react/hooks/useHostContext.d.ts.map +0 -1
  196. package/dist/openai/react/hooks/useHostContext.js.map +0 -1
  197. package/dist/openai/react/hooks/useToolInfo.d.ts +0 -3
  198. package/dist/openai/react/hooks/useToolInfo.d.ts.map +0 -1
  199. package/dist/openai/react/hooks/useToolInfo.js +0 -10
  200. package/dist/openai/react/hooks/useToolInfo.js.map +0 -1
  201. package/dist/openai/react/hooks/useToolInput.d.ts +0 -7
  202. package/dist/openai/react/hooks/useToolInput.d.ts.map +0 -1
  203. package/dist/openai/react/hooks/useToolInput.js +0 -9
  204. package/dist/openai/react/hooks/useToolInput.js.map +0 -1
  205. package/dist/openai/react/hooks/useToolMetadata.d.ts +0 -2
  206. package/dist/openai/react/hooks/useToolMetadata.d.ts.map +0 -1
  207. package/dist/openai/react/hooks/useToolMetadata.js +0 -5
  208. package/dist/openai/react/hooks/useToolMetadata.js.map +0 -1
  209. package/dist/openai/react/hooks/useToolName.d.ts +0 -7
  210. package/dist/openai/react/hooks/useToolName.d.ts.map +0 -1
  211. package/dist/openai/react/hooks/useToolName.js +0 -9
  212. package/dist/openai/react/hooks/useToolName.js.map +0 -1
  213. package/dist/react/index.mcp.d.ts +0 -3
  214. package/dist/react/index.mcp.d.ts.map +0 -1
  215. package/dist/react/index.mcp.js +0 -3
  216. package/dist/react/index.mcp.js.map +0 -1
  217. package/dist/react/index.openai.d.ts +0 -3
  218. package/dist/react/index.openai.d.ts.map +0 -1
  219. package/dist/react/index.openai.js +0 -3
  220. package/dist/react/index.openai.js.map +0 -1
  221. package/dist/react/missingHook.d.ts +0 -2
  222. package/dist/react/missingHook.d.ts.map +0 -1
  223. package/dist/react/missingHook.js +0 -6
  224. package/dist/react/missingHook.js.map +0 -1
  225. package/src/mcp/core/McpAppManager.ts +0 -136
  226. package/src/mcp/link/ToolCallLink.ts +0 -40
  227. package/src/mcp/link/__tests__/ToolCallLink.test.ts +0 -113
  228. package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1228
  229. package/src/mcp/react/hooks/__tests__/useApp.test.tsx +0 -46
  230. package/src/mcp/react/hooks/__tests__/useHostContext.test.tsx +0 -95
  231. package/src/mcp/react/hooks/__tests__/useToolInfo.test.tsx +0 -53
  232. package/src/mcp/react/hooks/__tests__/useToolInput.test.tsx +0 -50
  233. package/src/mcp/react/hooks/__tests__/useToolMetadata.test.tsx +0 -53
  234. package/src/mcp/react/hooks/__tests__/useToolName.test.tsx +0 -50
  235. package/src/mcp/react/hooks/useHostContext.ts +0 -14
  236. package/src/mcp/react/hooks/useToolInfo.ts +0 -13
  237. package/src/mcp/react/hooks/useToolInput.ts +0 -10
  238. package/src/mcp/react/hooks/useToolMetadata.ts +0 -5
  239. package/src/mcp/react/hooks/useToolName.ts +0 -10
  240. package/src/mcp/react/index.ts +0 -7
  241. package/src/openai/core/McpAppManager.ts +0 -148
  242. package/src/openai/link/ToolCallLink.ts +0 -40
  243. package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1333
  244. package/src/openai/react/hooks/__tests__/useToolInfo.test.tsx +0 -92
  245. package/src/openai/react/hooks/__tests__/useToolInput.test.tsx +0 -85
  246. package/src/openai/react/hooks/__tests__/useToolMetadata.test.tsx +0 -86
  247. package/src/openai/react/hooks/__tests__/useToolName.test.tsx +0 -50
  248. package/src/openai/react/hooks/createHydrationUtils.ts +0 -182
  249. package/src/openai/react/hooks/useApp.ts +0 -5
  250. package/src/openai/react/hooks/useToolInfo.ts +0 -13
  251. package/src/openai/react/hooks/useToolInput.ts +0 -10
  252. package/src/openai/react/hooks/useToolMetadata.ts +0 -5
  253. package/src/openai/react/hooks/useToolName.ts +0 -10
  254. package/src/react/index.mcp.ts +0 -10
  255. package/src/react/index.openai.ts +0 -10
  256. package/src/react/missingHook.ts +0 -9
  257. /package/dist/{mcp/react → react}/hooks/useApp.d.ts +0 -0
  258. /package/dist/{mcp/react → react}/hooks/useHostContext.d.ts +0 -0
@@ -1,14 +1,21 @@
1
1
  import { expect, test, describe, vi } from "vitest";
2
2
  import { ApolloClient } from "../ApolloClient.js";
3
- import { parse } from "graphql";
4
- import { ApolloLink, HttpLink, InMemoryCache, gql } from "@apollo/client";
5
- import { print } from "@apollo/client/utilities";
6
- import { ToolCallLink } from "../../link/ToolCallLink.js";
3
+ import { parse, type DocumentNode } from "graphql";
4
+ import {
5
+ ApolloLink,
6
+ HttpLink,
7
+ InMemoryCache,
8
+ NetworkStatus,
9
+ gql,
10
+ } from "@apollo/client";
11
+ import { ToolCallLink } from "../../../link/ToolCallLink.js";
7
12
  import {
8
13
  graphqlToolResult,
9
14
  minimalHostContextWithToolName,
10
15
  mockApplicationManifest,
11
16
  mockMcpHost,
17
+ ObservableStream,
18
+ parseManifestOperation,
12
19
  spyOnConsole,
13
20
  stubOpenAiGlobals,
14
21
  } from "../../../testing/internal/index.js";
@@ -152,19 +159,27 @@ describe("prefetchData", () => {
152
159
  test("caches tool response when data is provided", async () => {
153
160
  stubOpenAiGlobals({ toolInput: { id: 1 } });
154
161
  using _ = spyOnConsole("debug");
155
- const client = new ApolloClient({
156
- cache: new InMemoryCache(),
157
- manifest: mockApplicationManifest(),
158
- });
159
- using host = await mockMcpHost({
160
- hostContext: minimalHostContextWithToolName("GetProduct"),
161
- });
162
162
 
163
- host.onCleanup(() => client.stop());
163
+ const query = gql`
164
+ query Product($id: ID!)
165
+ @tool(name: "GetProduct", description: "Get a product") {
166
+ product(id: $id) {
167
+ id
168
+ title
169
+ rating
170
+ price
171
+ description
172
+ images
173
+ __typename
174
+ }
175
+ }
176
+ `;
177
+
178
+ const { client, host } = await setup({ query });
179
+ using _host = host;
164
180
 
165
181
  host.sendToolInput({ arguments: { id: 1 } });
166
182
  host.sendToolResult({
167
- content: [],
168
183
  structuredContent: {
169
184
  result: {
170
185
  data: {
@@ -184,25 +199,39 @@ describe("prefetchData", () => {
184
199
 
185
200
  await client.connect();
186
201
 
187
- expect(client.extract()).toMatchInlineSnapshot(`
188
- {
189
- "Product:1": {
190
- "__typename": "Product",
191
- "description": "Awesome pen",
192
- "id": "1",
193
- "images": [],
194
- "price": 1,
195
- "rating": 5,
196
- "title": "Pen",
202
+ await expect(
203
+ client.query({ query, variables: { id: 1 } })
204
+ ).resolves.toStrictEqual({
205
+ data: {
206
+ product: {
207
+ id: "1",
208
+ title: "Pen",
209
+ rating: 5,
210
+ price: 1.0,
211
+ description: "Awesome pen",
212
+ images: [],
213
+ __typename: "Product",
197
214
  },
198
- "ROOT_QUERY": {
199
- "__typename": "Query",
200
- "product({"id":1})": {
201
- "__ref": "Product:1",
202
- },
215
+ },
216
+ });
217
+
218
+ expect(client.extract()).toStrictEqual({
219
+ "Product:1": {
220
+ __typename: "Product",
221
+ description: "Awesome pen",
222
+ id: "1",
223
+ images: [],
224
+ price: 1,
225
+ rating: 5,
226
+ title: "Pen",
227
+ },
228
+ ROOT_QUERY: {
229
+ __typename: "Query",
230
+ 'product({"id":1})': {
231
+ __ref: "Product:1",
203
232
  },
204
- }
205
- `);
233
+ },
234
+ });
206
235
  });
207
236
 
208
237
  test("caches prefetched data when prefetched data is provided", async () => {
@@ -286,39 +315,42 @@ describe("prefetchData", () => {
286
315
  test("caches both prefetch and tool response when both are provided", async () => {
287
316
  stubOpenAiGlobals({ toolInput: { id: 1 } });
288
317
  using _ = spyOnConsole("debug");
318
+
319
+ const prefetchQuery = gql`
320
+ query TopProducts
321
+ @tool(description: "Shows the currently highest rated products.")
322
+ @prefetch {
323
+ topProducts {
324
+ id
325
+ title
326
+ rating
327
+ price
328
+ __typename
329
+ }
330
+ }
331
+ `;
332
+
333
+ const query = gql`
334
+ query Product($id: ID!)
335
+ @tool(name: "GetProduct", description: "Get a product") {
336
+ product(id: $id) {
337
+ id
338
+ title
339
+ rating
340
+ price
341
+ description
342
+ images
343
+ __typename
344
+ }
345
+ }
346
+ `;
347
+
289
348
  const client = new ApolloClient({
290
349
  cache: new InMemoryCache(),
291
350
  manifest: mockApplicationManifest({
292
351
  operations: [
293
- {
294
- id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
295
- name: "Product",
296
- type: "query",
297
- body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
298
- variables: { id: "ID" },
299
- prefetch: false,
300
- tools: [
301
- {
302
- name: "GetProduct",
303
- description: "Shows the details page for a specific product.",
304
- },
305
- ],
306
- },
307
- {
308
- id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
309
- name: "TopProducts",
310
- type: "query",
311
- body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
312
- variables: {},
313
- prefetch: true,
314
- prefetchID: "__anonymous",
315
- tools: [
316
- {
317
- name: "TopProducts",
318
- description: "Shows the currently highest rated products.",
319
- },
320
- ],
321
- },
352
+ parseManifestOperation(prefetchQuery),
353
+ parseManifestOperation(query),
322
354
  ],
323
355
  }),
324
356
  });
@@ -330,7 +362,6 @@ describe("prefetchData", () => {
330
362
 
331
363
  host.sendToolInput({ arguments: { id: 1 } });
332
364
  host.sendToolResult({
333
- content: [],
334
365
  structuredContent: {
335
366
  result: {
336
367
  data: {
@@ -367,7 +398,53 @@ describe("prefetchData", () => {
367
398
 
368
399
  await client.connect();
369
400
 
370
- expect(client.extract()).toMatchInlineSnapshot(`
401
+ await expect(
402
+ client.query({ query, variables: { id: 1 } })
403
+ ).resolves.toStrictEqual({
404
+ data: {
405
+ product: {
406
+ id: "1",
407
+ title: "Pen",
408
+ rating: 5,
409
+ price: 1.0,
410
+ description: "Awesome pen",
411
+ images: [],
412
+ __typename: "Product",
413
+ },
414
+ },
415
+ });
416
+
417
+ expect(client.extract()).toMatchInlineSnapshot(
418
+ {
419
+ "Product:1": {
420
+ __typename: "Product",
421
+ description: "Awesome pen",
422
+ id: "1",
423
+ images: [],
424
+ price: 1,
425
+ rating: 5,
426
+ title: "Pen",
427
+ },
428
+ "Product:2": {
429
+ __typename: "Product",
430
+ id: "2",
431
+ price: 999.99,
432
+ rating: 5,
433
+ title: "iPhone 17",
434
+ },
435
+ ROOT_QUERY: {
436
+ __typename: "Query",
437
+ 'product({"id":1})': {
438
+ __ref: "Product:1",
439
+ },
440
+ topProducts: [
441
+ {
442
+ __ref: "Product:2",
443
+ },
444
+ ],
445
+ },
446
+ },
447
+ `
371
448
  {
372
449
  "Product:1": {
373
450
  "__typename": "Product",
@@ -397,21 +474,30 @@ describe("prefetchData", () => {
397
474
  ],
398
475
  },
399
476
  }
400
- `);
477
+ `
478
+ );
401
479
  });
402
480
 
403
481
  test("excludes extra inputs when writing to cache", async () => {
404
482
  stubOpenAiGlobals({ toolInput: { id: 1, myOtherThing: 2 } });
405
483
  using _ = spyOnConsole("debug");
406
- const client = new ApolloClient({
407
- cache: new InMemoryCache(),
408
- manifest: mockApplicationManifest(),
409
- });
410
- using host = await mockMcpHost({
411
- hostContext: minimalHostContextWithToolName("GetProduct"),
412
- });
413
484
 
414
- host.onCleanup(() => client.stop());
485
+ const query = gql`
486
+ query Product($id: ID!)
487
+ @tool(name: "GetProduct", description: "Get a product") {
488
+ product(id: $id) {
489
+ id
490
+ title
491
+ rating
492
+ price
493
+ description
494
+ images
495
+ __typename
496
+ }
497
+ }
498
+ `;
499
+ const { client, host } = await setup({ query });
500
+ using _host = host;
415
501
 
416
502
  host.sendToolInput({ arguments: { id: 1, myOtherThing: 2 } });
417
503
  host.sendToolResult({
@@ -435,25 +521,39 @@ describe("prefetchData", () => {
435
521
 
436
522
  await client.connect();
437
523
 
438
- expect(client.extract()).toMatchInlineSnapshot(`
439
- {
440
- "Product:1": {
441
- "__typename": "Product",
442
- "description": "Awesome pen",
443
- "id": "1",
444
- "images": [],
445
- "price": 1,
446
- "rating": 5,
447
- "title": "Pen",
524
+ await expect(
525
+ client.query({ query, variables: { id: 1 } })
526
+ ).resolves.toStrictEqual({
527
+ data: {
528
+ product: {
529
+ id: "1",
530
+ title: "Pen",
531
+ rating: 5,
532
+ price: 1.0,
533
+ description: "Awesome pen",
534
+ images: [],
535
+ __typename: "Product",
448
536
  },
449
- "ROOT_QUERY": {
450
- "__typename": "Query",
451
- "product({"id":1})": {
452
- "__ref": "Product:1",
453
- },
537
+ },
538
+ });
539
+
540
+ expect(client.extract()).toStrictEqual({
541
+ "Product:1": {
542
+ __typename: "Product",
543
+ description: "Awesome pen",
544
+ id: "1",
545
+ images: [],
546
+ price: 1,
547
+ rating: 5,
548
+ title: "Pen",
549
+ },
550
+ ROOT_QUERY: {
551
+ __typename: "Query",
552
+ 'product({"id":1})': {
553
+ __ref: "Product:1",
454
554
  },
455
- }
456
- `);
555
+ },
556
+ });
457
557
  });
458
558
  });
459
559
 
@@ -473,7 +573,8 @@ test("reads result data from toolResponseMetadata.structuredContent", async () =
473
573
  using _ = spyOnConsole("debug");
474
574
 
475
575
  const query = gql`
476
- query Product($id: ID!) {
576
+ query Product($id: ID!)
577
+ @tool(name: "GetProduct", description: "Get a product") {
477
578
  product(id: $id) @private {
478
579
  id
479
580
  title
@@ -482,36 +583,23 @@ test("reads result data from toolResponseMetadata.structuredContent", async () =
482
583
  }
483
584
  `;
484
585
 
485
- const client = new ApolloClient({
486
- cache: new InMemoryCache(),
487
- manifest: mockApplicationManifest({
488
- operations: [
489
- {
490
- id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
491
- name: "Product",
492
- type: "query",
493
- body: print(query),
494
- variables: { id: "ID" },
495
- prefetch: false,
496
- tools: [{ name: "GetProduct", description: "Get a product" }],
497
- },
498
- ],
499
- }),
500
- });
501
- using host = await mockMcpHost({
502
- hostContext: minimalHostContextWithToolName("GetProduct"),
503
- });
504
- host.onCleanup(() => client.stop());
586
+ const { client, host } = await setup({ query });
587
+ using _host = host;
505
588
 
506
589
  host.sendToolInput({ arguments: { id: "1" } });
507
590
  host.sendToolResult({
508
- content: [],
509
591
  structuredContent: {},
510
592
  });
511
593
 
512
594
  await client.connect();
513
595
 
514
- expect(client.extract()).toEqual({
596
+ await expect(
597
+ client.query({ query, variables: { id: "1" } })
598
+ ).resolves.toStrictEqual({
599
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
600
+ });
601
+
602
+ expect(client.extract()).toStrictEqual({
515
603
  "Product:1": {
516
604
  __typename: "Product",
517
605
  id: "1",
@@ -540,7 +628,9 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
540
628
  using _ = spyOnConsole("debug");
541
629
 
542
630
  const prefetchQuery = gql`
543
- query TopProducts {
631
+ query TopProducts
632
+ @tool(description: "Shows the currently highest rated products.")
633
+ @prefetch {
544
634
  topProducts {
545
635
  id
546
636
  title
@@ -550,7 +640,8 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
550
640
  `;
551
641
 
552
642
  const query = gql`
553
- query Product($id: ID!) {
643
+ query Product($id: ID!)
644
+ @tool(name: "GetProduct", description: "Get a product") {
554
645
  product(id: $id) @private {
555
646
  id
556
647
  title
@@ -563,30 +654,8 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
563
654
  cache: new InMemoryCache(),
564
655
  manifest: mockApplicationManifest({
565
656
  operations: [
566
- {
567
- id: "1",
568
- name: "TopProducts",
569
- body: print(prefetchQuery),
570
- type: "query",
571
- variables: {},
572
- prefetch: true,
573
- prefetchID: "__anonymous",
574
- tools: [
575
- {
576
- name: "TopProducts",
577
- description: "Shows the currently highest rated products.",
578
- },
579
- ],
580
- },
581
- {
582
- id: "2",
583
- name: "Product",
584
- body: print(query),
585
- type: "query",
586
- variables: { id: "ID" },
587
- prefetch: false,
588
- tools: [{ name: "GetProduct", description: "Get a product" }],
589
- },
657
+ parseManifestOperation(prefetchQuery),
658
+ parseManifestOperation(query),
590
659
  ],
591
660
  }),
592
661
  });
@@ -597,7 +666,6 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
597
666
 
598
667
  host.sendToolInput({ arguments: { id: "2" } });
599
668
  host.sendToolResult({
600
- content: [],
601
669
  structuredContent: {
602
670
  prefetch: {
603
671
  __anonymous: {
@@ -611,6 +679,12 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
611
679
 
612
680
  await client.connect();
613
681
 
682
+ await expect(
683
+ client.query({ query, variables: { id: "2" } })
684
+ ).resolves.toStrictEqual({
685
+ data: { product: { id: "2", title: "iPad", __typename: "Product" } },
686
+ });
687
+
614
688
  expect(client.extract()).toEqual({
615
689
  "Product:1": {
616
690
  __typename: "Product",
@@ -646,7 +720,8 @@ test("toolResponseMetadata.structuredContent wins over structuredContent", async
646
720
  using _ = spyOnConsole("debug");
647
721
 
648
722
  const query = gql`
649
- query Product($id: ID!) {
723
+ query Product($id: ID!)
724
+ @tool(name: "GetProduct", description: "Get a product") {
650
725
  product(id: $id) {
651
726
  id
652
727
  title @private
@@ -655,30 +730,11 @@ test("toolResponseMetadata.structuredContent wins over structuredContent", async
655
730
  }
656
731
  `;
657
732
 
658
- const client = new ApolloClient({
659
- cache: new InMemoryCache(),
660
- manifest: mockApplicationManifest({
661
- operations: [
662
- {
663
- id: "1",
664
- name: "Product",
665
- body: print(query),
666
- type: "query",
667
- variables: { id: "ID" },
668
- prefetch: false,
669
- tools: [{ name: "GetProduct", description: "Get a product" }],
670
- },
671
- ],
672
- }),
673
- });
674
- using host = await mockMcpHost({
675
- hostContext: minimalHostContextWithToolName("GetProduct"),
676
- });
677
- host.onCleanup(() => client.stop());
733
+ const { client, host } = await setup({ query });
734
+ using _host = host;
678
735
 
679
736
  host.sendToolInput({ arguments: { id: "1" } });
680
737
  host.sendToolResult({
681
- content: [],
682
738
  structuredContent: {
683
739
  result: {
684
740
  data: {
@@ -690,7 +746,13 @@ test("toolResponseMetadata.structuredContent wins over structuredContent", async
690
746
 
691
747
  await client.connect();
692
748
 
693
- expect(client.extract()).toEqual({
749
+ await expect(
750
+ client.query({ query, variables: { id: "1" } })
751
+ ).resolves.toStrictEqual({
752
+ data: { product: { id: "1", title: "Meta title", __typename: "Product" } },
753
+ });
754
+
755
+ expect(client.extract()).toStrictEqual({
694
756
  "Product:1": {
695
757
  __typename: "Product",
696
758
  id: "1",
@@ -723,14 +785,24 @@ test("connects using window.openai.toolOutput when tool-result notification is n
723
785
  toolInput: { id: "1" },
724
786
  });
725
787
  using _ = spyOnConsole("debug");
726
- const client = new ApolloClient({
727
- cache: new InMemoryCache(),
728
- manifest: mockApplicationManifest(),
729
- });
730
- using host = await mockMcpHost({
731
- hostContext: minimalHostContextWithToolName("GetProduct"),
732
- });
733
- host.onCleanup(() => client.stop());
788
+
789
+ const query = gql`
790
+ query Product($id: ID!)
791
+ @tool(name: "GetProduct", description: "Get a product") {
792
+ product(id: $id) {
793
+ id
794
+ title
795
+ rating
796
+ price
797
+ description
798
+ images
799
+ __typename
800
+ }
801
+ }
802
+ `;
803
+
804
+ const { client, host } = await setup({ query });
805
+ using _host = host;
734
806
 
735
807
  host.sendToolInput({ arguments: { id: "1" } });
736
808
  // No host.sendToolResult() — simulates page reload where ChatGPT does not
@@ -742,25 +814,39 @@ test("connects using window.openai.toolOutput when tool-result notification is n
742
814
  // before `using host` disposes and closes the app connection.
743
815
  await new Promise((resolve) => setImmediate(resolve));
744
816
 
745
- expect(client.extract()).toMatchInlineSnapshot(`
746
- {
747
- "Product:1": {
748
- "__typename": "Product",
749
- "description": "Awesome pen",
750
- "id": "1",
751
- "images": [],
752
- "price": 1,
753
- "rating": 5,
754
- "title": "Pen",
817
+ await expect(
818
+ client.query({ query, variables: { id: "1" } })
819
+ ).resolves.toStrictEqual({
820
+ data: {
821
+ product: {
822
+ id: "1",
823
+ title: "Pen",
824
+ rating: 5,
825
+ price: 1.0,
826
+ description: "Awesome pen",
827
+ images: [],
828
+ __typename: "Product",
755
829
  },
756
- "ROOT_QUERY": {
757
- "__typename": "Query",
758
- "product({"id":"1"})": {
759
- "__ref": "Product:1",
760
- },
830
+ },
831
+ });
832
+
833
+ expect(client.extract()).toStrictEqual({
834
+ "Product:1": {
835
+ __typename: "Product",
836
+ description: "Awesome pen",
837
+ id: "1",
838
+ images: [],
839
+ price: 1,
840
+ rating: 5,
841
+ title: "Pen",
842
+ },
843
+ ROOT_QUERY: {
844
+ __typename: "Query",
845
+ 'product({"id":"1"})': {
846
+ __ref: "Product:1",
761
847
  },
762
- }
763
- `);
848
+ },
849
+ });
764
850
  });
765
851
 
766
852
  describe("custom links", () => {
@@ -900,6 +986,392 @@ describe("custom links", () => {
900
986
  });
901
987
  });
902
988
 
989
+ test("serves tool result data on network-only query without calling execute tool", async () => {
990
+ stubOpenAiGlobals({ toolInput: { id: "1" } });
991
+ using _ = spyOnConsole("debug");
992
+ const query = gql`
993
+ query Product($id: ID!)
994
+ @tool(name: "GetProduct", description: "Get a product") {
995
+ product(id: $id) {
996
+ id
997
+ title
998
+ __typename
999
+ }
1000
+ }
1001
+ `;
1002
+
1003
+ const data = {
1004
+ product: { id: "1", title: "Pen", __typename: "Product" },
1005
+ };
1006
+
1007
+ const { client, host } = await setup({ query });
1008
+ using _host = host;
1009
+
1010
+ const execute = vi.fn();
1011
+ host.mockToolCall("execute", execute);
1012
+
1013
+ host.sendToolResult({ structuredContent: { result: { data } } });
1014
+ host.sendToolInput({ arguments: { id: "1" } });
1015
+
1016
+ await client.connect();
1017
+
1018
+ await expect(
1019
+ client.query({
1020
+ query,
1021
+ variables: { id: "1" },
1022
+ fetchPolicy: "network-only",
1023
+ })
1024
+ ).resolves.toStrictEqual({ data });
1025
+ expect(execute).not.toHaveBeenCalled();
1026
+ });
1027
+
1028
+ test("calls execute tool on second network-only query after hydration is consumed", async () => {
1029
+ stubOpenAiGlobals({ toolInput: { id: "1" } });
1030
+ using _ = spyOnConsole("debug");
1031
+ const query = gql`
1032
+ query Product($id: ID!)
1033
+ @tool(name: "GetProduct", description: "Get a product") {
1034
+ product(id: $id) {
1035
+ id
1036
+ title
1037
+ __typename
1038
+ }
1039
+ }
1040
+ `;
1041
+
1042
+ const { client, host } = await setup({ query });
1043
+ using _host = host;
1044
+
1045
+ host.mockToolCall("execute", () => ({
1046
+ structuredContent: {
1047
+ data: {
1048
+ product: { id: "1", title: "Updated Pen", __typename: "Product" },
1049
+ },
1050
+ },
1051
+ }));
1052
+
1053
+ host.sendToolResult({
1054
+ structuredContent: {
1055
+ result: {
1056
+ data: {
1057
+ product: { id: "1", title: "Pen", __typename: "Product" },
1058
+ },
1059
+ },
1060
+ },
1061
+ });
1062
+ host.sendToolInput({ arguments: { id: "1" } });
1063
+
1064
+ await client.connect();
1065
+
1066
+ await client.query({
1067
+ query,
1068
+ variables: { id: "1" },
1069
+ fetchPolicy: "network-only",
1070
+ });
1071
+
1072
+ await expect(
1073
+ client.query({
1074
+ query,
1075
+ variables: { id: "1" },
1076
+ fetchPolicy: "network-only",
1077
+ })
1078
+ ).resolves.toStrictEqual({
1079
+ data: {
1080
+ product: { id: "1", title: "Updated Pen", __typename: "Product" },
1081
+ },
1082
+ });
1083
+ });
1084
+
1085
+ test("serves tool result data on cache-and-network query without calling execute tool", async () => {
1086
+ stubOpenAiGlobals({ toolInput: { id: "1" } });
1087
+ using _ = spyOnConsole("debug");
1088
+ const query = gql`
1089
+ query Product($id: ID!)
1090
+ @tool(name: "GetProduct", description: "Get a product") {
1091
+ product(id: $id) {
1092
+ id
1093
+ title
1094
+ __typename
1095
+ }
1096
+ }
1097
+ `;
1098
+
1099
+ const { client, host } = await setup({ query });
1100
+ using _host = host;
1101
+
1102
+ const execute = vi.fn();
1103
+ host.mockToolCall("execute", execute);
1104
+
1105
+ host.sendToolResult({
1106
+ structuredContent: {
1107
+ result: {
1108
+ data: {
1109
+ product: { id: "1", title: "Pen", __typename: "Product" },
1110
+ },
1111
+ },
1112
+ },
1113
+ });
1114
+ host.sendToolInput({ arguments: { id: "1" } });
1115
+
1116
+ await client.connect();
1117
+
1118
+ const stream = new ObservableStream(
1119
+ client.watchQuery({
1120
+ query,
1121
+ variables: { id: "1" },
1122
+ fetchPolicy: "cache-and-network",
1123
+ })
1124
+ );
1125
+
1126
+ // The hydrated result is emitted synchronously so we won't observe a loading
1127
+ // state like we normally would with `cache-and-network`
1128
+ await expect(stream).toEmitValue({
1129
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
1130
+ dataState: "complete",
1131
+ loading: false,
1132
+ networkStatus: NetworkStatus.ready,
1133
+ partial: false,
1134
+ });
1135
+
1136
+ await expect(stream).not.toEmitAnything();
1137
+
1138
+ expect(execute).not.toHaveBeenCalled();
1139
+ });
1140
+
1141
+ test("serves tool result data on no-cache query without calling execute tool and does not write to cache", async () => {
1142
+ stubOpenAiGlobals({ toolInput: { id: "1" } });
1143
+ using _ = spyOnConsole("debug");
1144
+ const query = gql`
1145
+ query Product($id: ID!)
1146
+ @tool(name: "GetProduct", description: "Get a product") {
1147
+ product(id: $id) {
1148
+ id
1149
+ title
1150
+ __typename
1151
+ }
1152
+ }
1153
+ `;
1154
+
1155
+ const data = {
1156
+ product: { id: "1", title: "Pen", __typename: "Product" },
1157
+ };
1158
+
1159
+ const { client, host } = await setup({ query });
1160
+ using _host = host;
1161
+
1162
+ const execute = vi.fn();
1163
+ host.mockToolCall("execute", execute);
1164
+
1165
+ host.sendToolInput({ arguments: { id: "1" } });
1166
+ host.sendToolResult({ structuredContent: { result: { data } } });
1167
+
1168
+ await client.connect();
1169
+
1170
+ await expect(
1171
+ client.query({ query, variables: { id: "1" }, fetchPolicy: "no-cache" })
1172
+ ).resolves.toStrictEqual({ data });
1173
+ expect(execute).not.toHaveBeenCalled();
1174
+ expect(client.extract()).toStrictEqual({});
1175
+ });
1176
+
1177
+ test("hydrates prefetch query with network-only fetch policy", async () => {
1178
+ stubOpenAiGlobals({ toolInput: {} });
1179
+ using _ = spyOnConsole("debug");
1180
+
1181
+ const query = gql`
1182
+ query TopProducts @tool(description: "Shows top products") @prefetch {
1183
+ topProducts {
1184
+ id
1185
+ title
1186
+ __typename
1187
+ }
1188
+ }
1189
+ `;
1190
+
1191
+ const data = {
1192
+ topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
1193
+ };
1194
+
1195
+ const { client, host } = await setup({ query, toolName: "OtherTool" });
1196
+ using _host = host;
1197
+
1198
+ const execute = vi.fn();
1199
+ host.mockToolCall("execute", execute);
1200
+
1201
+ host.sendToolResult({
1202
+ structuredContent: {
1203
+ prefetch: { __anonymous: { data } },
1204
+ },
1205
+ });
1206
+ host.sendToolInput({ arguments: {} });
1207
+
1208
+ await client.connect();
1209
+
1210
+ await expect(
1211
+ client.query({ query, fetchPolicy: "network-only" })
1212
+ ).resolves.toStrictEqual({ data });
1213
+ expect(execute).not.toHaveBeenCalled();
1214
+ });
1215
+
1216
+ test("serves hydrated query from tool result while other network-only queries call execute", async () => {
1217
+ stubOpenAiGlobals({ toolInput: { id: "1" } });
1218
+ using _ = spyOnConsole("debug");
1219
+
1220
+ const productQuery = gql`
1221
+ query Product($id: ID!)
1222
+ @tool(name: "GetProduct", description: "Get a product") {
1223
+ product(id: $id) {
1224
+ id
1225
+ title
1226
+ __typename
1227
+ }
1228
+ }
1229
+ `;
1230
+
1231
+ const cartQuery = gql`
1232
+ query Cart @tool(name: "GetCart", description: "Get the cart") {
1233
+ cart {
1234
+ id
1235
+ __typename
1236
+ }
1237
+ }
1238
+ `;
1239
+
1240
+ const productOperation = parseManifestOperation(productQuery);
1241
+ const cartOperation = parseManifestOperation(cartQuery);
1242
+
1243
+ const client = new ApolloClient({
1244
+ cache: new InMemoryCache(),
1245
+ manifest: mockApplicationManifest({
1246
+ operations: [productOperation, cartOperation],
1247
+ }),
1248
+ });
1249
+
1250
+ using host = await mockMcpHost({
1251
+ hostContext: minimalHostContextWithToolName("GetProduct"),
1252
+ });
1253
+ host.onCleanup(() => client.stop());
1254
+
1255
+ const execute = vi.fn(() => ({
1256
+ structuredContent: {
1257
+ data: { cart: { id: "1", __typename: "Cart" } },
1258
+ },
1259
+ }));
1260
+ host.mockToolCall("execute", execute);
1261
+
1262
+ host.sendToolResult({
1263
+ structuredContent: {
1264
+ result: {
1265
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
1266
+ },
1267
+ },
1268
+ });
1269
+ host.sendToolInput({ arguments: { id: "1" } });
1270
+
1271
+ await client.connect();
1272
+
1273
+ const [productResult, cartResult] = await Promise.all([
1274
+ client.query({
1275
+ query: productQuery,
1276
+ variables: { id: "1" },
1277
+ fetchPolicy: "network-only",
1278
+ }),
1279
+ client.query({
1280
+ query: cartQuery,
1281
+ fetchPolicy: "network-only",
1282
+ }),
1283
+ ]);
1284
+
1285
+ expect(productResult).toStrictEqual({
1286
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
1287
+ });
1288
+ expect(cartResult).toStrictEqual({
1289
+ data: { cart: { id: "1", __typename: "Cart" } },
1290
+ });
1291
+ expect(execute).toHaveBeenCalledOnce();
1292
+ });
1293
+
1294
+ test("serves hydrated query after tool result while earlier-queued non-matching query calls execute", async () => {
1295
+ stubOpenAiGlobals({ toolInput: { id: "1" } });
1296
+ using _ = spyOnConsole("debug");
1297
+
1298
+ const productQuery = gql`
1299
+ query Product($id: ID!)
1300
+ @tool(name: "GetProduct", description: "Get a product") {
1301
+ product(id: $id) {
1302
+ id
1303
+ title
1304
+ __typename
1305
+ }
1306
+ }
1307
+ `;
1308
+
1309
+ const cartQuery = gql`
1310
+ query Cart @tool(name: "GetCart", description: "Get the cart") {
1311
+ cart {
1312
+ id
1313
+ __typename
1314
+ }
1315
+ }
1316
+ `;
1317
+
1318
+ const productOperation = parseManifestOperation(productQuery);
1319
+ const cartOperation = parseManifestOperation(cartQuery);
1320
+
1321
+ const client = new ApolloClient({
1322
+ cache: new InMemoryCache(),
1323
+ manifest: mockApplicationManifest({
1324
+ operations: [productOperation, cartOperation],
1325
+ }),
1326
+ });
1327
+
1328
+ using host = await mockMcpHost({
1329
+ hostContext: minimalHostContextWithToolName("GetProduct"),
1330
+ });
1331
+ host.onCleanup(() => client.stop());
1332
+
1333
+ const execute = vi.fn(() => ({
1334
+ structuredContent: {
1335
+ data: { cart: { id: "1", __typename: "Cart" } },
1336
+ },
1337
+ }));
1338
+ host.mockToolCall("execute", execute);
1339
+
1340
+ const connectPromise = client.connect();
1341
+
1342
+ const cartPromise = client.query({
1343
+ query: cartQuery,
1344
+ fetchPolicy: "network-only",
1345
+ });
1346
+
1347
+ host.sendToolResult({
1348
+ structuredContent: {
1349
+ result: {
1350
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
1351
+ },
1352
+ },
1353
+ });
1354
+ host.sendToolInput({ arguments: { id: "1" } });
1355
+
1356
+ await connectPromise;
1357
+
1358
+ await expect(cartPromise).resolves.toStrictEqual({
1359
+ data: { cart: { id: "1", __typename: "Cart" } },
1360
+ });
1361
+
1362
+ await expect(
1363
+ client.query({
1364
+ query: productQuery,
1365
+ variables: { id: "1" },
1366
+ fetchPolicy: "network-only",
1367
+ })
1368
+ ).resolves.toStrictEqual({
1369
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
1370
+ });
1371
+
1372
+ expect(execute).toHaveBeenCalledOnce();
1373
+ });
1374
+
903
1375
  describe("watchQuery dev warnings", () => {
904
1376
  const query = gql`
905
1377
  query Products($category: String!, $page: Int!, $sortBy: String!)
@@ -1004,3 +1476,26 @@ describe("watchQuery dev warnings", () => {
1004
1476
  expect(console.warn).toHaveBeenCalledTimes(1);
1005
1477
  });
1006
1478
  });
1479
+
1480
+ async function setup({
1481
+ query,
1482
+ toolName,
1483
+ }: {
1484
+ query: DocumentNode;
1485
+ toolName?: string;
1486
+ }) {
1487
+ const operation = parseManifestOperation(query);
1488
+ const client = new ApolloClient({
1489
+ cache: new InMemoryCache(),
1490
+ manifest: mockApplicationManifest({ operations: [operation] }),
1491
+ });
1492
+
1493
+ const host = await mockMcpHost({
1494
+ hostContext: minimalHostContextWithToolName(
1495
+ toolName ?? operation.tools[0].name
1496
+ ),
1497
+ });
1498
+ host.onCleanup(() => client.stop());
1499
+
1500
+ return { client, host };
1501
+ }