@apollo/client-ai-apps 0.5.2 → 0.5.3

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 (96) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/core/ApolloClient.d.ts +1 -1
  3. package/dist/core/ApolloClient.d.ts.map +1 -1
  4. package/dist/core/ApolloClient.js +1 -1
  5. package/dist/core/ApolloClient.js.map +1 -1
  6. package/dist/mcp/core/ApolloClient.d.ts +6 -1
  7. package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
  8. package/dist/mcp/core/ApolloClient.js +36 -3
  9. package/dist/mcp/core/ApolloClient.js.map +1 -1
  10. package/dist/mcp/core/McpAppManager.d.ts +2 -2
  11. package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
  12. package/dist/mcp/core/McpAppManager.js +3 -3
  13. package/dist/mcp/core/McpAppManager.js.map +1 -1
  14. package/dist/mcp/react/hooks/createHydrationUtils.d.ts +15 -0
  15. package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +1 -0
  16. package/dist/mcp/react/hooks/createHydrationUtils.js +113 -0
  17. package/dist/mcp/react/hooks/createHydrationUtils.js.map +1 -0
  18. package/dist/mcp/react/index.d.ts +1 -0
  19. package/dist/mcp/react/index.d.ts.map +1 -1
  20. package/dist/mcp/react/index.js +1 -0
  21. package/dist/mcp/react/index.js.map +1 -1
  22. package/dist/openai/core/ApolloClient.d.ts +6 -1
  23. package/dist/openai/core/ApolloClient.d.ts.map +1 -1
  24. package/dist/openai/core/ApolloClient.js +37 -3
  25. package/dist/openai/core/ApolloClient.js.map +1 -1
  26. package/dist/openai/core/McpAppManager.d.ts +2 -2
  27. package/dist/openai/core/McpAppManager.d.ts.map +1 -1
  28. package/dist/openai/core/McpAppManager.js +5 -5
  29. package/dist/openai/core/McpAppManager.js.map +1 -1
  30. package/dist/openai/react/hooks/createHydrationUtils.d.ts +15 -0
  31. package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +1 -0
  32. package/dist/openai/react/hooks/createHydrationUtils.js +113 -0
  33. package/dist/openai/react/hooks/createHydrationUtils.js.map +1 -0
  34. package/dist/openai/react/index.d.ts +1 -0
  35. package/dist/openai/react/index.d.ts.map +1 -1
  36. package/dist/openai/react/index.js +1 -0
  37. package/dist/openai/react/index.js.map +1 -1
  38. package/dist/react/ApolloProvider.js +1 -1
  39. package/dist/react/ApolloProvider.js.map +1 -1
  40. package/dist/react/index.d.ts +4 -0
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +3 -0
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react/index.mcp.d.ts +1 -1
  45. package/dist/react/index.mcp.d.ts.map +1 -1
  46. package/dist/react/index.mcp.js +1 -1
  47. package/dist/react/index.mcp.js.map +1 -1
  48. package/dist/react/index.openai.d.ts +1 -1
  49. package/dist/react/index.openai.d.ts.map +1 -1
  50. package/dist/react/index.openai.js +1 -1
  51. package/dist/react/index.openai.js.map +1 -1
  52. package/dist/react/reactive.d.ts +9 -0
  53. package/dist/react/reactive.d.ts.map +1 -0
  54. package/dist/react/reactive.js +11 -0
  55. package/dist/react/reactive.js.map +1 -0
  56. package/dist/utilities/getToolNamesFromDocument.d.ts +3 -0
  57. package/dist/utilities/getToolNamesFromDocument.d.ts.map +1 -0
  58. package/dist/utilities/getToolNamesFromDocument.js +12 -0
  59. package/dist/utilities/getToolNamesFromDocument.js.map +1 -0
  60. package/dist/utilities/getVariableNamesFromDocument.d.ts +3 -0
  61. package/dist/utilities/getVariableNamesFromDocument.d.ts.map +1 -0
  62. package/dist/utilities/getVariableNamesFromDocument.js +6 -0
  63. package/dist/utilities/getVariableNamesFromDocument.js.map +1 -0
  64. package/dist/utilities/index.d.ts +3 -0
  65. package/dist/utilities/index.d.ts.map +1 -1
  66. package/dist/utilities/index.js +3 -0
  67. package/dist/utilities/index.js.map +1 -1
  68. package/dist/utilities/warnOnVariableMismatch.d.ts +3 -0
  69. package/dist/utilities/warnOnVariableMismatch.d.ts.map +1 -0
  70. package/dist/utilities/warnOnVariableMismatch.js +10 -0
  71. package/dist/utilities/warnOnVariableMismatch.js.map +1 -0
  72. package/package.json +2 -1
  73. package/src/core/ApolloClient.ts +1 -1
  74. package/src/mcp/core/ApolloClient.ts +67 -2
  75. package/src/mcp/core/McpAppManager.ts +3 -3
  76. package/src/mcp/core/__tests__/ApolloClient.test.ts +109 -6
  77. package/src/mcp/link/__tests__/ToolCallLink.test.ts +1 -1
  78. package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +1228 -0
  79. package/src/mcp/react/hooks/createHydrationUtils.ts +182 -0
  80. package/src/mcp/react/index.ts +1 -0
  81. package/src/openai/core/ApolloClient.ts +68 -2
  82. package/src/openai/core/McpAppManager.ts +5 -5
  83. package/src/openai/core/__tests__/ApolloClient.test.ts +113 -7
  84. package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +1333 -0
  85. package/src/openai/react/hooks/createHydrationUtils.ts +182 -0
  86. package/src/openai/react/index.ts +1 -0
  87. package/src/react/ApolloProvider.tsx +1 -1
  88. package/src/react/index.mcp.ts +1 -0
  89. package/src/react/index.openai.ts +1 -0
  90. package/src/react/index.ts +7 -0
  91. package/src/react/reactive.ts +19 -0
  92. package/src/testing/internal/mcp/graphqlToolResult.ts +5 -5
  93. package/src/utilities/getToolNamesFromDocument.ts +15 -0
  94. package/src/utilities/getVariableNamesFromDocument.ts +9 -0
  95. package/src/utilities/index.ts +3 -0
  96. package/src/utilities/warnOnVariableMismatch.ts +20 -0
@@ -3,15 +3,24 @@ import {
3
3
  ApolloClient as BaseApolloClient,
4
4
  DocumentTransform,
5
5
  } from "@apollo/client";
6
+ import type {
7
+ WatchQueryOptions,
8
+ ObservableQuery,
9
+ OperationVariables,
10
+ } from "@apollo/client";
6
11
  import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal";
7
12
  import { parse } from "graphql";
13
+ import { equal } from "@wry/equality";
8
14
  import { __DEV__ } from "@apollo/client/utilities/environment";
9
15
  import type { ApplicationManifest } from "../../types/application-manifest.js";
10
16
  import { ToolCallLink } from "../link/ToolCallLink.js";
11
17
  import {
12
18
  aiClientSymbol,
13
19
  cacheAsync,
20
+ getToolNamesFromDocument,
21
+ getVariableNamesFromDocument,
14
22
  getVariablesForOperationFromToolInput,
23
+ warnOnVariableMismatch,
15
24
  } from "../../utilities/index.js";
16
25
  import { McpAppManager } from "./McpAppManager.js";
17
26
 
@@ -29,6 +38,8 @@ export class ApolloClient extends BaseApolloClient {
29
38
  /** @internal */
30
39
  readonly [aiClientSymbol] = true;
31
40
 
41
+ #toolInput: Record<string, unknown> | undefined;
42
+
32
43
  constructor(options: ApolloClient.Options) {
33
44
  const link = options.link ?? new ToolCallLink();
34
45
 
@@ -57,9 +68,63 @@ export class ApolloClient extends BaseApolloClient {
57
68
  this.appManager.close().catch(() => {});
58
69
  }
59
70
 
60
- waitForInitialization = cacheAsync(async () => {
71
+ get toolInput() {
72
+ return this.#toolInput;
73
+ }
74
+
75
+ clearToolInput() {
76
+ this.#toolInput = undefined;
77
+ }
78
+
79
+ watchQuery<
80
+ T = any,
81
+ TVariables extends OperationVariables = OperationVariables,
82
+ >(options: WatchQueryOptions<TVariables, T>): ObservableQuery<T, TVariables> {
83
+ if (__DEV__) {
84
+ const toolInput = this.#toolInput;
85
+
86
+ if (toolInput) {
87
+ const toolName = this.appManager.toolName;
88
+ const hasMatchingTool =
89
+ !!toolName && getToolNamesFromDocument(options.query).has(toolName);
90
+
91
+ if (hasMatchingTool) {
92
+ // Clear after first matching comparison so this only fires once and
93
+ // remounting doesn't produce spurious warnings.
94
+ this.#toolInput = undefined;
95
+
96
+ const variableNames = getVariableNamesFromDocument(options.query);
97
+
98
+ if (variableNames.size > 0) {
99
+ const { variables } = options;
100
+ const toolInputVariables = Object.entries(toolInput).filter(
101
+ ([key]) => variableNames.has(key)
102
+ );
103
+
104
+ const hasToolInputMismatch = toolInputVariables.some(
105
+ ([key, value]) => !equal(value, variables?.[key])
106
+ );
107
+
108
+ if (hasToolInputMismatch) {
109
+ warnOnVariableMismatch(
110
+ options.query,
111
+ Object.fromEntries(toolInputVariables),
112
+ variables
113
+ );
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return super.watchQuery(options);
121
+ }
122
+
123
+ connect = cacheAsync(async () => {
61
124
  const { prefetch, result, toolName, args } =
62
- await this.appManager.waitForInitialization();
125
+ await this.appManager.connect();
126
+
127
+ this.#toolInput = args;
63
128
 
64
129
  this.manifest.operations.forEach((operation) => {
65
130
  if (operation.prefetchID && prefetch?.[operation.prefetchID]) {
@@ -35,7 +35,7 @@ export class McpAppManager {
35
35
  return this.#toolInput;
36
36
  }
37
37
 
38
- waitForInitialization = cacheAsync(async () => {
38
+ connect = cacheAsync(async () => {
39
39
  let toolResult = promiseWithResolvers<ApolloMcpServerApps.CallToolResult>();
40
40
  let toolInput = promiseWithResolvers<Parameters<App["ontoolinput"]>[0]>();
41
41
 
@@ -49,7 +49,7 @@ export class McpAppManager {
49
49
  toolInput.resolve(params);
50
50
  };
51
51
 
52
- await this.connect();
52
+ await this.connectToHost();
53
53
 
54
54
  const { structuredContent, _meta } = await toolResult.promise;
55
55
  const { arguments: args } = await toolInput.promise;
@@ -93,7 +93,7 @@ export class McpAppManager {
93
93
  return result.structuredContent;
94
94
  }
95
95
 
96
- private async connect() {
96
+ private async connectToHost() {
97
97
  try {
98
98
  return await this.app.connect(
99
99
  new PostMessageTransport(window.parent, window.parent)
@@ -1,9 +1,10 @@
1
- import { expect, test, vi } from "vitest";
1
+ import { expect, test, vi, describe } from "vitest";
2
2
  import { ApolloClient } from "../ApolloClient.js";
3
3
  import { ApolloLink, HttpLink, InMemoryCache, gql } from "@apollo/client";
4
4
  import { print } from "@apollo/client/utilities";
5
5
  import { ToolCallLink } from "../../link/ToolCallLink.js";
6
6
  import {
7
+ graphqlToolResult,
7
8
  minimalHostContextWithToolName,
8
9
  mockApplicationManifest,
9
10
  mockMcpHost,
@@ -57,7 +58,7 @@ test("writes tool result data to cache", async () => {
57
58
  });
58
59
  host.sendToolInput({ arguments: { id: "1" } });
59
60
 
60
- await client.waitForInitialization();
61
+ await client.connect();
61
62
 
62
63
  expect(client.extract()).toEqual({
63
64
  "Product:1": {
@@ -124,7 +125,7 @@ test("writes prefetch data to cache", async () => {
124
125
  });
125
126
  host.sendToolInput({ arguments: {} });
126
127
 
127
- await client.waitForInitialization();
128
+ await client.connect();
128
129
 
129
130
  expect(client.extract()).toEqual({
130
131
  "Product:1": {
@@ -213,7 +214,7 @@ test("writes prefetch and tool response data to cache when both are provided", a
213
214
  });
214
215
  host.sendToolInput({ arguments: { id: "2" } });
215
216
 
216
- await client.waitForInitialization();
217
+ await client.connect();
217
218
 
218
219
  expect(client.extract()).toEqual({
219
220
  "Product:1": {
@@ -281,7 +282,7 @@ test("excludes extra tool input variables not defined in the operation", async (
281
282
  });
282
283
  host.sendToolInput({ arguments: { id: "1", extraParam: "ignored" } });
283
284
 
284
- await client.waitForInitialization();
285
+ await client.connect();
285
286
 
286
287
  expect(client.extract()).toEqual({
287
288
  "Product:1": {
@@ -345,7 +346,7 @@ test("allows for custom links provided to the constructor", async () => {
345
346
  },
346
347
  }));
347
348
 
348
- await client.waitForInitialization();
349
+ await client.connect();
349
350
 
350
351
  const variables = { id: "1" };
351
352
  const query = gql(manifest.operations[0].body);
@@ -462,3 +463,105 @@ test("creates a default ToolCallLink when no link is provided", () => {
462
463
  });
463
464
  }).not.toThrow();
464
465
  });
466
+
467
+ describe("watchQuery dev warnings", () => {
468
+ const query = gql`
469
+ query Products($category: String!, $page: Int!, $sortBy: String!)
470
+ @tool(name: "GetProductsByCategory") {
471
+ products(category: $category, page: $page, sortBy: $sortBy) {
472
+ id
473
+ }
474
+ }
475
+ `;
476
+
477
+ async function setupClient({
478
+ toolInput,
479
+ }: {
480
+ toolInput: Record<string, unknown>;
481
+ }) {
482
+ const client = new ApolloClient({
483
+ cache: new InMemoryCache(),
484
+ manifest: mockApplicationManifest(),
485
+ });
486
+ using host = await mockMcpHost({
487
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
488
+ });
489
+ host.onCleanup(() => client.stop());
490
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
491
+ host.sendToolInput({ arguments: toolInput });
492
+ await client.connect();
493
+ return client;
494
+ }
495
+
496
+ test("warns when variables don't match tool input", async () => {
497
+ using _ = spyOnConsole("debug", "warn");
498
+ const client = await setupClient({
499
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
500
+ });
501
+
502
+ client.watchQuery({
503
+ query,
504
+ variables: { category: "music", page: 1, sortBy: "name" },
505
+ });
506
+
507
+ expect(console.warn).toHaveBeenCalledWith(
508
+ expect.stringContaining("useHydratedVariables"),
509
+ { category: "electronics", page: 1, sortBy: "title" },
510
+ { category: "music", page: 1, sortBy: "name" }
511
+ );
512
+ });
513
+
514
+ test("does not warn when variables match tool input", async () => {
515
+ using _ = spyOnConsole("debug", "warn");
516
+ const client = await setupClient({
517
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
518
+ });
519
+
520
+ client.watchQuery({
521
+ query,
522
+ variables: { category: "electronics", page: 1, sortBy: "title" },
523
+ });
524
+
525
+ expect(console.warn).not.toHaveBeenCalled();
526
+ });
527
+
528
+ test("does not warn when query has no matching @tool directive", async () => {
529
+ using _ = spyOnConsole("debug", "warn");
530
+ const client = await setupClient({
531
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
532
+ });
533
+
534
+ const queryWithoutTool = gql`
535
+ query Products($category: String!) {
536
+ products(category: $category) {
537
+ id
538
+ }
539
+ }
540
+ `;
541
+
542
+ client.watchQuery({
543
+ query: queryWithoutTool,
544
+ variables: { category: "music" },
545
+ });
546
+
547
+ expect(console.warn).not.toHaveBeenCalled();
548
+ });
549
+
550
+ test("warning fires at most once (subsequent calls don't re-warn)", async () => {
551
+ using _ = spyOnConsole("debug", "warn");
552
+ const client = await setupClient({
553
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
554
+ });
555
+
556
+ client.watchQuery({
557
+ query,
558
+ variables: { category: "music", page: 1, sortBy: "name" },
559
+ });
560
+ client.watchQuery({
561
+ query,
562
+ variables: { category: "music", page: 1, sortBy: "name" },
563
+ });
564
+
565
+ expect(console.warn).toHaveBeenCalledTimes(1);
566
+ });
567
+ });
@@ -49,7 +49,7 @@ test("delegates query execution to MCP host", async () => {
49
49
  },
50
50
  }));
51
51
 
52
- await client.waitForInitialization();
52
+ await client.connect();
53
53
 
54
54
  const observable = execute(new ToolCallLink(), { query }, { client });
55
55
  const stream = new ObservableStream(observable);