@apollo/client-ai-apps 0.6.5 → 0.7.1

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 +70 -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 +17 -26
  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
@@ -4,16 +4,14 @@ import type {
4
4
  OperationVariables,
5
5
  TypedDocumentNode,
6
6
  } from "@apollo/client";
7
- import { useApolloClient } from "./useApolloClient.js";
8
- import { useToolName } from "./useToolName.js";
9
- import { isReactive } from "../../../react/reactive.js";
10
- import type { Reactive } from "../../../react/reactive.js";
7
+ import { useApolloClient } from "./hooks/internal/useApolloClient.js";
8
+ import { isReactive, type Reactive } from "./reactive.js";
11
9
  import { equal } from "@wry/equality";
12
10
  import { __DEV__ } from "@apollo/client/utilities/environment";
13
11
  import {
14
12
  getToolNamesFromDocument,
15
13
  getVariableNamesFromDocument,
16
- } from "../../../utilities/index.js";
14
+ } from "../utilities/index.js";
17
15
 
18
16
  type HydratedVariablesInput<TVariables> = {
19
17
  [K in keyof TVariables]: TVariables[K] | Reactive<TVariables[K]>;
@@ -48,13 +46,12 @@ export function createHydrationUtils<
48
46
  setVariables: SetVariables<StateVariables<TVariables, TInputVariables>>,
49
47
  ] {
50
48
  const client = useApolloClient();
51
- const toolName = useToolName();
52
- const [toolInput] = useState(() => client.toolInput);
49
+ const [toolInput] = useState(() => client["hydratedToolInput"]);
53
50
 
54
51
  const toolMatches =
55
52
  toolInput !== undefined &&
56
- toolName !== undefined &&
57
- documentToolNames.has(toolName);
53
+ client.toolInfo?.toolName !== undefined &&
54
+ documentToolNames.has(client.toolInfo.toolName);
58
55
 
59
56
  const [stateVars, setStateVars] = useState<Record<string, unknown>>(() => {
60
57
  const values: Record<string, unknown> = {};
@@ -125,7 +122,7 @@ export function createHydrationUtils<
125
122
  // present, so both paths are idempotent.
126
123
  useLayoutEffect(() => {
127
124
  if (toolMatches) {
128
- client.clearToolInput();
125
+ client["clearHydratedToolInput"]();
129
126
  }
130
127
  // eslint-disable-next-line react-hooks/exhaustive-deps
131
128
  }, []);
@@ -0,0 +1,46 @@
1
+ import { test, expect } from "vitest";
2
+ import {
3
+ disableActEnvironment,
4
+ renderHookToSnapshotStream,
5
+ } from "@testing-library/react-render-stream";
6
+ import { Suspense } from "react";
7
+ import { InMemoryCache } from "@apollo/client";
8
+ import { App } from "@modelcontextprotocol/ext-apps";
9
+
10
+ import { useApp } from "../useApp.js";
11
+ import {
12
+ eachHostEnv,
13
+ mockApplicationManifest,
14
+ spyOnConsole,
15
+ } from "../../../testing/internal/index.js";
16
+ import { ApolloProvider } from "../../ApolloProvider.js";
17
+
18
+ eachHostEnv((setupHost, ApolloClient) => {
19
+ test("returns app instance created by ApolloClient", async () => {
20
+ using _ = spyOnConsole("debug");
21
+ const client = new ApolloClient({
22
+ cache: new InMemoryCache(),
23
+ manifest: mockApplicationManifest(),
24
+ });
25
+
26
+ using env = await setupHost({
27
+ client,
28
+ toolCall: { name: "Test", result: { structuredContent: {} } },
29
+ });
30
+ const { host, params } = env;
31
+
32
+ host.sendToolInput(params.toolInput);
33
+ host.sendToolResult(params.toolResult);
34
+
35
+ using _disabledAct = disableActEnvironment();
36
+ const { takeSnapshot } = await renderHookToSnapshotStream(() => useApp(), {
37
+ wrapper: ({ children }) => (
38
+ <Suspense>
39
+ <ApolloProvider client={client}>{children}</ApolloProvider>
40
+ </Suspense>
41
+ ),
42
+ });
43
+
44
+ await expect(takeSnapshot()).resolves.toBeInstanceOf(App);
45
+ });
46
+ });
@@ -0,0 +1,99 @@
1
+ import { expect, test } from "vitest";
2
+ import { InMemoryCache } from "@apollo/client";
3
+ import {
4
+ eachHostEnv,
5
+ minimalHostContextWithToolName,
6
+ mockApplicationManifest,
7
+ spyOnConsole,
8
+ } from "../../../testing/internal/index.js";
9
+ import {
10
+ disableActEnvironment,
11
+ renderHookToSnapshotStream,
12
+ } from "@testing-library/react-render-stream";
13
+ import { useHostContext } from "../useHostContext.js";
14
+ import { ApolloProvider } from "../../ApolloProvider.js";
15
+
16
+ eachHostEnv((setupHost, ApolloClient) => {
17
+ test("returns the host context from the host", async () => {
18
+ using _ = spyOnConsole("debug");
19
+ const client = new ApolloClient({
20
+ cache: new InMemoryCache(),
21
+ manifest: mockApplicationManifest(),
22
+ });
23
+
24
+ using env = await setupHost({
25
+ client,
26
+ toolCall: {
27
+ name: "GetProduct",
28
+ result: { structuredContent: { result: { data: { product: null } } } },
29
+ },
30
+ hostContext: { theme: "light" },
31
+ });
32
+ const { host, params } = env;
33
+
34
+ host.sendToolInput(params.toolInput);
35
+ host.sendToolResult(params.toolResult);
36
+
37
+ using _disabledAct = disableActEnvironment();
38
+ const { takeSnapshot } = await renderHookToSnapshotStream(
39
+ () => useHostContext(),
40
+ {
41
+ wrapper: ({ children }) => (
42
+ <ApolloProvider client={client}>{children}</ApolloProvider>
43
+ ),
44
+ }
45
+ );
46
+
47
+ await expect(takeSnapshot()).resolves.toStrictEqual({
48
+ ...minimalHostContextWithToolName("GetProduct"),
49
+ theme: "light",
50
+ });
51
+
52
+ await expect(takeSnapshot).not.toRerender();
53
+ });
54
+
55
+ test("rerenders when the host context changes", async () => {
56
+ using _ = spyOnConsole("debug");
57
+ const client = new ApolloClient({
58
+ cache: new InMemoryCache(),
59
+ manifest: mockApplicationManifest(),
60
+ });
61
+
62
+ using env = await setupHost({
63
+ client,
64
+ toolCall: {
65
+ name: "GetProduct",
66
+ result: { structuredContent: { result: { data: { product: null } } } },
67
+ },
68
+ hostContext: { theme: "light" },
69
+ });
70
+ const { host, params } = env;
71
+
72
+ host.sendToolInput(params.toolInput);
73
+ host.sendToolResult(params.toolResult);
74
+
75
+ using _disabledAct = disableActEnvironment();
76
+ const { takeSnapshot } = await renderHookToSnapshotStream(
77
+ () => useHostContext(),
78
+ {
79
+ wrapper: ({ children }) => (
80
+ <ApolloProvider client={client}>{children}</ApolloProvider>
81
+ ),
82
+ }
83
+ );
84
+
85
+ await expect(takeSnapshot()).resolves.toStrictEqual({
86
+ ...minimalHostContextWithToolName("GetProduct"),
87
+ theme: "light",
88
+ });
89
+
90
+ host.sendHostContextChanged({ theme: "dark" });
91
+
92
+ await expect(takeSnapshot()).resolves.toStrictEqual({
93
+ ...minimalHostContextWithToolName("GetProduct"),
94
+ theme: "dark",
95
+ });
96
+
97
+ await expect(takeSnapshot).not.toRerender();
98
+ });
99
+ });
@@ -0,0 +1,98 @@
1
+ import { test, expect } from "vitest";
2
+ import {
3
+ disableActEnvironment,
4
+ renderHookToSnapshotStream,
5
+ } from "@testing-library/react-render-stream";
6
+ import { Suspense } from "react";
7
+ import { InMemoryCache } from "@apollo/client";
8
+
9
+ import { useToolInfo } from "../useToolInfo.js";
10
+ import {
11
+ eachHostEnv,
12
+ mockApplicationManifest,
13
+ spyOnConsole,
14
+ } from "../../../testing/internal/index.js";
15
+ import { ApolloProvider } from "../../ApolloProvider.js";
16
+
17
+ eachHostEnv((setupHost, ApolloClient, { hostEnv }) => {
18
+ test("returns tool name and input combined", async () => {
19
+ using _ = spyOnConsole("debug");
20
+ const client = new ApolloClient({
21
+ cache: new InMemoryCache(),
22
+ manifest: mockApplicationManifest(),
23
+ });
24
+
25
+ using env = await setupHost({
26
+ client,
27
+ toolCall: {
28
+ name: "GetProduct",
29
+ input: { id: "1" },
30
+ result: { structuredContent: { result: { data: { product: null } } } },
31
+ },
32
+ });
33
+ const { host, params } = env;
34
+
35
+ host.sendToolInput(params.toolInput);
36
+ host.sendToolResult(params.toolResult);
37
+
38
+ using _disabledAct = disableActEnvironment();
39
+ const { takeSnapshot } = await renderHookToSnapshotStream(
40
+ () => useToolInfo(),
41
+ {
42
+ wrapper: ({ children }) => (
43
+ <Suspense>
44
+ <ApolloProvider client={client}>{children}</ApolloProvider>
45
+ </Suspense>
46
+ ),
47
+ }
48
+ );
49
+
50
+ await expect(takeSnapshot()).resolves.toEqual({
51
+ toolName: "GetProduct",
52
+ toolInput: { id: "1" },
53
+ });
54
+
55
+ await expect(takeSnapshot).not.toRerender();
56
+ });
57
+
58
+ if (hostEnv === "openai") {
59
+ test("returns undefined toolInput when toolInput is not provided", async () => {
60
+ using _ = spyOnConsole("debug");
61
+ const client = new ApolloClient({
62
+ cache: new InMemoryCache(),
63
+ manifest: mockApplicationManifest(),
64
+ });
65
+
66
+ using env = await setupHost({
67
+ client,
68
+ toolCall: {
69
+ name: "GetProduct",
70
+ result: {
71
+ structuredContent: { result: { data: { product: null } } },
72
+ },
73
+ },
74
+ });
75
+ const { host, params } = env;
76
+
77
+ host.sendToolResult(params.toolResult);
78
+
79
+ using _disabledAct = disableActEnvironment();
80
+ const { takeSnapshot } = await renderHookToSnapshotStream(
81
+ () => useToolInfo(),
82
+ {
83
+ wrapper: ({ children }) => (
84
+ <Suspense>
85
+ <ApolloProvider client={client}>{children}</ApolloProvider>
86
+ </Suspense>
87
+ ),
88
+ }
89
+ );
90
+
91
+ await expect(takeSnapshot()).resolves.toEqual({
92
+ toolName: "GetProduct",
93
+ toolInput: undefined,
94
+ });
95
+ await expect(takeSnapshot).not.toRerender();
96
+ });
97
+ }
98
+ });
@@ -0,0 +1,58 @@
1
+ import { test, expect } from "vitest";
2
+ import {
3
+ disableActEnvironment,
4
+ renderHookToSnapshotStream,
5
+ } from "@testing-library/react-render-stream";
6
+ import { Suspense } from "react";
7
+ import { InMemoryCache } from "@apollo/client";
8
+ import { useToolMetadata } from "../useToolMetadata.js";
9
+ import {
10
+ eachHostEnv,
11
+ mockApplicationManifest,
12
+ spyOnConsole,
13
+ } from "../../../testing/internal/index.js";
14
+ import { ApolloProvider } from "../../ApolloProvider.js";
15
+
16
+ eachHostEnv((setupHost, ApolloClient) => {
17
+ test("returns the tool metadata from the MCP host", async () => {
18
+ using _ = spyOnConsole("debug");
19
+ const client = new ApolloClient({
20
+ cache: new InMemoryCache(),
21
+ manifest: mockApplicationManifest(),
22
+ });
23
+
24
+ using env = await setupHost({
25
+ client,
26
+ toolCall: {
27
+ name: "TestTool",
28
+ result: {
29
+ structuredContent: {},
30
+ _meta: { customField: "customValue" },
31
+ },
32
+ },
33
+ });
34
+ const { host, params } = env;
35
+
36
+ host.sendToolInput(params.toolInput);
37
+ host.sendToolResult(params.toolResult);
38
+
39
+ using _disabledAct = disableActEnvironment();
40
+ const { takeSnapshot } = await renderHookToSnapshotStream(
41
+ () => useToolMetadata(),
42
+ {
43
+ wrapper: ({ children }) => (
44
+ <Suspense>
45
+ <ApolloProvider client={client}>{children}</ApolloProvider>
46
+ </Suspense>
47
+ ),
48
+ }
49
+ );
50
+
51
+ await expect(takeSnapshot()).resolves.toEqual({
52
+ toolName: "TestTool",
53
+ customField: "customValue",
54
+ });
55
+
56
+ await expect(takeSnapshot).not.toRerender();
57
+ });
58
+ });
@@ -1,9 +1,9 @@
1
1
  import { useApolloClient as useBaseApolloClient } from "@apollo/client/react";
2
- import { ApolloClient } from "../../core/ApolloClient.js";
2
+ import { AbstractApolloClient } from "../../../core/AbstractApolloClient.js";
3
3
  import { aiClientSymbol, invariant } from "../../../utilities/index.js";
4
4
 
5
- export function useApolloClient(override?: ApolloClient) {
6
- const client = useBaseApolloClient(override) as ApolloClient;
5
+ export function useApolloClient() {
6
+ const client = useBaseApolloClient() as AbstractApolloClient;
7
7
 
8
8
  invariant(
9
9
  client[aiClientSymbol],
@@ -1,4 +1,4 @@
1
- import { useApolloClient } from "./useApolloClient.js";
1
+ import { useApolloClient } from "./internal/useApolloClient.js";
2
2
 
3
3
  export function useApp() {
4
4
  return useApolloClient()["appManager"].app;
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useSyncExternalStore } from "react";
2
- import { useApolloClient } from "./useApolloClient";
2
+ import { useApolloClient } from "./internal/useApolloClient";
3
3
 
4
4
  export function useHostContext() {
5
5
  const appManager = useApolloClient()["appManager"];
@@ -0,0 +1,6 @@
1
+ import type { ToolInfo } from "../../core/typeRegistration.js";
2
+ import { useApolloClient } from "./internal/useApolloClient.js";
3
+
4
+ export function useToolInfo(): ToolInfo | undefined {
5
+ return useApolloClient().toolInfo;
6
+ }
@@ -0,0 +1,5 @@
1
+ import { useApolloClient } from "./internal/useApolloClient.js";
2
+
3
+ export function useToolMetadata() {
4
+ return useApolloClient().toolMetadata;
5
+ }
@@ -1,40 +1,9 @@
1
- import { missingHook } from "./missingHook.js";
2
-
3
1
  export { ApolloProvider } from "./ApolloProvider.js";
4
2
  export { reactive } from "./reactive.js";
5
3
  export type { Reactive } from "./reactive.js";
6
4
 
7
- // Use `mcp` related types since these are the most common between the two
8
- // targets
9
- export const useApp =
10
- missingHook<typeof import("./index.mcp.js").useApp>("useApp");
11
-
12
- export const useHostContext =
13
- missingHook<typeof import("./index.mcp.js").useHostContext>("useHostContext");
14
-
15
- /**
16
- * @deprecated Please use the `useToolInfo` hook. `useToolInput` will be removed
17
- * in the next major version.
18
- */
19
- export const useToolInput =
20
- missingHook<typeof import("./index.mcp.js").useToolInput>("useToolInput");
21
-
22
- export const useToolMetadata =
23
- missingHook<typeof import("./index.mcp.js").useToolMetadata>(
24
- "useToolMetadata"
25
- );
26
-
27
- /**
28
- * @deprecated Please use the `useToolInfo` hook. `useToolName` will be removed
29
- * in the next major version.
30
- */
31
- export const useToolName =
32
- missingHook<typeof import("./index.mcp.js").useToolName>("useToolName");
33
-
34
- export const useToolInfo =
35
- missingHook<typeof import("./index.mcp.js").useToolInfo>("useToolInfo");
36
-
37
- /** @experimental */
38
- export const createHydrationUtils = missingHook<
39
- typeof import("./index.mcp.js").createHydrationUtils
40
- >("createHydrationUtils");
5
+ export { useApp } from "./hooks/useApp.js";
6
+ export { useHostContext } from "./hooks/useHostContext.js";
7
+ export { useToolInfo } from "./hooks/useToolInfo.js";
8
+ export { useToolMetadata } from "./hooks/useToolMetadata.js";
9
+ export { createHydrationUtils } from "./createHydrationUtils.js";
@@ -0,0 +1,87 @@
1
+ import {
2
+ Kind,
3
+ print,
4
+ visit,
5
+ type DirectiveNode,
6
+ type DocumentNode,
7
+ type OperationDefinitionNode,
8
+ } from "graphql";
9
+ import type {
10
+ ManifestOperation,
11
+ ManifestTool,
12
+ } from "../../../types/application-manifest";
13
+ import {
14
+ getOperationDefinition,
15
+ removeDirectivesFromDocument,
16
+ } from "@apollo/client/utilities/internal";
17
+ import * as crypto from "node:crypto";
18
+ import {
19
+ getDirectiveArgument,
20
+ getTypeName,
21
+ maybeGetArgumentValue,
22
+ } from "../../../vite/utilities/graphql.js";
23
+ import { invariant } from "../../../utilities/index.js";
24
+
25
+ export function parseManifestOperation(
26
+ document: DocumentNode
27
+ ): ManifestOperation {
28
+ const operation = getOperationDefinition(document);
29
+ invariant(operation, "Must provide an operation to the document");
30
+
31
+ const variables: ManifestOperation["variables"] = {};
32
+ const tools: ManifestTool[] = [];
33
+ let prefetch = false;
34
+
35
+ const modified = removeDirectivesFromDocument(
36
+ [{ name: "prefetch" }, { name: "tool" }],
37
+ visit(document, {
38
+ Directive(node) {
39
+ if (node.name.value === "tool") {
40
+ tools.push(parseToolDefinition(node, operation));
41
+ }
42
+ prefetch ||= node.name.value === "prefetch";
43
+ },
44
+ VariableDefinition(node) {
45
+ variables[node.variable.name.value] = getTypeName(node.type);
46
+ },
47
+ })
48
+ )!;
49
+
50
+ const body = print(modified);
51
+ const hash = crypto.createHash("sha256").update(body).digest("hex");
52
+
53
+ const manifestOperation: ManifestOperation = {
54
+ id: hash,
55
+ name: operation.name!.value,
56
+ body,
57
+ type: operation.operation as "query" | "mutation",
58
+ prefetch,
59
+ variables,
60
+ tools,
61
+ };
62
+
63
+ if (prefetch) {
64
+ manifestOperation.prefetchID = "__anonymous";
65
+ }
66
+
67
+ return manifestOperation;
68
+ }
69
+
70
+ function parseToolDefinition(
71
+ directive: DirectiveNode,
72
+ operation: OperationDefinitionNode
73
+ ): ManifestTool {
74
+ const nameArg = maybeGetArgumentValue(
75
+ getDirectiveArgument("name", directive),
76
+ Kind.STRING
77
+ );
78
+ const descriptionArg = maybeGetArgumentValue(
79
+ getDirectiveArgument("description", directive),
80
+ Kind.STRING
81
+ );
82
+
83
+ return {
84
+ name: nameArg ?? operation.name!.value,
85
+ description: descriptionArg ?? operation.description!.value,
86
+ };
87
+ }
@@ -1,10 +1,13 @@
1
+ export { createHostEnv } from "./utilities/createHostEnv.js";
1
2
  export { dispatchStateChange } from "./openai/dispatchStateChange.js";
3
+ export { eachHostEnv } from "./tests/eachHostEnv.js";
2
4
  export { graphqlToolResult } from "./mcp/graphqlToolResult.js";
3
5
  export { mockApplicationManifest } from "./utilities/mockApplicationManifest.js";
4
6
  export { stubOpenAiGlobals } from "./openai/stubOpenAiGlobals.js";
5
7
  export { minimalHostContextWithToolName } from "./mcp/minimalHostContextWithToolName.js";
6
8
  export { mockMcpHost } from "./mcp/mockMcpHost.js";
7
9
  export { ObservableStream } from "./utilities/ObservableStream.js";
10
+ export { parseManifestOperation } from "./graphql/parseManifestOperation.js";
8
11
  export { renderAsync } from "./react/renderAsync.js";
9
12
  export { spyOnConsole } from "./utilities/spyOnConsole.js";
10
13
  export { wait } from "./utilities/wait.js";
@@ -1,3 +1,4 @@
1
1
  import "./toComplete.js";
2
+ import "./toEmitAnything.js";
2
3
  import "./toEmitValue.js";
3
4
  import "./toRerender.js";
@@ -0,0 +1,43 @@
1
+ import { expect } from "vitest";
2
+ import type {
3
+ ObservableStream,
4
+ TakeOptions,
5
+ } from "../utilities/ObservableStream";
6
+
7
+ expect.extend({
8
+ async toEmitAnything(actual, options: TakeOptions) {
9
+ const stream = actual as ObservableStream<any>;
10
+ const hint = this.utils.matcherHint("toEmitAnything", "stream", "", {
11
+ isNot: this.isNot,
12
+ });
13
+
14
+ try {
15
+ const value = await stream.peek(options);
16
+
17
+ return {
18
+ pass: true,
19
+ message: () => {
20
+ return (
21
+ hint +
22
+ "\n\nExpected stream not to emit anything but it did." +
23
+ "\n\nReceived:\n" +
24
+ this.utils.printReceived(value)
25
+ );
26
+ },
27
+ };
28
+ } catch (error) {
29
+ if (
30
+ error instanceof Error &&
31
+ error.message === "Timeout waiting for next event"
32
+ ) {
33
+ return {
34
+ pass: false,
35
+ message: () =>
36
+ hint + "\n\nExpected stream to emit an event but it did not.",
37
+ };
38
+ } else {
39
+ throw error;
40
+ }
41
+ }
42
+ },
43
+ });
@@ -5,6 +5,7 @@ interface CustomMatchers<R = unknown> {
5
5
  toRerender: (options?: NextRenderOptions) => Promise<R>;
6
6
  toComplete: (options?: TakeOptions) => Promise<R>;
7
7
  toEmitValue: (expected: unknown, options?: TakeOptions) => Promise<R>;
8
+ toEmitAnything: (options?: TakeOptions) => Promise<R>;
8
9
  }
9
10
 
10
11
  declare module "vitest" {
@@ -14,14 +14,16 @@ import type {
14
14
  import { invariant, promiseWithResolvers } from "../../../utilities/index.js";
15
15
 
16
16
  export interface MockMcpHost extends Disposable {
17
- sendToolResult(params: McpUiToolResultNotification["params"]): Promise<void>;
17
+ sendToolResult(
18
+ params: Partial<McpUiToolResultNotification["params"]>
19
+ ): Promise<void>;
18
20
  sendToolInput(params: McpUiToolInputNotification["params"]): Promise<void>;
19
21
  sendHostContextChanged(
20
22
  params: McpUiHostContextChangedNotification["params"]
21
23
  ): Promise<void>;
22
24
  mockToolCall(
23
25
  name: string,
24
- handler: (params: CallToolRequest["params"]) => CallToolResult
26
+ handler: (params: CallToolRequest["params"]) => Partial<CallToolResult>
25
27
  ): () => void;
26
28
  /**
27
29
  * Register an MCP App to be closed during cleanup. This prevents the App's
@@ -100,7 +102,7 @@ export async function mockMcpHost(
100
102
  const cleanupFns = new Set<() => void>();
101
103
  const toolCallHandlers = new Map<
102
104
  string,
103
- (params: CallToolRequest["params"]) => CallToolResult
105
+ (params: CallToolRequest["params"]) => Partial<CallToolResult>
104
106
  >();
105
107
 
106
108
  const listener = (event: MessageEvent<unknown>) => {
@@ -137,10 +139,20 @@ export async function mockMcpHost(
137
139
  `mockMcpHost: A mock tool call handler for '${params.name}' is not registered.`
138
140
  );
139
141
 
142
+ const result = handler(params);
143
+
144
+ if (result.structuredContent && !result.content) {
145
+ result.content = [
146
+ { type: "text", text: JSON.stringify(result.structuredContent) },
147
+ ];
148
+ }
149
+
150
+ result.content ||= [];
151
+
140
152
  window.postMessage({
141
153
  jsonrpc: "2.0",
142
154
  id: data.id,
143
- result: handler(params),
155
+ result,
144
156
  });
145
157
  }
146
158
  };
@@ -156,6 +168,15 @@ export async function mockMcpHost(
156
168
  return {
157
169
  async sendToolResult(params) {
158
170
  await initialized;
171
+
172
+ if (params.structuredContent && !params.content) {
173
+ params.content = [
174
+ { type: "text", text: JSON.stringify(params.structuredContent) },
175
+ ];
176
+ }
177
+
178
+ params.content ||= [];
179
+
159
180
  window.postMessage({
160
181
  jsonrpc: "2.0",
161
182
  method: "ui/notifications/tool-result",