Skip to content

MCP connections ​

An MCP connection gives the agent tools from an MCP server. Each file under bot/mcp-connections/ default-exports defineConnection from @grok/agent-sdk/connections, and its filename becomes the server name. A connection reaches a remote HTTP server, a local stdio process, the signed-in Cursor account's connectors, or a peer agent by slug.

Put a server in bot/host-connections/ when only host code should call it. Host connections use the same shape and support agent-sdk mcp oauth, but the model and playground don't see them.

Remote MCP server ​

Point a connection at a remote server with a URL.

ts
import { defineConnection } from "@grok/agent-sdk/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.

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

On Cursor hosting, your secrets aren't in process.env yet when the connection file loads. Agent SDK resolves a ${NAME} template each time the connection opens, so the template works locally and on hosting. Reading process.env.NAME at the top of the file only works locally.

agent-sdk validate warns when the environment doesn't define a template's variable. An unset variable resolves to an empty value, which the server reports as an auth failure.

Host MCP OAuth ​

For a server that speaks OAuth, set oauth: true and follow Connect a private MCP server with OAuth.

Per-session auth ​

For an HTTP or SSE connection whose credential depends on who the session is for, declare an auth callback instead of static headers. The headers it returns override the static ones.

ts
export default defineConnection({
  url: "https://api.cursor.com/v1/mcp/plugins",
  auth: async (session) => ({
    headers: { Authorization: `Bearer ${await grantFor(session)}` },
  }),
  advertiseTools: true,
});
  • The callback runs on the host before every turn, including reminder fires and follow-ups after a restart, so the model never sees or chooses the account. A callback that throws fails the turn.
  • Derive the identity from session.auth, session.id, or your channel's session state, not from session.continuationKey, which the HTTP channel rotates after every follow-up.
  • host.mcp calls from server tools send only the static headers, and auth can't be combined with oauth: true.

Set advertiseTools: true when the model should call an MCP server's tools by name. Agent SDK keeps each tool's description, schemas, and MCP annotations, and renames invalid or colliding names.

A listing or auth failure, an invalid tool name, or a name collision fails the turn. Set optional: true to skip an unavailable connection instead. Advertised tools behave like server tools, and direct tool calls use the same names.

In a dry-run session, read-only MCP tools run, Agent SDK stubs write tools, and tools without effect annotations are unavailable. A read-only tool gets the same transient retry as an authored read, and a tool with no effect annotation runs once.

Filter connection tools ​

Set tools to serve only the listed names, or disallowedTools to drop names. When both list a name, disallowedTools wins. The model and host.mcp see only what remains, and a filter the model sees requires advertiseTools: true.

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

Names are the server's tools/list names, and Agent SDK skips unknown names. A filter that matches nothing fails the turn unless optional: true. Add effects: "read" to keep only the listed tools the server marks as reads. On a host connection, a name list limits host.mcp without advertising anything.

Local stdio MCP server ​

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

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

To run TypeScript in the agent environment, define the tools on the connection instead. Here tools takes an object of handlers rather than a list of names. Agent SDK packages them as a stdio MCP server, so you write only execute.

ts
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 that the signed-in Cursor account already authorized in the dashboard, such as Linear, Notion, and Slack. Each 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.

ts
// bot/mcp-connections/account.ts
export default defineConnection({
  cursorAccount: true,
  advertiseTools: true,
});

The agent gets every connected HTTP or SSE connector, the same as servers: "*", unless you pass a list such as servers: ["Linear"]. Name the file account.ts, because cursor.ts collides with the IDE's cursor MCP namespace. The host must be signed in, or serve fails at startup.

CAUTION

Anyone who can talk to the agent can drive these connectors. serve refuses to start with --allow-anonymous and account connections unless you also pass --allow-anonymous-cursor-account-mcp, which belongs only behind a trusted boundary such as an SSO proxy. Prefer --bearer-token on shared hosts.

Peer MCP connection ​

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

The model gets the peer's ask and check tools, plus call_tool when the peer has server tools, so it can delegate work:

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

Declare the connection only on the caller. 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. See Peer agents.

Call MCP from host code ​

Server tools reach every connection, including host connections, through ctx.host.mcp. Channel and schedule handlers use args.host.mcp.

ts
// bot/tools/search_linear.ts
import { defineTool } from "@grok/agent-sdk/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, and listTools(name) and callTool(name, tool, args) call into a named connection.