@apollo/client-ai-apps 0.7.2 → 0.7.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.
- package/.github/workflows/pr.yaml +35 -0
- package/.github/workflows/prep-release.yml +4 -3
- package/.github/workflows/release.yaml +8 -5
- package/.github/workflows/verify-changeset.yml +4 -4
- package/CHANGELOG.md +8 -0
- package/dist/utilities/getToolNamesFromDocument.d.ts +1 -1
- package/dist/utilities/getToolNamesFromDocument.d.ts.map +1 -1
- package/dist/utilities/getToolNamesFromDocument.js +14 -5
- package/dist/utilities/getToolNamesFromDocument.js.map +1 -1
- package/integration-tests/docker-compose.yml +30 -0
- package/integration-tests/global-teardown.js +9 -0
- package/integration-tests/graphql-server/Dockerfile +12 -0
- package/integration-tests/graphql-server/package.json +10 -0
- package/integration-tests/graphql-server/server.ts +22 -0
- package/integration-tests/mcp-config.yaml +29 -0
- package/integration-tests/mock-app/index.html +12 -0
- package/integration-tests/mock-app/package.json +24 -0
- package/integration-tests/mock-app/src/App.tsx +23 -0
- package/integration-tests/mock-app/src/main.tsx +22 -0
- package/integration-tests/mock-app/src/tools/Echo.tsx +33 -0
- package/integration-tests/mock-app/src/tools/Hello.tsx +19 -0
- package/integration-tests/mock-app/src/tools/Private.tsx +34 -0
- package/integration-tests/mock-app/src/tools/SemiPrivate.tsx +34 -0
- package/integration-tests/mock-app/tsconfig.json +25 -0
- package/integration-tests/mock-app/vite.config.ts +22 -0
- package/integration-tests/package-lock.json +5749 -0
- package/integration-tests/package.json +20 -0
- package/integration-tests/playwright.config.ts +23 -0
- package/integration-tests/schema.graphql +10 -0
- package/integration-tests/tests/hello.spec.ts +11 -0
- package/integration-tests/tests/privateDirective.spec.ts +73 -0
- package/integration-tests/tests/variables.spec.ts +24 -0
- package/package.json +3 -2
- package/src/react/__tests__/createHydrationUtils.test.tsx +62 -0
- package/src/utilities/getToolNamesFromDocument.ts +19 -7
- package/vitest.config.ts +2 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "integration-tests",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"./graphql-server",
|
|
7
|
+
"./mock-app"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build:lib": "npm run build --prefix ..",
|
|
11
|
+
"build:mock-app": "npm run build --workspace mock-app",
|
|
12
|
+
"pretest": "npm-run-all build:*",
|
|
13
|
+
"test": "playwright test"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@apollo/mcp-impostor-host": "^0.3.0",
|
|
17
|
+
"@playwright/test": "^1.59.1",
|
|
18
|
+
"npm-run-all": "^4.1.5"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineConfig } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: "./tests",
|
|
5
|
+
globalTeardown: "./global-teardown.js",
|
|
6
|
+
webServer: [
|
|
7
|
+
{
|
|
8
|
+
command: "npx serve-impostor-host --playwright",
|
|
9
|
+
url: "http://localhost:8080",
|
|
10
|
+
reuseExistingServer: !process.env.CI,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
command: "docker compose up --build",
|
|
14
|
+
url: "http://localhost:8000/health",
|
|
15
|
+
reuseExistingServer: !process.env.CI,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
workers: 1,
|
|
19
|
+
use: {
|
|
20
|
+
browserName: "chromium",
|
|
21
|
+
trace: "retain-on-failure",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { test } from "@apollo/mcp-impostor-host/playwright";
|
|
2
|
+
import { expect } from "@playwright/test";
|
|
3
|
+
|
|
4
|
+
const URL = "http://localhost:8000/mcp?app=mock-app&appTarget=mcp";
|
|
5
|
+
|
|
6
|
+
test("renders data from a tool with no arguments", async ({ mcpHost }) => {
|
|
7
|
+
const connection = await mcpHost.connect({ url: URL });
|
|
8
|
+
const { appFrame } = await connection.callTool("Hello");
|
|
9
|
+
|
|
10
|
+
await expect(appFrame.getByTestId("greeting")).toHaveText("Hello, world!");
|
|
11
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { test } from "@apollo/mcp-impostor-host/playwright";
|
|
2
|
+
import { expect } from "@playwright/test";
|
|
3
|
+
|
|
4
|
+
const URL = "http://localhost:8000/mcp?app=mock-app&appTarget=mcp";
|
|
5
|
+
|
|
6
|
+
test("omits private field in structuredContent, but available to the view", async ({
|
|
7
|
+
mcpHost,
|
|
8
|
+
}) => {
|
|
9
|
+
const connection = await mcpHost.connect({ url: URL });
|
|
10
|
+
const { result, appFrame } = await connection.callTool("SemiPrivate");
|
|
11
|
+
|
|
12
|
+
expect(result.structuredContent).toEqual({
|
|
13
|
+
result: {
|
|
14
|
+
data: {
|
|
15
|
+
user: {
|
|
16
|
+
__typename: "User",
|
|
17
|
+
fullName: "MCP User",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
toolName: "SemiPrivate",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result._meta).toEqual({
|
|
25
|
+
toolName: "SemiPrivate",
|
|
26
|
+
structuredContent: {
|
|
27
|
+
result: {
|
|
28
|
+
data: {
|
|
29
|
+
user: {
|
|
30
|
+
__typename: "User",
|
|
31
|
+
address: "1234 Main St",
|
|
32
|
+
fullName: "MCP User",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
toolName: "SemiPrivate",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await expect(appFrame.getByTestId("fullName")).toHaveText("MCP User");
|
|
41
|
+
await expect(appFrame.getByTestId("address")).toHaveText("1234 Main St");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("omits full selection sets, but available to the view", async ({
|
|
45
|
+
mcpHost,
|
|
46
|
+
}) => {
|
|
47
|
+
const connection = await mcpHost.connect({ url: URL });
|
|
48
|
+
const { result, appFrame } = await connection.callTool("Private");
|
|
49
|
+
|
|
50
|
+
expect(result.structuredContent).toEqual({
|
|
51
|
+
result: { data: {} },
|
|
52
|
+
toolName: "Private",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result._meta).toEqual({
|
|
56
|
+
toolName: "Private",
|
|
57
|
+
structuredContent: {
|
|
58
|
+
result: {
|
|
59
|
+
data: {
|
|
60
|
+
user: {
|
|
61
|
+
__typename: "User",
|
|
62
|
+
address: "1234 Main St",
|
|
63
|
+
fullName: "MCP User",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
toolName: "Private",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await expect(appFrame.getByTestId("fullName")).toHaveText("MCP User");
|
|
72
|
+
await expect(appFrame.getByTestId("address")).toHaveText("1234 Main St");
|
|
73
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { test } from "@apollo/mcp-impostor-host/playwright";
|
|
2
|
+
import { expect } from "@playwright/test";
|
|
3
|
+
|
|
4
|
+
const URL = "http://localhost:8000/mcp?app=mock-app&appTarget=mcp";
|
|
5
|
+
|
|
6
|
+
test("renders data from a tool with arguments", async ({ mcpHost }) => {
|
|
7
|
+
const connection = await mcpHost.connect({ url: URL });
|
|
8
|
+
const { result, appFrame } = await connection.callTool("Echo", {
|
|
9
|
+
message: "Hello!",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result.structuredContent).toStrictEqual({
|
|
13
|
+
toolName: "Echo",
|
|
14
|
+
result: {
|
|
15
|
+
data: {
|
|
16
|
+
echo: "Hello! (Hello!)",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
expect(result._meta).toStrictEqual({
|
|
21
|
+
toolName: "Echo",
|
|
22
|
+
});
|
|
23
|
+
await expect(appFrame.getByTestId("echo")).toHaveText("Hello! (Hello!)");
|
|
24
|
+
});
|
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.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apollo",
|
|
7
7
|
"graphql",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"attw": "npm run build && attw --pack . --ignore-rules cjs-resolves-to-esm --profile node16"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
|
-
"@apollo/client": "^4.
|
|
107
|
+
"@apollo/client": "^4.1.7",
|
|
108
108
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
109
109
|
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
110
110
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"esbuild": "^0.25.12",
|
|
120
120
|
"graphql": "^16.12.0",
|
|
121
121
|
"happy-dom": "^20.0.10",
|
|
122
|
+
"jiti": "^2.6.1",
|
|
122
123
|
"memfs": "^4.56.10",
|
|
123
124
|
"mock-require": "^3.0.3",
|
|
124
125
|
"prettier": "^3.7.4",
|
|
@@ -77,6 +77,68 @@ eachHostEnv((setupHost, ApolloClient) => {
|
|
|
77
77
|
await expect(takeSnapshot).not.toRerender();
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
test("returns tool input value when @tool name is defined by operation name", async () => {
|
|
81
|
+
using _ = spyOnConsole("debug");
|
|
82
|
+
|
|
83
|
+
const query: TypedDocumentNode<
|
|
84
|
+
{ products: Array<{ __typename: "Product"; id: string }> },
|
|
85
|
+
{ category: string; page: number; sortBy: string }
|
|
86
|
+
> = gql`
|
|
87
|
+
"Get products by category"
|
|
88
|
+
query Products($category: String!, $page: Int!, $sortBy: String!) @tool {
|
|
89
|
+
products(category: $category, page: $page, sortBy: $sortBy) {
|
|
90
|
+
id
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const client = new ApolloClient({
|
|
96
|
+
cache: new InMemoryCache(),
|
|
97
|
+
manifest: mockApplicationManifest({
|
|
98
|
+
operations: [parseManifestOperation(query)],
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
using env = await setupHost({
|
|
103
|
+
client,
|
|
104
|
+
toolCall: {
|
|
105
|
+
name: "Products",
|
|
106
|
+
input: { category: "electronics", page: 1, sortBy: "title" },
|
|
107
|
+
result: { structuredContent: { result: { data: { products: [] } } } },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const { host, params } = env;
|
|
111
|
+
|
|
112
|
+
host.sendToolInput(params.toolInput);
|
|
113
|
+
host.sendToolResult(params.toolResult);
|
|
114
|
+
|
|
115
|
+
const { useHydratedVariables } = createHydrationUtils(query);
|
|
116
|
+
|
|
117
|
+
using _disabledAct = disableActEnvironment();
|
|
118
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(
|
|
119
|
+
() =>
|
|
120
|
+
useHydratedVariables({
|
|
121
|
+
category: "music",
|
|
122
|
+
page: 1,
|
|
123
|
+
sortBy: "name",
|
|
124
|
+
}),
|
|
125
|
+
{
|
|
126
|
+
wrapper: ({ children }) => (
|
|
127
|
+
<ApolloProvider client={client}>{children}</ApolloProvider>
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const [variables] = await takeSnapshot();
|
|
133
|
+
expect(variables).toStrictEqual({
|
|
134
|
+
category: "electronics",
|
|
135
|
+
page: 1,
|
|
136
|
+
sortBy: "title",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await expect(takeSnapshot).not.toRerender();
|
|
140
|
+
});
|
|
141
|
+
|
|
80
142
|
test("returns user-provided variables when tool name does not match", async () => {
|
|
81
143
|
using _ = spyOnConsole("debug");
|
|
82
144
|
const query = gql`
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { getOperationDefinition } from "@apollo/client/utilities/internal";
|
|
2
|
-
import { Kind, type DocumentNode } from "graphql";
|
|
2
|
+
import { Kind, type DirectiveNode, type DocumentNode } from "graphql";
|
|
3
3
|
|
|
4
4
|
export function getToolNamesFromDocument(document: DocumentNode) {
|
|
5
5
|
const operationDef = getOperationDefinition(document);
|
|
6
|
+
const operationName = operationDef?.name?.value;
|
|
7
|
+
const toolDirectives =
|
|
8
|
+
operationDef?.directives?.filter((d) => d.name.value === "tool") ?? [];
|
|
9
|
+
|
|
10
|
+
if (toolDirectives.length === 1) {
|
|
11
|
+
return new Set([
|
|
12
|
+
getToolNameFromDirective(toolDirectives[0]) ?? operationName,
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
6
15
|
|
|
7
16
|
return new Set(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return nameArg?.value.kind === Kind.STRING ? [nameArg.value.value] : [];
|
|
13
|
-
})
|
|
17
|
+
toolDirectives.flatMap((d) => {
|
|
18
|
+
const name = getToolNameFromDirective(d);
|
|
19
|
+
return name ? [name] : [];
|
|
20
|
+
})
|
|
14
21
|
);
|
|
15
22
|
}
|
|
23
|
+
|
|
24
|
+
function getToolNameFromDirective(directive: DirectiveNode) {
|
|
25
|
+
const nameArg = directive.arguments?.find((arg) => arg.name.value === "name");
|
|
26
|
+
return nameArg?.value.kind === Kind.STRING ? nameArg.value.value : undefined;
|
|
27
|
+
}
|
package/vitest.config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
1
|
+
import { defineConfig, defaultExclude } from "vitest/config";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
@@ -8,6 +8,7 @@ export default defineConfig({
|
|
|
8
8
|
setupFiles: ["./vitest-setup.ts"],
|
|
9
9
|
mockReset: true,
|
|
10
10
|
unstubGlobals: true,
|
|
11
|
+
exclude: [...defaultExclude, "integration-tests/**"],
|
|
11
12
|
tags: [
|
|
12
13
|
{
|
|
13
14
|
name: "flaky",
|