Skip to content
Draft
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
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,7 @@ export class CopilotClient {
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
defer: tool.defer,
_meta: tool._meta,
})),
canvases: config.canvases?.map((canvas) => canvas.declaration),
requestCanvasRenderer: config.requestCanvasRenderer,
Expand Down Expand Up @@ -1591,6 +1592,7 @@ export class CopilotClient {
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
defer: tool.defer,
_meta: tool._meta,
})),
canvases: config.canvases?.map((canvas) => canvas.declaration),
requestCanvasRenderer: config.requestCanvasRenderer,
Expand Down
10 changes: 10 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,15 @@ export interface Tool<TArgs = unknown> {
* Optional; defaults to `"auto"`.
*/
defer?: "auto" | "never";
/**
* Opaque, host-defined metadata associated with the tool definition.
*
* Keys are namespaced and are not part of the stable public API. The SDK
* does not interpret these values; it forwards them verbatim to the runtime,
* which may recognize specific namespaced keys to inform host-specific
* behavior. Unknown keys are preserved and round-tripped untouched.
*/
_meta?: Record<string, unknown>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd prefer not to adopt MCP's naming here. We could just use "metadata".

We will also need similar support added across all the 6 SDKs.

}

/**
Expand All @@ -575,6 +584,7 @@ export function defineTool<T = unknown>(
overridesBuiltInTool?: boolean;
skipPermission?: boolean;
defer?: "auto" | "never";
_meta?: Record<string, unknown>;
}
): Tool<T> {
return { name, ...config };
Expand Down
63 changes: 63 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,69 @@ describe("CopilotClient", () => {
expect(resumePayload.contextTier).toBe("default");
});

it("forwards tool _meta verbatim in session.create and session.resume", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.create") return { sessionId: params.sessionId };
if (method === "session.resume") return { sessionId: params.sessionId };
throw new Error(`Unexpected method: ${method}`);
});

const meta = { "github.com/copilot:safeForTelemetry": { name: true, inputsNames: false } };
const tool = {
name: "my_tool",
description: "a tool",
parameters: { type: "object", properties: {} },
_meta: meta,
};

const session = await client.createSession({
onPermissionRequest: approveAll,
tools: [tool],
});
await client.resumeSession(session.sessionId, {
onPermissionRequest: approveAll,
tools: [tool],
});

const createPayload = spy.mock.calls.find(
([method]) => method === "session.create"
)![1] as any;
const resumePayload = spy.mock.calls.find(
([method]) => method === "session.resume"
)![1] as any;
expect(createPayload.tools[0]._meta).toEqual(meta);
expect(resumePayload.tools[0]._meta).toEqual(meta);
});

it("omits tool _meta from session.create when unset", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.create") return { sessionId: params.sessionId };
throw new Error(`Unexpected method: ${method}`);
});

await client.createSession({
onPermissionRequest: approveAll,
tools: [{ name: "my_tool", description: "a tool" }],
});

const createPayload = spy.mock.calls.find(
([method]) => method === "session.create"
)![1] as any;
expect(createPayload.tools[0]._meta).toBeUndefined();
});

it("forwards expAssignments in session.create and session.resume", async () => {
const client = new CopilotClient();
await client.start();
Expand Down
Loading