# How to Define Custom Tools Using the `defineTool` API in the Copilot SDK

> Learn to define custom tools with the Copilot SDK's defineTool API. Register custom functionality and integrate it seamlessly with client.createSession for enhanced capabilities.

- Repository: [GitHub/copilot-sdk](https://github.com/github/copilot-sdk)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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`](https://github.com/github/copilot-sdk/blob/main/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.

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/types.ts) (lines 665-678). This function merges the supplied configuration with the tool name and returns a complete `Tool<T>` object.

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/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 to `true`, 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:

1. The model emits a `tool_call` in the chat message
2. The SDK matches the call name against registered `Tool` objects
3. Arguments are validated against the Zod schema provided in `parameters`
4. The `handler` receives parsed arguments and an `InvocationContext` containing the session ID and request ID
5. The handler's return value is wrapped into a `tool` message 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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/tools.e2e.test.ts) (lines 28-38) shows a minimal tool that transforms input using `toUpperCase()`:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/tools.e2e.test.ts) (lines 80-96) captures requests and approves single invocations:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/tools.e2e.test.ts) (lines 37-62) demonstrates returning database query results:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/tools.e2e.test.ts) (lines 96-134), thrown errors are replaced with `"unknown"`:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/types.ts)** (lines 629-678) – Contains the `Tool<T>` interface and `defineTool` function implementation
- **[`nodejs/src/index.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/index.ts)** (line 28) – Public API export point for consumers importing the SDK
- **[`nodejs/test/e2e/tools.e2e.test.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/tools.e2e.test.ts)** – End-to-end test suite demonstrating registration, permission handling, complex payloads, and error sanitization
- **[`nodejs/samples/manual-tool-resume.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/samples/manual-tool-resume.ts)** – Advanced example showing tool execution resumption after pausing
- **[`nodejs/examples/basic-example.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/examples/basic-example.ts)** – Minimal starter code forCreating sessions with custom tools

## Summary

- **Use `defineTool`** to create type-safe tool definitions with Zod schemas that validate LLM-provided arguments at runtime
- **Register tools** by passing them to the `tools` array in `client.createSession()`, available via the export in [`nodejs/src/index.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/index.ts)
- **Control permissions** via the `onPermissionRequest` callback or set `skipPermission: true` for 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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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.