@apollo/client-ai-apps 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/CONTRIBUTING.md +195 -0
- package/README.md +74 -0
- package/dist/core/AbstractApolloClient.d.ts +33 -0
- package/dist/core/AbstractApolloClient.d.ts.map +1 -0
- package/dist/core/AbstractApolloClient.js +129 -0
- package/dist/core/AbstractApolloClient.js.map +1 -0
- package/dist/core/ApolloClient.d.ts +3 -7
- package/dist/core/ApolloClient.d.ts.map +1 -1
- package/dist/core/ApolloClient.js +5 -4
- package/dist/core/ApolloClient.js.map +1 -1
- package/dist/core/McpAppManager.d.ts +42 -0
- package/dist/core/McpAppManager.d.ts.map +1 -0
- package/dist/core/McpAppManager.js +56 -0
- package/dist/core/McpAppManager.js.map +1 -0
- package/dist/core/typeRegistration.d.ts +0 -14
- package/dist/core/typeRegistration.d.ts.map +1 -1
- package/dist/core/typeRegistration.js.map +1 -1
- package/dist/core/types.d.ts +2 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mcp.d.ts +0 -1
- package/dist/index.mcp.d.ts.map +1 -1
- package/dist/index.mcp.js +0 -1
- package/dist/index.mcp.js.map +1 -1
- package/dist/index.openai.d.ts +0 -1
- package/dist/index.openai.d.ts.map +1 -1
- package/dist/index.openai.js +0 -1
- package/dist/index.openai.js.map +1 -1
- package/dist/link/ToolCallLink.d.ts +6 -1
- package/dist/link/ToolCallLink.d.ts.map +1 -1
- package/dist/link/ToolCallLink.js +17 -4
- package/dist/link/ToolCallLink.js.map +1 -1
- package/dist/link/ToolHydrationLink.d.ts +21 -0
- package/dist/link/ToolHydrationLink.d.ts.map +1 -0
- package/dist/link/ToolHydrationLink.js +57 -0
- package/dist/link/ToolHydrationLink.js.map +1 -0
- package/dist/mcp/core/ApolloClient.d.ts +3 -20
- package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
- package/dist/mcp/core/ApolloClient.js +20 -98
- package/dist/mcp/core/ApolloClient.js.map +1 -1
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +0 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/openai/core/ApolloClient.d.ts +3 -20
- package/dist/openai/core/ApolloClient.d.ts.map +1 -1
- package/dist/openai/core/ApolloClient.js +36 -98
- package/dist/openai/core/ApolloClient.js.map +1 -1
- package/dist/openai/index.d.ts +0 -1
- package/dist/openai/index.d.ts.map +1 -1
- package/dist/openai/index.js +0 -1
- package/dist/openai/index.js.map +1 -1
- package/dist/openai/react/index.d.ts +0 -7
- package/dist/openai/react/index.d.ts.map +1 -1
- package/dist/openai/react/index.js +0 -7
- package/dist/openai/react/index.js.map +1 -1
- package/dist/react/ApolloProvider.d.ts.map +1 -1
- package/dist/react/ApolloProvider.js +1 -1
- package/dist/react/ApolloProvider.js.map +1 -1
- package/dist/{mcp/react/hooks → react}/createHydrationUtils.d.ts +1 -1
- package/dist/react/createHydrationUtils.d.ts.map +1 -0
- package/dist/{mcp/react/hooks → react}/createHydrationUtils.js +7 -9
- package/dist/react/createHydrationUtils.js.map +1 -0
- package/dist/react/hooks/internal/useApolloClient.d.ts +3 -0
- package/dist/react/hooks/internal/useApolloClient.d.ts.map +1 -0
- package/dist/{mcp/react/hooks → react/hooks/internal}/useApolloClient.js +3 -3
- package/dist/react/hooks/internal/useApolloClient.js.map +1 -0
- package/dist/react/hooks/useApp.d.ts.map +1 -0
- package/dist/react/hooks/useApp.js +5 -0
- package/dist/react/hooks/useApp.js.map +1 -0
- package/dist/react/hooks/useHostContext.d.ts.map +1 -0
- package/dist/{openai/react → react}/hooks/useHostContext.js +1 -1
- package/dist/react/hooks/useHostContext.js.map +1 -0
- package/dist/react/hooks/useToolInfo.d.ts +3 -0
- package/dist/react/hooks/useToolInfo.d.ts.map +1 -0
- package/dist/react/hooks/useToolInfo.js +5 -0
- package/dist/react/hooks/useToolInfo.js.map +1 -0
- package/dist/react/hooks/useToolMetadata.d.ts +2 -0
- package/dist/react/hooks/useToolMetadata.d.ts.map +1 -0
- package/dist/react/hooks/useToolMetadata.js +5 -0
- package/dist/react/hooks/useToolMetadata.js.map +1 -0
- package/dist/react/index.d.ts +5 -16
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -19
- package/dist/react/index.js.map +1 -1
- package/dist/types/application-manifest.d.ts +1 -0
- package/dist/types/application-manifest.d.ts.map +1 -1
- package/dist/types/application-manifest.js.map +1 -1
- package/dist/utilities/connectToHost.d.ts +3 -0
- package/dist/utilities/connectToHost.d.ts.map +1 -0
- package/dist/utilities/connectToHost.js +11 -0
- package/dist/utilities/connectToHost.js.map +1 -0
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/index.js.map +1 -1
- package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
- package/dist/vite/apolloClientAiApps.js +2 -0
- package/dist/vite/apolloClientAiApps.js.map +1 -1
- package/package.json +5 -22
- package/src/core/AbstractApolloClient.ts +217 -0
- package/src/core/ApolloClient.ts +8 -10
- package/src/core/McpAppManager.ts +106 -0
- package/src/core/typeRegistration.ts +0 -15
- package/src/core/types.ts +2 -1
- package/src/index.mcp.ts +0 -1
- package/src/index.openai.ts +0 -1
- package/src/index.ts +1 -6
- package/src/link/ToolCallLink.ts +27 -5
- package/src/link/ToolHydrationLink.ts +90 -0
- package/src/link/__tests__/ToolCallLink.test.ts +99 -0
- package/src/mcp/core/ApolloClient.ts +32 -165
- package/src/mcp/core/__tests__/ApolloClient.test.ts +571 -71
- package/src/mcp/index.ts +0 -1
- package/src/openai/core/ApolloClient.ts +48 -161
- package/src/openai/core/__tests__/ApolloClient.test.ts +916 -118
- package/src/openai/index.ts +0 -1
- package/src/openai/react/index.ts +0 -7
- package/src/react/ApolloProvider.tsx +1 -6
- package/src/react/__tests__/ApolloProvider/mcp.test.tsx +66 -29
- package/src/react/__tests__/ApolloProvider/openai.test.tsx +16 -41
- package/src/react/__tests__/createHydrationUtils.test.tsx +1260 -0
- package/src/{mcp/react/hooks → react}/createHydrationUtils.ts +7 -10
- package/src/react/hooks/__tests__/useApp.test.tsx +46 -0
- package/src/react/hooks/__tests__/useHostContext.test.tsx +99 -0
- package/src/react/hooks/__tests__/useToolInfo.test.tsx +98 -0
- package/src/react/hooks/__tests__/useToolMetadata.test.tsx +58 -0
- package/src/{mcp/react/hooks → react/hooks/internal}/useApolloClient.ts +3 -3
- package/src/{mcp/react → react}/hooks/useApp.ts +1 -1
- package/src/{openai/react → react}/hooks/useHostContext.ts +1 -1
- package/src/react/hooks/useToolInfo.ts +6 -0
- package/src/react/hooks/useToolMetadata.ts +5 -0
- package/src/react/index.ts +5 -36
- package/src/testing/internal/graphql/parseManifestOperation.ts +87 -0
- package/src/testing/internal/index.ts +3 -0
- package/src/testing/internal/matchers/index.ts +1 -0
- package/src/testing/internal/matchers/toEmitAnything.ts +43 -0
- package/src/testing/internal/matchers/types.ts +1 -0
- package/src/testing/internal/mcp/mockMcpHost.ts +25 -4
- package/src/testing/internal/tests/eachHostEnv.ts +22 -0
- package/src/testing/internal/utilities/createHostEnv.ts +117 -0
- package/src/types/application-manifest.ts +1 -0
- package/src/utilities/connectToHost.ts +13 -0
- package/src/utilities/index.ts +1 -0
- package/src/vite/__tests__/apolloClientAiApps.test.ts +56 -0
- package/src/vite/apolloClientAiApps.ts +5 -0
- package/tsconfig.vite.json +1 -1
- package/vitest.config.ts +13 -0
- package/dist/mcp/core/McpAppManager.d.ts +0 -30
- package/dist/mcp/core/McpAppManager.d.ts.map +0 -1
- package/dist/mcp/core/McpAppManager.js +0 -82
- package/dist/mcp/core/McpAppManager.js.map +0 -1
- package/dist/mcp/link/ToolCallLink.d.ts +0 -28
- package/dist/mcp/link/ToolCallLink.d.ts.map +0 -1
- package/dist/mcp/link/ToolCallLink.js +0 -35
- package/dist/mcp/link/ToolCallLink.js.map +0 -1
- package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +0 -1
- package/dist/mcp/react/hooks/createHydrationUtils.js.map +0 -1
- package/dist/mcp/react/hooks/useApolloClient.d.ts +0 -3
- package/dist/mcp/react/hooks/useApolloClient.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useApolloClient.js.map +0 -1
- package/dist/mcp/react/hooks/useApp.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useApp.js +0 -5
- package/dist/mcp/react/hooks/useApp.js.map +0 -1
- package/dist/mcp/react/hooks/useHostContext.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useHostContext.js +0 -7
- package/dist/mcp/react/hooks/useHostContext.js.map +0 -1
- package/dist/mcp/react/hooks/useToolInfo.d.ts +0 -3
- package/dist/mcp/react/hooks/useToolInfo.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolInfo.js +0 -10
- package/dist/mcp/react/hooks/useToolInfo.js.map +0 -1
- package/dist/mcp/react/hooks/useToolInput.d.ts +0 -7
- package/dist/mcp/react/hooks/useToolInput.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolInput.js +0 -9
- package/dist/mcp/react/hooks/useToolInput.js.map +0 -1
- package/dist/mcp/react/hooks/useToolMetadata.d.ts +0 -2
- package/dist/mcp/react/hooks/useToolMetadata.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolMetadata.js +0 -5
- package/dist/mcp/react/hooks/useToolMetadata.js.map +0 -1
- package/dist/mcp/react/hooks/useToolName.d.ts +0 -7
- package/dist/mcp/react/hooks/useToolName.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolName.js +0 -9
- package/dist/mcp/react/hooks/useToolName.js.map +0 -1
- package/dist/mcp/react/index.d.ts +0 -8
- package/dist/mcp/react/index.d.ts.map +0 -1
- package/dist/mcp/react/index.js +0 -8
- package/dist/mcp/react/index.js.map +0 -1
- package/dist/openai/core/McpAppManager.d.ts +0 -29
- package/dist/openai/core/McpAppManager.d.ts.map +0 -1
- package/dist/openai/core/McpAppManager.js +0 -91
- package/dist/openai/core/McpAppManager.js.map +0 -1
- package/dist/openai/link/ToolCallLink.d.ts +0 -28
- package/dist/openai/link/ToolCallLink.d.ts.map +0 -1
- package/dist/openai/link/ToolCallLink.js +0 -35
- package/dist/openai/link/ToolCallLink.js.map +0 -1
- package/dist/openai/react/hooks/createHydrationUtils.d.ts +0 -15
- package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +0 -1
- package/dist/openai/react/hooks/createHydrationUtils.js +0 -113
- package/dist/openai/react/hooks/createHydrationUtils.js.map +0 -1
- package/dist/openai/react/hooks/useApp.d.ts +0 -2
- package/dist/openai/react/hooks/useApp.d.ts.map +0 -1
- package/dist/openai/react/hooks/useApp.js +0 -5
- package/dist/openai/react/hooks/useApp.js.map +0 -1
- package/dist/openai/react/hooks/useHostContext.d.ts +0 -2
- package/dist/openai/react/hooks/useHostContext.d.ts.map +0 -1
- package/dist/openai/react/hooks/useHostContext.js.map +0 -1
- package/dist/openai/react/hooks/useToolInfo.d.ts +0 -3
- package/dist/openai/react/hooks/useToolInfo.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolInfo.js +0 -10
- package/dist/openai/react/hooks/useToolInfo.js.map +0 -1
- package/dist/openai/react/hooks/useToolInput.d.ts +0 -7
- package/dist/openai/react/hooks/useToolInput.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolInput.js +0 -9
- package/dist/openai/react/hooks/useToolInput.js.map +0 -1
- package/dist/openai/react/hooks/useToolMetadata.d.ts +0 -2
- package/dist/openai/react/hooks/useToolMetadata.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolMetadata.js +0 -5
- package/dist/openai/react/hooks/useToolMetadata.js.map +0 -1
- package/dist/openai/react/hooks/useToolName.d.ts +0 -7
- package/dist/openai/react/hooks/useToolName.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolName.js +0 -9
- package/dist/openai/react/hooks/useToolName.js.map +0 -1
- package/dist/react/index.mcp.d.ts +0 -3
- package/dist/react/index.mcp.d.ts.map +0 -1
- package/dist/react/index.mcp.js +0 -3
- package/dist/react/index.mcp.js.map +0 -1
- package/dist/react/index.openai.d.ts +0 -3
- package/dist/react/index.openai.d.ts.map +0 -1
- package/dist/react/index.openai.js +0 -3
- package/dist/react/index.openai.js.map +0 -1
- package/dist/react/missingHook.d.ts +0 -2
- package/dist/react/missingHook.d.ts.map +0 -1
- package/dist/react/missingHook.js +0 -6
- package/dist/react/missingHook.js.map +0 -1
- package/src/mcp/core/McpAppManager.ts +0 -129
- package/src/mcp/link/ToolCallLink.ts +0 -40
- package/src/mcp/link/__tests__/ToolCallLink.test.ts +0 -62
- package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1228
- package/src/mcp/react/hooks/__tests__/useApp.test.tsx +0 -46
- package/src/mcp/react/hooks/__tests__/useHostContext.test.tsx +0 -95
- package/src/mcp/react/hooks/__tests__/useToolInfo.test.tsx +0 -53
- package/src/mcp/react/hooks/__tests__/useToolInput.test.tsx +0 -50
- package/src/mcp/react/hooks/__tests__/useToolMetadata.test.tsx +0 -53
- package/src/mcp/react/hooks/__tests__/useToolName.test.tsx +0 -50
- package/src/mcp/react/hooks/useHostContext.ts +0 -14
- package/src/mcp/react/hooks/useToolInfo.ts +0 -13
- package/src/mcp/react/hooks/useToolInput.ts +0 -10
- package/src/mcp/react/hooks/useToolMetadata.ts +0 -5
- package/src/mcp/react/hooks/useToolName.ts +0 -10
- package/src/mcp/react/index.ts +0 -7
- package/src/openai/core/McpAppManager.ts +0 -139
- package/src/openai/link/ToolCallLink.ts +0 -40
- package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1333
- package/src/openai/react/hooks/__tests__/useToolInfo.test.tsx +0 -92
- package/src/openai/react/hooks/__tests__/useToolInput.test.tsx +0 -85
- package/src/openai/react/hooks/__tests__/useToolMetadata.test.tsx +0 -86
- package/src/openai/react/hooks/__tests__/useToolName.test.tsx +0 -50
- package/src/openai/react/hooks/createHydrationUtils.ts +0 -182
- package/src/openai/react/hooks/useApp.ts +0 -5
- package/src/openai/react/hooks/useToolInfo.ts +0 -13
- package/src/openai/react/hooks/useToolInput.ts +0 -10
- package/src/openai/react/hooks/useToolMetadata.ts +0 -5
- package/src/openai/react/hooks/useToolName.ts +0 -10
- package/src/react/index.mcp.ts +0 -10
- package/src/react/index.openai.ts +0 -10
- package/src/react/missingHook.ts +0 -9
- /package/dist/{mcp/react → react}/hooks/useApp.d.ts +0 -0
- /package/dist/{mcp/react → react}/hooks/useHostContext.d.ts +0 -0
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { expect, test, describe, vi } from "vitest";
|
|
2
2
|
import { ApolloClient } from "../ApolloClient.js";
|
|
3
|
-
import { parse } from "graphql";
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { parse, type DocumentNode } from "graphql";
|
|
4
|
+
import {
|
|
5
|
+
ApolloLink,
|
|
6
|
+
HttpLink,
|
|
7
|
+
InMemoryCache,
|
|
8
|
+
NetworkStatus,
|
|
9
|
+
gql,
|
|
10
|
+
} from "@apollo/client";
|
|
11
|
+
import { ToolCallLink } from "../../../link/ToolCallLink.js";
|
|
6
12
|
import {
|
|
7
13
|
graphqlToolResult,
|
|
8
14
|
minimalHostContextWithToolName,
|
|
9
15
|
mockApplicationManifest,
|
|
10
16
|
mockMcpHost,
|
|
17
|
+
ObservableStream,
|
|
18
|
+
parseManifestOperation,
|
|
11
19
|
spyOnConsole,
|
|
12
20
|
stubOpenAiGlobals,
|
|
13
21
|
} from "../../../testing/internal/index.js";
|
|
@@ -91,23 +99,87 @@ describe("Client Basics", () => {
|
|
|
91
99
|
});
|
|
92
100
|
});
|
|
93
101
|
|
|
102
|
+
test("merges _meta.structuredContent into result for @private fields", async () => {
|
|
103
|
+
stubOpenAiGlobals();
|
|
104
|
+
using _ = spyOnConsole("debug");
|
|
105
|
+
const manifest = mockApplicationManifest();
|
|
106
|
+
const client = new ApolloClient({
|
|
107
|
+
cache: new InMemoryCache(),
|
|
108
|
+
manifest,
|
|
109
|
+
});
|
|
110
|
+
using host = await mockMcpHost();
|
|
111
|
+
|
|
112
|
+
const query = gql`
|
|
113
|
+
query Product {
|
|
114
|
+
id
|
|
115
|
+
title @private
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
host.onCleanup(() => client.stop());
|
|
120
|
+
|
|
121
|
+
host.sendToolInput({});
|
|
122
|
+
host.sendToolResult({
|
|
123
|
+
content: [],
|
|
124
|
+
structuredContent: {},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
host.mockToolCall("execute", () => ({
|
|
128
|
+
content: [],
|
|
129
|
+
structuredContent: {},
|
|
130
|
+
_meta: {
|
|
131
|
+
structuredContent: {
|
|
132
|
+
data: {
|
|
133
|
+
product: {
|
|
134
|
+
id: "1",
|
|
135
|
+
title: "Private Pen",
|
|
136
|
+
__typename: "Product",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
await client.connect();
|
|
144
|
+
|
|
145
|
+
await expect(
|
|
146
|
+
client.query({ query, variables: { id: "1" } })
|
|
147
|
+
).resolves.toStrictEqual({
|
|
148
|
+
data: {
|
|
149
|
+
product: {
|
|
150
|
+
__typename: "Product",
|
|
151
|
+
id: "1",
|
|
152
|
+
title: "Private Pen",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
94
158
|
describe("prefetchData", () => {
|
|
95
159
|
test("caches tool response when data is provided", async () => {
|
|
96
160
|
stubOpenAiGlobals({ toolInput: { id: 1 } });
|
|
97
161
|
using _ = spyOnConsole("debug");
|
|
98
|
-
const client = new ApolloClient({
|
|
99
|
-
cache: new InMemoryCache(),
|
|
100
|
-
manifest: mockApplicationManifest(),
|
|
101
|
-
});
|
|
102
|
-
using host = await mockMcpHost({
|
|
103
|
-
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
104
|
-
});
|
|
105
162
|
|
|
106
|
-
|
|
163
|
+
const query = gql`
|
|
164
|
+
query Product($id: ID!)
|
|
165
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
166
|
+
product(id: $id) {
|
|
167
|
+
id
|
|
168
|
+
title
|
|
169
|
+
rating
|
|
170
|
+
price
|
|
171
|
+
description
|
|
172
|
+
images
|
|
173
|
+
__typename
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const { client, host } = await setup({ query });
|
|
179
|
+
using _host = host;
|
|
107
180
|
|
|
108
181
|
host.sendToolInput({ arguments: { id: 1 } });
|
|
109
182
|
host.sendToolResult({
|
|
110
|
-
content: [],
|
|
111
183
|
structuredContent: {
|
|
112
184
|
result: {
|
|
113
185
|
data: {
|
|
@@ -127,25 +199,39 @@ describe("prefetchData", () => {
|
|
|
127
199
|
|
|
128
200
|
await client.connect();
|
|
129
201
|
|
|
130
|
-
expect(
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
await expect(
|
|
203
|
+
client.query({ query, variables: { id: 1 } })
|
|
204
|
+
).resolves.toStrictEqual({
|
|
205
|
+
data: {
|
|
206
|
+
product: {
|
|
207
|
+
id: "1",
|
|
208
|
+
title: "Pen",
|
|
209
|
+
rating: 5,
|
|
210
|
+
price: 1.0,
|
|
211
|
+
description: "Awesome pen",
|
|
212
|
+
images: [],
|
|
213
|
+
__typename: "Product",
|
|
140
214
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(client.extract()).toStrictEqual({
|
|
219
|
+
"Product:1": {
|
|
220
|
+
__typename: "Product",
|
|
221
|
+
description: "Awesome pen",
|
|
222
|
+
id: "1",
|
|
223
|
+
images: [],
|
|
224
|
+
price: 1,
|
|
225
|
+
rating: 5,
|
|
226
|
+
title: "Pen",
|
|
227
|
+
},
|
|
228
|
+
ROOT_QUERY: {
|
|
229
|
+
__typename: "Query",
|
|
230
|
+
'product({"id":1})': {
|
|
231
|
+
__ref: "Product:1",
|
|
146
232
|
},
|
|
147
|
-
}
|
|
148
|
-
|
|
233
|
+
},
|
|
234
|
+
});
|
|
149
235
|
});
|
|
150
236
|
|
|
151
237
|
test("caches prefetched data when prefetched data is provided", async () => {
|
|
@@ -229,39 +315,42 @@ describe("prefetchData", () => {
|
|
|
229
315
|
test("caches both prefetch and tool response when both are provided", async () => {
|
|
230
316
|
stubOpenAiGlobals({ toolInput: { id: 1 } });
|
|
231
317
|
using _ = spyOnConsole("debug");
|
|
318
|
+
|
|
319
|
+
const prefetchQuery = gql`
|
|
320
|
+
query TopProducts
|
|
321
|
+
@tool(description: "Shows the currently highest rated products.")
|
|
322
|
+
@prefetch {
|
|
323
|
+
topProducts {
|
|
324
|
+
id
|
|
325
|
+
title
|
|
326
|
+
rating
|
|
327
|
+
price
|
|
328
|
+
__typename
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const query = gql`
|
|
334
|
+
query Product($id: ID!)
|
|
335
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
336
|
+
product(id: $id) {
|
|
337
|
+
id
|
|
338
|
+
title
|
|
339
|
+
rating
|
|
340
|
+
price
|
|
341
|
+
description
|
|
342
|
+
images
|
|
343
|
+
__typename
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
|
|
232
348
|
const client = new ApolloClient({
|
|
233
349
|
cache: new InMemoryCache(),
|
|
234
350
|
manifest: mockApplicationManifest({
|
|
235
351
|
operations: [
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
name: "Product",
|
|
239
|
-
type: "query",
|
|
240
|
-
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
241
|
-
variables: { id: "ID" },
|
|
242
|
-
prefetch: false,
|
|
243
|
-
tools: [
|
|
244
|
-
{
|
|
245
|
-
name: "GetProduct",
|
|
246
|
-
description: "Shows the details page for a specific product.",
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
|
|
252
|
-
name: "TopProducts",
|
|
253
|
-
type: "query",
|
|
254
|
-
body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
|
|
255
|
-
variables: {},
|
|
256
|
-
prefetch: true,
|
|
257
|
-
prefetchID: "__anonymous",
|
|
258
|
-
tools: [
|
|
259
|
-
{
|
|
260
|
-
name: "TopProducts",
|
|
261
|
-
description: "Shows the currently highest rated products.",
|
|
262
|
-
},
|
|
263
|
-
],
|
|
264
|
-
},
|
|
352
|
+
parseManifestOperation(prefetchQuery),
|
|
353
|
+
parseManifestOperation(query),
|
|
265
354
|
],
|
|
266
355
|
}),
|
|
267
356
|
});
|
|
@@ -273,7 +362,6 @@ describe("prefetchData", () => {
|
|
|
273
362
|
|
|
274
363
|
host.sendToolInput({ arguments: { id: 1 } });
|
|
275
364
|
host.sendToolResult({
|
|
276
|
-
content: [],
|
|
277
365
|
structuredContent: {
|
|
278
366
|
result: {
|
|
279
367
|
data: {
|
|
@@ -310,7 +398,53 @@ describe("prefetchData", () => {
|
|
|
310
398
|
|
|
311
399
|
await client.connect();
|
|
312
400
|
|
|
313
|
-
expect(
|
|
401
|
+
await expect(
|
|
402
|
+
client.query({ query, variables: { id: 1 } })
|
|
403
|
+
).resolves.toStrictEqual({
|
|
404
|
+
data: {
|
|
405
|
+
product: {
|
|
406
|
+
id: "1",
|
|
407
|
+
title: "Pen",
|
|
408
|
+
rating: 5,
|
|
409
|
+
price: 1.0,
|
|
410
|
+
description: "Awesome pen",
|
|
411
|
+
images: [],
|
|
412
|
+
__typename: "Product",
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(client.extract()).toMatchInlineSnapshot(
|
|
418
|
+
{
|
|
419
|
+
"Product:1": {
|
|
420
|
+
__typename: "Product",
|
|
421
|
+
description: "Awesome pen",
|
|
422
|
+
id: "1",
|
|
423
|
+
images: [],
|
|
424
|
+
price: 1,
|
|
425
|
+
rating: 5,
|
|
426
|
+
title: "Pen",
|
|
427
|
+
},
|
|
428
|
+
"Product:2": {
|
|
429
|
+
__typename: "Product",
|
|
430
|
+
id: "2",
|
|
431
|
+
price: 999.99,
|
|
432
|
+
rating: 5,
|
|
433
|
+
title: "iPhone 17",
|
|
434
|
+
},
|
|
435
|
+
ROOT_QUERY: {
|
|
436
|
+
__typename: "Query",
|
|
437
|
+
'product({"id":1})': {
|
|
438
|
+
__ref: "Product:1",
|
|
439
|
+
},
|
|
440
|
+
topProducts: [
|
|
441
|
+
{
|
|
442
|
+
__ref: "Product:2",
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
`
|
|
314
448
|
{
|
|
315
449
|
"Product:1": {
|
|
316
450
|
"__typename": "Product",
|
|
@@ -340,21 +474,30 @@ describe("prefetchData", () => {
|
|
|
340
474
|
],
|
|
341
475
|
},
|
|
342
476
|
}
|
|
343
|
-
`
|
|
477
|
+
`
|
|
478
|
+
);
|
|
344
479
|
});
|
|
345
480
|
|
|
346
481
|
test("excludes extra inputs when writing to cache", async () => {
|
|
347
482
|
stubOpenAiGlobals({ toolInput: { id: 1, myOtherThing: 2 } });
|
|
348
483
|
using _ = spyOnConsole("debug");
|
|
349
|
-
const client = new ApolloClient({
|
|
350
|
-
cache: new InMemoryCache(),
|
|
351
|
-
manifest: mockApplicationManifest(),
|
|
352
|
-
});
|
|
353
|
-
using host = await mockMcpHost({
|
|
354
|
-
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
355
|
-
});
|
|
356
484
|
|
|
357
|
-
|
|
485
|
+
const query = gql`
|
|
486
|
+
query Product($id: ID!)
|
|
487
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
488
|
+
product(id: $id) {
|
|
489
|
+
id
|
|
490
|
+
title
|
|
491
|
+
rating
|
|
492
|
+
price
|
|
493
|
+
description
|
|
494
|
+
images
|
|
495
|
+
__typename
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
const { client, host } = await setup({ query });
|
|
500
|
+
using _host = host;
|
|
358
501
|
|
|
359
502
|
host.sendToolInput({ arguments: { id: 1, myOtherThing: 2 } });
|
|
360
503
|
host.sendToolResult({
|
|
@@ -378,60 +521,292 @@ describe("prefetchData", () => {
|
|
|
378
521
|
|
|
379
522
|
await client.connect();
|
|
380
523
|
|
|
381
|
-
expect(
|
|
382
|
-
{
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
"
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
524
|
+
await expect(
|
|
525
|
+
client.query({ query, variables: { id: 1 } })
|
|
526
|
+
).resolves.toStrictEqual({
|
|
527
|
+
data: {
|
|
528
|
+
product: {
|
|
529
|
+
id: "1",
|
|
530
|
+
title: "Pen",
|
|
531
|
+
rating: 5,
|
|
532
|
+
price: 1.0,
|
|
533
|
+
description: "Awesome pen",
|
|
534
|
+
images: [],
|
|
535
|
+
__typename: "Product",
|
|
391
536
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
expect(client.extract()).toStrictEqual({
|
|
541
|
+
"Product:1": {
|
|
542
|
+
__typename: "Product",
|
|
543
|
+
description: "Awesome pen",
|
|
544
|
+
id: "1",
|
|
545
|
+
images: [],
|
|
546
|
+
price: 1,
|
|
547
|
+
rating: 5,
|
|
548
|
+
title: "Pen",
|
|
549
|
+
},
|
|
550
|
+
ROOT_QUERY: {
|
|
551
|
+
__typename: "Query",
|
|
552
|
+
'product({"id":1})': {
|
|
553
|
+
__ref: "Product:1",
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("reads result data from toolResponseMetadata.structuredContent", async () => {
|
|
561
|
+
stubOpenAiGlobals({
|
|
562
|
+
toolInput: { id: "1" },
|
|
563
|
+
toolResponseMetadata: {
|
|
564
|
+
structuredContent: {
|
|
565
|
+
result: {
|
|
566
|
+
data: {
|
|
567
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
396
568
|
},
|
|
397
569
|
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
using _ = spyOnConsole("debug");
|
|
574
|
+
|
|
575
|
+
const query = gql`
|
|
576
|
+
query Product($id: ID!)
|
|
577
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
578
|
+
product(id: $id) @private {
|
|
579
|
+
id
|
|
580
|
+
title
|
|
581
|
+
__typename
|
|
398
582
|
}
|
|
399
|
-
|
|
583
|
+
}
|
|
584
|
+
`;
|
|
585
|
+
|
|
586
|
+
const { client, host } = await setup({ query });
|
|
587
|
+
using _host = host;
|
|
588
|
+
|
|
589
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
590
|
+
host.sendToolResult({
|
|
591
|
+
structuredContent: {},
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
await client.connect();
|
|
595
|
+
|
|
596
|
+
await expect(
|
|
597
|
+
client.query({ query, variables: { id: "1" } })
|
|
598
|
+
).resolves.toStrictEqual({
|
|
599
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(client.extract()).toStrictEqual({
|
|
603
|
+
"Product:1": {
|
|
604
|
+
__typename: "Product",
|
|
605
|
+
id: "1",
|
|
606
|
+
title: "Pen",
|
|
607
|
+
},
|
|
608
|
+
ROOT_QUERY: {
|
|
609
|
+
__typename: "Query",
|
|
610
|
+
'product({"id":"1"})@private': { __ref: "Product:1" },
|
|
611
|
+
},
|
|
400
612
|
});
|
|
401
613
|
});
|
|
402
614
|
|
|
403
|
-
test("
|
|
615
|
+
test("merges prefetch from structuredContent and result from toolResponseMetadata.structuredContent", async () => {
|
|
404
616
|
stubOpenAiGlobals({
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
title: "
|
|
411
|
-
rating: 5,
|
|
412
|
-
price: 1.0,
|
|
413
|
-
description: "Awesome pen",
|
|
414
|
-
images: [],
|
|
415
|
-
__typename: "Product",
|
|
617
|
+
toolInput: { id: "2" },
|
|
618
|
+
toolResponseMetadata: {
|
|
619
|
+
structuredContent: {
|
|
620
|
+
result: {
|
|
621
|
+
data: {
|
|
622
|
+
product: { id: "2", title: "iPad", __typename: "Product" },
|
|
416
623
|
},
|
|
417
624
|
},
|
|
418
625
|
},
|
|
419
626
|
},
|
|
420
|
-
toolInput: { id: "1" },
|
|
421
627
|
});
|
|
422
628
|
using _ = spyOnConsole("debug");
|
|
629
|
+
|
|
630
|
+
const prefetchQuery = gql`
|
|
631
|
+
query TopProducts
|
|
632
|
+
@tool(description: "Shows the currently highest rated products.")
|
|
633
|
+
@prefetch {
|
|
634
|
+
topProducts {
|
|
635
|
+
id
|
|
636
|
+
title
|
|
637
|
+
__typename
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
`;
|
|
641
|
+
|
|
642
|
+
const query = gql`
|
|
643
|
+
query Product($id: ID!)
|
|
644
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
645
|
+
product(id: $id) @private {
|
|
646
|
+
id
|
|
647
|
+
title
|
|
648
|
+
__typename
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
`;
|
|
652
|
+
|
|
423
653
|
const client = new ApolloClient({
|
|
424
654
|
cache: new InMemoryCache(),
|
|
425
|
-
manifest: mockApplicationManifest(
|
|
655
|
+
manifest: mockApplicationManifest({
|
|
656
|
+
operations: [
|
|
657
|
+
parseManifestOperation(prefetchQuery),
|
|
658
|
+
parseManifestOperation(query),
|
|
659
|
+
],
|
|
660
|
+
}),
|
|
426
661
|
});
|
|
427
662
|
using host = await mockMcpHost({
|
|
428
663
|
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
429
664
|
});
|
|
430
665
|
host.onCleanup(() => client.stop());
|
|
431
666
|
|
|
432
|
-
host.sendToolInput({ arguments: { id: "
|
|
433
|
-
|
|
434
|
-
|
|
667
|
+
host.sendToolInput({ arguments: { id: "2" } });
|
|
668
|
+
host.sendToolResult({
|
|
669
|
+
structuredContent: {
|
|
670
|
+
prefetch: {
|
|
671
|
+
__anonymous: {
|
|
672
|
+
data: {
|
|
673
|
+
topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await client.connect();
|
|
681
|
+
|
|
682
|
+
await expect(
|
|
683
|
+
client.query({ query, variables: { id: "2" } })
|
|
684
|
+
).resolves.toStrictEqual({
|
|
685
|
+
data: { product: { id: "2", title: "iPad", __typename: "Product" } },
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
expect(client.extract()).toEqual({
|
|
689
|
+
"Product:1": {
|
|
690
|
+
__typename: "Product",
|
|
691
|
+
id: "1",
|
|
692
|
+
title: "iPhone",
|
|
693
|
+
},
|
|
694
|
+
"Product:2": {
|
|
695
|
+
__typename: "Product",
|
|
696
|
+
id: "2",
|
|
697
|
+
title: "iPad",
|
|
698
|
+
},
|
|
699
|
+
ROOT_QUERY: {
|
|
700
|
+
__typename: "Query",
|
|
701
|
+
topProducts: [{ __ref: "Product:1" }],
|
|
702
|
+
'product({"id":"2"})@private': { __ref: "Product:2" },
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("toolResponseMetadata.structuredContent wins over structuredContent", async () => {
|
|
708
|
+
stubOpenAiGlobals({
|
|
709
|
+
toolInput: { id: "1" },
|
|
710
|
+
toolResponseMetadata: {
|
|
711
|
+
structuredContent: {
|
|
712
|
+
result: {
|
|
713
|
+
data: {
|
|
714
|
+
product: { id: "1", title: "Meta title", __typename: "Product" },
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
using _ = spyOnConsole("debug");
|
|
721
|
+
|
|
722
|
+
const query = gql`
|
|
723
|
+
query Product($id: ID!)
|
|
724
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
725
|
+
product(id: $id) {
|
|
726
|
+
id
|
|
727
|
+
title @private
|
|
728
|
+
__typename
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
`;
|
|
732
|
+
|
|
733
|
+
const { client, host } = await setup({ query });
|
|
734
|
+
using _host = host;
|
|
735
|
+
|
|
736
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
737
|
+
host.sendToolResult({
|
|
738
|
+
structuredContent: {
|
|
739
|
+
result: {
|
|
740
|
+
data: {
|
|
741
|
+
product: { id: "1", __typename: "Product" },
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
await client.connect();
|
|
748
|
+
|
|
749
|
+
await expect(
|
|
750
|
+
client.query({ query, variables: { id: "1" } })
|
|
751
|
+
).resolves.toStrictEqual({
|
|
752
|
+
data: { product: { id: "1", title: "Meta title", __typename: "Product" } },
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
expect(client.extract()).toStrictEqual({
|
|
756
|
+
"Product:1": {
|
|
757
|
+
__typename: "Product",
|
|
758
|
+
id: "1",
|
|
759
|
+
"title@private": "Meta title",
|
|
760
|
+
},
|
|
761
|
+
ROOT_QUERY: {
|
|
762
|
+
__typename: "Query",
|
|
763
|
+
'product({"id":"1"})': { __ref: "Product:1" },
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("connects using window.openai.toolOutput when tool-result notification is not sent", async () => {
|
|
769
|
+
stubOpenAiGlobals({
|
|
770
|
+
toolOutput: {
|
|
771
|
+
result: {
|
|
772
|
+
data: {
|
|
773
|
+
product: {
|
|
774
|
+
id: "1",
|
|
775
|
+
title: "Pen",
|
|
776
|
+
rating: 5,
|
|
777
|
+
price: 1.0,
|
|
778
|
+
description: "Awesome pen",
|
|
779
|
+
images: [],
|
|
780
|
+
__typename: "Product",
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
toolInput: { id: "1" },
|
|
786
|
+
});
|
|
787
|
+
using _ = spyOnConsole("debug");
|
|
788
|
+
|
|
789
|
+
const query = gql`
|
|
790
|
+
query Product($id: ID!)
|
|
791
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
792
|
+
product(id: $id) {
|
|
793
|
+
id
|
|
794
|
+
title
|
|
795
|
+
rating
|
|
796
|
+
price
|
|
797
|
+
description
|
|
798
|
+
images
|
|
799
|
+
__typename
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
`;
|
|
803
|
+
|
|
804
|
+
const { client, host } = await setup({ query });
|
|
805
|
+
using _host = host;
|
|
806
|
+
|
|
807
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
808
|
+
// No host.sendToolResult() — simulates page reload where ChatGPT does not
|
|
809
|
+
// re-send the tool-result notification
|
|
435
810
|
|
|
436
811
|
await client.connect();
|
|
437
812
|
|
|
@@ -439,25 +814,39 @@ test("connects using window.openai.toolOutput when tool-result notification is n
|
|
|
439
814
|
// before `using host` disposes and closes the app connection.
|
|
440
815
|
await new Promise((resolve) => setImmediate(resolve));
|
|
441
816
|
|
|
442
|
-
expect(
|
|
443
|
-
{
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
817
|
+
await expect(
|
|
818
|
+
client.query({ query, variables: { id: "1" } })
|
|
819
|
+
).resolves.toStrictEqual({
|
|
820
|
+
data: {
|
|
821
|
+
product: {
|
|
822
|
+
id: "1",
|
|
823
|
+
title: "Pen",
|
|
824
|
+
rating: 5,
|
|
825
|
+
price: 1.0,
|
|
826
|
+
description: "Awesome pen",
|
|
827
|
+
images: [],
|
|
828
|
+
__typename: "Product",
|
|
452
829
|
},
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
expect(client.extract()).toStrictEqual({
|
|
834
|
+
"Product:1": {
|
|
835
|
+
__typename: "Product",
|
|
836
|
+
description: "Awesome pen",
|
|
837
|
+
id: "1",
|
|
838
|
+
images: [],
|
|
839
|
+
price: 1,
|
|
840
|
+
rating: 5,
|
|
841
|
+
title: "Pen",
|
|
842
|
+
},
|
|
843
|
+
ROOT_QUERY: {
|
|
844
|
+
__typename: "Query",
|
|
845
|
+
'product({"id":"1"})': {
|
|
846
|
+
__ref: "Product:1",
|
|
458
847
|
},
|
|
459
|
-
}
|
|
460
|
-
|
|
848
|
+
},
|
|
849
|
+
});
|
|
461
850
|
});
|
|
462
851
|
|
|
463
852
|
describe("custom links", () => {
|
|
@@ -597,6 +986,392 @@ describe("custom links", () => {
|
|
|
597
986
|
});
|
|
598
987
|
});
|
|
599
988
|
|
|
989
|
+
test("serves tool result data on network-only query without calling execute tool", async () => {
|
|
990
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
991
|
+
using _ = spyOnConsole("debug");
|
|
992
|
+
const query = gql`
|
|
993
|
+
query Product($id: ID!)
|
|
994
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
995
|
+
product(id: $id) {
|
|
996
|
+
id
|
|
997
|
+
title
|
|
998
|
+
__typename
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
`;
|
|
1002
|
+
|
|
1003
|
+
const data = {
|
|
1004
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const { client, host } = await setup({ query });
|
|
1008
|
+
using _host = host;
|
|
1009
|
+
|
|
1010
|
+
const execute = vi.fn();
|
|
1011
|
+
host.mockToolCall("execute", execute);
|
|
1012
|
+
|
|
1013
|
+
host.sendToolResult({ structuredContent: { result: { data } } });
|
|
1014
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1015
|
+
|
|
1016
|
+
await client.connect();
|
|
1017
|
+
|
|
1018
|
+
await expect(
|
|
1019
|
+
client.query({
|
|
1020
|
+
query,
|
|
1021
|
+
variables: { id: "1" },
|
|
1022
|
+
fetchPolicy: "network-only",
|
|
1023
|
+
})
|
|
1024
|
+
).resolves.toStrictEqual({ data });
|
|
1025
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("calls execute tool on second network-only query after hydration is consumed", async () => {
|
|
1029
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1030
|
+
using _ = spyOnConsole("debug");
|
|
1031
|
+
const query = gql`
|
|
1032
|
+
query Product($id: ID!)
|
|
1033
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1034
|
+
product(id: $id) {
|
|
1035
|
+
id
|
|
1036
|
+
title
|
|
1037
|
+
__typename
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
`;
|
|
1041
|
+
|
|
1042
|
+
const { client, host } = await setup({ query });
|
|
1043
|
+
using _host = host;
|
|
1044
|
+
|
|
1045
|
+
host.mockToolCall("execute", () => ({
|
|
1046
|
+
structuredContent: {
|
|
1047
|
+
data: {
|
|
1048
|
+
product: { id: "1", title: "Updated Pen", __typename: "Product" },
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
}));
|
|
1052
|
+
|
|
1053
|
+
host.sendToolResult({
|
|
1054
|
+
structuredContent: {
|
|
1055
|
+
result: {
|
|
1056
|
+
data: {
|
|
1057
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1063
|
+
|
|
1064
|
+
await client.connect();
|
|
1065
|
+
|
|
1066
|
+
await client.query({
|
|
1067
|
+
query,
|
|
1068
|
+
variables: { id: "1" },
|
|
1069
|
+
fetchPolicy: "network-only",
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
await expect(
|
|
1073
|
+
client.query({
|
|
1074
|
+
query,
|
|
1075
|
+
variables: { id: "1" },
|
|
1076
|
+
fetchPolicy: "network-only",
|
|
1077
|
+
})
|
|
1078
|
+
).resolves.toStrictEqual({
|
|
1079
|
+
data: {
|
|
1080
|
+
product: { id: "1", title: "Updated Pen", __typename: "Product" },
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("serves tool result data on cache-and-network query without calling execute tool", async () => {
|
|
1086
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1087
|
+
using _ = spyOnConsole("debug");
|
|
1088
|
+
const query = gql`
|
|
1089
|
+
query Product($id: ID!)
|
|
1090
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1091
|
+
product(id: $id) {
|
|
1092
|
+
id
|
|
1093
|
+
title
|
|
1094
|
+
__typename
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
`;
|
|
1098
|
+
|
|
1099
|
+
const { client, host } = await setup({ query });
|
|
1100
|
+
using _host = host;
|
|
1101
|
+
|
|
1102
|
+
const execute = vi.fn();
|
|
1103
|
+
host.mockToolCall("execute", execute);
|
|
1104
|
+
|
|
1105
|
+
host.sendToolResult({
|
|
1106
|
+
structuredContent: {
|
|
1107
|
+
result: {
|
|
1108
|
+
data: {
|
|
1109
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1115
|
+
|
|
1116
|
+
await client.connect();
|
|
1117
|
+
|
|
1118
|
+
const stream = new ObservableStream(
|
|
1119
|
+
client.watchQuery({
|
|
1120
|
+
query,
|
|
1121
|
+
variables: { id: "1" },
|
|
1122
|
+
fetchPolicy: "cache-and-network",
|
|
1123
|
+
})
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
// The hydrated result is emitted synchronously so we won't observe a loading
|
|
1127
|
+
// state like we normally would with `cache-and-network`
|
|
1128
|
+
await expect(stream).toEmitValue({
|
|
1129
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1130
|
+
dataState: "complete",
|
|
1131
|
+
loading: false,
|
|
1132
|
+
networkStatus: NetworkStatus.ready,
|
|
1133
|
+
partial: false,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
await expect(stream).not.toEmitAnything();
|
|
1137
|
+
|
|
1138
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("serves tool result data on no-cache query without calling execute tool and does not write to cache", async () => {
|
|
1142
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1143
|
+
using _ = spyOnConsole("debug");
|
|
1144
|
+
const query = gql`
|
|
1145
|
+
query Product($id: ID!)
|
|
1146
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1147
|
+
product(id: $id) {
|
|
1148
|
+
id
|
|
1149
|
+
title
|
|
1150
|
+
__typename
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
`;
|
|
1154
|
+
|
|
1155
|
+
const data = {
|
|
1156
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const { client, host } = await setup({ query });
|
|
1160
|
+
using _host = host;
|
|
1161
|
+
|
|
1162
|
+
const execute = vi.fn();
|
|
1163
|
+
host.mockToolCall("execute", execute);
|
|
1164
|
+
|
|
1165
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1166
|
+
host.sendToolResult({ structuredContent: { result: { data } } });
|
|
1167
|
+
|
|
1168
|
+
await client.connect();
|
|
1169
|
+
|
|
1170
|
+
await expect(
|
|
1171
|
+
client.query({ query, variables: { id: "1" }, fetchPolicy: "no-cache" })
|
|
1172
|
+
).resolves.toStrictEqual({ data });
|
|
1173
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1174
|
+
expect(client.extract()).toStrictEqual({});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test("hydrates prefetch query with network-only fetch policy", async () => {
|
|
1178
|
+
stubOpenAiGlobals({ toolInput: {} });
|
|
1179
|
+
using _ = spyOnConsole("debug");
|
|
1180
|
+
|
|
1181
|
+
const query = gql`
|
|
1182
|
+
query TopProducts @tool(description: "Shows top products") @prefetch {
|
|
1183
|
+
topProducts {
|
|
1184
|
+
id
|
|
1185
|
+
title
|
|
1186
|
+
__typename
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
`;
|
|
1190
|
+
|
|
1191
|
+
const data = {
|
|
1192
|
+
topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const { client, host } = await setup({ query, toolName: "OtherTool" });
|
|
1196
|
+
using _host = host;
|
|
1197
|
+
|
|
1198
|
+
const execute = vi.fn();
|
|
1199
|
+
host.mockToolCall("execute", execute);
|
|
1200
|
+
|
|
1201
|
+
host.sendToolResult({
|
|
1202
|
+
structuredContent: {
|
|
1203
|
+
prefetch: { __anonymous: { data } },
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
host.sendToolInput({ arguments: {} });
|
|
1207
|
+
|
|
1208
|
+
await client.connect();
|
|
1209
|
+
|
|
1210
|
+
await expect(
|
|
1211
|
+
client.query({ query, fetchPolicy: "network-only" })
|
|
1212
|
+
).resolves.toStrictEqual({ data });
|
|
1213
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
test("serves hydrated query from tool result while other network-only queries call execute", async () => {
|
|
1217
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1218
|
+
using _ = spyOnConsole("debug");
|
|
1219
|
+
|
|
1220
|
+
const productQuery = gql`
|
|
1221
|
+
query Product($id: ID!)
|
|
1222
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1223
|
+
product(id: $id) {
|
|
1224
|
+
id
|
|
1225
|
+
title
|
|
1226
|
+
__typename
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
`;
|
|
1230
|
+
|
|
1231
|
+
const cartQuery = gql`
|
|
1232
|
+
query Cart @tool(name: "GetCart", description: "Get the cart") {
|
|
1233
|
+
cart {
|
|
1234
|
+
id
|
|
1235
|
+
__typename
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
`;
|
|
1239
|
+
|
|
1240
|
+
const productOperation = parseManifestOperation(productQuery);
|
|
1241
|
+
const cartOperation = parseManifestOperation(cartQuery);
|
|
1242
|
+
|
|
1243
|
+
const client = new ApolloClient({
|
|
1244
|
+
cache: new InMemoryCache(),
|
|
1245
|
+
manifest: mockApplicationManifest({
|
|
1246
|
+
operations: [productOperation, cartOperation],
|
|
1247
|
+
}),
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
using host = await mockMcpHost({
|
|
1251
|
+
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
1252
|
+
});
|
|
1253
|
+
host.onCleanup(() => client.stop());
|
|
1254
|
+
|
|
1255
|
+
const execute = vi.fn(() => ({
|
|
1256
|
+
structuredContent: {
|
|
1257
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1258
|
+
},
|
|
1259
|
+
}));
|
|
1260
|
+
host.mockToolCall("execute", execute);
|
|
1261
|
+
|
|
1262
|
+
host.sendToolResult({
|
|
1263
|
+
structuredContent: {
|
|
1264
|
+
result: {
|
|
1265
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1270
|
+
|
|
1271
|
+
await client.connect();
|
|
1272
|
+
|
|
1273
|
+
const [productResult, cartResult] = await Promise.all([
|
|
1274
|
+
client.query({
|
|
1275
|
+
query: productQuery,
|
|
1276
|
+
variables: { id: "1" },
|
|
1277
|
+
fetchPolicy: "network-only",
|
|
1278
|
+
}),
|
|
1279
|
+
client.query({
|
|
1280
|
+
query: cartQuery,
|
|
1281
|
+
fetchPolicy: "network-only",
|
|
1282
|
+
}),
|
|
1283
|
+
]);
|
|
1284
|
+
|
|
1285
|
+
expect(productResult).toStrictEqual({
|
|
1286
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1287
|
+
});
|
|
1288
|
+
expect(cartResult).toStrictEqual({
|
|
1289
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1290
|
+
});
|
|
1291
|
+
expect(execute).toHaveBeenCalledOnce();
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
test("serves hydrated query after tool result while earlier-queued non-matching query calls execute", async () => {
|
|
1295
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1296
|
+
using _ = spyOnConsole("debug");
|
|
1297
|
+
|
|
1298
|
+
const productQuery = gql`
|
|
1299
|
+
query Product($id: ID!)
|
|
1300
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1301
|
+
product(id: $id) {
|
|
1302
|
+
id
|
|
1303
|
+
title
|
|
1304
|
+
__typename
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
`;
|
|
1308
|
+
|
|
1309
|
+
const cartQuery = gql`
|
|
1310
|
+
query Cart @tool(name: "GetCart", description: "Get the cart") {
|
|
1311
|
+
cart {
|
|
1312
|
+
id
|
|
1313
|
+
__typename
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
`;
|
|
1317
|
+
|
|
1318
|
+
const productOperation = parseManifestOperation(productQuery);
|
|
1319
|
+
const cartOperation = parseManifestOperation(cartQuery);
|
|
1320
|
+
|
|
1321
|
+
const client = new ApolloClient({
|
|
1322
|
+
cache: new InMemoryCache(),
|
|
1323
|
+
manifest: mockApplicationManifest({
|
|
1324
|
+
operations: [productOperation, cartOperation],
|
|
1325
|
+
}),
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
using host = await mockMcpHost({
|
|
1329
|
+
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
1330
|
+
});
|
|
1331
|
+
host.onCleanup(() => client.stop());
|
|
1332
|
+
|
|
1333
|
+
const execute = vi.fn(() => ({
|
|
1334
|
+
structuredContent: {
|
|
1335
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1336
|
+
},
|
|
1337
|
+
}));
|
|
1338
|
+
host.mockToolCall("execute", execute);
|
|
1339
|
+
|
|
1340
|
+
const connectPromise = client.connect();
|
|
1341
|
+
|
|
1342
|
+
const cartPromise = client.query({
|
|
1343
|
+
query: cartQuery,
|
|
1344
|
+
fetchPolicy: "network-only",
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
host.sendToolResult({
|
|
1348
|
+
structuredContent: {
|
|
1349
|
+
result: {
|
|
1350
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1355
|
+
|
|
1356
|
+
await connectPromise;
|
|
1357
|
+
|
|
1358
|
+
await expect(cartPromise).resolves.toStrictEqual({
|
|
1359
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
await expect(
|
|
1363
|
+
client.query({
|
|
1364
|
+
query: productQuery,
|
|
1365
|
+
variables: { id: "1" },
|
|
1366
|
+
fetchPolicy: "network-only",
|
|
1367
|
+
})
|
|
1368
|
+
).resolves.toStrictEqual({
|
|
1369
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
expect(execute).toHaveBeenCalledOnce();
|
|
1373
|
+
});
|
|
1374
|
+
|
|
600
1375
|
describe("watchQuery dev warnings", () => {
|
|
601
1376
|
const query = gql`
|
|
602
1377
|
query Products($category: String!, $page: Int!, $sortBy: String!)
|
|
@@ -701,3 +1476,26 @@ describe("watchQuery dev warnings", () => {
|
|
|
701
1476
|
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
702
1477
|
});
|
|
703
1478
|
});
|
|
1479
|
+
|
|
1480
|
+
async function setup({
|
|
1481
|
+
query,
|
|
1482
|
+
toolName,
|
|
1483
|
+
}: {
|
|
1484
|
+
query: DocumentNode;
|
|
1485
|
+
toolName?: string;
|
|
1486
|
+
}) {
|
|
1487
|
+
const operation = parseManifestOperation(query);
|
|
1488
|
+
const client = new ApolloClient({
|
|
1489
|
+
cache: new InMemoryCache(),
|
|
1490
|
+
manifest: mockApplicationManifest({ operations: [operation] }),
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
const host = await mockMcpHost({
|
|
1494
|
+
hostContext: minimalHostContextWithToolName(
|
|
1495
|
+
toolName ?? operation.tools[0].name
|
|
1496
|
+
),
|
|
1497
|
+
});
|
|
1498
|
+
host.onCleanup(() => client.stop());
|
|
1499
|
+
|
|
1500
|
+
return { client, host };
|
|
1501
|
+
}
|