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 . --jsoninfo 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 namespace | Ignored |
|---|---|
| Instructions, tools, skills, MCP and host connections | agent.ts, storage, OpenTelemetry |
| Hooks, channels, schedules, and subagents | Playground code, custom sandbox backends |
| Artifact kinds and workspace seed files | Nested 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.
Related
- Cloud agents: delegate repository work through an extension
- Grok Bot agents: consult named bots
- Jev: typed answers, then gated writes
- Self-improvement: propose source changes through pull requests
- Project layout: contribution slots
- Tools and Skills: capability contracts