Skip to main content

Command Palette

Search for a command to run...

BDK

MCP connections

An MCP connection gives the agent tools from an MCP server. Define one file per server under bot/mcp-connections/; the filename becomes the server name. Default-export defineConnection from @cursor/bdk/connections. The transport is remote HTTP, local stdio, the signed-in Cursor account's connectors, or a peer agent addressed by slug.

Put a server in bot/host-connections/ when only host tools should call it. Host connections use the same defineConnection shape and support bdk mcp oauth; the model and playground don't see them.

Remote MCP server

Point an MCP connection at a remote server with a URL.

import { defineConnection } from "@cursor/bdk/connections";export default defineConnection({  url: "https://mcp.linear.app/mcp",  headers: { authorization: "Bearer ${LINEAR_TOKEN}" },});

Credentials in headers and env

Write a credential as a ${NAME} template in headers on a remote connection or in env on a stdio one. The connection stores the template as you wrote it and reads NAME from the environment every time it opens a client, so the value can arrive after the project loads.

export default defineConnection({  command: "slack-mcp-server",  args: ["--transport", "stdio"],  env: { SLACK_MCP_XOXB_TOKEN: "${SLACK_BOT_TOKEN}" },});

That timing is the point of the template. Your connection file is imported once, when the project loads, and a hosted deployment has none of its secrets in the environment yet, because the platform passes them to the agent per request. So a process.env.SLACK_BOT_TOKEN read at the top of the file captures an empty string and keeps sending it for the life of the process, while the same file works locally because .env.local was read before the import. The template form is right in both places.

bdk validate warns about a template the environment does not define and names the variable. It stays a warning rather than an error, since a hosted deployment resolves it later. An unset variable resolves to an empty value at connect time, which the server reports as an auth failure.

Host MCP OAuth

For servers that speak OAuth, set oauth: true and authorize with the CLI or mid-run Connect. Tokens live in mcp-auth.json under the CLI config directory of the machine that authorized them. A hosted agent reaches OAuth servers through a Cursor account connection instead.

export default defineConnection({  url: "https://mcp.example.com/inventory",  oauth: true,});
bdk mcp oauth inventory   # browser PKCE → local mcp-auth.json

Full walkthrough: Host MCP OAuth. Companion skill: skills/mcp-auth/SKILL.md.

Per-session auth

For http/sse connections whose credential depends on who the session is for, declare an auth callback instead of static headers. It runs host-side with the session's SessionInfo and returns headers merged over the static ones:

export default defineConnection({  url: "https://api.cursor.com/v1/mcp/plugins",  auth: async (session) => ({    headers: { Authorization: `Bearer ${await grantFor(session)}` },  }),  advertiseTools: true,});

The callback is evaluated on every turn, including reminder fires and post-restart follow-ups, so the identity always comes from the session itself, never from state parked in memory. The model never sees a downstream account parameter and cannot choose the account. A callback that throws fails the turn: a turn never silently runs without the connection's identity. host.mcp calls from server tools keep the static headers only. You can't combine auth with oauth: true; the host OAuth provider owns the Authorization header.

Derive the identity from durable session facts: session.auth, session.id, or your channel's own session state. Do not key it off session.continuationKey. The HTTP channel rotates that key after every accepted follow-up, so an account mapping keyed on it breaks mid-conversation.

auth works attached or advertised. Advertise the auth connection instead of attaching it next to a stateful stdio server.

Set advertiseTools: true when the model should call an MCP server's tools by name. BDK preserves each tool's description, input and output schemas, and MCP annotations. Exposed names normalize invalid characters and add numeric suffixes to avoid collisions.

export default defineConnection({  url: "https://api.cursor.com/v1/mcp/plugins",  auth: async (session) => ({ headers: await headersForSession(session) }),  advertiseTools: true,});

Listing, authentication, invalid tool name, and name collision failures fail the turn by default. Set optional: true to omit an unavailable connection instead. Advertised tools behave like server tools, and direct tool calls use the same names.

In a dry-run session, MCP tools marked read-only run normally. Tools marked as writes are stubbed. Tools without effect annotations are unavailable. A tool annotated read-only uses the same transient retry as an authored read. A tool with no effect annotation runs once.

Filter connection tools

Use tools the same way you allowlist harness tools on the agent. When set, the connection serves only those names. Use disallowedTools to drop names instead. The two combine as deny-wins. The model and host.mcp only see what remains. A model-visible filter requires advertiseTools: true.

export default defineConnection({  url: "https://mcp.example.com/inventory",  advertiseTools: true,  tools: ["search_skus", "get_stock"],});
export default defineConnection({  url: "https://mcp.example.com/inventory",  advertiseTools: true,  disallowedTools: ["place_order", "delete_sku"],});

Names are the server's tools/list names. Unknown names are omitted. A filter that matches nothing on the server fails the turn unless optional: true. Combine with effects: "read" to keep only the listed tools the server classifies as reads.

A list of names is an allowlist. An object of handlers authors TypeScript tools. On host-connections/, a name list restricts host.mcp without advertising.

Local stdio MCP server

Run a local MCP server as a child process with command.

export default defineConnection({  command: "node",  args: ["--import", "tsx", "mcp/units-server.ts"],  // env, cwd});

This suits small purpose-built servers, like a units converter shipped next to the agent.

To run TypeScript in the agent environment, author the tools on the connection instead. BDK packages authored tools as stdio MCP, so you write only execute.

export default defineConnection({  tools: {    probe_agent_tool: {      description: "Prove the tool ran in the agent environment.",      async execute({ label }) {        return { ok: true, label };      },    },  },});

Cursor account MCP connection

{ cursorAccount: true } forwards the MCP connectors the signed-in Cursor account already authorized (dashboard → MCP), such as Linear, Notion, and Slack. You don't configure tokens. Every tool runs on the Cursor backend with the account's stored OAuth credentials, so raw tokens never reach the serving host, session workspaces, or traces.

By default the agent gets every connected HTTP/SSE connector on the account. Pass servers: "*" (or ["*"]) for the same all-connectors behavior in an explicit form. Pass a name list when you want a smaller set.

// bot/mcp-connections/account.ts: every connected connectorexport default defineConnection({  cursorAccount: true,  advertiseTools: true,});
// only Linear:export default defineConnection({  cursorAccount: true,  servers: ["Linear"],  advertiseTools: true,});

Name the file account.ts. cursor.ts collides with the IDE cursor MCP namespace. advertiseTools: true puts connector tools on local turns by name.

The host must be signed in (bdk login, CURSOR_API_KEY, or CURSOR_SERVICE_ACCOUNT_KEY). serve fails at startup otherwise.

Use a { command } connection for stdio servers.

[!CAUTION] Whoever can talk to the agent can drive these connectors, because they are ordinary agent tools. serve refuses to start when --allow-anonymous is combined with account MCP connections unless you also pass --allow-anonymous-cursor-account-mcp (trusted boundary only; for example an SSO proxy or the hosted alias token). Prefer --bearer-token on shared hosts.

Peer MCP connection

{ agent: "<slug>" } addresses another agent without configuring a URL, headers, or secrets. On a local multi-agent serve, the target is another mount slug. On Cursor-managed hosting, it is the lowercase deployment slug of another agent on the same team.

On a local multi-agent serve, the model gets the peer's ask and check tools, plus call_tool when the peer has server tools, and can delegate work to it:

export default defineConnection({  agent: "weather-agent",  description: "Delegate weather questions to the weather agent.",});

Declare the connection only on the caller. The specialist needs no matching configuration.

In a local multi-agent serve, unknown slugs and self-references fail at startup. A single-agent serve omits peers with a warning unless its hosting platform sets --peers-url. Walkthrough: Peer agents.

Call MCP from host code

A file under bot/mcp-connections/ is available to the Cursor agent, to server tools through ctx.host.mcp, and to channel and schedule handlers through args.host.mcp. Host connections skip the model.

// bot/tools/search_linear.tsimport { defineTool } from "@cursor/bdk/tools";import { z } from "zod";export default defineTool({  description: "Search Linear issues.",  inputSchema: z.object({ query: z.string() }),  async execute({ query }, ctx) {    return ctx.host.mcp.callTool("linear", "list_issues", { query });  },});

host.mcp.names() lists connection names. listTools(name) / callTool(name, tool, args) call into a named connection.

  • Host MCP OAuth: mcp oauth, Connect, and hosted agents
  • Tools: authored tools that wrap MCP connections
  • Webhooks: calling MCP connections from handlers
  • Peer agents: when a specialist is its own agent