How to Define Custom Tools Using the `defineTool` API in the Copilot SDK
You register custom functionality by calling defineTool(name, config) from @github/copilot-sdk, which returns a Tool object containing a Zod-validated parameter schema and handler function that you pass to client.createSession().
The Copilot SDK enables hosts to expose custom capabilities to LLM-driven agents through typed tool definitions. By using the defineTool API, you can register functions with validated parameters that the model can invoke during a session. This pattern bridges host-side logic—such as database queries or encryption services—with Copilot's conversational runtime, as implemented in the github/copilot-sdk repository.
Understanding the Tool Interface
At the core of the system is the Tool<T> interface defined in nodejs/src/types.ts (lines 629-646). This interface represents a name-bound function that the LLM can call, complete with type-safe arguments and execution context.
export interface Tool<TArgs = unknown> {
name: string;
description?: string;
parameters?: ZodSchema<TArgs> | Record<string, unknown>;
handler?: ToolHandler<TArgs>;
overridesBuiltInTool?: boolean;
skipPermission?: boolean;
defer?: "auto" | "never";
metadata?: Record<string, unknown>;
}
The generic TArgs parameter ensures that the handler receives strongly-typed arguments parsed from the LLM's tool call. The interface also includes permission and deferral flags that control runtime behavior.
The defineTool Helper Function
Because TypeScript cannot infer argument types directly from a Zod schema without an explicit generic, the SDK provides the defineTool<T>() helper in nodejs/src/types.ts (lines 665-678). This function merges the supplied configuration with the tool name and returns a complete Tool<T> object.
export function defineTool<T = unknown>(
name: string,
config: {
description?: string;
parameters?: ZodSchema<T> | Record<string, unknown>;
handler?: ToolHandler<T>;
overridesBuiltInTool?: boolean;
skipPermission?: boolean;
defer?: "auto" | "never";
metadata?: Record<string, unknown>;
}
): Tool<T> {
return { name, ...config };
}
Using this helper ensures type safety across the parameter schema and handler function while maintaining a clean, declarative API.
Session Registration and Lifecycle
Tools are registered when creating a session via client.createSession(). The SDK serializes each Tool into the RPC payload consumed by the Copilot runtime. According to the implementation in nodejs/test/e2e/tools.e2e.test.ts, you supply an array of tools to the tools option.
Two critical flags affect invocation behavior:
skipPermission– When set totrue, the runtime invokes the tool without prompting the host for permission. The default value (false) triggers a permission request before execution.defer– Set to"auto"(default) to allow lazy loading via the built-in tool search mechanism, or"never"to load immediately when the session starts.
Tool Invocation Flow
When the LLM decides to use a tool, the SDK executes a strict validation and dispatch pipeline:
- The model emits a
tool_callin the chat message - The SDK matches the call name against registered
Toolobjects - Arguments are validated against the Zod schema provided in
parameters - The
handlerreceives parsed arguments and anInvocationContextcontaining the session ID and request ID - The handler's return value is wrapped into a
toolmessage sent back to the model
If the handler throws an error, the SDK sanitizes it before transmission. As demonstrated in nodejs/test/e2e/tools.e2e.test.ts (lines 96-134), the model receives the string "unknown" rather than the actual error message or stack trace.
Practical Implementation Examples
Basic String Encryption Tool
This example from nodejs/test/e2e/tools.e2e.test.ts (lines 28-38) shows a minimal tool that transforms input using toUpperCase():
import { defineTool, approveAll } from "@github/copilot-sdk";
import { z } from "zod";
const encryptTool = defineTool("encrypt_string", {
description: "Encrypts a string",
parameters: z.object({ input: z.string().describe("String to encrypt") }),
handler: ({ input }) => input.toUpperCase(),
});
const session = await client.createSession({
onPermissionRequest: approveAll,
tools: [encryptTool],
});
const reply = await session.sendAndWait({
prompt: "Use encrypt_string to encrypt this string: Hello",
});
console.log(reply?.data.content); // → "HELLO"
Handling Permission Requests
For tools requiring explicit approval, implement onPermissionRequest to control access. This pattern from nodejs/test/e2e/tools.e2e.test.ts (lines 80-96) captures requests and approves single invocations:
const permissionRequests: PermissionRequest[] = [];
const session = await client.createSession({
tools: [
defineTool("encrypt_string", {
description: "Encrypts a string",
parameters: z.object({ input: z.string() }),
handler: ({ input }) => input.toUpperCase(),
}),
],
onPermissionRequest: (req) => {
permissionRequests.push(req);
return { kind: "approve-once" }; // approve this single invocation
},
});
Complex Data Types and Return Values
Tool handlers can return sophisticated data structures that the model consumes as JSON. This example from nodejs/test/e2e/tools.e2e.test.ts (lines 37-62) demonstrates returning database query results:
defineTool("db_query", {
description: "Performs a database query",
parameters: z.object({
query: z.object({
table: z.string(),
ids: z.array(z.number()),
sortAscending: z.boolean(),
}),
}),
handler: ({ query }, invocation) => {
// Simulated DB rows
return [
{ countryId: 19, cityName: "Passos", population: 135460 },
{ countryId: 12, cityName: "San Lorenzo", population: 204356 },
];
},
});
Error Handling and Sanitization
When tools encounter failures, the SDK prevents error details from reaching the LLM. As shown in nodejs/test/e2e/tools.e2e.test.ts (lines 96-134), thrown errors are replaced with "unknown":
defineTool("get_user_location", {
description: "Gets the user's location",
handler: () => {
throw new Error("Melbourne"); // internal error
},
});
The assistant receives only the sanitized "unknown" value, ensuring internal implementation details remain concealed.
Key Source Files
The complete implementation spans these critical paths in the github/copilot-sdk repository:
nodejs/src/types.ts(lines 629-678) – Contains theTool<T>interface anddefineToolfunction implementationnodejs/src/index.ts(line 28) – Public API export point for consumers importing the SDKnodejs/test/e2e/tools.e2e.test.ts– End-to-end test suite demonstrating registration, permission handling, complex payloads, and error sanitizationnodejs/samples/manual-tool-resume.ts– Advanced example showing tool execution resumption after pausingnodejs/examples/basic-example.ts– Minimal starter code forCreating sessions with custom tools
Summary
- Use
defineToolto create type-safe tool definitions with Zod schemas that validate LLM-provided arguments at runtime - Register tools by passing them to the
toolsarray inclient.createSession(), available via the export innodejs/src/index.ts - Control permissions via the
onPermissionRequestcallback or setskipPermission: truefor trusted internal operations - Return structured data from handlers—the SDK automatically serializes complex objects for the model
- Expect sanitization—errors thrown in handlers are caught and replaced with
"unknown"to prevent information leakage
Frequently Asked Questions
What is the defineTool API in the Copilot SDK?
The defineTool API is a TypeScript helper function exported from @github/copilot-sdk (defined in nodejs/src/types.ts, lines 665-678) that constructs a Tool object by merging a tool name with configuration options including a Zod schema, handler function, and permission flags. It enables type-safe registration of custom functions that LLM agents can invoke during sessions.
How do I handle permissions for custom tools?
By default, the Copilot runtime requests permission before invoking any tool where skipPermission is not set to true. You provide an onPermissionRequest callback when calling client.createSession() to approve or deny individual invocations, or return { kind: "approve-once" } to authorize single executions without persistent approval.
Can I return complex objects from tool handlers?
Yes, tool handlers can return any JSON-serializable data structure. The SDK automatically serializes the return value into a tool message sent back to the model. As shown in the test suite at nodejs/test/e2e/tools.e2e.test.ts (lines 37-62), you can return arrays of objects with nested properties, and the model will receive the complete structured data.
What happens when a tool handler throws an error?
When a handler throws, the SDK catches the error and sanitizes it before sending to the model. According to the error handling tests in nodejs/test/e2e/tools.e2e.test.ts (lines 96-134), the assistant receives the string "unknown" rather than the actual error message or stack trace, preventing sensitive internal details from leaking to the LLM.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →