@apollo/client-ai-apps 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/dist/config/defineConfig.d.ts +1 -0
  3. package/dist/config/defineConfig.d.ts.map +1 -1
  4. package/dist/config/schema.d.ts +1 -0
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/config/schema.js +1 -0
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/core/typeRegistration.d.ts +33 -0
  9. package/dist/core/typeRegistration.d.ts.map +1 -0
  10. package/dist/core/typeRegistration.js +2 -0
  11. package/dist/core/typeRegistration.js.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp/core/McpAppManager.d.ts +2 -1
  16. package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
  17. package/dist/mcp/core/McpAppManager.js +11 -1
  18. package/dist/mcp/core/McpAppManager.js.map +1 -1
  19. package/dist/mcp/react/hooks/useHostContext.d.ts +2 -0
  20. package/dist/mcp/react/hooks/useHostContext.d.ts.map +1 -0
  21. package/dist/mcp/react/hooks/useHostContext.js +7 -0
  22. package/dist/mcp/react/hooks/useHostContext.js.map +1 -0
  23. package/dist/mcp/react/hooks/useToolInfo.d.ts +3 -0
  24. package/dist/mcp/react/hooks/useToolInfo.d.ts.map +1 -0
  25. package/dist/mcp/react/hooks/useToolInfo.js +10 -0
  26. package/dist/mcp/react/hooks/useToolInfo.js.map +1 -0
  27. package/dist/mcp/react/hooks/useToolInput.d.ts +6 -1
  28. package/dist/mcp/react/hooks/useToolInput.d.ts.map +1 -1
  29. package/dist/mcp/react/hooks/useToolInput.js +4 -0
  30. package/dist/mcp/react/hooks/useToolInput.js.map +1 -1
  31. package/dist/mcp/react/hooks/useToolName.d.ts +6 -1
  32. package/dist/mcp/react/hooks/useToolName.d.ts.map +1 -1
  33. package/dist/mcp/react/hooks/useToolName.js +4 -0
  34. package/dist/mcp/react/hooks/useToolName.js.map +1 -1
  35. package/dist/mcp/react/index.d.ts +2 -0
  36. package/dist/mcp/react/index.d.ts.map +1 -1
  37. package/dist/mcp/react/index.js +2 -0
  38. package/dist/mcp/react/index.js.map +1 -1
  39. package/dist/openai/core/McpAppManager.d.ts +2 -1
  40. package/dist/openai/core/McpAppManager.d.ts.map +1 -1
  41. package/dist/openai/core/McpAppManager.js +11 -1
  42. package/dist/openai/core/McpAppManager.js.map +1 -1
  43. package/dist/openai/react/hooks/useHostContext.d.ts +2 -0
  44. package/dist/openai/react/hooks/useHostContext.d.ts.map +1 -0
  45. package/dist/openai/react/hooks/useHostContext.js +7 -0
  46. package/dist/openai/react/hooks/useHostContext.js.map +1 -0
  47. package/dist/openai/react/hooks/useToolInfo.d.ts +3 -0
  48. package/dist/openai/react/hooks/useToolInfo.d.ts.map +1 -0
  49. package/dist/openai/react/hooks/useToolInfo.js +10 -0
  50. package/dist/openai/react/hooks/useToolInfo.js.map +1 -0
  51. package/dist/openai/react/hooks/useToolInput.d.ts +6 -1
  52. package/dist/openai/react/hooks/useToolInput.d.ts.map +1 -1
  53. package/dist/openai/react/hooks/useToolInput.js +4 -0
  54. package/dist/openai/react/hooks/useToolInput.js.map +1 -1
  55. package/dist/openai/react/hooks/useToolName.d.ts +6 -1
  56. package/dist/openai/react/hooks/useToolName.d.ts.map +1 -1
  57. package/dist/openai/react/hooks/useToolName.js +4 -0
  58. package/dist/openai/react/hooks/useToolName.js.map +1 -1
  59. package/dist/openai/react/index.d.ts +2 -0
  60. package/dist/openai/react/index.d.ts.map +1 -1
  61. package/dist/openai/react/index.js +2 -0
  62. package/dist/openai/react/index.js.map +1 -1
  63. package/dist/react/index.d.ts +10 -0
  64. package/dist/react/index.d.ts.map +1 -1
  65. package/dist/react/index.js +10 -0
  66. package/dist/react/index.js.map +1 -1
  67. package/dist/react/index.mcp.d.ts +1 -1
  68. package/dist/react/index.mcp.d.ts.map +1 -1
  69. package/dist/react/index.mcp.js +1 -1
  70. package/dist/react/index.mcp.js.map +1 -1
  71. package/dist/react/index.openai.d.ts +1 -1
  72. package/dist/react/index.openai.d.ts.map +1 -1
  73. package/dist/react/index.openai.js +1 -1
  74. package/dist/react/index.openai.js.map +1 -1
  75. package/dist/tsconfig/core/tsconfig.json +2 -0
  76. package/dist/tsconfig/mcp/tsconfig.json +2 -0
  77. package/dist/tsconfig/openai/tsconfig.json +2 -0
  78. package/dist/types/application-manifest.d.ts +1 -0
  79. package/dist/types/application-manifest.d.ts.map +1 -1
  80. package/dist/types/application-manifest.js.map +1 -1
  81. package/dist/vite/__tests__/utilities/build.d.ts.map +1 -1
  82. package/dist/vite/__tests__/utilities/build.js +0 -1
  83. package/dist/vite/__tests__/utilities/build.js.map +1 -1
  84. package/dist/vite/apolloClientAiApps.d.ts +2 -0
  85. package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
  86. package/dist/vite/apolloClientAiApps.js +362 -53
  87. package/dist/vite/apolloClientAiApps.js.map +1 -1
  88. package/dist/vite/utilities/recast.d.ts +54 -0
  89. package/dist/vite/utilities/recast.d.ts.map +1 -0
  90. package/dist/vite/utilities/recast.js +71 -0
  91. package/dist/vite/utilities/recast.js.map +1 -0
  92. package/package.json +7 -6
  93. package/src/config/schema.ts +1 -0
  94. package/src/core/typeRegistration.ts +32 -0
  95. package/src/index.ts +7 -0
  96. package/src/mcp/core/McpAppManager.ts +23 -1
  97. package/src/mcp/react/hooks/__tests__/useHostContext.test.tsx +95 -0
  98. package/src/mcp/react/hooks/__tests__/useToolInfo.test.tsx +53 -0
  99. package/src/mcp/react/hooks/useHostContext.ts +14 -0
  100. package/src/mcp/react/hooks/useToolInfo.ts +13 -0
  101. package/src/mcp/react/hooks/useToolInput.ts +6 -1
  102. package/src/mcp/react/hooks/useToolName.ts +6 -1
  103. package/src/mcp/react/index.ts +2 -0
  104. package/src/openai/core/McpAppManager.ts +22 -1
  105. package/src/openai/react/hooks/__tests__/useToolInfo.test.tsx +92 -0
  106. package/src/openai/react/hooks/useHostContext.ts +14 -0
  107. package/src/openai/react/hooks/useToolInfo.ts +13 -0
  108. package/src/openai/react/hooks/useToolInput.ts +6 -1
  109. package/src/openai/react/hooks/useToolName.ts +6 -1
  110. package/src/openai/react/index.ts +2 -0
  111. package/src/react/index.mcp.ts +2 -0
  112. package/src/react/index.openai.ts +2 -0
  113. package/src/react/index.ts +14 -0
  114. package/src/testing/internal/mcp/mockMcpHost.ts +12 -0
  115. package/src/testing/internal/utilities/mockApplicationManifest.ts +1 -0
  116. package/src/tsconfig/core/tsconfig.json +2 -0
  117. package/src/tsconfig/mcp/tsconfig.json +2 -0
  118. package/src/tsconfig/openai/tsconfig.json +2 -0
  119. package/src/types/application-manifest.ts +1 -0
  120. package/src/vite/__tests__/apolloClientAiApps.test.ts +1022 -66
  121. package/src/vite/__tests__/utilities/build.ts +0 -1
  122. package/src/vite/apolloClientAiApps.ts +604 -81
  123. package/src/vite/utilities/recast.ts +100 -0
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { spyOnConsole, wait } from "../../testing/internal/index.js";
2
3
  import fs from "node:fs";
3
4
  import { gql, type DocumentNode } from "@apollo/client";
4
5
  import { getMainDefinition, print } from "@apollo/client/utilities";
@@ -6,13 +7,11 @@ import { getOperationName } from "@apollo/client/utilities/internal";
6
7
  import { vol } from "memfs";
7
8
  import { apolloClientAiApps } from "../apolloClientAiApps.js";
8
9
  import { buildApp, setupServer } from "./utilities/build.js";
9
- import type {
10
- ApplicationManifest,
11
- ManifestWidgetSettings,
12
- } from "../../types/application-manifest.js";
10
+ import type { ApplicationManifest } from "../../types/application-manifest.js";
13
11
  import { explorer } from "../utilities/config.js";
14
12
  import { invariant } from "@apollo/client/utilities/invariant";
15
13
  import { Kind } from "graphql";
14
+ import type { ApolloClientAiAppsConfig } from "../../config/types.js";
16
15
 
17
16
  beforeEach(() => {
18
17
  explorer.clearCaches();
@@ -29,12 +28,19 @@ describe("operations", () => {
29
28
  invoked: "Tested global!",
30
29
  },
31
30
  },
31
+ csp: {
32
+ baseUriDomains: ["https://base.example.com"],
33
+ connectDomains: ["https://connect.example.com"],
34
+ frameDomains: ["https://frame.example.com"],
35
+ redirectDomains: ["https://redirect.example.com"],
36
+ resourceDomains: ["https://resource.example.com"],
37
+ },
32
38
  widgetSettings: {
33
39
  description: "Test",
34
40
  domain: "https://example.com",
35
41
  prefersBorder: true,
36
- } satisfies ManifestWidgetSettings,
37
- },
42
+ },
43
+ } satisfies ApolloClientAiAppsConfig.Config,
38
44
  }),
39
45
  "src/my-component.tsx": declareOperation(gql`
40
46
  query HelloWorldQuery($name: string!)
@@ -61,7 +67,9 @@ describe("operations", () => {
61
67
  });
62
68
 
63
69
  await using server = await setupServer({
64
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
70
+ plugins: [
71
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
72
+ ],
65
73
  });
66
74
  await server.listen();
67
75
 
@@ -70,10 +78,21 @@ describe("operations", () => {
70
78
  {
71
79
  "appVersion": "1.0.0",
72
80
  "csp": {
73
- "connectDomains": [],
74
- "frameDomains": [],
75
- "redirectDomains": [],
76
- "resourceDomains": [],
81
+ "baseUriDomains": [
82
+ "https://base.example.com",
83
+ ],
84
+ "connectDomains": [
85
+ "https://connect.example.com",
86
+ ],
87
+ "frameDomains": [
88
+ "https://frame.example.com",
89
+ ],
90
+ "redirectDomains": [
91
+ "https://redirect.example.com",
92
+ ],
93
+ "resourceDomains": [
94
+ "https://resource.example.com",
95
+ ],
77
96
  },
78
97
  "format": "apollo-ai-app-manifest",
79
98
  "hash": "abc",
@@ -81,6 +100,7 @@ describe("operations", () => {
81
100
  "toolInvocation/invoked": "Tested global!",
82
101
  "toolInvocation/invoking": "Testing global...",
83
102
  },
103
+ "name": "my-app",
84
104
  "operations": [
85
105
  {
86
106
  "body": "query HelloWorldQuery($name: string!) {
@@ -145,7 +165,9 @@ describe("operations", () => {
145
165
  });
146
166
 
147
167
  await using server = await setupServer({
148
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
168
+ plugins: [
169
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
170
+ ],
149
171
  });
150
172
  await server.listen();
151
173
 
@@ -154,6 +176,7 @@ describe("operations", () => {
154
176
  {
155
177
  "appVersion": "1.0.0",
156
178
  "csp": {
179
+ "baseUriDomains": [],
157
180
  "connectDomains": [],
158
181
  "frameDomains": [],
159
182
  "redirectDomains": [],
@@ -161,6 +184,7 @@ describe("operations", () => {
161
184
  },
162
185
  "format": "apollo-ai-app-manifest",
163
186
  "hash": "abc",
187
+ "name": "my-app",
164
188
  "operations": [
165
189
  {
166
190
  "body": "query HelloWorldQuery {
@@ -229,7 +253,9 @@ describe("operations", () => {
229
253
  });
230
254
 
231
255
  await using server = await setupServer({
232
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
256
+ plugins: [
257
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
258
+ ],
233
259
  });
234
260
  await server.listen();
235
261
 
@@ -238,6 +264,7 @@ describe("operations", () => {
238
264
  {
239
265
  "appVersion": "1.0.0",
240
266
  "csp": {
267
+ "baseUriDomains": [],
241
268
  "connectDomains": [],
242
269
  "frameDomains": [],
243
270
  "redirectDomains": [],
@@ -245,6 +272,7 @@ describe("operations", () => {
245
272
  },
246
273
  "format": "apollo-ai-app-manifest",
247
274
  "hash": "abc",
275
+ "name": "my-app",
248
276
  "operations": [
249
277
  {
250
278
  "body": "query HelloWorldQuery {
@@ -303,7 +331,9 @@ describe("operations", () => {
303
331
 
304
332
  await buildApp({
305
333
  mode: "production",
306
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
334
+ plugins: [
335
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
336
+ ],
307
337
  });
308
338
 
309
339
  const manifest = readManifestFile();
@@ -320,7 +350,9 @@ describe("operations", () => {
320
350
  });
321
351
 
322
352
  await using server = await setupServer({
323
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
353
+ plugins: [
354
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
355
+ ],
324
356
  });
325
357
  await server.listen();
326
358
 
@@ -329,6 +361,7 @@ describe("operations", () => {
329
361
  {
330
362
  "appVersion": "1.0.0",
331
363
  "csp": {
364
+ "baseUriDomains": [],
332
365
  "connectDomains": [],
333
366
  "frameDomains": [],
334
367
  "redirectDomains": [],
@@ -336,6 +369,7 @@ describe("operations", () => {
336
369
  },
337
370
  "format": "apollo-ai-app-manifest",
338
371
  "hash": "abc",
372
+ "name": "my-app",
339
373
  "operations": [],
340
374
  "resource": "http://localhost:3333",
341
375
  "version": "1",
@@ -354,7 +388,9 @@ describe("operations", () => {
354
388
  });
355
389
 
356
390
  await using server = await setupServer({
357
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
391
+ plugins: [
392
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
393
+ ],
358
394
  });
359
395
  await server.listen();
360
396
 
@@ -363,6 +399,7 @@ describe("operations", () => {
363
399
  {
364
400
  "appVersion": "1.0.0",
365
401
  "csp": {
402
+ "baseUriDomains": [],
366
403
  "connectDomains": [],
367
404
  "frameDomains": [],
368
405
  "redirectDomains": [],
@@ -370,6 +407,7 @@ describe("operations", () => {
370
407
  },
371
408
  "format": "apollo-ai-app-manifest",
372
409
  "hash": "abc",
410
+ "name": "my-app",
373
411
  "operations": [
374
412
  {
375
413
  "body": "query HelloWorldQuery {
@@ -401,7 +439,9 @@ describe("operations", () => {
401
439
  });
402
440
 
403
441
  await using server = await setupServer({
404
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
442
+ plugins: [
443
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
444
+ ],
405
445
  });
406
446
  await server.listen();
407
447
 
@@ -410,6 +450,7 @@ describe("operations", () => {
410
450
  {
411
451
  "appVersion": "1.0.0",
412
452
  "csp": {
453
+ "baseUriDomains": [],
413
454
  "connectDomains": [],
414
455
  "frameDomains": [],
415
456
  "redirectDomains": [],
@@ -417,6 +458,7 @@ describe("operations", () => {
417
458
  },
418
459
  "format": "apollo-ai-app-manifest",
419
460
  "hash": "abc",
461
+ "name": "my-app",
420
462
  "operations": [
421
463
  {
422
464
  "body": "mutation HelloWorldQuery {
@@ -454,7 +496,9 @@ describe("operations", () => {
454
496
 
455
497
  await expect(async () => {
456
498
  await using server = await setupServer({
457
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
499
+ plugins: [
500
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
501
+ ],
458
502
  });
459
503
  await server.listen();
460
504
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -482,7 +526,9 @@ describe("operations", () => {
482
526
  });
483
527
 
484
528
  await using server = await setupServer({
485
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
529
+ plugins: [
530
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
531
+ ],
486
532
  });
487
533
  await server.listen();
488
534
 
@@ -491,6 +537,7 @@ describe("operations", () => {
491
537
  {
492
538
  "appVersion": "1.0.0",
493
539
  "csp": {
540
+ "baseUriDomains": [],
494
541
  "connectDomains": [],
495
542
  "frameDomains": [],
496
543
  "redirectDomains": [],
@@ -498,6 +545,7 @@ describe("operations", () => {
498
545
  },
499
546
  "format": "apollo-ai-app-manifest",
500
547
  "hash": "abc",
548
+ "name": "my-app",
501
549
  "operations": [
502
550
  {
503
551
  "body": "query HelloWorldQuery {
@@ -557,7 +605,9 @@ describe("@prefetch", () => {
557
605
  });
558
606
 
559
607
  await using server = await setupServer({
560
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
608
+ plugins: [
609
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
610
+ ],
561
611
  });
562
612
  await server.listen();
563
613
 
@@ -566,6 +616,7 @@ describe("@prefetch", () => {
566
616
  {
567
617
  "appVersion": "1.0.0",
568
618
  "csp": {
619
+ "baseUriDomains": [],
569
620
  "connectDomains": [],
570
621
  "frameDomains": [],
571
622
  "redirectDomains": [],
@@ -573,6 +624,7 @@ describe("@prefetch", () => {
573
624
  },
574
625
  "format": "apollo-ai-app-manifest",
575
626
  "hash": "abc",
627
+ "name": "my-app",
576
628
  "operations": [
577
629
  {
578
630
  "body": "query HelloWorldQuery {
@@ -612,7 +664,9 @@ describe("@prefetch", () => {
612
664
 
613
665
  await expect(async () => {
614
666
  await using server = await setupServer({
615
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
667
+ plugins: [
668
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
669
+ ],
616
670
  });
617
671
  await server.listen();
618
672
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -622,11 +676,11 @@ describe("@prefetch", () => {
622
676
  });
623
677
 
624
678
  describe("@tool validation", () => {
625
- test("errors when tool name is not provided", async () => {
679
+ test("errors when tool name is not provided on anonymous operation", async () => {
626
680
  vol.fromJSON({
627
681
  "package.json": mockPackageJson(),
628
682
  "src/my-component.tsx": declareOperation(gql`
629
- query HelloWorldQuery @tool {
683
+ query @tool {
630
684
  helloWorld
631
685
  }
632
686
  `),
@@ -634,15 +688,17 @@ describe("@tool validation", () => {
634
688
 
635
689
  await expect(async () => {
636
690
  await using server = await setupServer({
637
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
691
+ plugins: [
692
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
693
+ ],
638
694
  });
639
695
  await server.listen();
640
696
  }).rejects.toThrowErrorMatchingInlineSnapshot(
641
- `[Error: 'name' argument must be supplied for @tool]`
697
+ `[Error: Anonymous operations cannot use @tool without providing a 'name' argument]`
642
698
  );
643
699
  });
644
700
 
645
- test("errors when tool description is not provided", async () => {
701
+ test("errors when tool description is not provided and operation has no description", async () => {
646
702
  vol.fromJSON({
647
703
  "package.json": mockPackageJson(),
648
704
  "src/my-component.tsx": declareOperation(gql`
@@ -654,14 +710,106 @@ describe("@tool validation", () => {
654
710
 
655
711
  await expect(async () => {
656
712
  await using server = await setupServer({
657
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
713
+ plugins: [
714
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
715
+ ],
658
716
  });
659
717
  await server.listen();
660
718
  }).rejects.toThrowErrorMatchingInlineSnapshot(
661
- `[Error: 'description' argument must be supplied for @tool]`
719
+ `[Error: Operations using @tool without a 'description' argument must have a description on the operation definition]`
662
720
  );
663
721
  });
664
722
 
723
+ test("uses operation name as tool name when name is omitted from @tool", async () => {
724
+ vol.fromJSON({
725
+ "package.json": mockPackageJson(),
726
+ "src/my-component.tsx": declareOperation(gql`
727
+ query HelloWorldQuery @tool(description: "A greeting tool") {
728
+ helloWorld
729
+ }
730
+ `),
731
+ });
732
+
733
+ await using server = await setupServer({
734
+ plugins: [
735
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
736
+ ],
737
+ });
738
+ await server.listen();
739
+
740
+ const manifest = readManifestFile();
741
+ expect(manifest.operations[0].tools).toMatchInlineSnapshot(`
742
+ [
743
+ {
744
+ "description": "A greeting tool",
745
+ "name": "HelloWorldQuery",
746
+ },
747
+ ]
748
+ `);
749
+ });
750
+
751
+ test("uses operation description as tool description when description is omitted from @tool", async () => {
752
+ vol.fromJSON({
753
+ "package.json": mockPackageJson(),
754
+ "src/my-component.tsx": declareOperation(gql`
755
+ """
756
+ A greeting tool
757
+ """
758
+ query HelloWorldQuery @tool(name: "hello-world") {
759
+ helloWorld
760
+ }
761
+ `),
762
+ });
763
+
764
+ await using server = await setupServer({
765
+ plugins: [
766
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
767
+ ],
768
+ });
769
+ await server.listen();
770
+
771
+ const manifest = readManifestFile();
772
+ expect(manifest.operations[0].tools).toMatchInlineSnapshot(`
773
+ [
774
+ {
775
+ "description": "A greeting tool",
776
+ "name": "hello-world",
777
+ },
778
+ ]
779
+ `);
780
+ });
781
+
782
+ test("uses operation name and description when both are omitted from @tool", async () => {
783
+ vol.fromJSON({
784
+ "package.json": mockPackageJson(),
785
+ "src/my-component.tsx": declareOperation(gql`
786
+ """
787
+ A greeting tool
788
+ """
789
+ query HelloWorldQuery @tool {
790
+ helloWorld
791
+ }
792
+ `),
793
+ });
794
+
795
+ await using server = await setupServer({
796
+ plugins: [
797
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
798
+ ],
799
+ });
800
+ await server.listen();
801
+
802
+ const manifest = readManifestFile();
803
+ expect(manifest.operations[0].tools).toMatchInlineSnapshot(`
804
+ [
805
+ {
806
+ "description": "A greeting tool",
807
+ "name": "HelloWorldQuery",
808
+ },
809
+ ]
810
+ `);
811
+ });
812
+
665
813
  test("errors when tool name contains spaces", async () => {
666
814
  vol.fromJSON({
667
815
  "package.json": mockPackageJson(),
@@ -675,7 +823,9 @@ describe("@tool validation", () => {
675
823
 
676
824
  await expect(async () => {
677
825
  await using server = await setupServer({
678
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
826
+ plugins: [
827
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
828
+ ],
679
829
  });
680
830
  await server.listen();
681
831
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -698,7 +848,9 @@ describe("@tool validation", () => {
698
848
 
699
849
  await expect(async () => {
700
850
  await using server = await setupServer({
701
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
851
+ plugins: [
852
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
853
+ ],
702
854
  });
703
855
  await server.listen();
704
856
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -718,7 +870,9 @@ describe("@tool validation", () => {
718
870
 
719
871
  await expect(async () => {
720
872
  await using server = await setupServer({
721
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
873
+ plugins: [
874
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
875
+ ],
722
876
  });
723
877
  await server.listen();
724
878
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -739,7 +893,9 @@ describe("@tool validation", () => {
739
893
 
740
894
  await expect(async () => {
741
895
  await using server = await setupServer({
742
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
896
+ plugins: [
897
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
898
+ ],
743
899
  });
744
900
  await server.listen();
745
901
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -764,13 +920,63 @@ describe("@tool validation", () => {
764
920
 
765
921
  await expect(async () => {
766
922
  await using server = await setupServer({
767
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
923
+ plugins: [
924
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
925
+ ],
768
926
  });
769
927
  await server.listen();
770
928
  }).rejects.toThrowErrorMatchingInlineSnapshot(
771
929
  `[Error: Error when parsing directive values: unexpected type 'FloatValue']`
772
930
  );
773
931
  });
932
+
933
+ test("errors when multiple @tool directives are used and one is missing a name", async () => {
934
+ vol.fromJSON({
935
+ "package.json": mockPackageJson(),
936
+ "src/my-component.tsx": declareOperation(gql`
937
+ query HelloWorldQuery
938
+ @tool(name: "tool-a", description: "Tool A")
939
+ @tool(description: "Tool B") {
940
+ helloWorld
941
+ }
942
+ `),
943
+ });
944
+
945
+ await expect(async () => {
946
+ await using server = await setupServer({
947
+ plugins: [
948
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
949
+ ],
950
+ });
951
+ await server.listen();
952
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
953
+ `[Error: Operations with multiple @tool directives must provide a 'name' argument on each @tool]`
954
+ );
955
+ });
956
+
957
+ test("errors when multiple @tool directives are used and one is missing a description", async () => {
958
+ vol.fromJSON({
959
+ "package.json": mockPackageJson(),
960
+ "src/my-component.tsx": declareOperation(gql`
961
+ query HelloWorldQuery
962
+ @tool(name: "tool-a", description: "Tool A")
963
+ @tool(name: "tool-b") {
964
+ helloWorld
965
+ }
966
+ `),
967
+ });
968
+
969
+ await expect(async () => {
970
+ await using server = await setupServer({
971
+ plugins: [
972
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
973
+ ],
974
+ });
975
+ await server.listen();
976
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
977
+ `[Error: Operations with multiple @tool directives must provide a 'description' argument on each @tool]`
978
+ );
979
+ });
774
980
  });
775
981
 
776
982
  describe("config validation", () => {
@@ -792,7 +998,9 @@ describe("config validation", () => {
792
998
 
793
999
  await expect(async () => {
794
1000
  await using server = await setupServer({
795
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1001
+ plugins: [
1002
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1003
+ ],
796
1004
  });
797
1005
  await server.listen();
798
1006
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -821,7 +1029,9 @@ describe("config validation", () => {
821
1029
 
822
1030
  await expect(async () => {
823
1031
  await using server = await setupServer({
824
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1032
+ plugins: [
1033
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1034
+ ],
825
1035
  });
826
1036
  await server.listen();
827
1037
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -850,7 +1060,9 @@ describe("config validation", () => {
850
1060
 
851
1061
  await expect(async () => {
852
1062
  await using server = await setupServer({
853
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1063
+ plugins: [
1064
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1065
+ ],
854
1066
  });
855
1067
  await server.listen();
856
1068
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -874,7 +1086,9 @@ describe("config validation", () => {
874
1086
  });
875
1087
 
876
1088
  await using server = await setupServer({
877
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1089
+ plugins: [
1090
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1091
+ ],
878
1092
  });
879
1093
  await server.listen();
880
1094
 
@@ -902,7 +1116,9 @@ describe("config validation", () => {
902
1116
 
903
1117
  await expect(async () => {
904
1118
  await using server = await setupServer({
905
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1119
+ plugins: [
1120
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1121
+ ],
906
1122
  });
907
1123
  await server.listen();
908
1124
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -930,7 +1146,9 @@ describe("config validation", () => {
930
1146
 
931
1147
  await expect(async () => {
932
1148
  await using server = await setupServer({
933
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1149
+ plugins: [
1150
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1151
+ ],
934
1152
  });
935
1153
  await server.listen();
936
1154
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -961,7 +1179,9 @@ describe("config validation", () => {
961
1179
 
962
1180
  await expect(async () => {
963
1181
  await using server = await setupServer({
964
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1182
+ plugins: [
1183
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1184
+ ],
965
1185
  });
966
1186
  await server.listen();
967
1187
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -989,7 +1209,9 @@ describe("config validation", () => {
989
1209
 
990
1210
  await expect(async () => {
991
1211
  await using server = await setupServer({
992
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1212
+ plugins: [
1213
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1214
+ ],
993
1215
  });
994
1216
  await server.listen();
995
1217
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -1012,7 +1234,9 @@ describe("config validation", () => {
1012
1234
  });
1013
1235
 
1014
1236
  await using server = await setupServer({
1015
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1237
+ plugins: [
1238
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1239
+ ],
1016
1240
  });
1017
1241
  await server.listen();
1018
1242
 
@@ -1041,7 +1265,9 @@ describe("entry points", () => {
1041
1265
 
1042
1266
  await using server = await setupServer({
1043
1267
  mode: "staging",
1044
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1268
+ plugins: [
1269
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1270
+ ],
1045
1271
  });
1046
1272
  await server.listen();
1047
1273
 
@@ -1070,7 +1296,13 @@ describe("entry points", () => {
1070
1296
 
1071
1297
  await using server = await setupServer({
1072
1298
  mode: "staging",
1073
- plugins: [apolloClientAiApps({ targets: ["mcp"], devTarget: "mcp" })],
1299
+ plugins: [
1300
+ apolloClientAiApps({
1301
+ targets: ["mcp"],
1302
+ devTarget: "mcp",
1303
+ appsOutDir: "dist/apps",
1304
+ }),
1305
+ ],
1074
1306
  });
1075
1307
  await server.listen();
1076
1308
 
@@ -1091,7 +1323,9 @@ describe("entry points", () => {
1091
1323
 
1092
1324
  await using server = await setupServer({
1093
1325
  server: { https: {}, port: 5678 },
1094
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1326
+ plugins: [
1327
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1328
+ ],
1095
1329
  });
1096
1330
  await server.listen();
1097
1331
 
@@ -1112,7 +1346,9 @@ describe("entry points", () => {
1112
1346
 
1113
1347
  await using server = await setupServer({
1114
1348
  server: { port: 5678, host: "0.0.0.0" },
1115
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1349
+ plugins: [
1350
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1351
+ ],
1116
1352
  });
1117
1353
  await server.listen();
1118
1354
 
@@ -1139,7 +1375,9 @@ describe("entry points", () => {
1139
1375
 
1140
1376
  await buildApp({
1141
1377
  mode: "staging",
1142
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1378
+ plugins: [
1379
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1380
+ ],
1143
1381
  });
1144
1382
 
1145
1383
  const manifest = readManifestFile();
@@ -1168,7 +1406,12 @@ describe("entry points", () => {
1168
1406
 
1169
1407
  await buildApp({
1170
1408
  mode: "staging",
1171
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1409
+ plugins: [
1410
+ apolloClientAiApps({
1411
+ targets: ["mcp", "openai"],
1412
+ appsOutDir: "dist/apps",
1413
+ }),
1414
+ ],
1172
1415
  });
1173
1416
 
1174
1417
  const manifest = readManifestFile();
@@ -1197,7 +1440,12 @@ describe("entry points", () => {
1197
1440
 
1198
1441
  await buildApp({
1199
1442
  mode: "staging",
1200
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1443
+ plugins: [
1444
+ apolloClientAiApps({
1445
+ targets: ["mcp", "openai"],
1446
+ appsOutDir: "dist/apps",
1447
+ }),
1448
+ ],
1201
1449
  });
1202
1450
 
1203
1451
  const manifest = readManifestFile();
@@ -1228,7 +1476,9 @@ describe("entry points", () => {
1228
1476
 
1229
1477
  await buildApp({
1230
1478
  mode: "staging",
1231
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1479
+ plugins: [
1480
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1481
+ ],
1232
1482
  });
1233
1483
 
1234
1484
  const manifest = readManifestFile();
@@ -1250,7 +1500,9 @@ describe("entry points", () => {
1250
1500
 
1251
1501
  await buildApp({
1252
1502
  mode: "production",
1253
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1503
+ plugins: [
1504
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1505
+ ],
1254
1506
  });
1255
1507
 
1256
1508
  const manifest = readManifestFile();
@@ -1270,7 +1522,12 @@ describe("entry points", () => {
1270
1522
 
1271
1523
  await buildApp({
1272
1524
  mode: "production",
1273
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1525
+ plugins: [
1526
+ apolloClientAiApps({
1527
+ targets: ["mcp", "openai"],
1528
+ appsOutDir: "dist/apps",
1529
+ }),
1530
+ ],
1274
1531
  });
1275
1532
 
1276
1533
  const manifest = readManifestFile();
@@ -1295,7 +1552,9 @@ describe("entry points", () => {
1295
1552
  async () =>
1296
1553
  await buildApp({
1297
1554
  mode: "staging",
1298
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1555
+ plugins: [
1556
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1557
+ ],
1299
1558
  })
1300
1559
  ).rejects.toThrowError(
1301
1560
  `[@apollo/client-ai-apps/vite] No entry point found for mode "staging". Entry points other than "development" and "production" must be defined in package.json file.`
@@ -1315,11 +1574,15 @@ describe("entry points", () => {
1315
1574
 
1316
1575
  await buildApp({
1317
1576
  mode: "production",
1318
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1577
+ plugins: [
1578
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1579
+ ],
1319
1580
  });
1320
1581
 
1321
1582
  expect(vol.existsSync(".application-manifest.json")).toBe(true);
1322
- expect(vol.existsSync("dist/.application-manifest.json")).toBe(true);
1583
+ expect(vol.existsSync("dist/apps/my-app/.application-manifest.json")).toBe(
1584
+ true
1585
+ );
1323
1586
  });
1324
1587
 
1325
1588
  test("writes to both locations when running in build mode with multiple targets", async () => {
@@ -1335,11 +1598,155 @@ describe("entry points", () => {
1335
1598
 
1336
1599
  await buildApp({
1337
1600
  mode: "production",
1338
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1601
+ plugins: [
1602
+ apolloClientAiApps({
1603
+ targets: ["mcp", "openai"],
1604
+ appsOutDir: "dist/apps",
1605
+ }),
1606
+ ],
1339
1607
  });
1340
1608
 
1341
1609
  expect(vol.existsSync(".application-manifest.json")).toBe(true);
1342
- expect(vol.existsSync("dist/.application-manifest.json")).toBe(true);
1610
+ expect(vol.existsSync("dist/apps/my-app/.application-manifest.json")).toBe(
1611
+ true
1612
+ );
1613
+ });
1614
+
1615
+ test("generates .application-manifest.d.json.ts at root in build mode", async () => {
1616
+ vol.fromJSON({
1617
+ "package.json": mockPackageJson(),
1618
+ "src/my-component.tsx": declareOperation(gql`
1619
+ query HelloWorldQuery
1620
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1621
+ helloWorld
1622
+ }
1623
+ `),
1624
+ });
1625
+
1626
+ await buildApp({
1627
+ mode: "production",
1628
+ plugins: [
1629
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1630
+ ],
1631
+ });
1632
+
1633
+ expect(vol.existsSync(".application-manifest.d.json.ts")).toBe(true);
1634
+ });
1635
+
1636
+ test("generates .application-manifest.d.json.ts at root in serve mode", async () => {
1637
+ vol.fromJSON({
1638
+ "package.json": mockPackageJson(),
1639
+ "src/my-component.tsx": declareOperation(gql`
1640
+ query HelloWorldQuery
1641
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1642
+ helloWorld
1643
+ }
1644
+ `),
1645
+ });
1646
+
1647
+ await using server = await setupServer({
1648
+ plugins: [
1649
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1650
+ ],
1651
+ });
1652
+ await server.listen();
1653
+
1654
+ expect(vol.existsSync(".application-manifest.d.json.ts")).toBe(true);
1655
+ });
1656
+
1657
+ test("does not write .application-manifest.json.d.ts to appsOutDir", async () => {
1658
+ vol.fromJSON({
1659
+ "package.json": mockPackageJson(),
1660
+ "src/my-component.tsx": declareOperation(gql`
1661
+ query HelloWorldQuery
1662
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1663
+ helloWorld
1664
+ }
1665
+ `),
1666
+ });
1667
+
1668
+ await buildApp({
1669
+ mode: "production",
1670
+ plugins: [
1671
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1672
+ ],
1673
+ });
1674
+
1675
+ expect(
1676
+ vol.existsSync("dist/apps/my-app/.application-manifest.json.d.ts")
1677
+ ).toBe(false);
1678
+ });
1679
+ });
1680
+
1681
+ describe("appsOutDir", () => {
1682
+ test("errors when last segment is not `apps`", () => {
1683
+ expect(() =>
1684
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/output" })
1685
+ ).toThrowError(
1686
+ "`appsOutDir` must end with `apps` as the final path segment (e.g. `path/to/apps`)."
1687
+ );
1688
+ });
1689
+
1690
+ test("accepts trailing slash", async () => {
1691
+ vol.fromJSON({
1692
+ "package.json": mockPackageJson(),
1693
+ });
1694
+
1695
+ await expect(
1696
+ buildApp({
1697
+ mode: "production",
1698
+ build: { write: false },
1699
+ plugins: [
1700
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps/" }),
1701
+ ],
1702
+ })
1703
+ ).resolves.not.toThrowError();
1704
+ });
1705
+
1706
+ test("warns when `build.outDir` is set", async () => {
1707
+ vol.fromJSON({
1708
+ "package.json": mockPackageJson(),
1709
+ });
1710
+
1711
+ using _ = spyOnConsole("warn");
1712
+
1713
+ await buildApp({
1714
+ mode: "production",
1715
+ build: { outDir: "custom-out", write: false },
1716
+ plugins: [
1717
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1718
+ ],
1719
+ });
1720
+
1721
+ expect(console.warn).toHaveBeenCalledWith(
1722
+ expect.stringContaining(
1723
+ "`build.outDir` is set in your Vite config but will be ignored"
1724
+ )
1725
+ );
1726
+ });
1727
+
1728
+ test("places output under `appsOutDir`", async () => {
1729
+ vol.fromJSON({
1730
+ "package.json": mockPackageJson(),
1731
+ "src/my-component.tsx": declareOperation(gql`
1732
+ query HelloWorldQuery
1733
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1734
+ helloWorld
1735
+ }
1736
+ `),
1737
+ });
1738
+
1739
+ await buildApp({
1740
+ mode: "production",
1741
+ plugins: [
1742
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1743
+ ],
1744
+ });
1745
+
1746
+ expect(vol.existsSync("dist/apps/my-app/.application-manifest.json")).toBe(
1747
+ true
1748
+ );
1749
+ expect(vol.existsSync(".application-manifest.json")).toBe(true);
1343
1750
  });
1344
1751
  });
1345
1752
 
@@ -1387,7 +1794,9 @@ export default config;
1387
1794
 
1388
1795
  await buildApp({
1389
1796
  mode: "production",
1390
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1797
+ plugins: [
1798
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1799
+ ],
1391
1800
  });
1392
1801
 
1393
1802
  const manifest = readManifestFile();
@@ -1423,7 +1832,9 @@ export default config;
1423
1832
 
1424
1833
  await buildApp({
1425
1834
  mode: "production",
1426
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1835
+ plugins: [
1836
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1837
+ ],
1427
1838
  });
1428
1839
 
1429
1840
  const manifest = readManifestFile();
@@ -1457,7 +1868,9 @@ describe("file watching", () => {
1457
1868
  });
1458
1869
 
1459
1870
  await using server = await setupServer({
1460
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1871
+ plugins: [
1872
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1873
+ ],
1461
1874
  });
1462
1875
  await server.listen();
1463
1876
 
@@ -1485,6 +1898,541 @@ describe("file watching", () => {
1485
1898
  });
1486
1899
  });
1487
1900
 
1901
+ describe("tool input types", () => {
1902
+ const schema = `
1903
+ type Query {
1904
+ todo(id: ID!): Todo
1905
+ }
1906
+
1907
+ type Mutation {
1908
+ createTodo(title: String!, description: String): Todo
1909
+ deleteTodo(id: ID!): Boolean
1910
+ }
1911
+
1912
+ type Todo {
1913
+ id: ID!
1914
+ title: String!
1915
+ description: String
1916
+ }
1917
+ `;
1918
+
1919
+ test("generates operation-types.d.ts with variable types when schema is provided", async () => {
1920
+ vol.fromJSON({
1921
+ "package.json": mockPackageJson(),
1922
+ "src/my-component.tsx": declareOperation(gql`
1923
+ mutation CreateTodo($title: String!, $description: String)
1924
+ @tool(name: "CreateTodo", description: "Creates a todo") {
1925
+ createTodo(title: $title, description: $description) {
1926
+ id
1927
+ }
1928
+ }
1929
+ `),
1930
+ });
1931
+
1932
+ await using server = await setupServer({
1933
+ plugins: [
1934
+ apolloClientAiApps({
1935
+ targets: ["mcp"],
1936
+ appsOutDir: "dist/apps",
1937
+ schema,
1938
+ }),
1939
+ ],
1940
+ });
1941
+ await server.listen();
1942
+
1943
+ const content = fs.readFileSync(
1944
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
1945
+ "utf-8"
1946
+ );
1947
+ expect(content).toMatchInlineSnapshot(`
1948
+ "// Auto-generated by @apollo/client-ai-apps. Do not edit manually.
1949
+ export type Maybe<T> = T | null;
1950
+
1951
+ export type InputMaybe<T> = Maybe<T>;
1952
+
1953
+ export type Exact<T extends {
1954
+ [key: string]: unknown;
1955
+ }> = {
1956
+ [K in keyof T]: T[K];
1957
+ };
1958
+
1959
+ /** All built-in and custom scalars, mapped to their actual values */
1960
+ export type Scalars = {
1961
+ ID: {
1962
+ input: string;
1963
+ output: string;
1964
+ };
1965
+ String: {
1966
+ input: string;
1967
+ output: string;
1968
+ };
1969
+ Boolean: {
1970
+ input: boolean;
1971
+ output: boolean;
1972
+ };
1973
+ Int: {
1974
+ input: number;
1975
+ output: number;
1976
+ };
1977
+ Float: {
1978
+ input: number;
1979
+ output: number;
1980
+ };
1981
+ };
1982
+
1983
+ export type CreateTodoMutationVariables = Exact<{
1984
+ title: Scalars["String"]["input"];
1985
+ description?: InputMaybe<Scalars["String"]["input"]>;
1986
+ }>;"
1987
+ `);
1988
+ });
1989
+
1990
+ test("generates register.d.ts with toolInputs when schema is provided", async () => {
1991
+ vol.fromJSON({
1992
+ "package.json": mockPackageJson(),
1993
+ "src/my-component.tsx": declareOperation(gql`
1994
+ mutation CreateTodo($title: String!, $description: String)
1995
+ @tool(name: "CreateTodo", description: "Creates a todo") {
1996
+ createTodo(title: $title, description: $description) {
1997
+ id
1998
+ }
1999
+ }
2000
+ `),
2001
+ });
2002
+
2003
+ await using server = await setupServer({
2004
+ plugins: [
2005
+ apolloClientAiApps({
2006
+ targets: ["mcp"],
2007
+ appsOutDir: "dist/apps",
2008
+ schema,
2009
+ }),
2010
+ ],
2011
+ });
2012
+ await server.listen();
2013
+
2014
+ const content = fs.readFileSync(
2015
+ ".apollo-client-ai-apps/types/register.d.ts",
2016
+ "utf-8"
2017
+ );
2018
+ expect(content).toMatchInlineSnapshot(`
2019
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2020
+ import "@apollo/client-ai-apps";
2021
+
2022
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2023
+
2024
+ declare module "@apollo/client-ai-apps" {
2025
+ interface Register {
2026
+ toolName: "CreateTodo";
2027
+ toolInputs: {
2028
+ CreateTodo: CreateTodoMutationVariables;
2029
+ };
2030
+ }
2031
+ }"
2032
+ `);
2033
+ });
2034
+
2035
+ test("generates register.d.ts with operations that contain fragments", async () => {
2036
+ vol.fromJSON({
2037
+ "package.json": mockPackageJson(),
2038
+ "src/my-component.tsx": declareOperation(gql`
2039
+ mutation CreateTodo($title: String!, $description: String)
2040
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2041
+ createTodo(title: $title, description: $description) {
2042
+ id
2043
+ ...TodoFragment
2044
+ }
2045
+ }
2046
+ `),
2047
+ "src/todo-fragment.tsx": declareFragment(gql`
2048
+ fragment TodoFragment on Todo {
2049
+ title
2050
+ }
2051
+ `),
2052
+ });
2053
+
2054
+ await using server = await setupServer({
2055
+ plugins: [
2056
+ apolloClientAiApps({
2057
+ targets: ["mcp"],
2058
+ appsOutDir: "dist/apps",
2059
+ schema,
2060
+ }),
2061
+ ],
2062
+ });
2063
+ await server.listen();
2064
+
2065
+ expect(
2066
+ fs.readFileSync(".apollo-client-ai-apps/types/register.d.ts", "utf-8")
2067
+ ).toMatchInlineSnapshot(`
2068
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2069
+ import "@apollo/client-ai-apps";
2070
+
2071
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2072
+
2073
+ declare module "@apollo/client-ai-apps" {
2074
+ interface Register {
2075
+ toolName: "CreateTodo";
2076
+ toolInputs: {
2077
+ CreateTodo: CreateTodoMutationVariables;
2078
+ };
2079
+ }
2080
+ }"
2081
+ `);
2082
+ expect(
2083
+ fs.readFileSync(
2084
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
2085
+ "utf-8"
2086
+ )
2087
+ ).toMatchInlineSnapshot(`
2088
+ "// Auto-generated by @apollo/client-ai-apps. Do not edit manually.
2089
+ export type Maybe<T> = T | null;
2090
+
2091
+ export type InputMaybe<T> = Maybe<T>;
2092
+
2093
+ export type Exact<T extends {
2094
+ [key: string]: unknown;
2095
+ }> = {
2096
+ [K in keyof T]: T[K];
2097
+ };
2098
+
2099
+ /** All built-in and custom scalars, mapped to their actual values */
2100
+ export type Scalars = {
2101
+ ID: {
2102
+ input: string;
2103
+ output: string;
2104
+ };
2105
+ String: {
2106
+ input: string;
2107
+ output: string;
2108
+ };
2109
+ Boolean: {
2110
+ input: boolean;
2111
+ output: boolean;
2112
+ };
2113
+ Int: {
2114
+ input: number;
2115
+ output: number;
2116
+ };
2117
+ Float: {
2118
+ input: number;
2119
+ output: number;
2120
+ };
2121
+ };
2122
+
2123
+ export type CreateTodoMutationVariables = Exact<{
2124
+ title: Scalars["String"]["input"];
2125
+ description?: InputMaybe<Scalars["String"]["input"]>;
2126
+ }>;"
2127
+ `);
2128
+ });
2129
+
2130
+ test("tool with no extraInputs uses only the Variables type", async () => {
2131
+ vol.fromJSON({
2132
+ "package.json": mockPackageJson(),
2133
+ "src/my-component.tsx": declareOperation(gql`
2134
+ mutation DeleteTodo($id: ID!)
2135
+ @tool(name: "DeleteTodo", description: "Deletes a todo") {
2136
+ deleteTodo(id: $id)
2137
+ }
2138
+ `),
2139
+ });
2140
+
2141
+ await using server = await setupServer({
2142
+ plugins: [
2143
+ apolloClientAiApps({
2144
+ targets: ["mcp"],
2145
+ appsOutDir: "dist/apps",
2146
+ schema,
2147
+ }),
2148
+ ],
2149
+ });
2150
+ await server.listen();
2151
+
2152
+ const content = fs.readFileSync(
2153
+ ".apollo-client-ai-apps/types/register.d.ts",
2154
+ "utf-8"
2155
+ );
2156
+ expect(content).toMatchInlineSnapshot(`
2157
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2158
+ import "@apollo/client-ai-apps";
2159
+
2160
+ import type { DeleteTodoMutationVariables } from "./operation-types.js";
2161
+
2162
+ declare module "@apollo/client-ai-apps" {
2163
+ interface Register {
2164
+ toolName: "DeleteTodo";
2165
+ toolInputs: {
2166
+ DeleteTodo: DeleteTodoMutationVariables;
2167
+ };
2168
+ }
2169
+ }"
2170
+ `);
2171
+ });
2172
+
2173
+ test("tool with extraInputs adds properties to tool input type", async () => {
2174
+ vol.fromJSON({
2175
+ "package.json": mockPackageJson(),
2176
+ "src/my-component.tsx": declareOperation(gql`
2177
+ mutation CreateTodo($title: String!)
2178
+ @tool(
2179
+ name: "CreateTodo"
2180
+ description: "Creates a todo"
2181
+ extraInputs: [
2182
+ { name: "priority", type: "string", description: "Priority" }
2183
+ { name: "urgent", type: "boolean", description: "Is urgent?" }
2184
+ {
2185
+ name: "odd-name"
2186
+ type: "boolean"
2187
+ description: "Test of odd name"
2188
+ }
2189
+ ]
2190
+ ) {
2191
+ createTodo(title: $title) {
2192
+ id
2193
+ }
2194
+ }
2195
+ `),
2196
+ });
2197
+
2198
+ await using server = await setupServer({
2199
+ plugins: [
2200
+ apolloClientAiApps({
2201
+ targets: ["mcp"],
2202
+ appsOutDir: "dist/apps",
2203
+ schema,
2204
+ }),
2205
+ ],
2206
+ });
2207
+ await server.listen();
2208
+
2209
+ const content = fs.readFileSync(
2210
+ ".apollo-client-ai-apps/types/register.d.ts",
2211
+ "utf-8"
2212
+ );
2213
+ expect(content).toMatchInlineSnapshot(`
2214
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2215
+ import "@apollo/client-ai-apps";
2216
+
2217
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2218
+
2219
+ declare module "@apollo/client-ai-apps" {
2220
+ interface Register {
2221
+ toolName: "CreateTodo";
2222
+ toolInputs: {
2223
+ CreateTodo: CreateTodoMutationVariables & {
2224
+ priority?: string;
2225
+ urgent?: boolean;
2226
+ "odd-name"?: boolean;
2227
+ };
2228
+ };
2229
+ }
2230
+ }"
2231
+ `);
2232
+ });
2233
+
2234
+ test("multiple @tool directives on an operation each get their own toolInputs entry", async () => {
2235
+ vol.fromJSON({
2236
+ "package.json": mockPackageJson(),
2237
+ "src/my-component.tsx": declareOperation(gql`
2238
+ mutation CreateTodo($title: String!)
2239
+ @tool(name: "CreateTodo", description: "Creates a todo")
2240
+ @tool(name: "AddTask", description: "Adds a task") {
2241
+ createTodo(title: $title) {
2242
+ id
2243
+ }
2244
+ }
2245
+ `),
2246
+ });
2247
+
2248
+ await using server = await setupServer({
2249
+ plugins: [
2250
+ apolloClientAiApps({
2251
+ targets: ["mcp"],
2252
+ appsOutDir: "dist/apps",
2253
+ schema,
2254
+ }),
2255
+ ],
2256
+ });
2257
+ await server.listen();
2258
+
2259
+ const content = fs.readFileSync(
2260
+ ".apollo-client-ai-apps/types/register.d.ts",
2261
+ "utf-8"
2262
+ );
2263
+ expect(content).toMatchInlineSnapshot(`
2264
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2265
+ import "@apollo/client-ai-apps";
2266
+
2267
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2268
+
2269
+ declare module "@apollo/client-ai-apps" {
2270
+ interface Register {
2271
+ toolName: "CreateTodo" | "AddTask";
2272
+ toolInputs: {
2273
+ CreateTodo: CreateTodoMutationVariables;
2274
+ AddTask: CreateTodoMutationVariables;
2275
+ };
2276
+ }
2277
+ }"
2278
+ `);
2279
+ });
2280
+
2281
+ test("operation-types.d.ts is not rewritten when operation content has not changed", async () => {
2282
+ vol.fromJSON({
2283
+ "package.json": mockPackageJson(),
2284
+ "src/my-component.tsx": declareOperation(gql`
2285
+ mutation CreateTodo($title: String!)
2286
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2287
+ createTodo(title: $title) {
2288
+ id
2289
+ }
2290
+ }
2291
+ `),
2292
+ });
2293
+
2294
+ await using server = await setupServer({
2295
+ plugins: [
2296
+ apolloClientAiApps({
2297
+ targets: ["mcp"],
2298
+ appsOutDir: "dist/apps",
2299
+ schema,
2300
+ }),
2301
+ ],
2302
+ });
2303
+ await server.listen();
2304
+
2305
+ const content = fs.readFileSync(
2306
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
2307
+ "utf-8"
2308
+ );
2309
+ const mtime = fs.statSync(
2310
+ ".apollo-client-ai-apps/types/operation-types.d.ts"
2311
+ ).mtimeMs;
2312
+
2313
+ // Trigger another manifest generation (simulating a file change to an unrelated file)
2314
+ server.watcher.emit("change", "package.json");
2315
+ await wait(100);
2316
+
2317
+ expect(
2318
+ fs.readFileSync(
2319
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
2320
+ "utf-8"
2321
+ )
2322
+ ).toBe(content);
2323
+
2324
+ expect(
2325
+ fs.statSync(".apollo-client-ai-apps/types/operation-types.d.ts").mtimeMs
2326
+ ).toBe(mtime);
2327
+ });
2328
+
2329
+ test("does not generate operation-types.d.ts when schema is not provided", async () => {
2330
+ vol.fromJSON({
2331
+ "package.json": mockPackageJson(),
2332
+ "src/my-component.tsx": declareOperation(gql`
2333
+ mutation CreateTodo($title: String!)
2334
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2335
+ createTodo(title: $title) {
2336
+ id
2337
+ }
2338
+ }
2339
+ `),
2340
+ });
2341
+
2342
+ await using server = await setupServer({
2343
+ plugins: [
2344
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2345
+ ],
2346
+ });
2347
+ await server.listen();
2348
+
2349
+ expect(
2350
+ fs.existsSync(".apollo-client-ai-apps/types/operation-types.d.ts")
2351
+ ).toBe(false);
2352
+ });
2353
+
2354
+ test("generates register.d.ts with error message when schema build fails in dev mode", async () => {
2355
+ using _ = spyOnConsole("error");
2356
+
2357
+ vol.fromJSON({
2358
+ "package.json": mockPackageJson(),
2359
+ "src/my-component.tsx": declareOperation(gql`
2360
+ query GetNonExistent
2361
+ @tool(name: "GetNonExistent", description: "A tool") {
2362
+ nonExistentField
2363
+ }
2364
+ `),
2365
+ });
2366
+
2367
+ await using server = await setupServer({
2368
+ plugins: [
2369
+ apolloClientAiApps({
2370
+ targets: ["mcp"],
2371
+ appsOutDir: "dist/apps",
2372
+ schema,
2373
+ }),
2374
+ ],
2375
+ });
2376
+ await server.listen();
2377
+
2378
+ expect(console.error).toHaveBeenCalledOnce();
2379
+
2380
+ const content = fs.readFileSync(
2381
+ ".apollo-client-ai-apps/types/register.d.ts",
2382
+ "utf-8"
2383
+ );
2384
+ expect(content).toMatchInlineSnapshot(`
2385
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2386
+ import "@apollo/client-ai-apps";
2387
+
2388
+ declare module "@apollo/client-ai-apps" {
2389
+ interface Register {
2390
+ toolName: "[@apollo/client-ai-apps/vite]: There was an error building generated types. See the vite build output for more details.\\n\\nGraphQL Document Validation failed with 1 errors;\\n Error 0: Cannot query field \\"nonExistentField\\" on type \\"Query\\".\\n at 116185304.graphql:2:3";
2391
+ toolInputs: {
2392
+ "[@apollo/client-ai-apps/vite]: There was an error building generated types. See the vite build output for more details.\\n\\nGraphQL Document Validation failed with 1 errors;\\n Error 0: Cannot query field \\"nonExistentField\\" on type \\"Query\\".\\n at 116185304.graphql:2:3": "[@apollo/client-ai-apps/vite]: There was an error building generated types. See the vite build output for more details.\\n\\nGraphQL Document Validation failed with 1 errors;\\n Error 0: Cannot query field \\"nonExistentField\\" on type \\"Query\\".\\n at 116185304.graphql:2:3";
2393
+ };
2394
+ }
2395
+ }"
2396
+ `);
2397
+ });
2398
+
2399
+ test("generates register.d.ts without toolInputs when schema is not provided", async () => {
2400
+ vol.fromJSON({
2401
+ "package.json": mockPackageJson(),
2402
+ "src/my-component.tsx": declareOperation(gql`
2403
+ mutation CreateTodo($title: String!)
2404
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2405
+ createTodo(title: $title) {
2406
+ id
2407
+ }
2408
+ }
2409
+ `),
2410
+ });
2411
+
2412
+ await using server = await setupServer({
2413
+ plugins: [
2414
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2415
+ ],
2416
+ });
2417
+ await server.listen();
2418
+
2419
+ const content = fs.readFileSync(
2420
+ ".apollo-client-ai-apps/types/register.d.ts",
2421
+ "utf-8"
2422
+ );
2423
+ expect(content).toMatchInlineSnapshot(`
2424
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2425
+ import "@apollo/client-ai-apps";
2426
+
2427
+ declare module "@apollo/client-ai-apps" {
2428
+ interface Register {
2429
+ toolName: "CreateTodo";
2430
+ }
2431
+ }"
2432
+ `);
2433
+ });
2434
+ });
2435
+
1488
2436
  describe("html transforms", () => {
1489
2437
  test("replaces root relative scripts with full url when origin is provided", async () => {
1490
2438
  vol.fromJSON({
@@ -1495,7 +2443,9 @@ describe("html transforms", () => {
1495
2443
  server: {
1496
2444
  origin: "http://localhost:3000",
1497
2445
  },
1498
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
2446
+ plugins: [
2447
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2448
+ ],
1499
2449
  });
1500
2450
 
1501
2451
  const html = `<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
@@ -1519,7 +2469,9 @@ describe("html transforms", () => {
1519
2469
  server: {
1520
2470
  port: 3000,
1521
2471
  },
1522
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
2472
+ plugins: [
2473
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2474
+ ],
1523
2475
  });
1524
2476
 
1525
2477
  await server.listen();
@@ -1547,7 +2499,9 @@ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1547
2499
  server: {
1548
2500
  origin: "http://localhost:3000",
1549
2501
  },
1550
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
2502
+ plugins: [
2503
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2504
+ ],
1551
2505
  });
1552
2506
 
1553
2507
  const html = `<html><head> <script type="module">import { injectIntoGlobalHook } from "/@react-refresh";
@@ -1573,7 +2527,9 @@ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1573
2527
  server: {
1574
2528
  port: 3000,
1575
2529
  },
1576
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
2530
+ plugins: [
2531
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2532
+ ],
1577
2533
  });
1578
2534
 
1579
2535
  await server.listen();
@@ -1604,7 +2560,7 @@ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1604
2560
  origin: "http://localhost:3000",
1605
2561
  },
1606
2562
  plugins: [
1607
- apolloClientAiApps({ targets: ["mcp"] }),
2563
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1608
2564
  {
1609
2565
  name: "capture-html",
1610
2566
  transformIndexHtml: {
@@ -1643,7 +2599,7 @@ function declareFragment(fragment: DocumentNode) {
1643
2599
  }
1644
2600
 
1645
2601
  function mockPackageJson(config?: Record<string, unknown>) {
1646
- return JSON.stringify({ version: "1.0.0", ...config });
2602
+ return JSON.stringify({ version: "1.0.0", name: "my-app", ...config });
1647
2603
  }
1648
2604
 
1649
2605
  function readManifestFile(