# 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
`@grok/agent-sdk/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 `agent-sdk mcp oauth`; the model and playground don't see them.

## Remote MCP server

Point an MCP 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. 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.

```ts
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.

`agent-sdk 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](#cursor-account-mcp-connection) instead.

```ts
export default defineConnection({
  url: "https://mcp.example.com/inventory",
  oauth: true,
});
```

```text
agent-sdk mcp oauth inventory   # browser PKCE → local mcp-auth.json
```

Full walkthrough: [Host MCP OAuth](/docs/guides/mcp-oauth.md). Companion
skill: [`skills/mcp-auth/SKILL.md`](/docs/package/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:

```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 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 {#advertise-tools}

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

```ts
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](/docs/reference/tools.md#call-a-tool-without-a-model-turn) 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`.

```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. 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`.

```ts
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. Agent SDK packages authored tools as stdio MCP, 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 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.

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

```ts
// 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 (`agent-sdk 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 {#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:

```ts
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](/docs/guides/agent-to-agent.md#delegate-a-question-to-a-specialist).

## 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.

```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.
`listTools(name)` / `callTool(name, tool, args)` call into a named
connection.

## Related

- [Host MCP OAuth](/docs/guides/mcp-oauth.md): `mcp oauth`, Connect, and
  hosted agents
- [Tools](/docs/reference/tools.md): authored tools that wrap MCP connections
- [Webhooks](/docs/guides/webhooks.md): calling MCP connections from handlers
- [Peer agents](/docs/guides/agent-to-agent.md): when a specialist is its
  own agent
