# MCP tools (/brain/docs/tools/mcp)

The Model Context Protocol (MCP) is an open protocol for serving tools to AI applications. Brain connects to remote MCP servers over Streamable HTTP with the official MCP Go SDK (pinned in `go.mod`), so you can give sessions tools from an MCP server you already run. MCP tools go through the same tenant allowlist, input validation, timeout, and output limits as [HTTP tools](/brain/docs/tools).

## What Brain supports [#what-brain-supports]

- **Transport:** Remote MCP servers over Streamable HTTP, or installed stdio commands through the [Go application SDK](/brain/docs/sdks/application#connect-managed-services).
- **Authentication:** API keys sent as bearer tokens from the environment variable named in `token_env`.
- **Results:** Tool calls that return text, images, or structured content.
- **Progress:** Progress notifications streamed during a call as `tool.progress` events.

## Discover tools [#discover-tools]

Run `cmd/mcp-tools` to turn tools from an MCP server into Brain tool bindings:

```sh
go run ./cmd/mcp-tools -url https://YOUR_SERVER/mcp -token-env MCP_API_KEY -tenant local -tools search,fetch
```

| Flag | Description |
| --- | --- |
| `-url` | The server's Streamable HTTP endpoint. |
| `-token-env` | The environment variable that holds the server's API key. The command uses it to connect, and the bindings tell Brain to send it as a bearer token. |
| `-tenant` | The tenant allowed to use the tools. |
| `-tools` | A comma-separated list of 1 to 128 remote tool names to include. |

The command connects to the server, reads its tool list, and prints a JSON array with one binding for each named tool. It fails if a named tool doesn't exist or if discovery takes longer than 30 seconds. The output looks like this:

```json
[
  {
    "name": "search",
    "description": "Search the knowledge base.",
    "input_schema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] },
    "url": "https://YOUR_SERVER/mcp",
    "mcp_tool": "search",
    "token_env": "MCP_API_KEY",
    "tenants": ["local"],
    "timeout_seconds": 300,
    "max_output_bytes": 1048576
  }
]
```

Review the descriptions and schemas, then copy the bindings into the `tools` array of your host config. Bindings are static: a tool's description and schema change only when you edit the config and restart Brain, and discovery never changes the tools of a running session.

## Configure an MCP binding [#configure-an-mcp-binding]

An MCP binding is a regular tool binding with one extra field, `mcp_tool`:

- **`name`** is the name the model sees. Rename tools to avoid collisions, for example `docs_search` for a `search` tool.
- **`mcp_tool`** is the tool's name on the MCP server.
- **`url`** is the server's MCP endpoint.
- **`token_env`** names the environment variable with the server's API key. Brain sends it as a bearer token on every request to the server.

`tenants`, `timeout_seconds`, `max_output_bytes`, and `output_schema` work as they do for [HTTP tools](/brain/docs/tools). `inspect_session` applies to HTTP and SSE tools; Brain rejects a host config that sets it on an MCP binding.

## How Brain calls an MCP tool [#how-brain-calls-an-mcp-tool]

1. **Check the call.** Brain confirms that the tool is allowed for the session's tenant and validates the arguments against `input_schema`.
2. **Open an MCP session.** Brain starts a new MCP session with the server for this call, over pooled HTTP connections. A separate MCP session per call keeps server-side session state from crossing tenants or calls, at the cost of one initialization round trip per call.
3. **Call the tool.** Brain sends the arguments and puts the session's identity in `_meta["brain.dev/scope"]` as `tenant_id`, `session_id`, and `call_id`. The call ID is also the progress token.
4. **Stream progress.** Each progress notification from the server becomes a live-only `tool.progress` event.
5. **Return the result.** Brain converts the result into a `tool.result` event, returns it to the harness, and closes the MCP session.

<Callout type="warn" title="Scope metadata is for attribution">
The `_meta["brain.dev/scope"]` values tell your server which tenant and session a call belongs to. Your server must still authenticate Brain's API key and enforce access to its own resources. Brain never sends session inspection tokens to MCP servers.
</Callout>

## Results and errors [#results-and-errors]

Brain converts MCP results into Brain content blocks:

| MCP result | Brain result |
| --- | --- |
| Text content | A text block. |
| Image content | An image block with base64 `data` and `mime_type`. |
| Structured content | `structured_content` plus a text block containing the JSON. Successful output schemas validate the structured value. |
| `isError: true` | `is_error: true`. The model sees the error as a tool result. |
| Any other content type | The call fails. |

When a call fails, the `tool.result` has `is_error: true`:

| What happened | `unknown_outcome` |
| --- | --- |
| Brain refused the call or couldn't connect to the server, so the tool was never called | No |
| The call failed after Brain sent it: the connection dropped, the call timed out, or the server returned a protocol error | Yes |
| The server asked for user input | Yes. Handle approval inside the tool instead. |
| The result had an unsupported content type, exceeded `max_output_bytes`, or didn't match `output_schema` | Yes |

Brain sends each MCP call at most once. With an unknown outcome, the model can use another tool to check the resource. Interrupting the session or reaching the timeout cancels the request; whether the work stops on the server depends on the server.

## Next steps [#next-steps]

<Cards>
  <Card icon={<Wrench />} title="Distributed tools" href="/docs/tools">Tool bindings, request payloads, and result formats.</Card>
  <Card icon={<ShieldCheck />} title="Authentication & tenants" href="/docs/guides/authentication">How tools receive the session's identity.</Card>
  <Card icon={<Braces />} title="Host configuration" href="/docs/reference/configuration">Every host config field and default.</Card>
</Cards>
