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.jsonFull 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.
Advertise tools
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.
serverefuses to start when--allow-anonymousis 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-tokenon 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.
Related
- 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