@apollo/client-ai-apps 0.5.2 → 0.5.4
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.
- package/CHANGELOG.md +39 -0
- package/dist/core/ApolloClient.d.ts +1 -1
- package/dist/core/ApolloClient.d.ts.map +1 -1
- package/dist/core/ApolloClient.js +1 -1
- package/dist/core/ApolloClient.js.map +1 -1
- package/dist/mcp/core/ApolloClient.d.ts +6 -1
- package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
- package/dist/mcp/core/ApolloClient.js +36 -3
- package/dist/mcp/core/ApolloClient.js.map +1 -1
- package/dist/mcp/core/McpAppManager.d.ts +2 -2
- package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
- package/dist/mcp/core/McpAppManager.js +3 -3
- package/dist/mcp/core/McpAppManager.js.map +1 -1
- package/dist/mcp/react/hooks/createHydrationUtils.d.ts +15 -0
- package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +1 -0
- package/dist/mcp/react/hooks/createHydrationUtils.js +113 -0
- package/dist/mcp/react/hooks/createHydrationUtils.js.map +1 -0
- package/dist/mcp/react/index.d.ts +1 -0
- package/dist/mcp/react/index.d.ts.map +1 -1
- package/dist/mcp/react/index.js +1 -0
- package/dist/mcp/react/index.js.map +1 -1
- package/dist/openai/core/ApolloClient.d.ts +6 -1
- package/dist/openai/core/ApolloClient.d.ts.map +1 -1
- package/dist/openai/core/ApolloClient.js +37 -3
- package/dist/openai/core/ApolloClient.js.map +1 -1
- package/dist/openai/core/McpAppManager.d.ts +2 -2
- package/dist/openai/core/McpAppManager.d.ts.map +1 -1
- package/dist/openai/core/McpAppManager.js +5 -5
- package/dist/openai/core/McpAppManager.js.map +1 -1
- package/dist/openai/react/hooks/createHydrationUtils.d.ts +15 -0
- package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +1 -0
- package/dist/openai/react/hooks/createHydrationUtils.js +113 -0
- package/dist/openai/react/hooks/createHydrationUtils.js.map +1 -0
- package/dist/openai/react/index.d.ts +1 -0
- package/dist/openai/react/index.d.ts.map +1 -1
- package/dist/openai/react/index.js +1 -0
- package/dist/openai/react/index.js.map +1 -1
- package/dist/react/ApolloProvider.js +1 -1
- package/dist/react/ApolloProvider.js.map +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mcp.d.ts +1 -1
- package/dist/react/index.mcp.d.ts.map +1 -1
- package/dist/react/index.mcp.js +1 -1
- package/dist/react/index.mcp.js.map +1 -1
- package/dist/react/index.openai.d.ts +1 -1
- package/dist/react/index.openai.d.ts.map +1 -1
- package/dist/react/index.openai.js +1 -1
- package/dist/react/index.openai.js.map +1 -1
- package/dist/react/reactive.d.ts +9 -0
- package/dist/react/reactive.d.ts.map +1 -0
- package/dist/react/reactive.js +11 -0
- package/dist/react/reactive.js.map +1 -0
- package/dist/utilities/getToolNamesFromDocument.d.ts +3 -0
- package/dist/utilities/getToolNamesFromDocument.d.ts.map +1 -0
- package/dist/utilities/getToolNamesFromDocument.js +12 -0
- package/dist/utilities/getToolNamesFromDocument.js.map +1 -0
- package/dist/utilities/getVariableNamesFromDocument.d.ts +3 -0
- package/dist/utilities/getVariableNamesFromDocument.d.ts.map +1 -0
- package/dist/utilities/getVariableNamesFromDocument.js +6 -0
- package/dist/utilities/getVariableNamesFromDocument.js.map +1 -0
- package/dist/utilities/index.d.ts +3 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +3 -0
- package/dist/utilities/index.js.map +1 -1
- package/dist/utilities/warnOnVariableMismatch.d.ts +3 -0
- package/dist/utilities/warnOnVariableMismatch.d.ts.map +1 -0
- package/dist/utilities/warnOnVariableMismatch.js +10 -0
- package/dist/utilities/warnOnVariableMismatch.js.map +1 -0
- package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
- package/dist/vite/apolloClientAiApps.js +35 -31
- package/dist/vite/apolloClientAiApps.js.map +1 -1
- package/package.json +2 -1
- package/src/core/ApolloClient.ts +1 -1
- package/src/mcp/core/ApolloClient.ts +67 -2
- package/src/mcp/core/McpAppManager.ts +3 -3
- package/src/mcp/core/__tests__/ApolloClient.test.ts +109 -6
- package/src/mcp/link/__tests__/ToolCallLink.test.ts +1 -1
- package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +1228 -0
- package/src/mcp/react/hooks/createHydrationUtils.ts +182 -0
- package/src/mcp/react/index.ts +1 -0
- package/src/openai/core/ApolloClient.ts +68 -2
- package/src/openai/core/McpAppManager.ts +5 -5
- package/src/openai/core/__tests__/ApolloClient.test.ts +113 -7
- package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +1333 -0
- package/src/openai/react/hooks/createHydrationUtils.ts +182 -0
- package/src/openai/react/index.ts +1 -0
- package/src/react/ApolloProvider.tsx +1 -1
- package/src/react/index.mcp.ts +1 -0
- package/src/react/index.openai.ts +1 -0
- package/src/react/index.ts +7 -0
- package/src/react/reactive.ts +19 -0
- package/src/testing/internal/mcp/graphqlToolResult.ts +5 -5
- package/src/utilities/getToolNamesFromDocument.ts +15 -0
- package/src/utilities/getVariableNamesFromDocument.ts +9 -0
- package/src/utilities/index.ts +3 -0
- package/src/utilities/warnOnVariableMismatch.ts +20 -0
- package/src/vite/__tests__/apolloClientAiApps.test.ts +183 -2
- package/src/vite/apolloClientAiApps.ts +46 -40
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
DocumentNode,
|
|
4
|
+
OperationVariables,
|
|
5
|
+
TypedDocumentNode,
|
|
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";
|
|
11
|
+
import { equal } from "@wry/equality";
|
|
12
|
+
import { __DEV__ } from "@apollo/client/utilities/environment";
|
|
13
|
+
import {
|
|
14
|
+
getToolNamesFromDocument,
|
|
15
|
+
getVariableNamesFromDocument,
|
|
16
|
+
} from "../../../utilities/index.js";
|
|
17
|
+
|
|
18
|
+
type HydratedVariablesInput<TVariables> = {
|
|
19
|
+
[K in keyof TVariables]: TVariables[K] | Reactive<TVariables[K]>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type StateVariables<TVariables, Input> = {
|
|
23
|
+
[K in keyof TVariables as K extends keyof Input ?
|
|
24
|
+
Input[K] extends Reactive<any> ?
|
|
25
|
+
never
|
|
26
|
+
: K
|
|
27
|
+
: K]: K extends keyof TVariables ? TVariables[K] : never;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SetVariables<TState> = (
|
|
31
|
+
update: Partial<TState> | ((prev: TState) => Partial<TState>)
|
|
32
|
+
) => void;
|
|
33
|
+
|
|
34
|
+
/** @experimental */
|
|
35
|
+
export function createHydrationUtils<
|
|
36
|
+
TVariables extends OperationVariables = OperationVariables,
|
|
37
|
+
>(document: TypedDocumentNode<any, TVariables> | DocumentNode) {
|
|
38
|
+
const documentToolNames = getToolNamesFromDocument(document);
|
|
39
|
+
const variableNames = getVariableNamesFromDocument(document);
|
|
40
|
+
|
|
41
|
+
function useHydratedVariables<
|
|
42
|
+
TInputVariables extends HydratedVariablesInput<TVariables>,
|
|
43
|
+
>(
|
|
44
|
+
variables: TInputVariables &
|
|
45
|
+
Record<Exclude<keyof TInputVariables, keyof TVariables>, never>
|
|
46
|
+
): [
|
|
47
|
+
variables: TVariables,
|
|
48
|
+
setVariables: SetVariables<StateVariables<TVariables, TInputVariables>>,
|
|
49
|
+
] {
|
|
50
|
+
const client = useApolloClient();
|
|
51
|
+
const toolName = useToolName();
|
|
52
|
+
const [toolInput] = useState(() => client.toolInput);
|
|
53
|
+
|
|
54
|
+
const toolMatches =
|
|
55
|
+
toolInput !== undefined &&
|
|
56
|
+
toolName !== undefined &&
|
|
57
|
+
documentToolNames.has(toolName);
|
|
58
|
+
|
|
59
|
+
const [stateVars, setStateVars] = useState<Record<string, unknown>>(() => {
|
|
60
|
+
const values: Record<string, unknown> = {};
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(
|
|
63
|
+
toolMatches ? toolInput : variables
|
|
64
|
+
)) {
|
|
65
|
+
if (variableNames.has(key) && !isReactive(value)) {
|
|
66
|
+
values[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return values;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const [initialReactiveVars] = useState<Record<string, unknown>>(() => {
|
|
74
|
+
const values: Record<string, unknown> = {};
|
|
75
|
+
|
|
76
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
77
|
+
if (variableNames.has(key) && isReactive(value)) {
|
|
78
|
+
values[key] = value.value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return values;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const [reactiveVars, setReactiveVars] = useState(() => {
|
|
86
|
+
const values: Record<string, unknown> = {};
|
|
87
|
+
|
|
88
|
+
for (const [key, value] of Object.entries(initialReactiveVars)) {
|
|
89
|
+
if (toolMatches && key in toolInput) {
|
|
90
|
+
values[key] = toolInput[key];
|
|
91
|
+
} else if (!toolMatches) {
|
|
92
|
+
values[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return values;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const changedKeysRef = useRef(new Set<string>());
|
|
100
|
+
const nextReactiveVars: Record<string, unknown> = {};
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
103
|
+
if (!variableNames.has(key) || !isReactive(value)) continue;
|
|
104
|
+
|
|
105
|
+
const hasChanged =
|
|
106
|
+
changedKeysRef.current.has(key) ||
|
|
107
|
+
!equal(value.value, initialReactiveVars[key]);
|
|
108
|
+
|
|
109
|
+
if (toolMatches && !hasChanged) {
|
|
110
|
+
if (key in toolInput) {
|
|
111
|
+
nextReactiveVars[key] = toolInput[key];
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
nextReactiveVars[key] = value.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!equal(nextReactiveVars, reactiveVars)) {
|
|
119
|
+
setReactiveVars(nextReactiveVars);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Clear the tool input after first mount so that remounting the component
|
|
123
|
+
// uses the user-provided variables rather than the hydrated tool input.
|
|
124
|
+
// This runs once on mount; watchQuery also clears it when useQuery is
|
|
125
|
+
// present, so both paths are idempotent.
|
|
126
|
+
useLayoutEffect(() => {
|
|
127
|
+
if (toolMatches) {
|
|
128
|
+
client.clearToolInput();
|
|
129
|
+
}
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
useLayoutEffect(() => {
|
|
134
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
135
|
+
if (
|
|
136
|
+
variableNames.has(key) &&
|
|
137
|
+
isReactive(value) &&
|
|
138
|
+
!changedKeysRef.current.has(key) &&
|
|
139
|
+
!equal(value.value, initialReactiveVars[key])
|
|
140
|
+
) {
|
|
141
|
+
changedKeysRef.current.add(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const resolvedVariables = useMemo(() => {
|
|
147
|
+
return { ...stateVars, ...reactiveVars } as TVariables;
|
|
148
|
+
}, [stateVars, reactiveVars]);
|
|
149
|
+
|
|
150
|
+
const setVariables = useCallback<
|
|
151
|
+
SetVariables<StateVariables<TVariables, TInputVariables>>
|
|
152
|
+
>((update) => {
|
|
153
|
+
setStateVars((prev) => {
|
|
154
|
+
const updates =
|
|
155
|
+
typeof update === "function" ? update(prev as any) : update;
|
|
156
|
+
|
|
157
|
+
const filtered = Object.fromEntries(
|
|
158
|
+
Object.entries(updates).filter(([key]) => {
|
|
159
|
+
if (key in initialReactiveVars) {
|
|
160
|
+
if (__DEV__) {
|
|
161
|
+
console.warn(
|
|
162
|
+
`Attempted to set reactive variable "${key}" via setVariables. ` +
|
|
163
|
+
`Reactive variables are read-only and are ignored. `
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (Object.keys(filtered).length === 0) return prev;
|
|
173
|
+
|
|
174
|
+
return { ...prev, ...filtered };
|
|
175
|
+
});
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
return [resolvedVariables, setVariables];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { useHydratedVariables };
|
|
182
|
+
}
|
package/src/mcp/react/index.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { useApp } from "./hooks/useApp.js";
|
|
|
2
2
|
export { useToolName } from "./hooks/useToolName.js";
|
|
3
3
|
export { useToolMetadata } from "./hooks/useToolMetadata.js";
|
|
4
4
|
export { useToolInput } from "./hooks/useToolInput.js";
|
|
5
|
+
export { createHydrationUtils } from "./hooks/createHydrationUtils.js";
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { ApolloLink } from "@apollo/client";
|
|
2
2
|
import { ApolloClient as BaseApolloClient } from "@apollo/client";
|
|
3
3
|
import { DocumentTransform } from "@apollo/client";
|
|
4
|
+
import type {
|
|
5
|
+
WatchQueryOptions,
|
|
6
|
+
ObservableQuery,
|
|
7
|
+
OperationVariables,
|
|
8
|
+
} from "@apollo/client";
|
|
4
9
|
import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal";
|
|
5
10
|
import { parse } from "graphql";
|
|
11
|
+
import { equal } from "@wry/equality";
|
|
6
12
|
import { __DEV__ } from "@apollo/client/utilities/environment";
|
|
7
13
|
import type { ApplicationManifest } from "../../types/application-manifest.js";
|
|
8
14
|
import { ToolCallLink } from "../link/ToolCallLink.js";
|
|
9
15
|
import {
|
|
10
16
|
aiClientSymbol,
|
|
11
17
|
cacheAsync,
|
|
18
|
+
getToolNamesFromDocument,
|
|
12
19
|
getVariablesForOperationFromToolInput,
|
|
20
|
+
warnOnVariableMismatch,
|
|
13
21
|
} from "../../utilities/index.js";
|
|
14
22
|
import { McpAppManager } from "./McpAppManager.js";
|
|
23
|
+
import { getVariableNamesFromDocument } from "../../utilities/getVariableNamesFromDocument.js";
|
|
15
24
|
|
|
16
25
|
export declare namespace ApolloClient {
|
|
17
26
|
export interface Options extends Omit<BaseApolloClient.Options, "link"> {
|
|
@@ -27,6 +36,8 @@ export class ApolloClient extends BaseApolloClient {
|
|
|
27
36
|
/** @internal */
|
|
28
37
|
readonly [aiClientSymbol] = true;
|
|
29
38
|
|
|
39
|
+
#toolInput: Record<string, unknown> | undefined;
|
|
40
|
+
|
|
30
41
|
constructor(options: ApolloClient.Options) {
|
|
31
42
|
const link = options.link ?? new ToolCallLink();
|
|
32
43
|
|
|
@@ -55,9 +66,64 @@ export class ApolloClient extends BaseApolloClient {
|
|
|
55
66
|
this.appManager.close().catch(() => {});
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
get toolInput() {
|
|
70
|
+
return this.#toolInput;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
clearToolInput() {
|
|
74
|
+
this.#toolInput = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
watchQuery<
|
|
78
|
+
T = any,
|
|
79
|
+
TVariables extends OperationVariables = OperationVariables,
|
|
80
|
+
>(options: WatchQueryOptions<TVariables, T>): ObservableQuery<T, TVariables> {
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
const toolInput = this.#toolInput;
|
|
83
|
+
|
|
84
|
+
if (toolInput) {
|
|
85
|
+
const toolName = this.appManager.toolName;
|
|
86
|
+
const hasMatchingTool =
|
|
87
|
+
!!toolName && getToolNamesFromDocument(options.query).has(toolName);
|
|
88
|
+
|
|
89
|
+
if (hasMatchingTool) {
|
|
90
|
+
// Clear after first matching comparison so this only fires once and
|
|
91
|
+
// remounting doesn't produce spurious warnings.
|
|
92
|
+
this.#toolInput = undefined;
|
|
93
|
+
|
|
94
|
+
const variableNames = getVariableNamesFromDocument(options.query);
|
|
95
|
+
|
|
96
|
+
if (variableNames.size > 0) {
|
|
97
|
+
const { variables } = options;
|
|
98
|
+
|
|
99
|
+
const toolInputVariables = Object.entries(toolInput).filter(
|
|
100
|
+
([key]) => variableNames.has(key)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const hasToolInputMismatch = toolInputVariables.some(
|
|
104
|
+
([key, value]) => !equal(value, variables?.[key])
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (hasToolInputMismatch) {
|
|
108
|
+
warnOnVariableMismatch(
|
|
109
|
+
options.query,
|
|
110
|
+
Object.fromEntries(toolInputVariables),
|
|
111
|
+
variables
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return super.watchQuery(options);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
connect = cacheAsync(async () => {
|
|
59
123
|
const { prefetch, result, toolName, args } =
|
|
60
|
-
await this.appManager.
|
|
124
|
+
await this.appManager.connect();
|
|
125
|
+
|
|
126
|
+
this.#toolInput = args;
|
|
61
127
|
|
|
62
128
|
this.manifest.operations.forEach((operation) => {
|
|
63
129
|
if (operation.prefetchID && prefetch?.[operation.prefetchID]) {
|
|
@@ -35,7 +35,7 @@ export class McpAppManager {
|
|
|
35
35
|
return this.#toolInput;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
connect = cacheAsync(async () => {
|
|
39
39
|
let toolResult = promiseWithResolvers<ApolloMcpServerApps.CallToolResult>();
|
|
40
40
|
|
|
41
41
|
this.app.ontoolresult = (params) => {
|
|
@@ -44,7 +44,7 @@ export class McpAppManager {
|
|
|
44
44
|
);
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
await this.
|
|
47
|
+
await this.connectToHost();
|
|
48
48
|
|
|
49
49
|
const { structuredContent } = await toolResult.promise;
|
|
50
50
|
|
|
@@ -55,12 +55,12 @@ export class McpAppManager {
|
|
|
55
55
|
// before we get the tool result (which should always happen and at most
|
|
56
56
|
// once according to the spec). Rather than relying on the
|
|
57
57
|
// `ui/notifications/tool-input` notification to set the tool input value,
|
|
58
|
-
// we read from `window.openai.toolInput so that we
|
|
58
|
+
// we read from `window.openai.toolInput so that we have the most recent
|
|
59
59
|
// set value.
|
|
60
60
|
//
|
|
61
61
|
// When OpenAI fixes this issue and sends `ui/notifications/tool-input`
|
|
62
62
|
// consistently according to the MCP Apps specification, this can be
|
|
63
|
-
//
|
|
63
|
+
// reverted to use the `app.ontoolinput` callback.
|
|
64
64
|
this.#toolInput = window.openai.toolInput;
|
|
65
65
|
|
|
66
66
|
// OpenAI doesn't provide access to `_meta`, so we need to use
|
|
@@ -93,7 +93,7 @@ export class McpAppManager {
|
|
|
93
93
|
return result.structuredContent;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
private async
|
|
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
1
|
import { expect, test, describe, vi } from "vitest";
|
|
2
2
|
import { ApolloClient } from "../ApolloClient.js";
|
|
3
3
|
import { parse } from "graphql";
|
|
4
|
-
import { ApolloLink, HttpLink, InMemoryCache } from "@apollo/client";
|
|
4
|
+
import { ApolloLink, HttpLink, InMemoryCache, gql } from "@apollo/client";
|
|
5
5
|
import { ToolCallLink } from "../../link/ToolCallLink.js";
|
|
6
6
|
import {
|
|
7
|
+
graphqlToolResult,
|
|
7
8
|
minimalHostContextWithToolName,
|
|
8
9
|
mockApplicationManifest,
|
|
9
10
|
mockMcpHost,
|
|
@@ -30,7 +31,7 @@ describe("Client Basics", () => {
|
|
|
30
31
|
structuredContent: {},
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
await client.
|
|
34
|
+
await client.connect();
|
|
34
35
|
|
|
35
36
|
host.mockToolCall("execute", () => ({
|
|
36
37
|
content: [],
|
|
@@ -124,7 +125,7 @@ describe("prefetchData", () => {
|
|
|
124
125
|
},
|
|
125
126
|
});
|
|
126
127
|
|
|
127
|
-
await client.
|
|
128
|
+
await client.connect();
|
|
128
129
|
|
|
129
130
|
expect(client.extract()).toMatchInlineSnapshot(`
|
|
130
131
|
{
|
|
@@ -202,7 +203,7 @@ describe("prefetchData", () => {
|
|
|
202
203
|
},
|
|
203
204
|
});
|
|
204
205
|
|
|
205
|
-
await client.
|
|
206
|
+
await client.connect();
|
|
206
207
|
|
|
207
208
|
expect(client.extract()).toMatchInlineSnapshot(`
|
|
208
209
|
{
|
|
@@ -307,7 +308,7 @@ describe("prefetchData", () => {
|
|
|
307
308
|
},
|
|
308
309
|
});
|
|
309
310
|
|
|
310
|
-
await client.
|
|
311
|
+
await client.connect();
|
|
311
312
|
|
|
312
313
|
expect(client.extract()).toMatchInlineSnapshot(`
|
|
313
314
|
{
|
|
@@ -375,7 +376,7 @@ describe("prefetchData", () => {
|
|
|
375
376
|
},
|
|
376
377
|
});
|
|
377
378
|
|
|
378
|
-
await client.
|
|
379
|
+
await client.connect();
|
|
379
380
|
|
|
380
381
|
expect(client.extract()).toMatchInlineSnapshot(`
|
|
381
382
|
{
|
|
@@ -440,7 +441,7 @@ describe("custom links", () => {
|
|
|
440
441
|
},
|
|
441
442
|
}));
|
|
442
443
|
|
|
443
|
-
await client.
|
|
444
|
+
await client.connect();
|
|
444
445
|
|
|
445
446
|
const variables = { id: "1" };
|
|
446
447
|
const query = parse(manifest.operations[0].body);
|
|
@@ -535,3 +536,108 @@ describe("custom links", () => {
|
|
|
535
536
|
}).not.toThrow();
|
|
536
537
|
});
|
|
537
538
|
});
|
|
539
|
+
|
|
540
|
+
describe("watchQuery dev warnings", () => {
|
|
541
|
+
const query = gql`
|
|
542
|
+
query Products($category: String!, $page: Int!, $sortBy: String!)
|
|
543
|
+
@tool(name: "GetProductsByCategory") {
|
|
544
|
+
products(category: $category, page: $page, sortBy: $sortBy) {
|
|
545
|
+
id
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
`;
|
|
549
|
+
|
|
550
|
+
async function setupClient({
|
|
551
|
+
toolInput,
|
|
552
|
+
}: {
|
|
553
|
+
toolInput: Record<string, unknown>;
|
|
554
|
+
}) {
|
|
555
|
+
stubOpenAiGlobals({ toolInput });
|
|
556
|
+
const client = new ApolloClient({
|
|
557
|
+
cache: new InMemoryCache(),
|
|
558
|
+
manifest: mockApplicationManifest(),
|
|
559
|
+
});
|
|
560
|
+
using host = await mockMcpHost({
|
|
561
|
+
hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
|
|
562
|
+
});
|
|
563
|
+
host.onCleanup(() => client.stop());
|
|
564
|
+
host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
|
|
565
|
+
host.sendToolInput({
|
|
566
|
+
arguments: toolInput,
|
|
567
|
+
});
|
|
568
|
+
await client.connect();
|
|
569
|
+
return client;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
test("warns when variables don't match tool input", async () => {
|
|
573
|
+
using _ = spyOnConsole("debug", "warn");
|
|
574
|
+
const client = await setupClient({
|
|
575
|
+
toolInput: { category: "electronics", page: 1, sortBy: "title" },
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
client.watchQuery({
|
|
579
|
+
query,
|
|
580
|
+
variables: { category: "music", page: 1, sortBy: "name" },
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
584
|
+
expect.stringContaining("useHydratedVariables"),
|
|
585
|
+
{ category: "electronics", page: 1, sortBy: "title" },
|
|
586
|
+
{ category: "music", page: 1, sortBy: "name" }
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("does not warn when variables match tool input", async () => {
|
|
591
|
+
using _ = spyOnConsole("debug", "warn");
|
|
592
|
+
const client = await setupClient({
|
|
593
|
+
toolInput: { category: "electronics", page: 1, sortBy: "title" },
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
client.watchQuery({
|
|
597
|
+
query,
|
|
598
|
+
variables: { category: "electronics", page: 1, sortBy: "title" },
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
expect(console.warn).not.toHaveBeenCalled();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("does not warn when query has no matching @tool directive", async () => {
|
|
605
|
+
using _ = spyOnConsole("debug", "warn");
|
|
606
|
+
const client = await setupClient({
|
|
607
|
+
toolInput: { category: "electronics", page: 1, sortBy: "title" },
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const queryWithoutTool = gql`
|
|
611
|
+
query Products($category: String!) {
|
|
612
|
+
products(category: $category) {
|
|
613
|
+
id
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
`;
|
|
617
|
+
|
|
618
|
+
client.watchQuery({
|
|
619
|
+
query: queryWithoutTool,
|
|
620
|
+
variables: { category: "music" },
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(console.warn).not.toHaveBeenCalled();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("warning fires at most once (subsequent calls don't re-warn)", async () => {
|
|
627
|
+
using _ = spyOnConsole("debug", "warn");
|
|
628
|
+
const client = await setupClient({
|
|
629
|
+
toolInput: { category: "electronics", page: 1, sortBy: "title" },
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
client.watchQuery({
|
|
633
|
+
query,
|
|
634
|
+
variables: { category: "music", page: 1, sortBy: "name" },
|
|
635
|
+
});
|
|
636
|
+
client.watchQuery({
|
|
637
|
+
query,
|
|
638
|
+
variables: { category: "music", page: 1, sortBy: "name" },
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
642
|
+
});
|
|
643
|
+
});
|