From 6a0ad63e570d5d5f828e572c6d2442cf680d8a8f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:02:19 -0700 Subject: [PATCH 01/11] Tweak formatting in test --- .../__tests__/application_manifest_plugin.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/vite/__tests__/application_manifest_plugin.test.ts b/src/vite/__tests__/application_manifest_plugin.test.ts index ecc39c1d..5ac1e246 100644 --- a/src/vite/__tests__/application_manifest_plugin.test.ts +++ b/src/vite/__tests__/application_manifest_plugin.test.ts @@ -50,11 +50,15 @@ describe("buildStart", () => { } satisfies ManifestWidgetSettings, }), [`${root}/my-component.tsx`]: ` -const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{ - name: "doStuff", - type: "boolean", - description: "Should we do stuff?" -}]) { helloWorld(name: $name) }\`; +const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool( + name: "hello-world", + description: "This is an awesome tool!", + extraInputs: [{ + name: "doStuff", + type: "boolean", + description: "Should we do stuff?" + }] +) { helloWorld(name: $name) }\`; `, }); From bc5f09d696f7b26cb328c56581a12e9c9e805644 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:07:54 -0700 Subject: [PATCH 02/11] Add ManifestLabels type --- src/index.ts | 1 + src/types/application-manifest.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/index.ts b/src/index.ts index bc651075..e93431b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ export type { ManifestTool, ManifestExtraInput, ManifestCsp, + ManifestLabels, } from "./types/application-manifest.js"; export { ToolUseProvider } from "./react/context/ToolUseContext.js"; diff --git a/src/types/application-manifest.ts b/src/types/application-manifest.ts index 8c46475a..483f6067 100644 --- a/src/types/application-manifest.ts +++ b/src/types/application-manifest.ts @@ -8,6 +8,7 @@ export type ApplicationManifest = { operations: ManifestOperation[]; csp: ManifestCsp; widgetSettings?: ManifestWidgetSettings; + labels?: ManifestLabels; }; export type ManifestOperation = { @@ -19,6 +20,7 @@ export type ManifestOperation = { prefetch: boolean; prefetchID?: string; tools: ManifestTool[]; + labels?: ManifestLabels; }; export type ManifestTool = { @@ -43,3 +45,8 @@ export type ManifestCsp = { connectDomains: string[]; resourceDomains: string[]; }; + +export type ManifestLabels = { + "toolInvocation/invoking"?: string; + "toolInvocation/invoked"?: string; +}; From 3d310e3bbf87763d128b119101065c1df890bbae Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:08:20 -0700 Subject: [PATCH 03/11] Add missing export type --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index e93431b6..29e2ecc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export type { ManifestExtraInput, ManifestCsp, ManifestLabels, + ManifestWidgetSettings, } from "./types/application-manifest.js"; export { ToolUseProvider } from "./react/context/ToolUseContext.js"; From e05a3acbe34ad108dc141bc6c247d3ebee94c518 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:38:21 -0700 Subject: [PATCH 04/11] Pull labels from package.json --- .../application_manifest_plugin.test.ts | 10 +++ src/vite/application_manifest_plugin.ts | 76 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/vite/__tests__/application_manifest_plugin.test.ts b/src/vite/__tests__/application_manifest_plugin.test.ts index 5ac1e246..7fc200f2 100644 --- a/src/vite/__tests__/application_manifest_plugin.test.ts +++ b/src/vite/__tests__/application_manifest_plugin.test.ts @@ -43,6 +43,12 @@ describe("buildStart", () => { test("Should write to dev application manifest file when using a serve command", async () => { mockReadFile({ "package.json": JSON.stringify({ + labels: { + toolInvocation: { + invoking: "Testing global...", + invoked: "Tested global!", + }, + }, widgetSettings: { description: "Test", domain: "https://example.com", @@ -90,6 +96,10 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool( }, "format": "apollo-ai-app-manifest", "hash": "abc", + "labels": { + "toolInvocation/invoked": "Tested global!", + "toolInvocation/invoking": "Testing global...", + }, "operations": [ { "body": "query HelloWorldQuery($name: string!) { diff --git a/src/vite/application_manifest_plugin.ts b/src/vite/application_manifest_plugin.ts index 3a1c7c47..84dd483b 100644 --- a/src/vite/application_manifest_plugin.ts +++ b/src/vite/application_manifest_plugin.ts @@ -21,6 +21,7 @@ import path from "path"; import type { ApplicationManifest, ManifestExtraInput, + ManifestLabels, ManifestTool, ManifestWidgetSettings, } from "../types/application-manifest.js"; @@ -325,6 +326,14 @@ export const ApplicationManifestPlugin = () => { manifest.widgetSettings = packageJson.widgetSettings; } + if (packageJson.labels) { + const labels = getLabelsFromConfig(packageJson.labels); + + if (labels) { + manifest.labels = labels; + } + } + // Always write to build directory so the MCP server picks it up const dest = path.resolve( root, @@ -429,6 +438,40 @@ export function sortTopLevelDefinitions(query: DocumentNode): DocumentNode { }; } +interface LabelConfig { + toolInvocation?: { + invoking?: string; + invoked?: string; + }; +} + +function getLabelsFromConfig(config: LabelConfig): ManifestLabels | undefined { + if (!("toolInvocation" in config)) { + return; + } + + const { toolInvocation } = config; + const labels: ManifestLabels = {}; + + if (Object.hasOwn(toolInvocation, "invoking")) { + validateType(toolInvocation.invoking, "string", { + propertyName: "labels.toolInvocation.invoking", + }); + + labels["toolInvocation/invoking"] = toolInvocation.invoking; + } + + if (Object.hasOwn(toolInvocation, "invoked")) { + validateType(toolInvocation.invoked, "string", { + propertyName: "labels.toolInvocation.invoked", + }); + + labels["toolInvocation/invoked"] = toolInvocation.invoked; + } + + return labels; +} + function removeClientDirective(doc: DocumentNode) { return removeDirectivesFromDocument( [{ name: "prefetch" }, { name: "tool" }], @@ -441,3 +484,36 @@ function invariant(condition: any, message: string): asserts condition { throw new Error(message); } } + +// possible values of `typeof` +type TypeofResult = + | "string" + | "number" + | "bigint" + | "boolean" + | "symbol" + | "undefined" + | "object" + | "function"; + +type TypeofResultToConcreteType = + T extends "string" ? string + : T extends "number" ? number + : T extends "bigint" ? bigint + : T extends "boolean" ? boolean + : T extends "symbol" ? symbol + : T extends "undefined" ? undefined + : T extends "object" ? object + : T extends "function" ? Function + : never; + +function validateType( + value: unknown, + expectedType: Typeof, + options: { propertyName: string } +): asserts value is TypeofResultToConcreteType { + invariant( + typeof value === expectedType, + `Expected '${options.propertyName}' to be of type '${expectedType}' but found '${typeof value}' instead.` + ); +} From 931bedffae969f0a51586f300da4b09ca2fe004f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:41:42 -0700 Subject: [PATCH 05/11] Move labels to right spot for tools --- src/types/application-manifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/application-manifest.ts b/src/types/application-manifest.ts index 483f6067..ed588f7d 100644 --- a/src/types/application-manifest.ts +++ b/src/types/application-manifest.ts @@ -20,13 +20,13 @@ export type ManifestOperation = { prefetch: boolean; prefetchID?: string; tools: ManifestTool[]; - labels?: ManifestLabels; }; export type ManifestTool = { name: string; description: string; extraInputs?: ManifestExtraInput[]; + labels?: ManifestLabels; }; export type ManifestWidgetSettings = { From dc9c2af92f318a26cbb5a56b30b1ec9a099d33fb Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:43:06 -0700 Subject: [PATCH 06/11] Add labels to tool config --- .../__tests__/application_manifest_plugin.test.ts | 12 +++++++++++- src/vite/application_manifest_plugin.ts | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/vite/__tests__/application_manifest_plugin.test.ts b/src/vite/__tests__/application_manifest_plugin.test.ts index 7fc200f2..7a62a747 100644 --- a/src/vite/__tests__/application_manifest_plugin.test.ts +++ b/src/vite/__tests__/application_manifest_plugin.test.ts @@ -63,7 +63,13 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool( name: "doStuff", type: "boolean", description: "Should we do stuff?" - }] + }], + labels: { + toolInvocation: { + invoking: "Testing tool...", + invoked: "Tested tool!" + } + } ) { helloWorld(name: $name) }\`; `, }); @@ -118,6 +124,10 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool( "type": "boolean", }, ], + "labels": { + "toolInvocation/invoked": "Tested tool!", + "toolInvocation/invoking": "Testing tool...", + }, "name": "hello-world", }, ], diff --git a/src/vite/application_manifest_plugin.ts b/src/vite/application_manifest_plugin.ts index 84dd483b..ceeb2db7 100644 --- a/src/vite/application_manifest_plugin.ts +++ b/src/vite/application_manifest_plugin.ts @@ -181,6 +181,8 @@ export const ApplicationManifestPlugin = () => { directive ); + const labelsNode = getDirectiveArgument("labels", directive); + const toolOptions: ManifestTool = { name, description, @@ -193,6 +195,16 @@ export const ApplicationManifestPlugin = () => { ) as ManifestExtraInput[]; } + if (labelsNode) { + const labels = getLabelsFromConfig( + getArgumentValue(labelsNode, Kind.OBJECT) + ); + + if (labels) { + toolOptions.labels = labels; + } + } + return toolOptions; }); From 0166d39bce6c421e5b4167c9a7b3cd919b7beeac Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:47:50 -0700 Subject: [PATCH 07/11] Add tests --- .../application_manifest_plugin.test.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/vite/__tests__/application_manifest_plugin.test.ts b/src/vite/__tests__/application_manifest_plugin.test.ts index 7a62a747..bd61919f 100644 --- a/src/vite/__tests__/application_manifest_plugin.test.ts +++ b/src/vite/__tests__/application_manifest_plugin.test.ts @@ -750,6 +750,131 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool( await expect(plugin.buildStart()).resolves.toBeUndefined(); }); + test("Should error when labels.toolInvocation.invoking in package.json is not a string", async () => { + mockReadFile({ + "package.json": JSON.stringify({ + labels: { + toolInvocation: { + invoking: true, + }, + }, + }), + "my-component.tsx": ` + const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`; + `, + }); + vi.spyOn(glob, "glob").mockImplementation(() => + Promise.resolve(["my-component.tsx"]) + ); + vi.spyOn(path, "resolve").mockImplementation((_, file) => file); + vi.spyOn(fs, "writeFileSync"); + + const plugin = ApplicationManifestPlugin(); + plugin.configResolved({ command: "serve", server: {} }); + + await expect( + async () => await plugin.buildStart() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Expected 'labels.toolInvocation.invoking' to be of type 'string' but found 'boolean' instead.]` + ); + }); + + test("Should error when labels.toolInvocation.invoking in @tool is not a string", async () => { + mockReadFile({ + "package.json": JSON.stringify({}), + "my-component.tsx": ` + const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test", labels: { toolInvocation: { invoking: true } }) { helloWorld }\`; + `, + }); + vi.spyOn(glob, "glob").mockImplementation(() => + Promise.resolve(["my-component.tsx"]) + ); + vi.spyOn(path, "resolve").mockImplementation((_, file) => file); + vi.spyOn(fs, "writeFileSync"); + + const plugin = ApplicationManifestPlugin(); + plugin.configResolved({ command: "serve", server: {} }); + + await expect( + async () => await plugin.buildStart() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Expected 'labels.toolInvocation.invoking' to be of type 'string' but found 'boolean' instead.]` + ); + }); + + test("Should error when labels.toolInvocation.invoked in package.json is not a string", async () => { + mockReadFile({ + "package.json": JSON.stringify({ + labels: { + toolInvocation: { + invoked: true, + }, + }, + }), + "my-component.tsx": ` + const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`; + `, + }); + vi.spyOn(glob, "glob").mockImplementation(() => + Promise.resolve(["my-component.tsx"]) + ); + vi.spyOn(path, "resolve").mockImplementation((_, file) => file); + vi.spyOn(fs, "writeFileSync"); + + const plugin = ApplicationManifestPlugin(); + plugin.configResolved({ command: "serve", server: {} }); + + await expect( + async () => await plugin.buildStart() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Expected 'labels.toolInvocation.invoked' to be of type 'string' but found 'boolean' instead.]` + ); + }); + + test("Should error when labels.toolInvocation.invoked in @tool is not a string", async () => { + mockReadFile({ + "package.json": JSON.stringify({}), + "my-component.tsx": ` + const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test", labels: { toolInvocation: { invoked: true } }) { helloWorld }\`; + `, + }); + vi.spyOn(glob, "glob").mockImplementation(() => + Promise.resolve(["my-component.tsx"]) + ); + vi.spyOn(path, "resolve").mockImplementation((_, file) => file); + vi.spyOn(fs, "writeFileSync"); + + const plugin = ApplicationManifestPlugin(); + plugin.configResolved({ command: "serve", server: {} }); + + await expect( + async () => await plugin.buildStart() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Expected 'labels.toolInvocation.invoked' to be of type 'string' but found 'boolean' instead.]` + ); + }); + + test("Should allow empty labels value", async () => { + mockReadFile({ + "package.json": JSON.stringify({ + labels: {}, + }), + "my-component.tsx": ` + const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test", labels: {}) { helloWorld }\`; + `, + }); + vi.spyOn(glob, "glob").mockImplementation(() => + Promise.resolve(["my-component.tsx"]) + ); + vi.spyOn(path, "resolve").mockImplementation((_, file) => file); + vi.spyOn(fs, "writeFileSync"); + + const plugin = ApplicationManifestPlugin(); + plugin.configResolved({ command: "serve", server: {}, build: {} }); + + await expect(plugin.buildStart()).resolves.toBeUndefined(); + }); + test("Should error when an unknown type is discovered", async () => { mockReadFile({ "package.json": JSON.stringify({}), From cead6ad0b4f009ecadc50ffb5ef764714a49085f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:51:51 -0700 Subject: [PATCH 08/11] Add changeset --- .changeset/support_labels.md | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .changeset/support_labels.md diff --git a/.changeset/support_labels.md b/.changeset/support_labels.md new file mode 100644 index 00000000..f2e6dbb0 --- /dev/null +++ b/.changeset/support_labels.md @@ -0,0 +1,37 @@ +--- +default: minor +--- + +Add support for `labels` config for both `package.json` and `@tool` directives. + +```ts +// package.json +{ + "labels": { + "toolInvocation": { + "invoking": "Invoking...", + "invoked": "Invoked!" + } + } +} +``` + +```gql +query { + MyQuery + @tool( + name: "MyQuery" + description: "..." + labels: { + toolInvocation: { invoking: "Invoking...", invoked: "Invoked!" } + } + ) { + myField + } +} +``` + +These labels map to the following MCP server config: + +- `toolInvocation.invoking` -> `toolInvocation/invoking` +- `toolInvocation.invoked` -> `toolInvocation/invoked` From 905a09446a465f34a0f69b9cf32d488ce321bb16 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:55:35 -0700 Subject: [PATCH 09/11] Don't return labels if empty --- src/vite/application_manifest_plugin.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vite/application_manifest_plugin.ts b/src/vite/application_manifest_plugin.ts index ceeb2db7..7e452a7e 100644 --- a/src/vite/application_manifest_plugin.ts +++ b/src/vite/application_manifest_plugin.ts @@ -481,7 +481,9 @@ function getLabelsFromConfig(config: LabelConfig): ManifestLabels | undefined { labels["toolInvocation/invoked"] = toolInvocation.invoked; } - return labels; + if (isNonEmptyObject(labels)) { + return labels; + } } function removeClientDirective(doc: DocumentNode) { @@ -529,3 +531,7 @@ function validateType( `Expected '${options.propertyName}' to be of type '${expectedType}' but found '${typeof value}' instead.` ); } + +function isNonEmptyObject(obj: object) { + return Object.keys(obj).length > 0; +} From 2e77e63fcd623f8131c16c458d2264553b493df9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 12 Jan 2026 18:55:58 -0700 Subject: [PATCH 10/11] Use helper function --- src/vite/application_manifest_plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vite/application_manifest_plugin.ts b/src/vite/application_manifest_plugin.ts index 7e452a7e..1926e91c 100644 --- a/src/vite/application_manifest_plugin.ts +++ b/src/vite/application_manifest_plugin.ts @@ -314,7 +314,7 @@ export const ApplicationManifestPlugin = () => { if ( packageJson.widgetSettings && - Object.keys(packageJson.widgetSettings).length > 0 + isNonEmptyObject(packageJson.widgetSettings) ) { function validateWidgetSetting( key: keyof ManifestWidgetSettings, From e42e9599f3230313fc40ff1b6195ee8f3dc94aa2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 13 Jan 2026 17:12:56 -0700 Subject: [PATCH 11/11] Update changeset --- .changeset/support_labels.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/support_labels.md b/.changeset/support_labels.md index f2e6dbb0..238fcad3 100644 --- a/.changeset/support_labels.md +++ b/.changeset/support_labels.md @@ -2,6 +2,8 @@ default: minor --- +# Support `toolInvocation` labels + Add support for `labels` config for both `package.json` and `@tool` directives. ```ts