Skip to main content

Command Palette

Search for a command to run...

BDK

Extensions

An extension packages capabilities that would otherwise live under bot/: tools, skills, instructions, connections, hooks, channels, schedules, subagents, artifacts, and workspace seeds. One mount file adds them under a namespace without copying their source into your project.

Extensions and Cursor plugins execute as trusted host code. A namespace prevents name collisions; it does not sandbox the package or limit its side effects. Review the package before mounting it.

Mount an extension

Install the package and add one file under bot/extensions/. The filename becomes the namespace, so this mount contributes names such as ci__failed_jobs and ci__triage.

// bot/extensions/ci.tsimport ci from "@acme/ci-triage";export default ci({  org: "acme",  token: process.env.BUILDKITE_TOKEN,});

The extension validates its configuration while the project loads. Missing or mistyped settings fail bdk validate before a turn can call the contributed code.

Extension instructions are appended to the root agent's instructions. The root agent still needs its own bot/instructions.md.

Inspect a mount

Run discovery after mounting or upgrading an extension:

bdk validate --dir .bdk info --dir . --json

info lists each mount and its namespaced tools, skills, connections, hooks, channels, schedules, subagents, and artifact kinds. The same data appears under extensions in GET /v1/info.

A mount can also widen what a hosted deployment reaches. info shows the egress hosts and secret names each mount adds, and bdk deploy names every extension that adds egress before it deploys.

Names inside the extension stay short. A tool authored as tools/failed_jobs.ts appears to the mounting agent as <namespace>__failed_jobs. The __ separator is reserved, so root contributions cannot impersonate a mounted one.

Adjust a mounted extension

Mount as a directory when you need to replace or remove one contribution. Keep the package mount in extension.ts, then overlay the extension's short name beneath it.

bot/extensions/ci/  extension.ts  tools/    job_log.ts    retry_job.ts
// bot/extensions/ci/tools/job_log.tsimport { defineTool } from "@cursor/bdk/tools";import {  jobLogInput,  readRedactedLog,} from "../../../lib/ci.js";export default defineTool({  description: "Read a redacted CI log.",  inputSchema: jobLogInput,  execute: readRedactedLog,});
// bot/extensions/ci/tools/retry_job.tsimport { disableTool } from "@cursor/bdk/extensions";export default disableTool();

The first file replaces ci__job_log; the second removes ci__retry_job. Use the matching disable* helper for another slot, or generic disable() when the file location already identifies it. Validation fails when an overlay names a contribution the package does not provide.

Mount a Cursor plugin

cursorPlugin() adapts a vendored Cursor or Claude plugin into the same namespace model. Skills, commands, agents, and MCP servers become BDK skills, subagents, and connections.

// bot/extensions/notion.tsimport { cursorPlugin } from "@cursor/bdk/extensions";export default cursorPlugin({  path: "plugins/notion-workspace",  variables: {    NOTION_TOKEN: process.env.NOTION_TOKEN,  },});

path is relative to the project root. variables fills placeholders declared by the plugin's MCP configuration; unresolved placeholders fail validation instead of mounting a partial server.

Plugin rules and hooks are not mounted. Overlay a generated mcp-connections/<server>.ts file to replace or disable one plugin server. OAuth-backed plugin servers use Host MCP OAuth; transport and tool-filter details live in MCP connections.

Extension contents

Ships under the namespaceIgnored
Instructions, tools, skills, MCP and host connectionsagent.ts, storage, OpenTelemetry
Hooks, channels, schedules, and subagentsPlayground code, custom sandbox backends
Artifact kinds and workspace seed filesNested extensions

bdk validate reports unsupported paths.

Build an extension

Lay out the package like the shareable part of bot/. The root extension.ts supplies a description and validates any settings the consumer must provide.

@acme/ci-triage/  package.json  extension.ts  instructions.md  tools/    failed_jobs.ts    job_log.ts  skills/    triage.md  hooks/    audit.ts  lib/    buildkite.ts
// extension.tsimport { defineExtension } from "@cursor/bdk/extensions";import { z } from "zod";export default defineExtension({  root: import.meta.dirname,  description: "Investigate failing Buildkite jobs.",  config: z.object({    org: z.string(),    token: z.string(),    logTailLines: z.number().int().positive().default(200),  }),  hosting: () => ({ egressDomains: ["api.buildkite.com"] }),});

Modules inside the package import this definition and read extension.config, which is already validated and has defaults applied. Publish the built entry point with its slot directories and prose assets beside it.

hosting names the hosts the package calls and the secrets it reads from the environment. It receives the validated settings, and the mounting agent's hosting.egressDomains and hosting.secretNames gain whatever it returns, so a hosted deploy reaches Buildkite without the consumer listing api.buildkite.com. Leave out secrets that arrive through settings, such as token here; the consumer decides where that value comes from.

The Project layout owns each contribution's authoring contract. Keep extension instructions and tool descriptions namespace-neutral because the consumer chooses the final prefix.