@apollo/client-ai-apps 0.6.4 → 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 (272) hide show
  1. package/CHANGELOG.md +100 -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/core/McpAppManager.d.ts +42 -0
  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/core/types.d.ts +2 -1
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/core/types.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mcp.d.ts +0 -1
  26. package/dist/index.mcp.d.ts.map +1 -1
  27. package/dist/index.mcp.js +0 -1
  28. package/dist/index.mcp.js.map +1 -1
  29. package/dist/index.openai.d.ts +0 -1
  30. package/dist/index.openai.d.ts.map +1 -1
  31. package/dist/index.openai.js +0 -1
  32. package/dist/index.openai.js.map +1 -1
  33. package/dist/link/ToolCallLink.d.ts +6 -1
  34. package/dist/link/ToolCallLink.d.ts.map +1 -1
  35. package/dist/link/ToolCallLink.js +17 -4
  36. package/dist/link/ToolCallLink.js.map +1 -1
  37. package/dist/link/ToolHydrationLink.d.ts +21 -0
  38. package/dist/link/ToolHydrationLink.d.ts.map +1 -0
  39. package/dist/link/ToolHydrationLink.js +57 -0
  40. package/dist/link/ToolHydrationLink.js.map +1 -0
  41. package/dist/mcp/core/ApolloClient.d.ts +3 -20
  42. package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
  43. package/dist/mcp/core/ApolloClient.js +20 -98
  44. package/dist/mcp/core/ApolloClient.js.map +1 -1
  45. package/dist/mcp/index.d.ts +0 -1
  46. package/dist/mcp/index.d.ts.map +1 -1
  47. package/dist/mcp/index.js +0 -1
  48. package/dist/mcp/index.js.map +1 -1
  49. package/dist/openai/core/ApolloClient.d.ts +3 -20
  50. package/dist/openai/core/ApolloClient.d.ts.map +1 -1
  51. package/dist/openai/core/ApolloClient.js +36 -98
  52. package/dist/openai/core/ApolloClient.js.map +1 -1
  53. package/dist/openai/index.d.ts +0 -1
  54. package/dist/openai/index.d.ts.map +1 -1
  55. package/dist/openai/index.js +0 -1
  56. package/dist/openai/index.js.map +1 -1
  57. package/dist/openai/react/index.d.ts +0 -7
  58. package/dist/openai/react/index.d.ts.map +1 -1
  59. package/dist/openai/react/index.js +0 -7
  60. package/dist/openai/react/index.js.map +1 -1
  61. package/dist/react/ApolloProvider.d.ts.map +1 -1
  62. package/dist/react/ApolloProvider.js +1 -1
  63. package/dist/react/ApolloProvider.js.map +1 -1
  64. package/dist/{mcp/react/hooks → react}/createHydrationUtils.d.ts +1 -1
  65. package/dist/react/createHydrationUtils.d.ts.map +1 -0
  66. package/dist/{mcp/react/hooks → react}/createHydrationUtils.js +7 -9
  67. package/dist/react/createHydrationUtils.js.map +1 -0
  68. package/dist/react/hooks/internal/useApolloClient.d.ts +3 -0
  69. package/dist/react/hooks/internal/useApolloClient.d.ts.map +1 -0
  70. package/dist/{mcp/react/hooks → react/hooks/internal}/useApolloClient.js +3 -3
  71. package/dist/react/hooks/internal/useApolloClient.js.map +1 -0
  72. package/dist/react/hooks/useApp.d.ts.map +1 -0
  73. package/dist/react/hooks/useApp.js +5 -0
  74. package/dist/react/hooks/useApp.js.map +1 -0
  75. package/dist/react/hooks/useHostContext.d.ts.map +1 -0
  76. package/dist/{openai/react → react}/hooks/useHostContext.js +1 -1
  77. package/dist/react/hooks/useHostContext.js.map +1 -0
  78. package/dist/react/hooks/useToolInfo.d.ts +3 -0
  79. package/dist/react/hooks/useToolInfo.d.ts.map +1 -0
  80. package/dist/react/hooks/useToolInfo.js +5 -0
  81. package/dist/react/hooks/useToolInfo.js.map +1 -0
  82. package/dist/react/hooks/useToolMetadata.d.ts +2 -0
  83. package/dist/react/hooks/useToolMetadata.d.ts.map +1 -0
  84. package/dist/react/hooks/useToolMetadata.js +5 -0
  85. package/dist/react/hooks/useToolMetadata.js.map +1 -0
  86. package/dist/react/index.d.ts +5 -16
  87. package/dist/react/index.d.ts.map +1 -1
  88. package/dist/react/index.js +5 -19
  89. package/dist/react/index.js.map +1 -1
  90. package/dist/types/application-manifest.d.ts +1 -0
  91. package/dist/types/application-manifest.d.ts.map +1 -1
  92. package/dist/types/application-manifest.js.map +1 -1
  93. package/dist/utilities/connectToHost.d.ts +3 -0
  94. package/dist/utilities/connectToHost.d.ts.map +1 -0
  95. package/dist/utilities/connectToHost.js +11 -0
  96. package/dist/utilities/connectToHost.js.map +1 -0
  97. package/dist/utilities/index.d.ts +1 -0
  98. package/dist/utilities/index.d.ts.map +1 -1
  99. package/dist/utilities/index.js +1 -0
  100. package/dist/utilities/index.js.map +1 -1
  101. package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
  102. package/dist/vite/apolloClientAiApps.js +2 -0
  103. package/dist/vite/apolloClientAiApps.js.map +1 -1
  104. package/package.json +5 -22
  105. package/src/core/AbstractApolloClient.ts +217 -0
  106. package/src/core/ApolloClient.ts +8 -10
  107. package/src/core/McpAppManager.ts +106 -0
  108. package/src/core/typeRegistration.ts +0 -15
  109. package/src/core/types.ts +2 -1
  110. package/src/index.mcp.ts +0 -1
  111. package/src/index.openai.ts +0 -1
  112. package/src/index.ts +1 -6
  113. package/src/link/ToolCallLink.ts +27 -5
  114. package/src/link/ToolHydrationLink.ts +90 -0
  115. package/src/link/__tests__/ToolCallLink.test.ts +99 -0
  116. package/src/mcp/core/ApolloClient.ts +32 -165
  117. package/src/mcp/core/__tests__/ApolloClient.test.ts +571 -71
  118. package/src/mcp/index.ts +0 -1
  119. package/src/openai/core/ApolloClient.ts +48 -161
  120. package/src/openai/core/__tests__/ApolloClient.test.ts +916 -118
  121. package/src/openai/index.ts +0 -1
  122. package/src/openai/react/index.ts +0 -7
  123. package/src/react/ApolloProvider.tsx +1 -6
  124. package/src/react/__tests__/ApolloProvider/mcp.test.tsx +66 -29
  125. package/src/react/__tests__/ApolloProvider/openai.test.tsx +16 -41
  126. package/src/react/__tests__/createHydrationUtils.test.tsx +1260 -0
  127. package/src/{mcp/react/hooks → react}/createHydrationUtils.ts +7 -10
  128. package/src/react/hooks/__tests__/useApp.test.tsx +46 -0
  129. package/src/react/hooks/__tests__/useHostContext.test.tsx +99 -0
  130. package/src/react/hooks/__tests__/useToolInfo.test.tsx +98 -0
  131. package/src/react/hooks/__tests__/useToolMetadata.test.tsx +58 -0
  132. package/src/{mcp/react/hooks → react/hooks/internal}/useApolloClient.ts +3 -3
  133. package/src/{mcp/react → react}/hooks/useApp.ts +1 -1
  134. package/src/{openai/react → react}/hooks/useHostContext.ts +1 -1
  135. package/src/react/hooks/useToolInfo.ts +6 -0
  136. package/src/react/hooks/useToolMetadata.ts +5 -0
  137. package/src/react/index.ts +5 -36
  138. package/src/testing/internal/graphql/parseManifestOperation.ts +87 -0
  139. package/src/testing/internal/index.ts +3 -0
  140. package/src/testing/internal/matchers/index.ts +1 -0
  141. package/src/testing/internal/matchers/toEmitAnything.ts +43 -0
  142. package/src/testing/internal/matchers/types.ts +1 -0
  143. package/src/testing/internal/mcp/mockMcpHost.ts +25 -4
  144. package/src/testing/internal/tests/eachHostEnv.ts +22 -0
  145. package/src/testing/internal/utilities/createHostEnv.ts +117 -0
  146. package/src/types/application-manifest.ts +1 -0
  147. package/src/utilities/connectToHost.ts +13 -0
  148. package/src/utilities/index.ts +1 -0
  149. package/src/vite/__tests__/apolloClientAiApps.test.ts +56 -0
  150. package/src/vite/apolloClientAiApps.ts +5 -0
  151. package/tsconfig.vite.json +1 -1
  152. package/vitest.config.ts +13 -0
  153. package/dist/mcp/core/McpAppManager.d.ts +0 -30
  154. package/dist/mcp/core/McpAppManager.d.ts.map +0 -1
  155. package/dist/mcp/core/McpAppManager.js +0 -82
  156. package/dist/mcp/core/McpAppManager.js.map +0 -1
  157. package/dist/mcp/link/ToolCallLink.d.ts +0 -28
  158. package/dist/mcp/link/ToolCallLink.d.ts.map +0 -1
  159. package/dist/mcp/link/ToolCallLink.js +0 -35
  160. package/dist/mcp/link/ToolCallLink.js.map +0 -1
  161. package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +0 -1
  162. package/dist/mcp/react/hooks/createHydrationUtils.js.map +0 -1
  163. package/dist/mcp/react/hooks/useApolloClient.d.ts +0 -3
  164. package/dist/mcp/react/hooks/useApolloClient.d.ts.map +0 -1
  165. package/dist/mcp/react/hooks/useApolloClient.js.map +0 -1
  166. package/dist/mcp/react/hooks/useApp.d.ts.map +0 -1
  167. package/dist/mcp/react/hooks/useApp.js +0 -5
  168. package/dist/mcp/react/hooks/useApp.js.map +0 -1
  169. package/dist/mcp/react/hooks/useHostContext.d.ts.map +0 -1
  170. package/dist/mcp/react/hooks/useHostContext.js +0 -7
  171. package/dist/mcp/react/hooks/useHostContext.js.map +0 -1
  172. package/dist/mcp/react/hooks/useToolInfo.d.ts +0 -3
  173. package/dist/mcp/react/hooks/useToolInfo.d.ts.map +0 -1
  174. package/dist/mcp/react/hooks/useToolInfo.js +0 -10
  175. package/dist/mcp/react/hooks/useToolInfo.js.map +0 -1
  176. package/dist/mcp/react/hooks/useToolInput.d.ts +0 -7
  177. package/dist/mcp/react/hooks/useToolInput.d.ts.map +0 -1
  178. package/dist/mcp/react/hooks/useToolInput.js +0 -9
  179. package/dist/mcp/react/hooks/useToolInput.js.map +0 -1
  180. package/dist/mcp/react/hooks/useToolMetadata.d.ts +0 -2
  181. package/dist/mcp/react/hooks/useToolMetadata.d.ts.map +0 -1
  182. package/dist/mcp/react/hooks/useToolMetadata.js +0 -5
  183. package/dist/mcp/react/hooks/useToolMetadata.js.map +0 -1
  184. package/dist/mcp/react/hooks/useToolName.d.ts +0 -7
  185. package/dist/mcp/react/hooks/useToolName.d.ts.map +0 -1
  186. package/dist/mcp/react/hooks/useToolName.js +0 -9
  187. package/dist/mcp/react/hooks/useToolName.js.map +0 -1
  188. package/dist/mcp/react/index.d.ts +0 -8
  189. package/dist/mcp/react/index.d.ts.map +0 -1
  190. package/dist/mcp/react/index.js +0 -8
  191. package/dist/mcp/react/index.js.map +0 -1
  192. package/dist/openai/core/McpAppManager.d.ts +0 -29
  193. package/dist/openai/core/McpAppManager.d.ts.map +0 -1
  194. package/dist/openai/core/McpAppManager.js +0 -91
  195. package/dist/openai/core/McpAppManager.js.map +0 -1
  196. package/dist/openai/link/ToolCallLink.d.ts +0 -28
  197. package/dist/openai/link/ToolCallLink.d.ts.map +0 -1
  198. package/dist/openai/link/ToolCallLink.js +0 -35
  199. package/dist/openai/link/ToolCallLink.js.map +0 -1
  200. package/dist/openai/react/hooks/createHydrationUtils.d.ts +0 -15
  201. package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +0 -1
  202. package/dist/openai/react/hooks/createHydrationUtils.js +0 -113
  203. package/dist/openai/react/hooks/createHydrationUtils.js.map +0 -1
  204. package/dist/openai/react/hooks/useApp.d.ts +0 -2
  205. package/dist/openai/react/hooks/useApp.d.ts.map +0 -1
  206. package/dist/openai/react/hooks/useApp.js +0 -5
  207. package/dist/openai/react/hooks/useApp.js.map +0 -1
  208. package/dist/openai/react/hooks/useHostContext.d.ts +0 -2
  209. package/dist/openai/react/hooks/useHostContext.d.ts.map +0 -1
  210. package/dist/openai/react/hooks/useHostContext.js.map +0 -1
  211. package/dist/openai/react/hooks/useToolInfo.d.ts +0 -3
  212. package/dist/openai/react/hooks/useToolInfo.d.ts.map +0 -1
  213. package/dist/openai/react/hooks/useToolInfo.js +0 -10
  214. package/dist/openai/react/hooks/useToolInfo.js.map +0 -1
  215. package/dist/openai/react/hooks/useToolInput.d.ts +0 -7
  216. package/dist/openai/react/hooks/useToolInput.d.ts.map +0 -1
  217. package/dist/openai/react/hooks/useToolInput.js +0 -9
  218. package/dist/openai/react/hooks/useToolInput.js.map +0 -1
  219. package/dist/openai/react/hooks/useToolMetadata.d.ts +0 -2
  220. package/dist/openai/react/hooks/useToolMetadata.d.ts.map +0 -1
  221. package/dist/openai/react/hooks/useToolMetadata.js +0 -5
  222. package/dist/openai/react/hooks/useToolMetadata.js.map +0 -1
  223. package/dist/openai/react/hooks/useToolName.d.ts +0 -7
  224. package/dist/openai/react/hooks/useToolName.d.ts.map +0 -1
  225. package/dist/openai/react/hooks/useToolName.js +0 -9
  226. package/dist/openai/react/hooks/useToolName.js.map +0 -1
  227. package/dist/react/index.mcp.d.ts +0 -3
  228. package/dist/react/index.mcp.d.ts.map +0 -1
  229. package/dist/react/index.mcp.js +0 -3
  230. package/dist/react/index.mcp.js.map +0 -1
  231. package/dist/react/index.openai.d.ts +0 -3
  232. package/dist/react/index.openai.d.ts.map +0 -1
  233. package/dist/react/index.openai.js +0 -3
  234. package/dist/react/index.openai.js.map +0 -1
  235. package/dist/react/missingHook.d.ts +0 -2
  236. package/dist/react/missingHook.d.ts.map +0 -1
  237. package/dist/react/missingHook.js +0 -6
  238. package/dist/react/missingHook.js.map +0 -1
  239. package/src/mcp/core/McpAppManager.ts +0 -129
  240. package/src/mcp/link/ToolCallLink.ts +0 -40
  241. package/src/mcp/link/__tests__/ToolCallLink.test.ts +0 -62
  242. package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1228
  243. package/src/mcp/react/hooks/__tests__/useApp.test.tsx +0 -46
  244. package/src/mcp/react/hooks/__tests__/useHostContext.test.tsx +0 -95
  245. package/src/mcp/react/hooks/__tests__/useToolInfo.test.tsx +0 -53
  246. package/src/mcp/react/hooks/__tests__/useToolInput.test.tsx +0 -50
  247. package/src/mcp/react/hooks/__tests__/useToolMetadata.test.tsx +0 -53
  248. package/src/mcp/react/hooks/__tests__/useToolName.test.tsx +0 -50
  249. package/src/mcp/react/hooks/useHostContext.ts +0 -14
  250. package/src/mcp/react/hooks/useToolInfo.ts +0 -13
  251. package/src/mcp/react/hooks/useToolInput.ts +0 -10
  252. package/src/mcp/react/hooks/useToolMetadata.ts +0 -5
  253. package/src/mcp/react/hooks/useToolName.ts +0 -10
  254. package/src/mcp/react/index.ts +0 -7
  255. package/src/openai/core/McpAppManager.ts +0 -139
  256. package/src/openai/link/ToolCallLink.ts +0 -40
  257. package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1333
  258. package/src/openai/react/hooks/__tests__/useToolInfo.test.tsx +0 -92
  259. package/src/openai/react/hooks/__tests__/useToolInput.test.tsx +0 -85
  260. package/src/openai/react/hooks/__tests__/useToolMetadata.test.tsx +0 -86
  261. package/src/openai/react/hooks/__tests__/useToolName.test.tsx +0 -50
  262. package/src/openai/react/hooks/createHydrationUtils.ts +0 -182
  263. package/src/openai/react/hooks/useApp.ts +0 -5
  264. package/src/openai/react/hooks/useToolInfo.ts +0 -13
  265. package/src/openai/react/hooks/useToolInput.ts +0 -10
  266. package/src/openai/react/hooks/useToolMetadata.ts +0 -5
  267. package/src/openai/react/hooks/useToolName.ts +0 -10
  268. package/src/react/index.mcp.ts +0 -10
  269. package/src/react/index.openai.ts +0 -10
  270. package/src/react/missingHook.ts +0 -9
  271. /package/dist/{mcp/react → react}/hooks/useApp.d.ts +0 -0
  272. /package/dist/{mcp/react → react}/hooks/useHostContext.d.ts +0 -0
@@ -1,13 +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 { 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";
6
12
  import {
7
13
  graphqlToolResult,
8
14
  minimalHostContextWithToolName,
9
15
  mockApplicationManifest,
10
16
  mockMcpHost,
17
+ ObservableStream,
18
+ parseManifestOperation,
11
19
  spyOnConsole,
12
20
  stubOpenAiGlobals,
13
21
  } from "../../../testing/internal/index.js";
@@ -91,23 +99,87 @@ describe("Client Basics", () => {
91
99
  });
92
100
  });
93
101
 
102
+ test("merges _meta.structuredContent into result for @private fields", async () => {
103
+ stubOpenAiGlobals();
104
+ using _ = spyOnConsole("debug");
105
+ const manifest = mockApplicationManifest();
106
+ const client = new ApolloClient({
107
+ cache: new InMemoryCache(),
108
+ manifest,
109
+ });
110
+ using host = await mockMcpHost();
111
+
112
+ const query = gql`
113
+ query Product {
114
+ id
115
+ title @private
116
+ }
117
+ `;
118
+
119
+ host.onCleanup(() => client.stop());
120
+
121
+ host.sendToolInput({});
122
+ host.sendToolResult({
123
+ content: [],
124
+ structuredContent: {},
125
+ });
126
+
127
+ host.mockToolCall("execute", () => ({
128
+ content: [],
129
+ structuredContent: {},
130
+ _meta: {
131
+ structuredContent: {
132
+ data: {
133
+ product: {
134
+ id: "1",
135
+ title: "Private Pen",
136
+ __typename: "Product",
137
+ },
138
+ },
139
+ },
140
+ },
141
+ }));
142
+
143
+ await client.connect();
144
+
145
+ await expect(
146
+ client.query({ query, variables: { id: "1" } })
147
+ ).resolves.toStrictEqual({
148
+ data: {
149
+ product: {
150
+ __typename: "Product",
151
+ id: "1",
152
+ title: "Private Pen",
153
+ },
154
+ },
155
+ });
156
+ });
157
+
94
158
  describe("prefetchData", () => {
95
159
  test("caches tool response when data is provided", async () => {
96
160
  stubOpenAiGlobals({ toolInput: { id: 1 } });
97
161
  using _ = spyOnConsole("debug");
98
- const client = new ApolloClient({
99
- cache: new InMemoryCache(),
100
- manifest: mockApplicationManifest(),
101
- });
102
- using host = await mockMcpHost({
103
- hostContext: minimalHostContextWithToolName("GetProduct"),
104
- });
105
162
 
106
- 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;
107
180
 
108
181
  host.sendToolInput({ arguments: { id: 1 } });
109
182
  host.sendToolResult({
110
- content: [],
111
183
  structuredContent: {
112
184
  result: {
113
185
  data: {
@@ -127,25 +199,39 @@ describe("prefetchData", () => {
127
199
 
128
200
  await client.connect();
129
201
 
130
- expect(client.extract()).toMatchInlineSnapshot(`
131
- {
132
- "Product:1": {
133
- "__typename": "Product",
134
- "description": "Awesome pen",
135
- "id": "1",
136
- "images": [],
137
- "price": 1,
138
- "rating": 5,
139
- "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",
140
214
  },
141
- "ROOT_QUERY": {
142
- "__typename": "Query",
143
- "product({"id":1})": {
144
- "__ref": "Product:1",
145
- },
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",
146
232
  },
147
- }
148
- `);
233
+ },
234
+ });
149
235
  });
150
236
 
151
237
  test("caches prefetched data when prefetched data is provided", async () => {
@@ -229,39 +315,42 @@ describe("prefetchData", () => {
229
315
  test("caches both prefetch and tool response when both are provided", async () => {
230
316
  stubOpenAiGlobals({ toolInput: { id: 1 } });
231
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
+
232
348
  const client = new ApolloClient({
233
349
  cache: new InMemoryCache(),
234
350
  manifest: mockApplicationManifest({
235
351
  operations: [
236
- {
237
- id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
238
- name: "Product",
239
- type: "query",
240
- 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}",
241
- variables: { id: "ID" },
242
- prefetch: false,
243
- tools: [
244
- {
245
- name: "GetProduct",
246
- description: "Shows the details page for a specific product.",
247
- },
248
- ],
249
- },
250
- {
251
- id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
252
- name: "TopProducts",
253
- type: "query",
254
- body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
255
- variables: {},
256
- prefetch: true,
257
- prefetchID: "__anonymous",
258
- tools: [
259
- {
260
- name: "TopProducts",
261
- description: "Shows the currently highest rated products.",
262
- },
263
- ],
264
- },
352
+ parseManifestOperation(prefetchQuery),
353
+ parseManifestOperation(query),
265
354
  ],
266
355
  }),
267
356
  });
@@ -273,7 +362,6 @@ describe("prefetchData", () => {
273
362
 
274
363
  host.sendToolInput({ arguments: { id: 1 } });
275
364
  host.sendToolResult({
276
- content: [],
277
365
  structuredContent: {
278
366
  result: {
279
367
  data: {
@@ -310,7 +398,53 @@ describe("prefetchData", () => {
310
398
 
311
399
  await client.connect();
312
400
 
313
- 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
+ `
314
448
  {
315
449
  "Product:1": {
316
450
  "__typename": "Product",
@@ -340,21 +474,30 @@ describe("prefetchData", () => {
340
474
  ],
341
475
  },
342
476
  }
343
- `);
477
+ `
478
+ );
344
479
  });
345
480
 
346
481
  test("excludes extra inputs when writing to cache", async () => {
347
482
  stubOpenAiGlobals({ toolInput: { id: 1, myOtherThing: 2 } });
348
483
  using _ = spyOnConsole("debug");
349
- const client = new ApolloClient({
350
- cache: new InMemoryCache(),
351
- manifest: mockApplicationManifest(),
352
- });
353
- using host = await mockMcpHost({
354
- hostContext: minimalHostContextWithToolName("GetProduct"),
355
- });
356
484
 
357
- 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;
358
501
 
359
502
  host.sendToolInput({ arguments: { id: 1, myOtherThing: 2 } });
360
503
  host.sendToolResult({
@@ -378,60 +521,292 @@ describe("prefetchData", () => {
378
521
 
379
522
  await client.connect();
380
523
 
381
- expect(client.extract()).toMatchInlineSnapshot(`
382
- {
383
- "Product:1": {
384
- "__typename": "Product",
385
- "description": "Awesome pen",
386
- "id": "1",
387
- "images": [],
388
- "price": 1,
389
- "rating": 5,
390
- "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",
391
536
  },
392
- "ROOT_QUERY": {
393
- "__typename": "Query",
394
- "product({"id":1})": {
395
- "__ref": "Product:1",
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",
554
+ },
555
+ },
556
+ });
557
+ });
558
+ });
559
+
560
+ test("reads result data from toolResponseMetadata.structuredContent", async () => {
561
+ stubOpenAiGlobals({
562
+ toolInput: { id: "1" },
563
+ toolResponseMetadata: {
564
+ structuredContent: {
565
+ result: {
566
+ data: {
567
+ product: { id: "1", title: "Pen", __typename: "Product" },
396
568
  },
397
569
  },
570
+ },
571
+ },
572
+ });
573
+ using _ = spyOnConsole("debug");
574
+
575
+ const query = gql`
576
+ query Product($id: ID!)
577
+ @tool(name: "GetProduct", description: "Get a product") {
578
+ product(id: $id) @private {
579
+ id
580
+ title
581
+ __typename
398
582
  }
399
- `);
583
+ }
584
+ `;
585
+
586
+ const { client, host } = await setup({ query });
587
+ using _host = host;
588
+
589
+ host.sendToolInput({ arguments: { id: "1" } });
590
+ host.sendToolResult({
591
+ structuredContent: {},
592
+ });
593
+
594
+ await client.connect();
595
+
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({
603
+ "Product:1": {
604
+ __typename: "Product",
605
+ id: "1",
606
+ title: "Pen",
607
+ },
608
+ ROOT_QUERY: {
609
+ __typename: "Query",
610
+ 'product({"id":"1"})@private': { __ref: "Product:1" },
611
+ },
400
612
  });
401
613
  });
402
614
 
403
- test("connects using window.openai.toolOutput when tool-result notification is not sent", async () => {
615
+ test("merges prefetch from structuredContent and result from toolResponseMetadata.structuredContent", async () => {
404
616
  stubOpenAiGlobals({
405
- toolOutput: {
406
- result: {
407
- data: {
408
- product: {
409
- id: "1",
410
- title: "Pen",
411
- rating: 5,
412
- price: 1.0,
413
- description: "Awesome pen",
414
- images: [],
415
- __typename: "Product",
617
+ toolInput: { id: "2" },
618
+ toolResponseMetadata: {
619
+ structuredContent: {
620
+ result: {
621
+ data: {
622
+ product: { id: "2", title: "iPad", __typename: "Product" },
416
623
  },
417
624
  },
418
625
  },
419
626
  },
420
- toolInput: { id: "1" },
421
627
  });
422
628
  using _ = spyOnConsole("debug");
629
+
630
+ const prefetchQuery = gql`
631
+ query TopProducts
632
+ @tool(description: "Shows the currently highest rated products.")
633
+ @prefetch {
634
+ topProducts {
635
+ id
636
+ title
637
+ __typename
638
+ }
639
+ }
640
+ `;
641
+
642
+ const query = gql`
643
+ query Product($id: ID!)
644
+ @tool(name: "GetProduct", description: "Get a product") {
645
+ product(id: $id) @private {
646
+ id
647
+ title
648
+ __typename
649
+ }
650
+ }
651
+ `;
652
+
423
653
  const client = new ApolloClient({
424
654
  cache: new InMemoryCache(),
425
- manifest: mockApplicationManifest(),
655
+ manifest: mockApplicationManifest({
656
+ operations: [
657
+ parseManifestOperation(prefetchQuery),
658
+ parseManifestOperation(query),
659
+ ],
660
+ }),
426
661
  });
427
662
  using host = await mockMcpHost({
428
663
  hostContext: minimalHostContextWithToolName("GetProduct"),
429
664
  });
430
665
  host.onCleanup(() => client.stop());
431
666
 
432
- host.sendToolInput({ arguments: { id: "1" } });
433
- // No host.sendToolResult() — simulates page reload where ChatGPT does not
434
- // re-send the tool-result notification
667
+ host.sendToolInput({ arguments: { id: "2" } });
668
+ host.sendToolResult({
669
+ structuredContent: {
670
+ prefetch: {
671
+ __anonymous: {
672
+ data: {
673
+ topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
674
+ },
675
+ },
676
+ },
677
+ },
678
+ });
679
+
680
+ await client.connect();
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
+
688
+ expect(client.extract()).toEqual({
689
+ "Product:1": {
690
+ __typename: "Product",
691
+ id: "1",
692
+ title: "iPhone",
693
+ },
694
+ "Product:2": {
695
+ __typename: "Product",
696
+ id: "2",
697
+ title: "iPad",
698
+ },
699
+ ROOT_QUERY: {
700
+ __typename: "Query",
701
+ topProducts: [{ __ref: "Product:1" }],
702
+ 'product({"id":"2"})@private': { __ref: "Product:2" },
703
+ },
704
+ });
705
+ });
706
+
707
+ test("toolResponseMetadata.structuredContent wins over structuredContent", async () => {
708
+ stubOpenAiGlobals({
709
+ toolInput: { id: "1" },
710
+ toolResponseMetadata: {
711
+ structuredContent: {
712
+ result: {
713
+ data: {
714
+ product: { id: "1", title: "Meta title", __typename: "Product" },
715
+ },
716
+ },
717
+ },
718
+ },
719
+ });
720
+ using _ = spyOnConsole("debug");
721
+
722
+ const query = gql`
723
+ query Product($id: ID!)
724
+ @tool(name: "GetProduct", description: "Get a product") {
725
+ product(id: $id) {
726
+ id
727
+ title @private
728
+ __typename
729
+ }
730
+ }
731
+ `;
732
+
733
+ const { client, host } = await setup({ query });
734
+ using _host = host;
735
+
736
+ host.sendToolInput({ arguments: { id: "1" } });
737
+ host.sendToolResult({
738
+ structuredContent: {
739
+ result: {
740
+ data: {
741
+ product: { id: "1", __typename: "Product" },
742
+ },
743
+ },
744
+ },
745
+ });
746
+
747
+ await client.connect();
748
+
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({
756
+ "Product:1": {
757
+ __typename: "Product",
758
+ id: "1",
759
+ "title@private": "Meta title",
760
+ },
761
+ ROOT_QUERY: {
762
+ __typename: "Query",
763
+ 'product({"id":"1"})': { __ref: "Product:1" },
764
+ },
765
+ });
766
+ });
767
+
768
+ test("connects using window.openai.toolOutput when tool-result notification is not sent", async () => {
769
+ stubOpenAiGlobals({
770
+ toolOutput: {
771
+ result: {
772
+ data: {
773
+ product: {
774
+ id: "1",
775
+ title: "Pen",
776
+ rating: 5,
777
+ price: 1.0,
778
+ description: "Awesome pen",
779
+ images: [],
780
+ __typename: "Product",
781
+ },
782
+ },
783
+ },
784
+ },
785
+ toolInput: { id: "1" },
786
+ });
787
+ using _ = spyOnConsole("debug");
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;
806
+
807
+ host.sendToolInput({ arguments: { id: "1" } });
808
+ // No host.sendToolResult() — simulates page reload where ChatGPT does not
809
+ // re-send the tool-result notification
435
810
 
436
811
  await client.connect();
437
812
 
@@ -439,25 +814,39 @@ test("connects using window.openai.toolOutput when tool-result notification is n
439
814
  // before `using host` disposes and closes the app connection.
440
815
  await new Promise((resolve) => setImmediate(resolve));
441
816
 
442
- expect(client.extract()).toMatchInlineSnapshot(`
443
- {
444
- "Product:1": {
445
- "__typename": "Product",
446
- "description": "Awesome pen",
447
- "id": "1",
448
- "images": [],
449
- "price": 1,
450
- "rating": 5,
451
- "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",
452
829
  },
453
- "ROOT_QUERY": {
454
- "__typename": "Query",
455
- "product({"id":"1"})": {
456
- "__ref": "Product:1",
457
- },
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",
458
847
  },
459
- }
460
- `);
848
+ },
849
+ });
461
850
  });
462
851
 
463
852
  describe("custom links", () => {
@@ -597,6 +986,392 @@ describe("custom links", () => {
597
986
  });
598
987
  });
599
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
+
600
1375
  describe("watchQuery dev warnings", () => {
601
1376
  const query = gql`
602
1377
  query Products($category: String!, $page: Int!, $sortBy: String!)
@@ -701,3 +1476,26 @@ describe("watchQuery dev warnings", () => {
701
1476
  expect(console.warn).toHaveBeenCalledTimes(1);
702
1477
  });
703
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
+ }