Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .changeset/support_labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
default: minor
---

# Support `toolInvocation` labels

Add support for `labels` config for both `package.json` and `@tool` directives.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gunna want to add the header back in (h1) to avoid the changelog issue :)


```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`
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type {
ManifestTool,
ManifestExtraInput,
ManifestCsp,
ManifestLabels,
ManifestWidgetSettings,
} from "./types/application-manifest.js";

export { ToolUseProvider } from "./react/context/ToolUseContext.js";
Expand Down
7 changes: 7 additions & 0 deletions src/types/application-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ApplicationManifest = {
operations: ManifestOperation[];
csp: ManifestCsp;
widgetSettings?: ManifestWidgetSettings;
labels?: ManifestLabels;
};

export type ManifestOperation = {
Expand All @@ -25,6 +26,7 @@ export type ManifestTool = {
name: string;
description: string;
extraInputs?: ManifestExtraInput[];
labels?: ManifestLabels;
};

export type ManifestWidgetSettings = {
Expand All @@ -43,3 +45,8 @@ export type ManifestCsp = {
connectDomains: string[];
resourceDomains: string[];
};

export type ManifestLabels = {
"toolInvocation/invoking"?: string;
"toolInvocation/invoked"?: string;
};
159 changes: 154 additions & 5 deletions src/vite/__tests__/application_manifest_plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,34 @@ 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",
prefersBorder: true,
} 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?"
}],
labels: {
toolInvocation: {
invoking: "Testing tool...",
invoked: "Tested tool!"
}
}
) { helloWorld(name: $name) }\`;
`,
});

Expand Down Expand Up @@ -86,6 +102,10 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-w
},
"format": "apollo-ai-app-manifest",
"hash": "abc",
"labels": {
"toolInvocation/invoked": "Tested global!",
"toolInvocation/invoking": "Testing global...",
},
"operations": [
{
"body": "query HelloWorldQuery($name: string!) {
Expand All @@ -104,6 +124,10 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-w
"type": "boolean",
},
],
"labels": {
"toolInvocation/invoked": "Tested tool!",
"toolInvocation/invoking": "Testing tool...",
},
"name": "hello-world",
},
],
Expand Down Expand Up @@ -726,6 +750,131 @@ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-w
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({}),
Expand Down
Loading