@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,22 @@
1
1
  import { expect, test, vi, describe } from "vitest";
2
2
  import { ApolloClient } from "../ApolloClient.js";
3
- import { ApolloLink, HttpLink, InMemoryCache, gql } from "@apollo/client";
3
+ import {
4
+ ApolloLink,
5
+ HttpLink,
6
+ InMemoryCache,
7
+ NetworkStatus,
8
+ gql,
9
+ type DocumentNode,
10
+ } from "@apollo/client";
4
11
  import { print } from "@apollo/client/utilities";
5
- import { ToolCallLink } from "../../link/ToolCallLink.js";
12
+ import { ToolCallLink } from "../../../link/ToolCallLink.js";
6
13
  import {
7
14
  graphqlToolResult,
8
15
  minimalHostContextWithToolName,
9
16
  mockApplicationManifest,
10
17
  mockMcpHost,
18
+ ObservableStream,
19
+ parseManifestOperation,
11
20
  spyOnConsole,
12
21
  } from "../../../testing/internal/index.js";
13
22
 
@@ -15,7 +24,8 @@ test("writes tool result data to cache", async () => {
15
24
  using _ = spyOnConsole("debug");
16
25
 
17
26
  const query = gql`
18
- query Product($id: ID!) {
27
+ query Product($id: ID!)
28
+ @tool(name: "GetProduct", description: "Get a product") {
19
29
  product(id: $id) {
20
30
  id
21
31
  title
@@ -24,27 +34,8 @@ test("writes tool result data to cache", async () => {
24
34
  }
25
35
  `;
26
36
 
27
- const client = new ApolloClient({
28
- cache: new InMemoryCache(),
29
- manifest: mockApplicationManifest({
30
- operations: [
31
- {
32
- id: "1",
33
- name: "Product",
34
- body: print(query),
35
- type: "query",
36
- prefetch: false,
37
- variables: { id: "ID" },
38
- tools: [{ name: "GetProduct", description: "Get a product" }],
39
- },
40
- ],
41
- }),
42
- });
43
-
44
- using host = await mockMcpHost({
45
- hostContext: minimalHostContextWithToolName("GetProduct"),
46
- });
47
- host.onCleanup(() => client.stop());
37
+ const { client, host } = await setup({ query });
38
+ using _host = host;
48
39
 
49
40
  host.sendToolResult({
50
41
  content: [],
@@ -60,6 +51,12 @@ test("writes tool result data to cache", async () => {
60
51
 
61
52
  await client.connect();
62
53
 
54
+ await expect(
55
+ client.query({ query, variables: { id: "1" } })
56
+ ).resolves.toStrictEqual({
57
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
58
+ });
59
+
63
60
  expect(client.extract()).toEqual({
64
61
  "Product:1": {
65
62
  __typename: "Product",
@@ -140,11 +137,11 @@ test("writes prefetch data to cache", async () => {
140
137
  });
141
138
  });
142
139
 
143
- test("writes prefetch and tool response data to cache when both are provided", async () => {
140
+ test("writes prefetch response data to cache when both are provided", async () => {
144
141
  using _ = spyOnConsole("debug");
145
142
 
146
143
  const prefetchQuery = gql`
147
- query TopProducts {
144
+ query TopProducts @tool(description: "Shows top products") @prefetch {
148
145
  topProducts {
149
146
  id
150
147
  title
@@ -154,7 +151,7 @@ test("writes prefetch and tool response data to cache when both are provided", a
154
151
  `;
155
152
 
156
153
  const query = gql`
157
- query Product($id: ID!) {
154
+ query Product($id: ID!) @tool(description: "Get a product by id") {
158
155
  product(id: $id) {
159
156
  id
160
157
  title
@@ -167,25 +164,8 @@ test("writes prefetch and tool response data to cache when both are provided", a
167
164
  cache: new InMemoryCache(),
168
165
  manifest: mockApplicationManifest({
169
166
  operations: [
170
- {
171
- id: "1",
172
- name: "TopProducts",
173
- body: print(prefetchQuery),
174
- type: "query",
175
- prefetch: true,
176
- prefetchID: "__anonymous",
177
- variables: {},
178
- tools: [{ name: "TopProducts", description: "Shows top products" }],
179
- },
180
- {
181
- id: "2",
182
- name: "Product",
183
- body: print(query),
184
- type: "query",
185
- prefetch: false,
186
- variables: { id: "ID" },
187
- tools: [{ name: "Product", description: "Get a product by id" }],
188
- },
167
+ parseManifestOperation(prefetchQuery),
168
+ parseManifestOperation(query),
189
169
  ],
190
170
  }),
191
171
  });
@@ -196,7 +176,6 @@ test("writes prefetch and tool response data to cache when both are provided", a
196
176
  host.onCleanup(() => client.stop());
197
177
 
198
178
  host.sendToolResult({
199
- content: [],
200
179
  structuredContent: {
201
180
  prefetch: {
202
181
  __anonymous: {
@@ -216,6 +195,12 @@ test("writes prefetch and tool response data to cache when both are provided", a
216
195
 
217
196
  await client.connect();
218
197
 
198
+ await expect(
199
+ client.query({ query, variables: { id: "2" } })
200
+ ).resolves.toStrictEqual({
201
+ data: { product: { __typename: "Product", id: "2", title: "iPad" } },
202
+ });
203
+
219
204
  expect(client.extract()).toEqual({
220
205
  "Product:1": {
221
206
  __typename: "Product",
@@ -239,7 +224,8 @@ test("excludes extra tool input variables not defined in the operation", async (
239
224
  using _ = spyOnConsole("debug");
240
225
 
241
226
  const query = gql`
242
- query Product($id: ID!) {
227
+ query Product($id: ID!)
228
+ @tool(name: "GetProduct", description: "Get a product") {
243
229
  product(id: $id) {
244
230
  id
245
231
  title
@@ -248,30 +234,10 @@ test("excludes extra tool input variables not defined in the operation", async (
248
234
  }
249
235
  `;
250
236
 
251
- const client = new ApolloClient({
252
- cache: new InMemoryCache(),
253
- manifest: mockApplicationManifest({
254
- operations: [
255
- {
256
- id: "1",
257
- name: "Product",
258
- body: print(query),
259
- type: "query",
260
- prefetch: false,
261
- variables: { id: "ID" },
262
- tools: [{ name: "GetProduct", description: "Get a product" }],
263
- },
264
- ],
265
- }),
266
- });
267
-
268
- using host = await mockMcpHost({
269
- hostContext: minimalHostContextWithToolName("GetProduct"),
270
- });
271
- host.onCleanup(() => client.stop());
237
+ const { client, host } = await setup({ query });
238
+ using _host = host;
272
239
 
273
240
  host.sendToolResult({
274
- content: [],
275
241
  structuredContent: {
276
242
  result: {
277
243
  data: {
@@ -284,7 +250,13 @@ test("excludes extra tool input variables not defined in the operation", async (
284
250
 
285
251
  await client.connect();
286
252
 
287
- expect(client.extract()).toEqual({
253
+ await expect(
254
+ client.query({ query, variables: { id: "1" } })
255
+ ).resolves.toStrictEqual({
256
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
257
+ });
258
+
259
+ expect(client.extract()).toStrictEqual({
288
260
  "Product:1": {
289
261
  __typename: "Product",
290
262
  id: "1",
@@ -464,6 +436,511 @@ test("creates a default ToolCallLink when no link is provided", () => {
464
436
  }).not.toThrow();
465
437
  });
466
438
 
439
+ test("reads result data from _meta.structuredContent", async () => {
440
+ using _ = spyOnConsole("debug");
441
+
442
+ const query = gql`
443
+ query Product($id: ID!)
444
+ @tool(name: "GetProduct", description: "Get a product") {
445
+ product(id: $id) @private {
446
+ id
447
+ title
448
+ __typename
449
+ }
450
+ }
451
+ `;
452
+
453
+ const { client, host } = await setup({ query });
454
+ using _host = host;
455
+
456
+ host.sendToolResult({
457
+ structuredContent: {},
458
+ _meta: {
459
+ toolName: "GetProduct",
460
+ structuredContent: {
461
+ result: {
462
+ data: {
463
+ product: { id: "1", title: "Pen", __typename: "Product" },
464
+ },
465
+ },
466
+ },
467
+ },
468
+ });
469
+ host.sendToolInput({ arguments: { id: "1" } });
470
+
471
+ await client.connect();
472
+
473
+ await expect(
474
+ client.query({ query, variables: { id: "1" } })
475
+ ).resolves.toStrictEqual({
476
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
477
+ });
478
+
479
+ expect(client.extract()).toEqual({
480
+ "Product:1": {
481
+ __typename: "Product",
482
+ id: "1",
483
+ title: "Pen",
484
+ },
485
+ ROOT_QUERY: {
486
+ __typename: "Query",
487
+ 'product({"id":"1"})@private': {
488
+ __ref: "Product:1",
489
+ },
490
+ },
491
+ });
492
+ });
493
+
494
+ test("merges prefetch from structuredContent and result from _meta.structuredContent", async () => {
495
+ using _ = spyOnConsole("debug");
496
+
497
+ const prefetchQuery = gql`
498
+ query TopProducts @tool(description: "Shows top products") @prefetch {
499
+ topProducts {
500
+ id
501
+ title
502
+ __typename
503
+ }
504
+ }
505
+ `;
506
+
507
+ const query = gql`
508
+ query Product($id: ID!)
509
+ @tool(name: "GetProduct", description: "Get a product") {
510
+ product(id: $id) @private {
511
+ id
512
+ title
513
+ __typename
514
+ }
515
+ }
516
+ `;
517
+
518
+ const client = new ApolloClient({
519
+ cache: new InMemoryCache(),
520
+ manifest: mockApplicationManifest({
521
+ operations: [
522
+ parseManifestOperation(prefetchQuery),
523
+ parseManifestOperation(query),
524
+ ],
525
+ }),
526
+ });
527
+
528
+ using host = await mockMcpHost({
529
+ hostContext: minimalHostContextWithToolName("GetProduct"),
530
+ });
531
+ host.onCleanup(() => client.stop());
532
+
533
+ host.sendToolResult({
534
+ structuredContent: {
535
+ prefetch: {
536
+ __anonymous: {
537
+ data: {
538
+ topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
539
+ },
540
+ },
541
+ },
542
+ },
543
+ _meta: {
544
+ toolName: "GetProduct",
545
+ structuredContent: {
546
+ result: {
547
+ data: {
548
+ product: { id: "2", title: "iPad", __typename: "Product" },
549
+ },
550
+ },
551
+ },
552
+ },
553
+ });
554
+ host.sendToolInput({ arguments: { id: "2" } });
555
+
556
+ await client.connect();
557
+
558
+ await expect(
559
+ client.query({ query, variables: { id: "2" } })
560
+ ).resolves.toStrictEqual({
561
+ data: { product: { id: "2", title: "iPad", __typename: "Product" } },
562
+ });
563
+
564
+ expect(client.extract()).toStrictEqual({
565
+ "Product:1": {
566
+ __typename: "Product",
567
+ id: "1",
568
+ title: "iPhone",
569
+ },
570
+ "Product:2": {
571
+ __typename: "Product",
572
+ id: "2",
573
+ title: "iPad",
574
+ },
575
+ ROOT_QUERY: {
576
+ __typename: "Query",
577
+ topProducts: [{ __ref: "Product:1" }],
578
+ 'product({"id":"2"})@private': { __ref: "Product:2" },
579
+ },
580
+ });
581
+ });
582
+
583
+ test("_meta.structuredContent wins over structuredContent", async () => {
584
+ using _ = spyOnConsole("debug");
585
+
586
+ const query = gql`
587
+ query Product($id: ID!) {
588
+ product(id: $id) @tool(name: "GetProduct", description: "Get a product") {
589
+ id
590
+ title @private
591
+ __typename
592
+ }
593
+ }
594
+ `;
595
+
596
+ const { client, host } = await setup({ query });
597
+ using _host = host;
598
+
599
+ host.sendToolResult({
600
+ structuredContent: {
601
+ result: {
602
+ data: {
603
+ product: { id: "1", __typename: "Product" },
604
+ },
605
+ },
606
+ },
607
+ _meta: {
608
+ toolName: "GetProduct",
609
+ structuredContent: {
610
+ result: {
611
+ data: {
612
+ product: { id: "1", title: "Meta title", __typename: "Product" },
613
+ },
614
+ },
615
+ },
616
+ },
617
+ });
618
+ host.sendToolInput({ arguments: { id: "1" } });
619
+
620
+ await client.connect();
621
+
622
+ await expect(
623
+ client.query({ query, variables: { id: "1" } })
624
+ ).resolves.toStrictEqual({
625
+ data: { product: { id: "1", title: "Meta title", __typename: "Product" } },
626
+ });
627
+
628
+ expect(client.extract()).toEqual({
629
+ "Product:1": {
630
+ __typename: "Product",
631
+ id: "1",
632
+ "title@private": "Meta title",
633
+ },
634
+ ROOT_QUERY: {
635
+ __typename: "Query",
636
+ 'product({"id":"1"})': {
637
+ __ref: "Product:1",
638
+ },
639
+ },
640
+ });
641
+ });
642
+
643
+ test("serves tool result data on network-only query without calling execute tool", async () => {
644
+ using _ = spyOnConsole("debug");
645
+ const query = gql`
646
+ query Product($id: ID!)
647
+ @tool(name: "GetProduct", description: "Get a product") {
648
+ product(id: $id) {
649
+ id
650
+ title
651
+ __typename
652
+ }
653
+ }
654
+ `;
655
+
656
+ const data = {
657
+ product: { id: "1", title: "Pen", __typename: "Product" },
658
+ };
659
+
660
+ const { client, host } = await setup({ query });
661
+ using _host = host;
662
+
663
+ const execute = vi.fn();
664
+ host.mockToolCall("execute", execute);
665
+
666
+ host.sendToolResult({ structuredContent: { result: { data } } });
667
+ host.sendToolInput({ arguments: { id: "1" } });
668
+
669
+ await client.connect();
670
+
671
+ await expect(
672
+ client.query({
673
+ query,
674
+ variables: { id: "1" },
675
+ fetchPolicy: "network-only",
676
+ })
677
+ ).resolves.toStrictEqual({ data });
678
+ expect(execute).not.toHaveBeenCalled();
679
+ });
680
+
681
+ test("calls execute tool on second network-only query after hydration is consumed", async () => {
682
+ using _ = spyOnConsole("debug");
683
+ const query = gql`
684
+ query Product($id: ID!)
685
+ @tool(name: "GetProduct", description: "Get a product") {
686
+ product(id: $id) {
687
+ id
688
+ title
689
+ __typename
690
+ }
691
+ }
692
+ `;
693
+
694
+ const { client, host } = await setup({ query });
695
+ using _host = host;
696
+
697
+ host.mockToolCall("execute", () => ({
698
+ structuredContent: {
699
+ data: {
700
+ product: { id: "1", title: "Updated Pen", __typename: "Product" },
701
+ },
702
+ },
703
+ }));
704
+
705
+ host.sendToolResult({
706
+ structuredContent: {
707
+ result: {
708
+ data: {
709
+ product: { id: "1", title: "Pen", __typename: "Product" },
710
+ },
711
+ },
712
+ },
713
+ });
714
+ host.sendToolInput({ arguments: { id: "1" } });
715
+
716
+ await client.connect();
717
+
718
+ await client.query({
719
+ query,
720
+ variables: { id: "1" },
721
+ fetchPolicy: "network-only",
722
+ });
723
+
724
+ await expect(
725
+ client.query({
726
+ query,
727
+ variables: { id: "1" },
728
+ fetchPolicy: "network-only",
729
+ })
730
+ ).resolves.toStrictEqual({
731
+ data: {
732
+ product: { id: "1", title: "Updated Pen", __typename: "Product" },
733
+ },
734
+ });
735
+ });
736
+
737
+ test("serves tool result data on cache-and-network query without calling execute tool", async () => {
738
+ using _ = spyOnConsole("debug");
739
+ const query = gql`
740
+ query Product($id: ID!)
741
+ @tool(name: "GetProduct", description: "Get a product") {
742
+ product(id: $id) {
743
+ id
744
+ title
745
+ __typename
746
+ }
747
+ }
748
+ `;
749
+
750
+ const { client, host } = await setup({ query });
751
+ using _host = host;
752
+
753
+ const execute = vi.fn();
754
+ host.mockToolCall("execute", execute);
755
+
756
+ host.sendToolResult({
757
+ structuredContent: {
758
+ result: {
759
+ data: {
760
+ product: { id: "1", title: "Pen", __typename: "Product" },
761
+ },
762
+ },
763
+ },
764
+ });
765
+ host.sendToolInput({ arguments: { id: "1" } });
766
+
767
+ await client.connect();
768
+
769
+ const stream = new ObservableStream(
770
+ client.watchQuery({
771
+ query,
772
+ variables: { id: "1" },
773
+ fetchPolicy: "cache-and-network",
774
+ })
775
+ );
776
+
777
+ // The hydrated result is emitted synchronously so we won't observe a loading
778
+ // state like we normally would with `cache-and-network`
779
+ await expect(stream).toEmitValue({
780
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
781
+ dataState: "complete",
782
+ loading: false,
783
+ networkStatus: NetworkStatus.ready,
784
+ partial: false,
785
+ });
786
+
787
+ await expect(stream).not.toEmitAnything();
788
+
789
+ expect(execute).not.toHaveBeenCalled();
790
+ });
791
+
792
+ test("serves tool result data on no-cache query without calling execute tool and does not write to cache", async () => {
793
+ using _ = spyOnConsole("debug");
794
+ const query = gql`
795
+ query Product($id: ID!)
796
+ @tool(name: "GetProduct", description: "Get a product") {
797
+ product(id: $id) {
798
+ id
799
+ title
800
+ __typename
801
+ }
802
+ }
803
+ `;
804
+
805
+ const data = {
806
+ product: { id: "1", title: "Pen", __typename: "Product" },
807
+ };
808
+
809
+ const { client, host } = await setup({ query });
810
+ using _host = host;
811
+
812
+ const execute = vi.fn();
813
+ host.mockToolCall("execute", execute);
814
+
815
+ host.sendToolInput({ arguments: { id: "1" } });
816
+ host.sendToolResult({ structuredContent: { result: { data } } });
817
+
818
+ await client.connect();
819
+
820
+ await expect(
821
+ client.query({ query, variables: { id: "1" }, fetchPolicy: "no-cache" })
822
+ ).resolves.toStrictEqual({ data });
823
+ expect(execute).not.toHaveBeenCalled();
824
+ expect(client.extract()).toStrictEqual({});
825
+ });
826
+
827
+ test("serves hydrated query from tool result while other pending network-only queries call execute", async () => {
828
+ using _ = spyOnConsole("debug");
829
+
830
+ const productQuery = gql`
831
+ query Product($id: ID!)
832
+ @tool(name: "GetProduct", description: "Get a product") {
833
+ product(id: $id) {
834
+ id
835
+ title
836
+ __typename
837
+ }
838
+ }
839
+ `;
840
+
841
+ const cartQuery = gql`
842
+ query Cart @tool(name: "GetCart", description: "Get the cart") {
843
+ cart {
844
+ id
845
+ __typename
846
+ }
847
+ }
848
+ `;
849
+
850
+ const productOperation = parseManifestOperation(productQuery);
851
+ const cartOperation = parseManifestOperation(cartQuery);
852
+
853
+ const client = new ApolloClient({
854
+ cache: new InMemoryCache(),
855
+ manifest: mockApplicationManifest({
856
+ operations: [productOperation, cartOperation],
857
+ }),
858
+ });
859
+
860
+ using host = await mockMcpHost({
861
+ hostContext: minimalHostContextWithToolName("GetProduct"),
862
+ });
863
+ host.onCleanup(() => client.stop());
864
+
865
+ const execute = vi.fn(() => ({
866
+ structuredContent: {
867
+ data: { cart: { id: "1", __typename: "Cart" } },
868
+ },
869
+ }));
870
+ host.mockToolCall("execute", execute);
871
+ host.sendToolResult({
872
+ structuredContent: {
873
+ result: {
874
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
875
+ },
876
+ },
877
+ });
878
+ host.sendToolInput({ arguments: { id: "1" } });
879
+
880
+ await client.connect();
881
+
882
+ const productPromise = client.query({
883
+ query: productQuery,
884
+ variables: { id: "1" },
885
+ fetchPolicy: "network-only",
886
+ });
887
+
888
+ const cartPromise = client.query({
889
+ query: cartQuery,
890
+ fetchPolicy: "network-only",
891
+ });
892
+
893
+ await expect(productPromise).resolves.toStrictEqual({
894
+ data: { product: { id: "1", title: "Pen", __typename: "Product" } },
895
+ });
896
+ await expect(cartPromise).resolves.toStrictEqual({
897
+ data: { cart: { id: "1", __typename: "Cart" } },
898
+ });
899
+ expect(execute).toHaveBeenCalledOnce();
900
+ });
901
+
902
+ test("hydrates prefetch query with network-only fetch policy", async () => {
903
+ using _ = spyOnConsole("debug");
904
+
905
+ const query = gql`
906
+ query TopProducts @tool(description: "Shows top products") @prefetch {
907
+ topProducts {
908
+ id
909
+ title
910
+ __typename
911
+ }
912
+ }
913
+ `;
914
+
915
+ const data = {
916
+ topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
917
+ };
918
+
919
+ const { client, host } = await setup({ query, toolName: "OtherTool" });
920
+ using _host = host;
921
+
922
+ const execute = vi.fn();
923
+ host.mockToolCall("execute", execute);
924
+
925
+ host.sendToolResult({
926
+ structuredContent: {
927
+ prefetch: { __anonymous: { data } },
928
+ },
929
+ });
930
+ host.sendToolInput({ arguments: {} });
931
+
932
+ await client.connect();
933
+
934
+ await expect(
935
+ client.query({ query, fetchPolicy: "network-only" })
936
+ ).resolves.toStrictEqual({
937
+ data: {
938
+ topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
939
+ },
940
+ });
941
+ expect(execute).not.toHaveBeenCalled();
942
+ });
943
+
467
944
  describe("watchQuery dev warnings", () => {
468
945
  const query = gql`
469
946
  query Products($category: String!, $page: Int!, $sortBy: String!)
@@ -565,3 +1042,26 @@ describe("watchQuery dev warnings", () => {
565
1042
  expect(console.warn).toHaveBeenCalledTimes(1);
566
1043
  });
567
1044
  });
1045
+
1046
+ async function setup({
1047
+ query,
1048
+ toolName,
1049
+ }: {
1050
+ query: DocumentNode;
1051
+ toolName?: string;
1052
+ }) {
1053
+ const operation = parseManifestOperation(query);
1054
+ const client = new ApolloClient({
1055
+ cache: new InMemoryCache(),
1056
+ manifest: mockApplicationManifest({ operations: [operation] }),
1057
+ });
1058
+
1059
+ const host = await mockMcpHost({
1060
+ hostContext: minimalHostContextWithToolName(
1061
+ toolName ?? operation.tools[0].name
1062
+ ),
1063
+ });
1064
+ host.onCleanup(() => client.stop());
1065
+
1066
+ return { client, host };
1067
+ }