@apollo/client-ai-apps 0.5.1 → 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 (103) hide show
  1. package/CHANGELOG.md +45 -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/core/types.d.ts +6 -0
  7. package/dist/core/types.d.ts.map +1 -1
  8. package/dist/core/types.js.map +1 -1
  9. package/dist/mcp/core/ApolloClient.d.ts +6 -1
  10. package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
  11. package/dist/mcp/core/ApolloClient.js +36 -3
  12. package/dist/mcp/core/ApolloClient.js.map +1 -1
  13. package/dist/mcp/core/McpAppManager.d.ts +4 -9
  14. package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
  15. package/dist/mcp/core/McpAppManager.js +13 -4
  16. package/dist/mcp/core/McpAppManager.js.map +1 -1
  17. package/dist/mcp/react/hooks/createHydrationUtils.d.ts +15 -0
  18. package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +1 -0
  19. package/dist/mcp/react/hooks/createHydrationUtils.js +113 -0
  20. package/dist/mcp/react/hooks/createHydrationUtils.js.map +1 -0
  21. package/dist/mcp/react/hooks/useToolMetadata.d.ts +1 -7
  22. package/dist/mcp/react/hooks/useToolMetadata.d.ts.map +1 -1
  23. package/dist/mcp/react/index.d.ts +1 -0
  24. package/dist/mcp/react/index.d.ts.map +1 -1
  25. package/dist/mcp/react/index.js +1 -0
  26. package/dist/mcp/react/index.js.map +1 -1
  27. package/dist/openai/core/ApolloClient.d.ts +6 -1
  28. package/dist/openai/core/ApolloClient.d.ts.map +1 -1
  29. package/dist/openai/core/ApolloClient.js +37 -3
  30. package/dist/openai/core/ApolloClient.js.map +1 -1
  31. package/dist/openai/core/McpAppManager.d.ts +2 -2
  32. package/dist/openai/core/McpAppManager.d.ts.map +1 -1
  33. package/dist/openai/core/McpAppManager.js +16 -18
  34. package/dist/openai/core/McpAppManager.js.map +1 -1
  35. package/dist/openai/react/hooks/createHydrationUtils.d.ts +15 -0
  36. package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +1 -0
  37. package/dist/openai/react/hooks/createHydrationUtils.js +113 -0
  38. package/dist/openai/react/hooks/createHydrationUtils.js.map +1 -0
  39. package/dist/openai/react/index.d.ts +1 -0
  40. package/dist/openai/react/index.d.ts.map +1 -1
  41. package/dist/openai/react/index.js +1 -0
  42. package/dist/openai/react/index.js.map +1 -1
  43. package/dist/react/ApolloProvider.js +1 -1
  44. package/dist/react/ApolloProvider.js.map +1 -1
  45. package/dist/react/index.d.ts +4 -0
  46. package/dist/react/index.d.ts.map +1 -1
  47. package/dist/react/index.js +3 -0
  48. package/dist/react/index.js.map +1 -1
  49. package/dist/react/index.mcp.d.ts +1 -1
  50. package/dist/react/index.mcp.d.ts.map +1 -1
  51. package/dist/react/index.mcp.js +1 -1
  52. package/dist/react/index.mcp.js.map +1 -1
  53. package/dist/react/index.openai.d.ts +1 -1
  54. package/dist/react/index.openai.d.ts.map +1 -1
  55. package/dist/react/index.openai.js +1 -1
  56. package/dist/react/index.openai.js.map +1 -1
  57. package/dist/react/reactive.d.ts +9 -0
  58. package/dist/react/reactive.d.ts.map +1 -0
  59. package/dist/react/reactive.js +11 -0
  60. package/dist/react/reactive.js.map +1 -0
  61. package/dist/utilities/getToolNamesFromDocument.d.ts +3 -0
  62. package/dist/utilities/getToolNamesFromDocument.d.ts.map +1 -0
  63. package/dist/utilities/getToolNamesFromDocument.js +12 -0
  64. package/dist/utilities/getToolNamesFromDocument.js.map +1 -0
  65. package/dist/utilities/getVariableNamesFromDocument.d.ts +3 -0
  66. package/dist/utilities/getVariableNamesFromDocument.d.ts.map +1 -0
  67. package/dist/utilities/getVariableNamesFromDocument.js +6 -0
  68. package/dist/utilities/getVariableNamesFromDocument.js.map +1 -0
  69. package/dist/utilities/index.d.ts +3 -0
  70. package/dist/utilities/index.d.ts.map +1 -1
  71. package/dist/utilities/index.js +3 -0
  72. package/dist/utilities/index.js.map +1 -1
  73. package/dist/utilities/warnOnVariableMismatch.d.ts +3 -0
  74. package/dist/utilities/warnOnVariableMismatch.d.ts.map +1 -0
  75. package/dist/utilities/warnOnVariableMismatch.js +10 -0
  76. package/dist/utilities/warnOnVariableMismatch.js.map +1 -0
  77. package/package.json +2 -1
  78. package/src/core/ApolloClient.ts +1 -1
  79. package/src/core/types.ts +7 -0
  80. package/src/mcp/core/ApolloClient.ts +67 -2
  81. package/src/mcp/core/McpAppManager.ts +14 -5
  82. package/src/mcp/core/__tests__/ApolloClient.test.ts +109 -6
  83. package/src/mcp/link/__tests__/ToolCallLink.test.ts +13 -4
  84. package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +1228 -0
  85. package/src/mcp/react/hooks/createHydrationUtils.ts +182 -0
  86. package/src/mcp/react/index.ts +1 -0
  87. package/src/openai/core/ApolloClient.ts +68 -2
  88. package/src/openai/core/McpAppManager.ts +17 -20
  89. package/src/openai/core/__tests__/ApolloClient.test.ts +117 -11
  90. package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +1333 -0
  91. package/src/openai/react/hooks/__tests__/useToolInput.test.tsx +1 -1
  92. package/src/openai/react/hooks/createHydrationUtils.ts +182 -0
  93. package/src/openai/react/index.ts +1 -0
  94. package/src/react/ApolloProvider.tsx +1 -1
  95. package/src/react/index.mcp.ts +1 -0
  96. package/src/react/index.openai.ts +1 -0
  97. package/src/react/index.ts +7 -0
  98. package/src/react/reactive.ts +19 -0
  99. package/src/testing/internal/mcp/graphqlToolResult.ts +5 -5
  100. package/src/utilities/getToolNamesFromDocument.ts +15 -0
  101. package/src/utilities/getVariableNamesFromDocument.ts +9 -0
  102. package/src/utilities/index.ts +3 -0
  103. package/src/utilities/warnOnVariableMismatch.ts +20 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getVariableNamesFromDocument.js","sourceRoot":"","sources":["../../src/utilities/getVariableNamesFromDocument.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAG3E,MAAM,UAAU,4BAA4B,CAAC,QAAsB;IACjE,MAAM,YAAY,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IACtD,OAAO,IAAI,GAAG,CACZ,YAAY,EAAE,mBAAmB,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAC3E,CAAC;AACJ,CAAC","sourcesContent":["import { getOperationDefinition } from \"@apollo/client/utilities/internal\";\nimport type { DocumentNode } from \"graphql\";\n\nexport function getVariableNamesFromDocument(document: DocumentNode) {\n const operationDef = getOperationDefinition(document);\n return new Set(\n operationDef?.variableDefinitions?.map((v) => v.variable.name.value) ?? []\n );\n}\n"]}
@@ -1,6 +1,9 @@
1
1
  export { aiClientSymbol } from "./constants.js";
2
2
  export { getVariablesForOperationFromToolInput } from "./getVariablesForOperationFromToolInput.js";
3
+ export { getToolNamesFromDocument } from "./getToolNamesFromDocument.js";
4
+ export { getVariableNamesFromDocument } from "./getVariableNamesFromDocument.js";
3
5
  export { cacheAsync } from "./cacheAsync.js";
4
6
  export { invariant } from "./invariant.js";
5
7
  export { promiseWithResolvers } from "./promiseWithResolvers.js";
8
+ export { warnOnVariableMismatch } from "./warnOnVariableMismatch.js";
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utilities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utilities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,6 +1,9 @@
1
1
  export { aiClientSymbol } from "./constants.js";
2
2
  export { getVariablesForOperationFromToolInput } from "./getVariablesForOperationFromToolInput.js";
3
+ export { getToolNamesFromDocument } from "./getToolNamesFromDocument.js";
4
+ export { getVariableNamesFromDocument } from "./getVariableNamesFromDocument.js";
3
5
  export { cacheAsync } from "./cacheAsync.js";
4
6
  export { invariant } from "./invariant.js";
5
7
  export { promiseWithResolvers } from "./promiseWithResolvers.js";
8
+ export { warnOnVariableMismatch } from "./warnOnVariableMismatch.js";
6
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utilities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["export { aiClientSymbol } from \"./constants.js\";\nexport { getVariablesForOperationFromToolInput } from \"./getVariablesForOperationFromToolInput.js\";\nexport { cacheAsync } from \"./cacheAsync.js\";\nexport { invariant } from \"./invariant.js\";\nexport { promiseWithResolvers } from \"./promiseWithResolvers.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utilities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["export { aiClientSymbol } from \"./constants.js\";\nexport { getVariablesForOperationFromToolInput } from \"./getVariablesForOperationFromToolInput.js\";\nexport { getToolNamesFromDocument } from \"./getToolNamesFromDocument.js\";\nexport { getVariableNamesFromDocument } from \"./getVariableNamesFromDocument.js\";\nexport { cacheAsync } from \"./cacheAsync.js\";\nexport { invariant } from \"./invariant.js\";\nexport { promiseWithResolvers } from \"./promiseWithResolvers.js\";\nexport { warnOnVariableMismatch } from \"./warnOnVariableMismatch.js\";\n"]}
@@ -0,0 +1,3 @@
1
+ import type { DocumentNode, OperationVariables } from "@apollo/client";
2
+ export declare function warnOnVariableMismatch(document: DocumentNode, toolInput: OperationVariables, actualVariables: OperationVariables | undefined): void;
3
+ //# sourceMappingURL=warnOnVariableMismatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warnOnVariableMismatch.d.ts","sourceRoot":"","sources":["../../src/utilities/warnOnVariableMismatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGvE,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,YAAY,EACtB,SAAS,EAAE,kBAAkB,EAC7B,eAAe,EAAE,kBAAkB,GAAG,SAAS,QAahD"}
@@ -0,0 +1,10 @@
1
+ import { getOperationName } from "@apollo/client/utilities/internal/internal.cjs";
2
+ export function warnOnVariableMismatch(document, toolInput, actualVariables) {
3
+ const operationName = getOperationName(document, "(anonymous)");
4
+ console.warn(`The operation "${operationName}" has a @tool directive matching the current ` +
5
+ "tool call, but the variables provided to the query don't match the tool " +
6
+ "input. Use the `useHydratedVariables` hook returned from `createHydrationUtils` " +
7
+ "to provide the hydrated variables to the query. " +
8
+ "\n\nExpected variables:\n%o\n\nReceived:\n%o", toolInput, actualVariables);
9
+ }
10
+ //# sourceMappingURL=warnOnVariableMismatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warnOnVariableMismatch.js","sourceRoot":"","sources":["../../src/utilities/warnOnVariableMismatch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gDAAgD,CAAC;AAElF,MAAM,UAAU,sBAAsB,CACpC,QAAsB,EACtB,SAA6B,EAC7B,eAA+C;IAE/C,MAAM,aAAa,GAAG,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAEhE,OAAO,CAAC,IAAI,CACV,kBAAkB,aAAa,+CAA+C;QAC5E,0EAA0E;QAC1E,kFAAkF;QAClF,kDAAkD;QAClD,8CAA8C,EAChD,SAAS,EACT,eAAe,CAChB,CAAC;AACJ,CAAC","sourcesContent":["import type { DocumentNode, OperationVariables } from \"@apollo/client\";\nimport { getOperationName } from \"@apollo/client/utilities/internal/internal.cjs\";\n\nexport function warnOnVariableMismatch(\n document: DocumentNode,\n toolInput: OperationVariables,\n actualVariables: OperationVariables | undefined\n) {\n const operationName = getOperationName(document, \"(anonymous)\");\n\n console.warn(\n `The operation \"${operationName}\" has a @tool directive matching the current ` +\n \"tool call, but the variables provided to the query don't match the tool \" +\n \"input. Use the `useHydratedVariables` hook returned from `createHydrationUtils` \" +\n \"to provide the hydrated variables to the query. \" +\n \"\\n\\nExpected variables:\\n%o\\n\\nReceived:\\n%o\",\n toolInput,\n actualVariables\n );\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "@apollo/client-ai-apps",
4
- "version": "0.5.1",
4
+ "version": "0.5.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -157,6 +157,7 @@
157
157
  },
158
158
  "dependencies": {
159
159
  "@graphql-tools/graphql-tag-pluck": "^8.3.23",
160
+ "@wry/equality": "^0.5.7",
160
161
  "cosmiconfig": "^9.0.0",
161
162
  "crypto-hash": "^4.0.0",
162
163
  "glob": "^11.0.3",
@@ -21,5 +21,5 @@ export class ApolloClient extends BaseApolloClient {
21
21
  );
22
22
  }
23
23
 
24
- async waitForInitialization() {}
24
+ async connect() {}
25
25
  }
package/src/core/types.ts CHANGED
@@ -2,13 +2,20 @@ import type { CallToolResult as McpCallToolResult } from "@modelcontextprotocol/
2
2
  import type { FormattedExecutionResult } from "graphql";
3
3
 
4
4
  export namespace ApolloMcpServerApps {
5
+ export interface Meta {
6
+ toolName: string;
7
+ [x: string]: unknown;
8
+ }
9
+
5
10
  export interface StructuredContent {
6
11
  result: FormattedExecutionResult;
7
12
  prefetch?: Record<string, FormattedExecutionResult>;
13
+ toolName?: string;
8
14
  [x: string]: unknown;
9
15
  }
10
16
 
11
17
  export interface CallToolResult extends McpCallToolResult {
18
+ _meta?: ApolloMcpServerApps.Meta;
12
19
  structuredContent: ApolloMcpServerApps.StructuredContent;
13
20
  }
14
21
  }
@@ -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]) {
@@ -16,7 +16,7 @@ export class McpAppManager {
16
16
  readonly app: App;
17
17
 
18
18
  #toolName: string | undefined;
19
- #toolMetadata: ApolloMcpServerApps.CallToolResult["_meta"];
19
+ #toolMetadata: ApolloMcpServerApps.CallToolResult["_meta"] | undefined;
20
20
  #toolInput: Record<string, unknown> | undefined;
21
21
 
22
22
  constructor(manifest: ApplicationManifest) {
@@ -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,12 +49,21 @@ 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;
56
56
 
57
- this.#toolName = this.app.getHostContext()?.toolInfo?.tool.name;
57
+ // Some hosts do not provide toolInfo in the ui/initialize response, so we
58
+ // fallback to `_meta.toolName` provided by Apollo MCP server if the value
59
+ // is not available.
60
+ this.#toolName =
61
+ this.app.getHostContext()?.toolInfo?.tool.name ??
62
+ _meta?.toolName ??
63
+ // Some hosts do not forward `_meta` nor do they provide `toolInfo`. Our
64
+ // MCP server provides `toolName` in `structuredContent` as a workaround
65
+ // that we can use if all else fails
66
+ structuredContent.toolName;
58
67
  this.#toolMetadata = _meta;
59
68
  this.#toolInput = args;
60
69
 
@@ -84,7 +93,7 @@ export class McpAppManager {
84
93
  return result.structuredContent;
85
94
  }
86
95
 
87
- private async connect() {
96
+ private async connectToHost() {
88
97
  try {
89
98
  return await this.app.connect(
90
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
+ });
@@ -3,6 +3,7 @@ import { gql, InMemoryCache } from "@apollo/client";
3
3
  import { execute } from "@apollo/client/link";
4
4
  import { ApolloClient } from "../../core/ApolloClient.js";
5
5
  import {
6
+ minimalHostContextWithToolName,
6
7
  mockApplicationManifest,
7
8
  mockMcpHost,
8
9
  ObservableStream,
@@ -23,15 +24,23 @@ test("delegates query execution to MCP host", async () => {
23
24
  manifest: mockApplicationManifest(),
24
25
  });
25
26
 
26
- using host = await mockMcpHost();
27
+ using host = await mockMcpHost({
28
+ hostContext: minimalHostContextWithToolName("GetProduct"),
29
+ });
27
30
  host.onCleanup(() => client.stop());
28
31
 
32
+ host.sendToolInput({ arguments: {} });
29
33
  host.sendToolResult({
30
34
  _meta: { toolName: "GetProduct" },
31
35
  content: [],
32
- structuredContent: {},
36
+ structuredContent: {
37
+ result: {
38
+ data: {
39
+ product: null,
40
+ },
41
+ },
42
+ },
33
43
  });
34
- host.sendToolInput({ arguments: {} });
35
44
 
36
45
  host.mockToolCall("execute", () => ({
37
46
  content: [],
@@ -40,7 +49,7 @@ test("delegates query execution to MCP host", async () => {
40
49
  },
41
50
  }));
42
51
 
43
- await client.waitForInitialization();
52
+ await client.connect();
44
53
 
45
54
  const observable = execute(new ToolCallLink(), { query }, { client });
46
55
  const stream = new ObservableStream(observable);