@apollo/client-ai-apps 0.6.3 → 0.6.5

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 (36) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/core/types.d.ts +2 -1
  3. package/dist/core/types.d.ts.map +1 -1
  4. package/dist/core/types.js.map +1 -1
  5. package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
  6. package/dist/mcp/core/ApolloClient.js +11 -8
  7. package/dist/mcp/core/ApolloClient.js.map +1 -1
  8. package/dist/mcp/core/McpAppManager.d.ts +11 -3
  9. package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
  10. package/dist/mcp/core/McpAppManager.js +8 -2
  11. package/dist/mcp/core/McpAppManager.js.map +1 -1
  12. package/dist/openai/core/ApolloClient.d.ts.map +1 -1
  13. package/dist/openai/core/ApolloClient.js +11 -8
  14. package/dist/openai/core/ApolloClient.js.map +1 -1
  15. package/dist/openai/core/McpAppManager.d.ts +12 -4
  16. package/dist/openai/core/McpAppManager.d.ts.map +1 -1
  17. package/dist/openai/core/McpAppManager.js +9 -3
  18. package/dist/openai/core/McpAppManager.js.map +1 -1
  19. package/dist/types/application-manifest.d.ts +1 -0
  20. package/dist/types/application-manifest.d.ts.map +1 -1
  21. package/dist/types/application-manifest.js.map +1 -1
  22. package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
  23. package/dist/vite/apolloClientAiApps.js +3 -7
  24. package/dist/vite/apolloClientAiApps.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/core/types.ts +2 -1
  27. package/src/mcp/core/ApolloClient.ts +13 -8
  28. package/src/mcp/core/McpAppManager.ts +9 -2
  29. package/src/mcp/core/__tests__/ApolloClient.test.ts +242 -0
  30. package/src/mcp/link/__tests__/ToolCallLink.test.ts +51 -0
  31. package/src/openai/core/ApolloClient.ts +13 -8
  32. package/src/openai/core/McpAppManager.ts +12 -3
  33. package/src/openai/core/__tests__/ApolloClient.test.ts +303 -0
  34. package/src/types/application-manifest.ts +1 -0
  35. package/src/vite/__tests__/apolloClientAiApps.test.ts +56 -0
  36. package/src/vite/apolloClientAiApps.ts +6 -7
@@ -11,6 +11,57 @@ import {
11
11
  } from "../../../testing/internal/index.js";
12
12
  import { ToolCallLink } from "../ToolCallLink.js";
13
13
 
14
+ test("merges _meta.structuredContent into result for @private fields", async () => {
15
+ using _ = spyOnConsole("debug");
16
+ const query = gql`
17
+ query GreetingQuery {
18
+ greeting @private
19
+ }
20
+ `;
21
+
22
+ const client = new ApolloClient({
23
+ cache: new InMemoryCache(),
24
+ manifest: mockApplicationManifest(),
25
+ });
26
+
27
+ using host = await mockMcpHost({
28
+ hostContext: minimalHostContextWithToolName("GetProduct"),
29
+ });
30
+ host.onCleanup(() => client.stop());
31
+
32
+ host.sendToolInput({ arguments: {} });
33
+ host.sendToolResult({
34
+ _meta: { toolName: "GetProduct" },
35
+ content: [],
36
+ structuredContent: {
37
+ result: {
38
+ data: {
39
+ product: null,
40
+ },
41
+ },
42
+ },
43
+ });
44
+
45
+ host.mockToolCall("execute", () => ({
46
+ content: [],
47
+ structuredContent: {},
48
+ _meta: {
49
+ structuredContent: { data: { greeting: "Hello, private world" } },
50
+ },
51
+ }));
52
+
53
+ await client.connect();
54
+
55
+ const observable = execute(new ToolCallLink(), { query }, { client });
56
+ const stream = new ObservableStream(observable);
57
+
58
+ await expect(stream).toEmitValue({
59
+ data: { greeting: "Hello, private world" },
60
+ });
61
+
62
+ await expect(stream).toComplete();
63
+ });
64
+
14
65
  test("delegates query execution to MCP host", async () => {
15
66
  using _ = spyOnConsole("debug");
16
67
  const query = gql`
@@ -126,25 +126,30 @@ export class ApolloClient extends BaseApolloClient {
126
126
  }
127
127
 
128
128
  connect = cacheAsync(async () => {
129
- const { prefetch, result, toolName, args } =
129
+ const { structuredContent, toolName, args } =
130
130
  await this.appManager.connect();
131
131
 
132
132
  this.#toolInput = args;
133
133
 
134
134
  this.manifest.operations.forEach((operation) => {
135
- if (operation.prefetchID && prefetch?.[operation.prefetchID]) {
135
+ if (
136
+ operation.prefetchID &&
137
+ structuredContent.prefetch?.[operation.prefetchID]
138
+ ) {
136
139
  this.writeQuery({
137
140
  query: parse(operation.body),
138
- data: prefetch[operation.prefetchID].data,
141
+ data: structuredContent.prefetch[operation.prefetchID].data,
139
142
  });
140
143
  }
141
144
 
142
145
  if (operation.tools.find((tool) => tool.name === toolName)) {
143
- this.writeQuery({
144
- query: parse(operation.body),
145
- data: result.data,
146
- variables: getVariablesForOperationFromToolInput(operation, args),
147
- });
146
+ if (structuredContent.result?.data) {
147
+ this.writeQuery({
148
+ query: parse(operation.body),
149
+ data: structuredContent.result.data,
150
+ variables: getVariablesForOperationFromToolInput(operation, args),
151
+ });
152
+ }
148
153
  }
149
154
  });
150
155
  });
@@ -13,6 +13,7 @@ import type { ApolloMcpServerApps } from "../../core/types";
13
13
 
14
14
  type ExecuteQueryCallToolResult = Omit<CallToolResult, "structuredContent"> & {
15
15
  structuredContent: FormattedExecutionResult;
16
+ _meta?: { structuredContent?: FormattedExecutionResult };
16
17
  };
17
18
 
18
19
  /** @internal */
@@ -100,9 +101,14 @@ export class McpAppManager {
100
101
  this.#toolMetadata = window.openai.toolResponseMetadata;
101
102
 
102
103
  return {
103
- ...structuredContent,
104
+ structuredContent: {
105
+ ...structuredContent,
106
+ ...(
107
+ window.openai.toolResponseMetadata as ApolloMcpServerApps.Meta | null
108
+ )?.structuredContent,
109
+ },
104
110
  toolName: this.toolName,
105
- args: this.toolInput,
111
+ args: this.#toolInput,
106
112
  };
107
113
  });
108
114
 
@@ -122,7 +128,10 @@ export class McpAppManager {
122
128
  arguments: { query: print(query), variables },
123
129
  })) as ExecuteQueryCallToolResult;
124
130
 
125
- return result.structuredContent;
131
+ return {
132
+ ...result.structuredContent,
133
+ ...result._meta?.structuredContent,
134
+ };
126
135
  }
127
136
 
128
137
  private async connectToHost() {
@@ -2,6 +2,7 @@ import { expect, test, describe, vi } from "vitest";
2
2
  import { ApolloClient } from "../ApolloClient.js";
3
3
  import { parse } from "graphql";
4
4
  import { ApolloLink, HttpLink, InMemoryCache, gql } from "@apollo/client";
5
+ import { print } from "@apollo/client/utilities";
5
6
  import { ToolCallLink } from "../../link/ToolCallLink.js";
6
7
  import {
7
8
  graphqlToolResult,
@@ -91,6 +92,62 @@ describe("Client Basics", () => {
91
92
  });
92
93
  });
93
94
 
95
+ test("merges _meta.structuredContent into result for @private fields", async () => {
96
+ stubOpenAiGlobals();
97
+ using _ = spyOnConsole("debug");
98
+ const manifest = mockApplicationManifest();
99
+ const client = new ApolloClient({
100
+ cache: new InMemoryCache(),
101
+ manifest,
102
+ });
103
+ using host = await mockMcpHost();
104
+
105
+ const query = gql`
106
+ query Product {
107
+ id
108
+ title @private
109
+ }
110
+ `;
111
+
112
+ host.onCleanup(() => client.stop());
113
+
114
+ host.sendToolInput({});
115
+ host.sendToolResult({
116
+ content: [],
117
+ structuredContent: {},
118
+ });
119
+
120
+ host.mockToolCall("execute", () => ({
121
+ content: [],
122
+ structuredContent: {},
123
+ _meta: {
124
+ structuredContent: {
125
+ data: {
126
+ product: {
127
+ id: "1",
128
+ title: "Private Pen",
129
+ __typename: "Product",
130
+ },
131
+ },
132
+ },
133
+ },
134
+ }));
135
+
136
+ await client.connect();
137
+
138
+ await expect(
139
+ client.query({ query, variables: { id: "1" } })
140
+ ).resolves.toStrictEqual({
141
+ data: {
142
+ product: {
143
+ __typename: "Product",
144
+ id: "1",
145
+ title: "Private Pen",
146
+ },
147
+ },
148
+ });
149
+ });
150
+
94
151
  describe("prefetchData", () => {
95
152
  test("caches tool response when data is provided", async () => {
96
153
  stubOpenAiGlobals({ toolInput: { id: 1 } });
@@ -400,6 +457,252 @@ describe("prefetchData", () => {
400
457
  });
401
458
  });
402
459
 
460
+ test("reads result data from toolResponseMetadata.structuredContent", async () => {
461
+ stubOpenAiGlobals({
462
+ toolInput: { id: "1" },
463
+ toolResponseMetadata: {
464
+ structuredContent: {
465
+ result: {
466
+ data: {
467
+ product: { id: "1", title: "Pen", __typename: "Product" },
468
+ },
469
+ },
470
+ },
471
+ },
472
+ });
473
+ using _ = spyOnConsole("debug");
474
+
475
+ const query = gql`
476
+ query Product($id: ID!) {
477
+ product(id: $id) @private {
478
+ id
479
+ title
480
+ __typename
481
+ }
482
+ }
483
+ `;
484
+
485
+ const client = new ApolloClient({
486
+ cache: new InMemoryCache(),
487
+ manifest: mockApplicationManifest({
488
+ operations: [
489
+ {
490
+ id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
491
+ name: "Product",
492
+ type: "query",
493
+ body: print(query),
494
+ variables: { id: "ID" },
495
+ prefetch: false,
496
+ tools: [{ name: "GetProduct", description: "Get a product" }],
497
+ },
498
+ ],
499
+ }),
500
+ });
501
+ using host = await mockMcpHost({
502
+ hostContext: minimalHostContextWithToolName("GetProduct"),
503
+ });
504
+ host.onCleanup(() => client.stop());
505
+
506
+ host.sendToolInput({ arguments: { id: "1" } });
507
+ host.sendToolResult({
508
+ content: [],
509
+ structuredContent: {},
510
+ });
511
+
512
+ await client.connect();
513
+
514
+ expect(client.extract()).toEqual({
515
+ "Product:1": {
516
+ __typename: "Product",
517
+ id: "1",
518
+ title: "Pen",
519
+ },
520
+ ROOT_QUERY: {
521
+ __typename: "Query",
522
+ 'product({"id":"1"})@private': { __ref: "Product:1" },
523
+ },
524
+ });
525
+ });
526
+
527
+ test("merges prefetch from structuredContent and result from toolResponseMetadata.structuredContent", async () => {
528
+ stubOpenAiGlobals({
529
+ toolInput: { id: "2" },
530
+ toolResponseMetadata: {
531
+ structuredContent: {
532
+ result: {
533
+ data: {
534
+ product: { id: "2", title: "iPad", __typename: "Product" },
535
+ },
536
+ },
537
+ },
538
+ },
539
+ });
540
+ using _ = spyOnConsole("debug");
541
+
542
+ const prefetchQuery = gql`
543
+ query TopProducts {
544
+ topProducts {
545
+ id
546
+ title
547
+ __typename
548
+ }
549
+ }
550
+ `;
551
+
552
+ const query = gql`
553
+ query Product($id: ID!) {
554
+ product(id: $id) @private {
555
+ id
556
+ title
557
+ __typename
558
+ }
559
+ }
560
+ `;
561
+
562
+ const client = new ApolloClient({
563
+ cache: new InMemoryCache(),
564
+ manifest: mockApplicationManifest({
565
+ operations: [
566
+ {
567
+ id: "1",
568
+ name: "TopProducts",
569
+ body: print(prefetchQuery),
570
+ type: "query",
571
+ variables: {},
572
+ prefetch: true,
573
+ prefetchID: "__anonymous",
574
+ tools: [
575
+ {
576
+ name: "TopProducts",
577
+ description: "Shows the currently highest rated products.",
578
+ },
579
+ ],
580
+ },
581
+ {
582
+ id: "2",
583
+ name: "Product",
584
+ body: print(query),
585
+ type: "query",
586
+ variables: { id: "ID" },
587
+ prefetch: false,
588
+ tools: [{ name: "GetProduct", description: "Get a product" }],
589
+ },
590
+ ],
591
+ }),
592
+ });
593
+ using host = await mockMcpHost({
594
+ hostContext: minimalHostContextWithToolName("GetProduct"),
595
+ });
596
+ host.onCleanup(() => client.stop());
597
+
598
+ host.sendToolInput({ arguments: { id: "2" } });
599
+ host.sendToolResult({
600
+ content: [],
601
+ structuredContent: {
602
+ prefetch: {
603
+ __anonymous: {
604
+ data: {
605
+ topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
606
+ },
607
+ },
608
+ },
609
+ },
610
+ });
611
+
612
+ await client.connect();
613
+
614
+ expect(client.extract()).toEqual({
615
+ "Product:1": {
616
+ __typename: "Product",
617
+ id: "1",
618
+ title: "iPhone",
619
+ },
620
+ "Product:2": {
621
+ __typename: "Product",
622
+ id: "2",
623
+ title: "iPad",
624
+ },
625
+ ROOT_QUERY: {
626
+ __typename: "Query",
627
+ topProducts: [{ __ref: "Product:1" }],
628
+ 'product({"id":"2"})@private': { __ref: "Product:2" },
629
+ },
630
+ });
631
+ });
632
+
633
+ test("toolResponseMetadata.structuredContent wins over structuredContent", async () => {
634
+ stubOpenAiGlobals({
635
+ toolInput: { id: "1" },
636
+ toolResponseMetadata: {
637
+ structuredContent: {
638
+ result: {
639
+ data: {
640
+ product: { id: "1", title: "Meta title", __typename: "Product" },
641
+ },
642
+ },
643
+ },
644
+ },
645
+ });
646
+ using _ = spyOnConsole("debug");
647
+
648
+ const query = gql`
649
+ query Product($id: ID!) {
650
+ product(id: $id) {
651
+ id
652
+ title @private
653
+ __typename
654
+ }
655
+ }
656
+ `;
657
+
658
+ const client = new ApolloClient({
659
+ cache: new InMemoryCache(),
660
+ manifest: mockApplicationManifest({
661
+ operations: [
662
+ {
663
+ id: "1",
664
+ name: "Product",
665
+ body: print(query),
666
+ type: "query",
667
+ variables: { id: "ID" },
668
+ prefetch: false,
669
+ tools: [{ name: "GetProduct", description: "Get a product" }],
670
+ },
671
+ ],
672
+ }),
673
+ });
674
+ using host = await mockMcpHost({
675
+ hostContext: minimalHostContextWithToolName("GetProduct"),
676
+ });
677
+ host.onCleanup(() => client.stop());
678
+
679
+ host.sendToolInput({ arguments: { id: "1" } });
680
+ host.sendToolResult({
681
+ content: [],
682
+ structuredContent: {
683
+ result: {
684
+ data: {
685
+ product: { id: "1", __typename: "Product" },
686
+ },
687
+ },
688
+ },
689
+ });
690
+
691
+ await client.connect();
692
+
693
+ expect(client.extract()).toEqual({
694
+ "Product:1": {
695
+ __typename: "Product",
696
+ id: "1",
697
+ "title@private": "Meta title",
698
+ },
699
+ ROOT_QUERY: {
700
+ __typename: "Query",
701
+ 'product({"id":"1"})': { __ref: "Product:1" },
702
+ },
703
+ });
704
+ });
705
+
403
706
  test("connects using window.openai.toolOutput when tool-result notification is not sent", async () => {
404
707
  stubOpenAiGlobals({
405
708
  toolOutput: {
@@ -32,6 +32,7 @@ export type ManifestTool = {
32
32
  name: string;
33
33
  description: string;
34
34
  extraInputs?: ManifestExtraInput[];
35
+ extraOutputs?: Record<string, unknown>;
35
36
  labels?: ManifestLabels;
36
37
  };
37
38
 
@@ -143,6 +143,35 @@ describe("operations", () => {
143
143
  `);
144
144
  });
145
145
 
146
+ test("writes extraOutputs to the manifest", async () => {
147
+ vol.fromJSON({
148
+ "package.json": mockPackageJson(),
149
+ "src/my-component.tsx": declareOperation(gql`
150
+ query HelloWorldQuery
151
+ @tool(
152
+ name: "hello-world"
153
+ description: "This is an awesome tool!"
154
+ extraOutputs: { foo: "bar", nested: { label: "count" } }
155
+ ) {
156
+ helloWorld
157
+ }
158
+ `),
159
+ });
160
+
161
+ await using server = await setupServer({
162
+ plugins: [
163
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
164
+ ],
165
+ });
166
+ await server.listen();
167
+
168
+ const manifest = readManifestFile();
169
+ expect(manifest.operations[0].tools[0].extraOutputs).toEqual({
170
+ foo: "bar",
171
+ nested: { label: "count" },
172
+ });
173
+ });
174
+
146
175
  test("handles operations with fragments in the same file", async () => {
147
176
  vol.fromJSON({
148
177
  "package.json": mockPackageJson(),
@@ -890,6 +919,33 @@ describe("@tool validation", () => {
890
919
  );
891
920
  });
892
921
 
922
+ test("errors when extraOutputs is not an object", async () => {
923
+ vol.fromJSON({
924
+ "package.json": mockPackageJson(),
925
+ "src/my-component.tsx": declareOperation(gql`
926
+ query HelloWorldQuery
927
+ @tool(
928
+ name: "hello-world"
929
+ description: "hello"
930
+ extraOutputs: [1, 2, 3]
931
+ ) {
932
+ helloWorld
933
+ }
934
+ `),
935
+ });
936
+
937
+ await expect(async () => {
938
+ await using server = await setupServer({
939
+ plugins: [
940
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
941
+ ],
942
+ });
943
+ await server.listen();
944
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
945
+ `[Error: Expected argument 'extraOutputs' to be of type 'ObjectValue' but found 'ListValue' instead.]`
946
+ );
947
+ });
948
+
893
949
  test("errors when extraInputs is not an array", async () => {
894
950
  vol.fromJSON({
895
951
  "package.json": mockPackageJson(),
@@ -690,10 +690,7 @@ export function apolloClientAiApps(
690
690
  await processFile(fullPath);
691
691
  }
692
692
 
693
- // We don't want to do this here on builds cause it just gets overwritten anyways. We'll call it on writeBundle instead.
694
- if (config.command === "serve") {
695
- await Promise.all([generateManifest(), generateTypesFiles()]);
696
- }
693
+ await Promise.all([generateManifest(), generateTypesFiles()]);
697
694
  },
698
695
  configResolved(resolvedConfig) {
699
696
  config = resolvedConfig;
@@ -797,9 +794,6 @@ export function apolloClientAiApps(
797
794
  .replace(/(src=["'])\/([^"']+)/gi, `$1${baseUrl}/$2`)
798
795
  );
799
796
  },
800
- async writeBundle() {
801
- await Promise.all([generateManifest(), generateTypesFiles()]);
802
- },
803
797
  } satisfies Plugin;
804
798
  }
805
799
 
@@ -880,6 +874,10 @@ const processQueryLink = new ApolloLink((operation) => {
880
874
  getDirectiveArgument("extraInputs", directive),
881
875
  Kind.LIST
882
876
  ),
877
+ extraOutputs: maybeGetArgumentValue(
878
+ getDirectiveArgument("extraOutputs", directive),
879
+ Kind.OBJECT
880
+ ),
883
881
  labels: maybeGetArgumentValue(
884
882
  getDirectiveArgument("labels", directive),
885
883
  Kind.OBJECT
@@ -1050,5 +1048,6 @@ const ToolDirectiveSchema = z.strictObject({
1050
1048
  })
1051
1049
  )
1052
1050
  ),
1051
+ extraOutputs: z.optional(z.record(z.string(), z.unknown())),
1053
1052
  labels: ApolloClientAiAppsConfigSchema.shape.labels.optional(),
1054
1053
  });