@apollo/client-ai-apps 0.7.1 → 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 +16 -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/dist/vite/apolloClientAiApps.d.ts.map +1 -1
- package/dist/vite/apolloClientAiApps.js +8 -2
- package/dist/vite/apolloClientAiApps.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/src/vite/__tests__/apolloClientAiApps.test.ts +51 -2
- package/src/vite/apolloClientAiApps.ts +8 -2
- 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
|
+
}
|
|
@@ -410,7 +410,8 @@ describe("operations", () => {
|
|
|
410
410
|
vol.fromJSON({
|
|
411
411
|
"package.json": mockPackageJson(),
|
|
412
412
|
"src/my-component.tsx": declareOperation(gql`
|
|
413
|
-
|
|
413
|
+
"Returns a greeting"
|
|
414
|
+
query HelloWorldQuery @tool {
|
|
414
415
|
helloWorld
|
|
415
416
|
}
|
|
416
417
|
`),
|
|
@@ -445,7 +446,12 @@ describe("operations", () => {
|
|
|
445
446
|
"id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
|
|
446
447
|
"name": "HelloWorldQuery",
|
|
447
448
|
"prefetch": false,
|
|
448
|
-
"tools": [
|
|
449
|
+
"tools": [
|
|
450
|
+
{
|
|
451
|
+
"description": "Returns a greeting",
|
|
452
|
+
"name": "HelloWorldQuery",
|
|
453
|
+
},
|
|
454
|
+
],
|
|
449
455
|
"type": "query",
|
|
450
456
|
"variables": {},
|
|
451
457
|
},
|
|
@@ -512,6 +518,49 @@ describe("operations", () => {
|
|
|
512
518
|
`);
|
|
513
519
|
});
|
|
514
520
|
|
|
521
|
+
test("does not write operations if not annotated with `@tool` or `@prefetch`", async () => {
|
|
522
|
+
vol.fromJSON({
|
|
523
|
+
"package.json": mockPackageJson(),
|
|
524
|
+
"src/my-component.tsx": declareOperation(gql`
|
|
525
|
+
query HelloWorldQuery {
|
|
526
|
+
helloWorld
|
|
527
|
+
}
|
|
528
|
+
`),
|
|
529
|
+
"src/my-mutation.tsx": declareOperation(gql`
|
|
530
|
+
mutation SendGreeting {
|
|
531
|
+
sendGreeting(message: "Hello")
|
|
532
|
+
}
|
|
533
|
+
`),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
await using server = await setupServer({
|
|
537
|
+
plugins: [
|
|
538
|
+
apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
|
|
539
|
+
],
|
|
540
|
+
});
|
|
541
|
+
await server.listen();
|
|
542
|
+
|
|
543
|
+
const manifest = readManifestFile();
|
|
544
|
+
expect(manifest).toMatchInlineSnapshot(`
|
|
545
|
+
{
|
|
546
|
+
"appVersion": "1.0.0",
|
|
547
|
+
"csp": {
|
|
548
|
+
"baseUriDomains": [],
|
|
549
|
+
"connectDomains": [],
|
|
550
|
+
"frameDomains": [],
|
|
551
|
+
"redirectDomains": [],
|
|
552
|
+
"resourceDomains": [],
|
|
553
|
+
},
|
|
554
|
+
"format": "apollo-ai-app-manifest",
|
|
555
|
+
"hash": "abc",
|
|
556
|
+
"name": "my-app",
|
|
557
|
+
"operations": [],
|
|
558
|
+
"resource": "http://localhost:3333",
|
|
559
|
+
"version": "1",
|
|
560
|
+
}
|
|
561
|
+
`);
|
|
562
|
+
});
|
|
563
|
+
|
|
515
564
|
test("errors when a subscription operation type is discovered", async () => {
|
|
516
565
|
vol.fromJSON({
|
|
517
566
|
"package.json": mockPackageJson(),
|
|
@@ -453,8 +453,11 @@ export function apolloClientAiApps(
|
|
|
453
453
|
query: source,
|
|
454
454
|
fetchPolicy: "no-cache",
|
|
455
455
|
});
|
|
456
|
+
const data = result.data!;
|
|
456
457
|
|
|
457
|
-
|
|
458
|
+
if (data.tools.length > 0 || data.prefetch) {
|
|
459
|
+
manifestOperations.push(data);
|
|
460
|
+
}
|
|
458
461
|
break;
|
|
459
462
|
}
|
|
460
463
|
case OperationTypeNode.MUTATION: {
|
|
@@ -462,8 +465,11 @@ export function apolloClientAiApps(
|
|
|
462
465
|
mutation: source,
|
|
463
466
|
fetchPolicy: "no-cache",
|
|
464
467
|
});
|
|
468
|
+
const data = result.data!;
|
|
465
469
|
|
|
466
|
-
|
|
470
|
+
if (data.tools.length > 0 || data.prefetch) {
|
|
471
|
+
manifestOperations.push(data);
|
|
472
|
+
}
|
|
467
473
|
break;
|
|
468
474
|
}
|
|
469
475
|
default:
|
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",
|