@apollo/client-ai-apps 0.6.0 → 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 (85) hide show
  1. package/CHANGELOG.md +123 -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 +337 -41
  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 +744 -6
  84. package/src/vite/apolloClientAiApps.ts +552 -66
  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,100 @@ 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
+ });
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
+
723
813
  test("errors when tool name contains spaces", async () => {
724
814
  vol.fromJSON({
725
815
  "package.json": mockPackageJson(),
@@ -839,6 +929,54 @@ describe("@tool validation", () => {
839
929
  `[Error: Error when parsing directive values: unexpected type 'FloatValue']`
840
930
  );
841
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
+ });
842
980
  });
843
981
 
844
982
  describe("config validation", () => {
@@ -1473,6 +1611,71 @@ describe("entry points", () => {
1473
1611
  true
1474
1612
  );
1475
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
+ });
1476
1679
  });
1477
1680
 
1478
1681
  describe("appsOutDir", () => {
@@ -1695,6 +1898,541 @@ describe("file watching", () => {
1695
1898
  });
1696
1899
  });
1697
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
+
1698
2436
  describe("html transforms", () => {
1699
2437
  test("replaces root relative scripts with full url when origin is provided", async () => {
1700
2438
  vol.fromJSON({