@apollo/client-ai-apps 0.6.0 → 0.6.2

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 (85) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/dist/core/typeRegistration.d.ts +33 -0
  3. package/dist/core/typeRegistration.d.ts.map +1 -0
  4. package/dist/core/typeRegistration.js +2 -0
  5. package/dist/core/typeRegistration.js.map +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/react/hooks/useToolInfo.d.ts +3 -0
  10. package/dist/mcp/react/hooks/useToolInfo.d.ts.map +1 -0
  11. package/dist/mcp/react/hooks/useToolInfo.js +10 -0
  12. package/dist/mcp/react/hooks/useToolInfo.js.map +1 -0
  13. package/dist/mcp/react/hooks/useToolInput.d.ts +6 -1
  14. package/dist/mcp/react/hooks/useToolInput.d.ts.map +1 -1
  15. package/dist/mcp/react/hooks/useToolInput.js +4 -0
  16. package/dist/mcp/react/hooks/useToolInput.js.map +1 -1
  17. package/dist/mcp/react/hooks/useToolName.d.ts +6 -1
  18. package/dist/mcp/react/hooks/useToolName.d.ts.map +1 -1
  19. package/dist/mcp/react/hooks/useToolName.js +4 -0
  20. package/dist/mcp/react/hooks/useToolName.js.map +1 -1
  21. package/dist/mcp/react/index.d.ts +1 -0
  22. package/dist/mcp/react/index.d.ts.map +1 -1
  23. package/dist/mcp/react/index.js +1 -0
  24. package/dist/mcp/react/index.js.map +1 -1
  25. package/dist/openai/react/hooks/useToolInfo.d.ts +3 -0
  26. package/dist/openai/react/hooks/useToolInfo.d.ts.map +1 -0
  27. package/dist/openai/react/hooks/useToolInfo.js +10 -0
  28. package/dist/openai/react/hooks/useToolInfo.js.map +1 -0
  29. package/dist/openai/react/hooks/useToolInput.d.ts +6 -1
  30. package/dist/openai/react/hooks/useToolInput.d.ts.map +1 -1
  31. package/dist/openai/react/hooks/useToolInput.js +4 -0
  32. package/dist/openai/react/hooks/useToolInput.js.map +1 -1
  33. package/dist/openai/react/hooks/useToolName.d.ts +6 -1
  34. package/dist/openai/react/hooks/useToolName.d.ts.map +1 -1
  35. package/dist/openai/react/hooks/useToolName.js +4 -0
  36. package/dist/openai/react/hooks/useToolName.js.map +1 -1
  37. package/dist/openai/react/index.d.ts +1 -0
  38. package/dist/openai/react/index.d.ts.map +1 -1
  39. package/dist/openai/react/index.js +1 -0
  40. package/dist/openai/react/index.js.map +1 -1
  41. package/dist/react/index.d.ts +9 -0
  42. package/dist/react/index.d.ts.map +1 -1
  43. package/dist/react/index.js +9 -0
  44. package/dist/react/index.js.map +1 -1
  45. package/dist/react/index.mcp.d.ts +1 -1
  46. package/dist/react/index.mcp.d.ts.map +1 -1
  47. package/dist/react/index.mcp.js +1 -1
  48. package/dist/react/index.mcp.js.map +1 -1
  49. package/dist/react/index.openai.d.ts +1 -1
  50. package/dist/react/index.openai.d.ts.map +1 -1
  51. package/dist/react/index.openai.js +1 -1
  52. package/dist/react/index.openai.js.map +1 -1
  53. package/dist/tsconfig/core/tsconfig.json +2 -0
  54. package/dist/tsconfig/mcp/tsconfig.json +2 -0
  55. package/dist/tsconfig/openai/tsconfig.json +2 -0
  56. package/dist/vite/apolloClientAiApps.d.ts +1 -0
  57. package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
  58. package/dist/vite/apolloClientAiApps.js +346 -43
  59. package/dist/vite/apolloClientAiApps.js.map +1 -1
  60. package/dist/vite/utilities/recast.d.ts +54 -0
  61. package/dist/vite/utilities/recast.d.ts.map +1 -0
  62. package/dist/vite/utilities/recast.js +71 -0
  63. package/dist/vite/utilities/recast.js.map +1 -0
  64. package/package.json +7 -3
  65. package/src/core/typeRegistration.ts +32 -0
  66. package/src/index.ts +7 -0
  67. package/src/mcp/react/hooks/__tests__/useToolInfo.test.tsx +53 -0
  68. package/src/mcp/react/hooks/useToolInfo.ts +13 -0
  69. package/src/mcp/react/hooks/useToolInput.ts +6 -1
  70. package/src/mcp/react/hooks/useToolName.ts +6 -1
  71. package/src/mcp/react/index.ts +1 -0
  72. package/src/openai/react/hooks/__tests__/useToolInfo.test.tsx +92 -0
  73. package/src/openai/react/hooks/useToolInfo.ts +13 -0
  74. package/src/openai/react/hooks/useToolInput.ts +6 -1
  75. package/src/openai/react/hooks/useToolName.ts +6 -1
  76. package/src/openai/react/index.ts +1 -0
  77. package/src/react/index.mcp.ts +1 -0
  78. package/src/react/index.openai.ts +1 -0
  79. package/src/react/index.ts +11 -0
  80. package/src/tsconfig/core/tsconfig.json +2 -0
  81. package/src/tsconfig/mcp/tsconfig.json +2 -0
  82. package/src/tsconfig/openai/tsconfig.json +2 -0
  83. package/src/vite/__tests__/apolloClientAiApps.test.ts +754 -6
  84. package/src/vite/apolloClientAiApps.ts +564 -68
  85. package/src/vite/utilities/recast.ts +100 -0
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
- import { spyOnConsole } from "../../testing/internal/index.js";
2
+ import { spyOnConsole, wait } from "../../testing/internal/index.js";
3
3
  import fs from "node:fs";
4
4
  import { gql, type DocumentNode } from "@apollo/client";
5
5
  import { getMainDefinition, print } from "@apollo/client/utilities";
@@ -676,11 +676,11 @@ describe("@prefetch", () => {
676
676
  });
677
677
 
678
678
  describe("@tool validation", () => {
679
- test("errors when tool name is not provided", async () => {
679
+ test("errors when tool name is not provided on anonymous operation", async () => {
680
680
  vol.fromJSON({
681
681
  "package.json": mockPackageJson(),
682
682
  "src/my-component.tsx": declareOperation(gql`
683
- query HelloWorldQuery @tool {
683
+ query @tool {
684
684
  helloWorld
685
685
  }
686
686
  `),
@@ -694,11 +694,11 @@ describe("@tool validation", () => {
694
694
  });
695
695
  await server.listen();
696
696
  }).rejects.toThrowErrorMatchingInlineSnapshot(
697
- `[Error: 'name' argument must be supplied for @tool]`
697
+ `[Error: Anonymous operations cannot use @tool without providing a 'name' argument]`
698
698
  );
699
699
  });
700
700
 
701
- test("errors when tool description is not provided", async () => {
701
+ test("errors when tool description is not provided and operation has no description", async () => {
702
702
  vol.fromJSON({
703
703
  "package.json": mockPackageJson(),
704
704
  "src/my-component.tsx": declareOperation(gql`
@@ -716,10 +716,110 @@ describe("@tool validation", () => {
716
716
  });
717
717
  await server.listen();
718
718
  }).rejects.toThrowErrorMatchingInlineSnapshot(
719
- `[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]`
720
720
  );
721
721
  });
722
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
+ expect(manifest.operations[0].body).toMatchInlineSnapshot(`
781
+ "query HelloWorldQuery {
782
+ helloWorld
783
+ }"
784
+ `);
785
+ });
786
+
787
+ test("uses operation name and description when both are omitted from @tool", async () => {
788
+ vol.fromJSON({
789
+ "package.json": mockPackageJson(),
790
+ "src/my-component.tsx": declareOperation(gql`
791
+ """
792
+ A greeting tool
793
+ """
794
+ query HelloWorldQuery @tool {
795
+ helloWorld
796
+ }
797
+ `),
798
+ });
799
+
800
+ await using server = await setupServer({
801
+ plugins: [
802
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
803
+ ],
804
+ });
805
+ await server.listen();
806
+
807
+ const manifest = readManifestFile();
808
+ expect(manifest.operations[0].tools).toMatchInlineSnapshot(`
809
+ [
810
+ {
811
+ "description": "A greeting tool",
812
+ "name": "HelloWorldQuery",
813
+ },
814
+ ]
815
+ `);
816
+ expect(manifest.operations[0].body).toMatchInlineSnapshot(`
817
+ "query HelloWorldQuery {
818
+ helloWorld
819
+ }"
820
+ `);
821
+ });
822
+
723
823
  test("errors when tool name contains spaces", async () => {
724
824
  vol.fromJSON({
725
825
  "package.json": mockPackageJson(),
@@ -839,6 +939,54 @@ describe("@tool validation", () => {
839
939
  `[Error: Error when parsing directive values: unexpected type 'FloatValue']`
840
940
  );
841
941
  });
942
+
943
+ test("errors when multiple @tool directives are used and one is missing a name", async () => {
944
+ vol.fromJSON({
945
+ "package.json": mockPackageJson(),
946
+ "src/my-component.tsx": declareOperation(gql`
947
+ query HelloWorldQuery
948
+ @tool(name: "tool-a", description: "Tool A")
949
+ @tool(description: "Tool B") {
950
+ helloWorld
951
+ }
952
+ `),
953
+ });
954
+
955
+ await expect(async () => {
956
+ await using server = await setupServer({
957
+ plugins: [
958
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
959
+ ],
960
+ });
961
+ await server.listen();
962
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
963
+ `[Error: Operations with multiple @tool directives must provide a 'name' argument on each @tool]`
964
+ );
965
+ });
966
+
967
+ test("errors when multiple @tool directives are used and one is missing a description", async () => {
968
+ vol.fromJSON({
969
+ "package.json": mockPackageJson(),
970
+ "src/my-component.tsx": declareOperation(gql`
971
+ query HelloWorldQuery
972
+ @tool(name: "tool-a", description: "Tool A")
973
+ @tool(name: "tool-b") {
974
+ helloWorld
975
+ }
976
+ `),
977
+ });
978
+
979
+ await expect(async () => {
980
+ await using server = await setupServer({
981
+ plugins: [
982
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
983
+ ],
984
+ });
985
+ await server.listen();
986
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
987
+ `[Error: Operations with multiple @tool directives must provide a 'description' argument on each @tool]`
988
+ );
989
+ });
842
990
  });
843
991
 
844
992
  describe("config validation", () => {
@@ -1473,6 +1621,71 @@ describe("entry points", () => {
1473
1621
  true
1474
1622
  );
1475
1623
  });
1624
+
1625
+ test("generates .application-manifest.d.json.ts at root in build mode", async () => {
1626
+ vol.fromJSON({
1627
+ "package.json": mockPackageJson(),
1628
+ "src/my-component.tsx": declareOperation(gql`
1629
+ query HelloWorldQuery
1630
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1631
+ helloWorld
1632
+ }
1633
+ `),
1634
+ });
1635
+
1636
+ await buildApp({
1637
+ mode: "production",
1638
+ plugins: [
1639
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1640
+ ],
1641
+ });
1642
+
1643
+ expect(vol.existsSync(".application-manifest.d.json.ts")).toBe(true);
1644
+ });
1645
+
1646
+ test("generates .application-manifest.d.json.ts at root in serve mode", async () => {
1647
+ vol.fromJSON({
1648
+ "package.json": mockPackageJson(),
1649
+ "src/my-component.tsx": declareOperation(gql`
1650
+ query HelloWorldQuery
1651
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1652
+ helloWorld
1653
+ }
1654
+ `),
1655
+ });
1656
+
1657
+ await using server = await setupServer({
1658
+ plugins: [
1659
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1660
+ ],
1661
+ });
1662
+ await server.listen();
1663
+
1664
+ expect(vol.existsSync(".application-manifest.d.json.ts")).toBe(true);
1665
+ });
1666
+
1667
+ test("does not write .application-manifest.json.d.ts to appsOutDir", async () => {
1668
+ vol.fromJSON({
1669
+ "package.json": mockPackageJson(),
1670
+ "src/my-component.tsx": declareOperation(gql`
1671
+ query HelloWorldQuery
1672
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1673
+ helloWorld
1674
+ }
1675
+ `),
1676
+ });
1677
+
1678
+ await buildApp({
1679
+ mode: "production",
1680
+ plugins: [
1681
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1682
+ ],
1683
+ });
1684
+
1685
+ expect(
1686
+ vol.existsSync("dist/apps/my-app/.application-manifest.json.d.ts")
1687
+ ).toBe(false);
1688
+ });
1476
1689
  });
1477
1690
 
1478
1691
  describe("appsOutDir", () => {
@@ -1695,6 +1908,541 @@ describe("file watching", () => {
1695
1908
  });
1696
1909
  });
1697
1910
 
1911
+ describe("tool input types", () => {
1912
+ const schema = `
1913
+ type Query {
1914
+ todo(id: ID!): Todo
1915
+ }
1916
+
1917
+ type Mutation {
1918
+ createTodo(title: String!, description: String): Todo
1919
+ deleteTodo(id: ID!): Boolean
1920
+ }
1921
+
1922
+ type Todo {
1923
+ id: ID!
1924
+ title: String!
1925
+ description: String
1926
+ }
1927
+ `;
1928
+
1929
+ test("generates operation-types.d.ts with variable types when schema is provided", async () => {
1930
+ vol.fromJSON({
1931
+ "package.json": mockPackageJson(),
1932
+ "src/my-component.tsx": declareOperation(gql`
1933
+ mutation CreateTodo($title: String!, $description: String)
1934
+ @tool(name: "CreateTodo", description: "Creates a todo") {
1935
+ createTodo(title: $title, description: $description) {
1936
+ id
1937
+ }
1938
+ }
1939
+ `),
1940
+ });
1941
+
1942
+ await using server = await setupServer({
1943
+ plugins: [
1944
+ apolloClientAiApps({
1945
+ targets: ["mcp"],
1946
+ appsOutDir: "dist/apps",
1947
+ schema,
1948
+ }),
1949
+ ],
1950
+ });
1951
+ await server.listen();
1952
+
1953
+ const content = fs.readFileSync(
1954
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
1955
+ "utf-8"
1956
+ );
1957
+ expect(content).toMatchInlineSnapshot(`
1958
+ "// Auto-generated by @apollo/client-ai-apps. Do not edit manually.
1959
+ export type Maybe<T> = T | null;
1960
+
1961
+ export type InputMaybe<T> = Maybe<T>;
1962
+
1963
+ export type Exact<T extends {
1964
+ [key: string]: unknown;
1965
+ }> = {
1966
+ [K in keyof T]: T[K];
1967
+ };
1968
+
1969
+ /** All built-in and custom scalars, mapped to their actual values */
1970
+ export type Scalars = {
1971
+ ID: {
1972
+ input: string;
1973
+ output: string;
1974
+ };
1975
+ String: {
1976
+ input: string;
1977
+ output: string;
1978
+ };
1979
+ Boolean: {
1980
+ input: boolean;
1981
+ output: boolean;
1982
+ };
1983
+ Int: {
1984
+ input: number;
1985
+ output: number;
1986
+ };
1987
+ Float: {
1988
+ input: number;
1989
+ output: number;
1990
+ };
1991
+ };
1992
+
1993
+ export type CreateTodoMutationVariables = Exact<{
1994
+ title: Scalars["String"]["input"];
1995
+ description?: InputMaybe<Scalars["String"]["input"]>;
1996
+ }>;"
1997
+ `);
1998
+ });
1999
+
2000
+ test("generates register.d.ts with toolInputs when schema is provided", async () => {
2001
+ vol.fromJSON({
2002
+ "package.json": mockPackageJson(),
2003
+ "src/my-component.tsx": declareOperation(gql`
2004
+ mutation CreateTodo($title: String!, $description: String)
2005
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2006
+ createTodo(title: $title, description: $description) {
2007
+ id
2008
+ }
2009
+ }
2010
+ `),
2011
+ });
2012
+
2013
+ await using server = await setupServer({
2014
+ plugins: [
2015
+ apolloClientAiApps({
2016
+ targets: ["mcp"],
2017
+ appsOutDir: "dist/apps",
2018
+ schema,
2019
+ }),
2020
+ ],
2021
+ });
2022
+ await server.listen();
2023
+
2024
+ const content = fs.readFileSync(
2025
+ ".apollo-client-ai-apps/types/register.d.ts",
2026
+ "utf-8"
2027
+ );
2028
+ expect(content).toMatchInlineSnapshot(`
2029
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2030
+ import "@apollo/client-ai-apps";
2031
+
2032
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2033
+
2034
+ declare module "@apollo/client-ai-apps" {
2035
+ interface Register {
2036
+ toolName: "CreateTodo";
2037
+ toolInputs: {
2038
+ CreateTodo: CreateTodoMutationVariables;
2039
+ };
2040
+ }
2041
+ }"
2042
+ `);
2043
+ });
2044
+
2045
+ test("generates register.d.ts with operations that contain fragments", async () => {
2046
+ vol.fromJSON({
2047
+ "package.json": mockPackageJson(),
2048
+ "src/my-component.tsx": declareOperation(gql`
2049
+ mutation CreateTodo($title: String!, $description: String)
2050
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2051
+ createTodo(title: $title, description: $description) {
2052
+ id
2053
+ ...TodoFragment
2054
+ }
2055
+ }
2056
+ `),
2057
+ "src/todo-fragment.tsx": declareFragment(gql`
2058
+ fragment TodoFragment on Todo {
2059
+ title
2060
+ }
2061
+ `),
2062
+ });
2063
+
2064
+ await using server = await setupServer({
2065
+ plugins: [
2066
+ apolloClientAiApps({
2067
+ targets: ["mcp"],
2068
+ appsOutDir: "dist/apps",
2069
+ schema,
2070
+ }),
2071
+ ],
2072
+ });
2073
+ await server.listen();
2074
+
2075
+ expect(
2076
+ fs.readFileSync(".apollo-client-ai-apps/types/register.d.ts", "utf-8")
2077
+ ).toMatchInlineSnapshot(`
2078
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2079
+ import "@apollo/client-ai-apps";
2080
+
2081
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2082
+
2083
+ declare module "@apollo/client-ai-apps" {
2084
+ interface Register {
2085
+ toolName: "CreateTodo";
2086
+ toolInputs: {
2087
+ CreateTodo: CreateTodoMutationVariables;
2088
+ };
2089
+ }
2090
+ }"
2091
+ `);
2092
+ expect(
2093
+ fs.readFileSync(
2094
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
2095
+ "utf-8"
2096
+ )
2097
+ ).toMatchInlineSnapshot(`
2098
+ "// Auto-generated by @apollo/client-ai-apps. Do not edit manually.
2099
+ export type Maybe<T> = T | null;
2100
+
2101
+ export type InputMaybe<T> = Maybe<T>;
2102
+
2103
+ export type Exact<T extends {
2104
+ [key: string]: unknown;
2105
+ }> = {
2106
+ [K in keyof T]: T[K];
2107
+ };
2108
+
2109
+ /** All built-in and custom scalars, mapped to their actual values */
2110
+ export type Scalars = {
2111
+ ID: {
2112
+ input: string;
2113
+ output: string;
2114
+ };
2115
+ String: {
2116
+ input: string;
2117
+ output: string;
2118
+ };
2119
+ Boolean: {
2120
+ input: boolean;
2121
+ output: boolean;
2122
+ };
2123
+ Int: {
2124
+ input: number;
2125
+ output: number;
2126
+ };
2127
+ Float: {
2128
+ input: number;
2129
+ output: number;
2130
+ };
2131
+ };
2132
+
2133
+ export type CreateTodoMutationVariables = Exact<{
2134
+ title: Scalars["String"]["input"];
2135
+ description?: InputMaybe<Scalars["String"]["input"]>;
2136
+ }>;"
2137
+ `);
2138
+ });
2139
+
2140
+ test("tool with no extraInputs uses only the Variables type", async () => {
2141
+ vol.fromJSON({
2142
+ "package.json": mockPackageJson(),
2143
+ "src/my-component.tsx": declareOperation(gql`
2144
+ mutation DeleteTodo($id: ID!)
2145
+ @tool(name: "DeleteTodo", description: "Deletes a todo") {
2146
+ deleteTodo(id: $id)
2147
+ }
2148
+ `),
2149
+ });
2150
+
2151
+ await using server = await setupServer({
2152
+ plugins: [
2153
+ apolloClientAiApps({
2154
+ targets: ["mcp"],
2155
+ appsOutDir: "dist/apps",
2156
+ schema,
2157
+ }),
2158
+ ],
2159
+ });
2160
+ await server.listen();
2161
+
2162
+ const content = fs.readFileSync(
2163
+ ".apollo-client-ai-apps/types/register.d.ts",
2164
+ "utf-8"
2165
+ );
2166
+ expect(content).toMatchInlineSnapshot(`
2167
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2168
+ import "@apollo/client-ai-apps";
2169
+
2170
+ import type { DeleteTodoMutationVariables } from "./operation-types.js";
2171
+
2172
+ declare module "@apollo/client-ai-apps" {
2173
+ interface Register {
2174
+ toolName: "DeleteTodo";
2175
+ toolInputs: {
2176
+ DeleteTodo: DeleteTodoMutationVariables;
2177
+ };
2178
+ }
2179
+ }"
2180
+ `);
2181
+ });
2182
+
2183
+ test("tool with extraInputs adds properties to tool input type", async () => {
2184
+ vol.fromJSON({
2185
+ "package.json": mockPackageJson(),
2186
+ "src/my-component.tsx": declareOperation(gql`
2187
+ mutation CreateTodo($title: String!)
2188
+ @tool(
2189
+ name: "CreateTodo"
2190
+ description: "Creates a todo"
2191
+ extraInputs: [
2192
+ { name: "priority", type: "string", description: "Priority" }
2193
+ { name: "urgent", type: "boolean", description: "Is urgent?" }
2194
+ {
2195
+ name: "odd-name"
2196
+ type: "boolean"
2197
+ description: "Test of odd name"
2198
+ }
2199
+ ]
2200
+ ) {
2201
+ createTodo(title: $title) {
2202
+ id
2203
+ }
2204
+ }
2205
+ `),
2206
+ });
2207
+
2208
+ await using server = await setupServer({
2209
+ plugins: [
2210
+ apolloClientAiApps({
2211
+ targets: ["mcp"],
2212
+ appsOutDir: "dist/apps",
2213
+ schema,
2214
+ }),
2215
+ ],
2216
+ });
2217
+ await server.listen();
2218
+
2219
+ const content = fs.readFileSync(
2220
+ ".apollo-client-ai-apps/types/register.d.ts",
2221
+ "utf-8"
2222
+ );
2223
+ expect(content).toMatchInlineSnapshot(`
2224
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2225
+ import "@apollo/client-ai-apps";
2226
+
2227
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2228
+
2229
+ declare module "@apollo/client-ai-apps" {
2230
+ interface Register {
2231
+ toolName: "CreateTodo";
2232
+ toolInputs: {
2233
+ CreateTodo: CreateTodoMutationVariables & {
2234
+ priority?: string;
2235
+ urgent?: boolean;
2236
+ "odd-name"?: boolean;
2237
+ };
2238
+ };
2239
+ }
2240
+ }"
2241
+ `);
2242
+ });
2243
+
2244
+ test("multiple @tool directives on an operation each get their own toolInputs entry", async () => {
2245
+ vol.fromJSON({
2246
+ "package.json": mockPackageJson(),
2247
+ "src/my-component.tsx": declareOperation(gql`
2248
+ mutation CreateTodo($title: String!)
2249
+ @tool(name: "CreateTodo", description: "Creates a todo")
2250
+ @tool(name: "AddTask", description: "Adds a task") {
2251
+ createTodo(title: $title) {
2252
+ id
2253
+ }
2254
+ }
2255
+ `),
2256
+ });
2257
+
2258
+ await using server = await setupServer({
2259
+ plugins: [
2260
+ apolloClientAiApps({
2261
+ targets: ["mcp"],
2262
+ appsOutDir: "dist/apps",
2263
+ schema,
2264
+ }),
2265
+ ],
2266
+ });
2267
+ await server.listen();
2268
+
2269
+ const content = fs.readFileSync(
2270
+ ".apollo-client-ai-apps/types/register.d.ts",
2271
+ "utf-8"
2272
+ );
2273
+ expect(content).toMatchInlineSnapshot(`
2274
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2275
+ import "@apollo/client-ai-apps";
2276
+
2277
+ import type { CreateTodoMutationVariables } from "./operation-types.js";
2278
+
2279
+ declare module "@apollo/client-ai-apps" {
2280
+ interface Register {
2281
+ toolName: "CreateTodo" | "AddTask";
2282
+ toolInputs: {
2283
+ CreateTodo: CreateTodoMutationVariables;
2284
+ AddTask: CreateTodoMutationVariables;
2285
+ };
2286
+ }
2287
+ }"
2288
+ `);
2289
+ });
2290
+
2291
+ test("operation-types.d.ts is not rewritten when operation content has not changed", async () => {
2292
+ vol.fromJSON({
2293
+ "package.json": mockPackageJson(),
2294
+ "src/my-component.tsx": declareOperation(gql`
2295
+ mutation CreateTodo($title: String!)
2296
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2297
+ createTodo(title: $title) {
2298
+ id
2299
+ }
2300
+ }
2301
+ `),
2302
+ });
2303
+
2304
+ await using server = await setupServer({
2305
+ plugins: [
2306
+ apolloClientAiApps({
2307
+ targets: ["mcp"],
2308
+ appsOutDir: "dist/apps",
2309
+ schema,
2310
+ }),
2311
+ ],
2312
+ });
2313
+ await server.listen();
2314
+
2315
+ const content = fs.readFileSync(
2316
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
2317
+ "utf-8"
2318
+ );
2319
+ const mtime = fs.statSync(
2320
+ ".apollo-client-ai-apps/types/operation-types.d.ts"
2321
+ ).mtimeMs;
2322
+
2323
+ // Trigger another manifest generation (simulating a file change to an unrelated file)
2324
+ server.watcher.emit("change", "package.json");
2325
+ await wait(100);
2326
+
2327
+ expect(
2328
+ fs.readFileSync(
2329
+ ".apollo-client-ai-apps/types/operation-types.d.ts",
2330
+ "utf-8"
2331
+ )
2332
+ ).toBe(content);
2333
+
2334
+ expect(
2335
+ fs.statSync(".apollo-client-ai-apps/types/operation-types.d.ts").mtimeMs
2336
+ ).toBe(mtime);
2337
+ });
2338
+
2339
+ test("does not generate operation-types.d.ts when schema is not provided", async () => {
2340
+ vol.fromJSON({
2341
+ "package.json": mockPackageJson(),
2342
+ "src/my-component.tsx": declareOperation(gql`
2343
+ mutation CreateTodo($title: String!)
2344
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2345
+ createTodo(title: $title) {
2346
+ id
2347
+ }
2348
+ }
2349
+ `),
2350
+ });
2351
+
2352
+ await using server = await setupServer({
2353
+ plugins: [
2354
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2355
+ ],
2356
+ });
2357
+ await server.listen();
2358
+
2359
+ expect(
2360
+ fs.existsSync(".apollo-client-ai-apps/types/operation-types.d.ts")
2361
+ ).toBe(false);
2362
+ });
2363
+
2364
+ test("generates register.d.ts with error message when schema build fails in dev mode", async () => {
2365
+ using _ = spyOnConsole("error");
2366
+
2367
+ vol.fromJSON({
2368
+ "package.json": mockPackageJson(),
2369
+ "src/my-component.tsx": declareOperation(gql`
2370
+ query GetNonExistent
2371
+ @tool(name: "GetNonExistent", description: "A tool") {
2372
+ nonExistentField
2373
+ }
2374
+ `),
2375
+ });
2376
+
2377
+ await using server = await setupServer({
2378
+ plugins: [
2379
+ apolloClientAiApps({
2380
+ targets: ["mcp"],
2381
+ appsOutDir: "dist/apps",
2382
+ schema,
2383
+ }),
2384
+ ],
2385
+ });
2386
+ await server.listen();
2387
+
2388
+ expect(console.error).toHaveBeenCalledOnce();
2389
+
2390
+ const content = fs.readFileSync(
2391
+ ".apollo-client-ai-apps/types/register.d.ts",
2392
+ "utf-8"
2393
+ );
2394
+ expect(content).toMatchInlineSnapshot(`
2395
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2396
+ import "@apollo/client-ai-apps";
2397
+
2398
+ declare module "@apollo/client-ai-apps" {
2399
+ interface Register {
2400
+ 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";
2401
+ toolInputs: {
2402
+ "[@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";
2403
+ };
2404
+ }
2405
+ }"
2406
+ `);
2407
+ });
2408
+
2409
+ test("generates register.d.ts without toolInputs when schema is not provided", async () => {
2410
+ vol.fromJSON({
2411
+ "package.json": mockPackageJson(),
2412
+ "src/my-component.tsx": declareOperation(gql`
2413
+ mutation CreateTodo($title: String!)
2414
+ @tool(name: "CreateTodo", description: "Creates a todo") {
2415
+ createTodo(title: $title) {
2416
+ id
2417
+ }
2418
+ }
2419
+ `),
2420
+ });
2421
+
2422
+ await using server = await setupServer({
2423
+ plugins: [
2424
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
2425
+ ],
2426
+ });
2427
+ await server.listen();
2428
+
2429
+ const content = fs.readFileSync(
2430
+ ".apollo-client-ai-apps/types/register.d.ts",
2431
+ "utf-8"
2432
+ );
2433
+ expect(content).toMatchInlineSnapshot(`
2434
+ "// This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.
2435
+ import "@apollo/client-ai-apps";
2436
+
2437
+ declare module "@apollo/client-ai-apps" {
2438
+ interface Register {
2439
+ toolName: "CreateTodo";
2440
+ }
2441
+ }"
2442
+ `);
2443
+ });
2444
+ });
2445
+
1698
2446
  describe("html transforms", () => {
1699
2447
  test("replaces root relative scripts with full url when origin is provided", async () => {
1700
2448
  vol.fromJSON({