MCP in the Agent SDK
Connect Model Context Protocol servers to an Agent SDK agent over stdio, HTTP or SSE, approve their tools, handle authentication and diagnose failed connections.
The Model Context Protocol (MCP) is an open standard for plugging tools and data into AI agents. Rather than writing a GitHub integration or a database client yourself, you point the agent at an MCP server that already speaks the protocol, and its tools appear alongside the built-ins. Servers can run as local processes, sit behind a URL, or live inside your own application.
This page is about MCP in SDK agents. To add servers to the Claude Code CLI for every project, see MCP in Claude Code.
A quick example
Connect a remote server over HTTP and allow all its tools with a wildcard. Here I use the GitHub MCP server, which needs a personal access token with read access to the repositories you want to query:
export GITHUB_TOKEN=github_pat_...
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const msg of query({
prompt: "Summarise the five newest open issues in my-org/web-app and suggest which to tackle first",
options: {
mcpServers: {
github: {
type: "http",
url: "https://api.githubcopilot.com/mcp/",
headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
},
},
allowedTools: ["mcp__github__*"],
},
})) {
if (msg.type === "system" && msg.subtype === "init") console.log("servers:", msg.mcp_servers);
if (msg.type === "result" && msg.subtype === "success") console.log(msg.result);
}
import os
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage, SystemMessage
opts = ClaudeAgentOptions(
mcp_servers={
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
}
},
allowed_tools=["mcp__github__*"],
)
If the init message shows github with status connected, the token works. A status of pending can also be fine (more on that below). If it says failed or needs-auth, do not trust the answer: Claude may have fallen back to other tools.
Adding servers
In code
Pass servers in mcpServers (Python: mcp_servers). The key is the server's name.
options: {
mcpServers: {
files: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/srv/shared/contracts"],
},
},
allowedTools: ["mcp__files__*"],
}
From .mcp.json
Put a .mcp.json at the project root. It loads through the project setting source, which default options include. If you set settingSources yourself, include "project".
{
"mcpServers": {
"files": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/srv/shared/contracts"]
}
}
}
Choosing a transport
Check the server's documentation:
- It gives you a command to run: use stdio.
- It gives you a URL: use HTTP (streamable) or SSE.
- You are writing the tools yourself in your app: use an SDK server.
| Transport | Config fields | Notes |
|---|---|---|
| stdio | command, args, optional env | A local child process talking over stdin and stdout |
| HTTP | type: "http", url, optional headers | Streamable HTTP. In JSON config files "streamable-http" is accepted as an alias, but the SDK's McpHttpServerConfig type only declares "http", so use that in code. |
| SSE | type: "sse", url, optional headers | Server-sent events, for older remote servers |
| SDK | A server from createSdkMcpServer / create_sdk_mcp_server | Runs inside your process; see custom tools |
An SDK server registered through an initialize control request starts connecting as soon as Claude Code processes that request.
Approving MCP tools
Claude can see MCP tools without permission, but cannot call them until something approves the call.
Names
MCP tools are named mcp__<server>__<tool>. A server keyed crm with a find_contact tool gives mcp__crm__find_contact.
allowedTools
List exact tools, or use * for everything on one server:
allowed_tools=[
"mcp__github__*", # every GitHub tool
"mcp__warehouse__query", # just one tool from the warehouse server
"mcp__slack__post_message", # just one Slack tool
]
Note: Use
allowedToolsrather than a permission mode to grant MCP access.acceptEditsdoes not approve MCP tools at all; it only covers file edits and filesystem commands.bypassPermissionsdoes approve them, but also switches off most other safety prompts. A wildcard for one server grants exactly what you intend and nothing more. See SDK permissions.
Finding out what a server offers
Read the server's docs, ask Claude to list them, or inspect the tools array on the init system message and filter for the mcp__ prefix:
async for msg in query(prompt="...", options=opts):
if isinstance(msg, SystemMessage) and msg.subtype == "init":
print([t for t in msg.data.get("tools", []) if t.startswith("mcp__")])
In TypeScript, msg.tools is directly on the init message. The list includes tools from servers that have connected by the time init is sent, plus servers with a cached tool list; tools from other servers are missing until they connect.
Connection timing
Servers you pass in options.mcpServers are registered at startup, and the init message is sent once the first-turn wait (if any) has finished. Whether a server holds up the first turn depends on its type:
| Server | Holds up the first turn? | Deadline |
|---|---|---|
| stdio, or HTTP/SSE with no cached tool list | Yes, until connected | MCP_TIMEOUT (30 seconds by default); the connection fails at that point |
| Remote server with a cached tool list from a previous connection | No; cached tools are usable immediately | Connects on its first tool call, with its own timeout |
| In-process SDK server | Yes, until connected and its tools are listed | MCP_TIMEOUT per attempt |
Servers from settings files (such as .mcp.json) or plugins often show pending at init. If options.mcpServers holds any stdio, HTTP or SSE server, the first turn waits for those pending servers too, up to MCP_TIMEOUT. If it is empty or holds only SDK servers, the wait is 2 seconds:
- With tool search on (the default), the wait only covers pending servers configured with
alwaysLoad: true. The rest keep connecting in the background. - With tool search off, the wait covers every pending server. Removing the
ToolSearchtool (for example viadisallowedTools) also turns tool search off. See tool search.
If you set permissionPromptToolName, the first turn always waits for that tool's server, up to MCP_TIMEOUT.
Tuning the wait
| Setting | Effect |
|---|---|
CLAUDE_CODE_MCP_STARTUP_WAIT_MS in env | Waits up to this many milliseconds for every pending server, with or without tool search, and replaces the MCP_TIMEOUT first-turn wait for stdio, HTTP and SSE servers in options.mcpServers. 0 skips the wait. A permissionPromptToolName server keeps its own wait. Needs Claude Code v2.1.274 or later. |
MCP_CONNECTION_NONBLOCKING=0 | Blocks startup itself, before init is sent, on the whole connection batch, capped at 5 seconds by default |
MCP_CONNECT_TIMEOUT_MS | Changes that startup cap, in milliseconds |
alwaysLoad: true on a server | Its tools are available at full schema from the first turn, exempt from tool search deferral. Startup waits for it (same cap); a remote server with a cached list supplies tools without connecting. |
Servers still pending when a wait ends carry on connecting in the background.
Tool search
With many MCP tools, their definitions alone can eat a big slice of context. Tool search, on by default, keeps definitions out of context and loads only those Claude needs. Configuration and best practice are on the tool search page.
Authentication
Environment variables for local servers
Pass secrets to a stdio server through its env field:
mcpServers: {
accounts: {
command: "npx",
args: ["-y", "@acme/accounts-mcp"],
env: { ACCOUNTS_API_KEY: process.env.ACCOUNTS_API_KEY! },
},
},
allowedTools: ["mcp__accounts__*"],
In .mcp.json, ${VAR} is expanded from the environment at runtime, which keeps secrets out of the file:
{
"mcpServers": {
"accounts": {
"command": "npx",
"args": ["-y", "@acme/accounts-mcp"],
"env": { "ACCOUNTS_API_KEY": "${ACCOUNTS_API_KEY}" }
}
}
}
Headers for remote servers
For HTTP and SSE servers, put credentials in headers, in code or in .mcp.json (again with ${VAR} expansion):
{
"mcpServers": {
"accounts": {
"type": "http",
"url": "https://mcp.accounts.example.com/mcp",
"headers": { "Authorization": "Bearer ${ACCOUNTS_TOKEN}" }
}
}
}
OAuth
MCP supports OAuth 2.1, but the SDK never opens a browser or runs an interactive flow. If a server issues an auth challenge and no stored token exists, the run carries on without that server's tools and the server's status becomes needs-auth. The init message may still say pending at that moment, so poll mcpServerStatus() (TypeScript) or get_mcp_status() on ClaudeSDKClient (Python) to be sure.
To supply credentials, run the OAuth flow in your own app and pass the access token as a header:
token = await my_oauth.get_access_token(user_id)
opts = ClaudeAgentOptions(
mcp_servers={"crm": {"type": "http", "url": "https://mcp.crm.example.com/mcp",
"headers": {"Authorization": f"Bearer {token}"}}},
allowed_tools=["mcp__crm__*"],
)
Example: read-only database questions
DBHub exposes a database over MCP with an execute_sql tool. Out of the box that tool runs whatever SQL the agent writes, including writes, so lock it down in DBHub's TOML config:
[[sources]]
id = "reporting"
dsn = "${DATABASE_URL}"
[[tools]]
name = "execute_sql"
source = "reporting"
readonly = true
With readonly = true, DBHub refuses INSERT, UPDATE, DELETE and DDL. ${DATABASE_URL} is read from the environment, so the connection string never sits in the file.
for await (const msg of query({
prompt: "Which five products had the most returns last month, and what share of sales does that represent?",
options: {
mcpServers: {
reporting: { command: "npx", args: ["-y", "@bytebase/dbhub", "--config", "dbhub.toml"] },
},
allowedTools: ["mcp__reporting__execute_sql"],
},
})) {
if (msg.type === "result" && msg.subtype === "success") console.log(msg.result);
}
Claude discovers the schema, writes the SQL and explains the result. I would still point it at a read replica rather than production.
When things go wrong
Reading server status
Every query starts with an init system message listing each MCP server with a status of pending, connected, failed, needs-auth or disabled. Servers from options.mcpServers that connected during the first-turn wait show connected.
Do not treat pending as failure on its own. It can mean the server is still connecting, that its tools came from the cache and it will connect on first use, or that its deadline passed (in which case it shows pending or failed depending on timing). Treat failed and needs-auth as unusable:
async for msg in query(prompt="Reconcile last week's payouts", options=opts):
if isinstance(msg, SystemMessage) and msg.subtype == "init":
broken = [s for s in msg.data.get("mcp_servers", []) if s.get("status") in ("failed", "needs-auth")]
if broken:
log.warning("MCP servers unavailable: %s", broken)
A server failing to connect never raises an exception; you have to check status. A single-message query() raises only after an error result, or if Claude Code itself cannot start.
Status can also change mid-session. If a remote server's connection drops, it goes back to pending while Claude Code reconnects, so a later status check can show pending for a server that was connected. After five failed reconnection attempts it becomes failed (or needs-auth if it needs authorising again). Retry manually with reconnectMcpServer() in TypeScript or ClaudeSDKClient.reconnect_mcp_server() in Python.
Server shows failed
Usual causes:
- Missing environment variables. Check the server's
envhas everything it expects. - Not installed. For
npxservers, confirm the package name and that Node.js is on thePATHof the process running your agent. - Bad connection string. For database servers, check the format and that the database is reachable.
- Network. For remote servers, confirm the URL is reachable and no firewall is in the way.
Claude sees the tools but never calls them
Almost always a permission issue. Add mcp__<server>__* (or the specific tool names) to allowedTools.
A tool is missing from your SDK server
In TypeScript, if a tool's input schema cannot be converted to JSON Schema, createSdkMcpServer() leaves that tool out of its listing and emits a warning. Under Node.js it is a process warning with code CLAUDE_SDK_MCP_TOOL_SCHEMA_UNCONVERTIBLE, beginning Tool "<name>" on SDK MCP server "<server>" was left out of the server's tool list, because its input schema cannot be converted to JSON Schema, followed by the conversion error and advice. Before TypeScript Agent SDK v0.3.286 a single bad schema made the whole server's listing fail silently.
Timeouts
| Variable | Controls | Default |
|---|---|---|
MCP_TIMEOUT | How long a server has to connect (milliseconds) | 30 seconds |
MCP_TOOL_TIMEOUT | How long a running tool call may take |
For slow-starting servers, raise MCP_TIMEOUT, use a lighter server if one exists, warm the server up before starting the agent, or check its logs for what is slow. In TypeScript you can also set a per-server tool-call limit by passing timeout to createSdkMcpServer().
Output too large
The SDK uses the same output limits as Claude Code. A successful, image-free result over 25,000 tokens is saved to a file, and Claude receives an error naming that file so it can read it in pieces. Change the limit with MAX_MCP_OUTPUT_TOKENS. Separately, unless the tool declares anthropic/maxResultSizeChars, any successful text result over 50,000 characters is saved to a file regardless of the token limit. MCP in Claude Code covers how a server declares that annotation.