@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,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",
@@ -468,7 +440,8 @@ test("reads result data from _meta.structuredContent", async () => {
468
440
  using _ = spyOnConsole("debug");
469
441
 
470
442
  const query = gql`
471
- query Product($id: ID!) {
443
+ query Product($id: ID!)
444
+ @tool(name: "GetProduct", description: "Get a product") {
472
445
  product(id: $id) @private {
473
446
  id
474
447
  title
@@ -477,30 +450,10 @@ test("reads result data from _meta.structuredContent", async () => {
477
450
  }
478
451
  `;
479
452
 
480
- const client = new ApolloClient({
481
- cache: new InMemoryCache(),
482
- manifest: mockApplicationManifest({
483
- operations: [
484
- {
485
- id: "1",
486
- name: "Product",
487
- body: print(query),
488
- type: "query",
489
- prefetch: false,
490
- variables: { id: "ID" },
491
- tools: [{ name: "GetProduct", description: "Get a product" }],
492
- },
493
- ],
494
- }),
495
- });
496
-
497
- using host = await mockMcpHost({
498
- hostContext: minimalHostContextWithToolName("GetProduct"),
499
- });
500
- host.onCleanup(() => client.stop());
453
+ const { client, host } = await setup({ query });
454
+ using _host = host;
501
455
 
502
456
  host.sendToolResult({
503
- content: [],
504
457
  structuredContent: {},
505
458
  _meta: {
506
459
  toolName: "GetProduct",
@@ -517,6 +470,12 @@ test("reads result data from _meta.structuredContent", async () => {
517
470
 
518
471
  await client.connect();
519
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
+
520
479
  expect(client.extract()).toEqual({
521
480
  "Product:1": {
522
481
  __typename: "Product",
@@ -536,7 +495,7 @@ test("merges prefetch from structuredContent and result from _meta.structuredCon
536
495
  using _ = spyOnConsole("debug");
537
496
 
538
497
  const prefetchQuery = gql`
539
- query TopProducts {
498
+ query TopProducts @tool(description: "Shows top products") @prefetch {
540
499
  topProducts {
541
500
  id
542
501
  title
@@ -546,7 +505,8 @@ test("merges prefetch from structuredContent and result from _meta.structuredCon
546
505
  `;
547
506
 
548
507
  const query = gql`
549
- query Product($id: ID!) {
508
+ query Product($id: ID!)
509
+ @tool(name: "GetProduct", description: "Get a product") {
550
510
  product(id: $id) @private {
551
511
  id
552
512
  title
@@ -559,25 +519,8 @@ test("merges prefetch from structuredContent and result from _meta.structuredCon
559
519
  cache: new InMemoryCache(),
560
520
  manifest: mockApplicationManifest({
561
521
  operations: [
562
- {
563
- id: "1",
564
- name: "TopProducts",
565
- body: print(prefetchQuery),
566
- type: "query",
567
- prefetch: true,
568
- prefetchID: "__anonymous",
569
- variables: {},
570
- tools: [{ name: "TopProducts", description: "Shows top products" }],
571
- },
572
- {
573
- id: "2",
574
- name: "Product",
575
- body: print(query),
576
- type: "query",
577
- prefetch: false,
578
- variables: { id: "ID" },
579
- tools: [{ name: "GetProduct", description: "Get a product" }],
580
- },
522
+ parseManifestOperation(prefetchQuery),
523
+ parseManifestOperation(query),
581
524
  ],
582
525
  }),
583
526
  });
@@ -588,7 +531,6 @@ test("merges prefetch from structuredContent and result from _meta.structuredCon
588
531
  host.onCleanup(() => client.stop());
589
532
 
590
533
  host.sendToolResult({
591
- content: [],
592
534
  structuredContent: {
593
535
  prefetch: {
594
536
  __anonymous: {
@@ -613,7 +555,13 @@ test("merges prefetch from structuredContent and result from _meta.structuredCon
613
555
 
614
556
  await client.connect();
615
557
 
616
- expect(client.extract()).toEqual({
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({
617
565
  "Product:1": {
618
566
  __typename: "Product",
619
567
  id: "1",
@@ -637,7 +585,7 @@ test("_meta.structuredContent wins over structuredContent", async () => {
637
585
 
638
586
  const query = gql`
639
587
  query Product($id: ID!) {
640
- product(id: $id) {
588
+ product(id: $id) @tool(name: "GetProduct", description: "Get a product") {
641
589
  id
642
590
  title @private
643
591
  __typename
@@ -645,30 +593,10 @@ test("_meta.structuredContent wins over structuredContent", async () => {
645
593
  }
646
594
  `;
647
595
 
648
- const client = new ApolloClient({
649
- cache: new InMemoryCache(),
650
- manifest: mockApplicationManifest({
651
- operations: [
652
- {
653
- id: "1",
654
- name: "Product",
655
- body: print(query),
656
- type: "query",
657
- prefetch: false,
658
- variables: { id: "ID" },
659
- tools: [{ name: "GetProduct", description: "Get a product" }],
660
- },
661
- ],
662
- }),
663
- });
664
-
665
- using host = await mockMcpHost({
666
- hostContext: minimalHostContextWithToolName("GetProduct"),
667
- });
668
- host.onCleanup(() => client.stop());
596
+ const { client, host } = await setup({ query });
597
+ using _host = host;
669
598
 
670
599
  host.sendToolResult({
671
- content: [],
672
600
  structuredContent: {
673
601
  result: {
674
602
  data: {
@@ -691,6 +619,12 @@ test("_meta.structuredContent wins over structuredContent", async () => {
691
619
 
692
620
  await client.connect();
693
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
+
694
628
  expect(client.extract()).toEqual({
695
629
  "Product:1": {
696
630
  __typename: "Product",
@@ -706,6 +640,307 @@ test("_meta.structuredContent wins over structuredContent", async () => {
706
640
  });
707
641
  });
708
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
+
709
944
  describe("watchQuery dev warnings", () => {
710
945
  const query = gql`
711
946
  query Products($category: String!, $page: Int!, $sortBy: String!)
@@ -807,3 +1042,26 @@ describe("watchQuery dev warnings", () => {
807
1042
  expect(console.warn).toHaveBeenCalledTimes(1);
808
1043
  });
809
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
+ }
package/src/mcp/index.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { ApolloClient } from "./core/ApolloClient.js";
2
- export { ToolCallLink } from "./link/ToolCallLink.js";