Claude Code and MCP, past the install command
Adding an MCP server takes one command. Deciding which servers earn their place, what they cost your context window, and which of their features you are not using is the harder part. Scopes, auth without hardcoded tokens, the cost model, the four underused capabilities, and when a hook is the better answer.
MCP is how Claude Code gets capabilities it does not ship with: your issue tracker, your database, your monitoring, your internal API. Adding one is a single command, which is why most guides stop there and why most setups end up with six servers connected and two of them used.
This post covers the parts that matter after the install: which scope to add a server in, how to authenticate without pasting a token into a file, what MCP actually costs your context window, the four capabilities almost nobody uses, and when the thing you want is a hook instead.
The install, and the flag that trips everyone
Three transports. Remote servers over HTTP, and local ones as a subprocess over stdio:
claude mcp add --transport http notion https://mcp.notion.com/mcp
claude mcp add --env AIRTABLE_API_KEY=KEY --transport stdio airtable -- npx -y airtable-mcp-server
The -- in the stdio form is not decoration. Everything after it is passed to the server command untouched, and leaving it out is the most common reason a local server fails to start. The other frequent error is hand-editing JSON and omitting the type: a config with a url and no "type": "http" will tell you so directly.
Tools arrive namespaced as mcp__<server>__<tool>, which is also how you write permission rules for them:
{ "permissions": { "rules": [ { "pattern": "mcp__github__.*", "allow": true } ] } }
Pick the scope deliberately
| Scope | Lives in | Applies to | Shared |
|---|---|---|---|
| Local (default) | ~/.claude.json |
This project, you | No |
| Project | .mcp.json at the repo root |
This project, everyone | Yes, via git |
| User | ~/.claude.json |
All your projects | No |
Precedence runs local, then project, then user, then plugin-provided servers, then claude.ai connectors.
Project scope is the one worth understanding, because it is how a team shares an integration. Commit .mcp.json, and each teammate gets a trust dialog on first use, which is the right default: a repo you cloned can otherwise start a subprocess on your machine. (claude -p and SDK sessions skip the prompt, which is worth remembering before you run untrusted repos non-interactively.)
Authentication without hardcoded tokens
Three options, in ascending order of how much I would trust them in a shared file:
OAuth, when the server supports it. /mcp in a session opens the panel to sign in; claude mcp login <name> does it from the shell, and claude mcp logout clears the credentials. You can narrow what you grant:
{ "mcpServers": { "slack": { "type": "http", "url": "https://mcp.slack.com/mcp",
"oauth": { "scopes": "channels:read chat:write search:read" } } } }
Environment variable expansion, so a committed .mcp.json references a secret without containing one. ${VAR} expands, ${VAR:-default} falls back, and a missing variable leaves the literal text plus a warning rather than failing the load:
{ "mcpServers": { "api": { "type": "http", "url": "${API_BASE_URL:-https://api.example.com}/mcp",
"headers": { "Authorization": "Bearer ${API_KEY}" } } } }
headersHelper for everything else: Kerberos, SSO, short-lived tokens. It points at a script that prints a JSON object of headers to stdout, runs fresh on every connection with a ten-second timeout, and gets the server name and URL in its environment. Two safety details worth knowing: it only runs after you have accepted the workspace trust dialog for project or local scope, and by default Claude Code strips credential-looking environment variables (anything matching TOKEN, SECRET, KEY, AUTH, PASSWORD and friends) before running it.
Whatever you choose, do not paste a bearer token into a committed .mcp.json. That file is version-controlled configuration, not a secret store.
What MCP actually costs
This is the part that decides whether your setup stays pleasant.
Tool definitions are context rent. Every tool a connected server exposes has a name, description, and schema that has to be in the model's context to be callable. A server with forty tools is a meaningful standing cost on every turn.
The mitigation is tool search, on by default with Claude 4.5 and later: definitions are retrieved when relevant instead of all loaded upfront, and unused servers are deferred rather than connected at startup. It turns off automatically in a few situations worth knowing, including a custom ANTHROPIC_BASE_URL, older models, and ENABLE_TOOL_SEARCH=false. Without it, everything connects at startup and slow HTTP servers become slow launches.
Results are capped. MCP output warns at 10,000 tokens and is limited to 25,000 by default. Raise it with MAX_MCP_OUTPUT_TOKENS if you must, but the better fix is a server that paginates. A server author can raise the per-tool text threshold with anthropic/maxResultSizeChars up to 500,000 characters; images still count against the token limit either way.
Each stdio server is a subprocess. Six connected local servers are six processes living as long as your session.
The practical rule: connect the two or three servers you use weekly, and add the rest when a task actually needs them. This is the same context-rent argument as installing plugins, and MCP servers are the most expensive thing in that category.
Four capabilities almost nobody uses
Most people use MCP for tools and stop. Servers can do three other things, and one of them is genuinely different:
Prompts become slash commands. A server's prompts show up as / commands, discovered automatically. If you maintain an internal server, this is the cheapest way to ship your team a workflow.
Resources become @ mentions. Server-exposed resources can be referenced inline the way you reference a file.
Channels push messages into your session. A server with the claude/channel capability can send a message into a running session unprompted: a CI run finished, a monitor fired, a webhook landed. Add it with claude mcp add --channels .... Everything else in MCP is pull; this is push, and it is a different integration shape from anything hooks give you.
Elicitation asks you for input mid-task. A server can request structured input with a form, or send you to a browser for an approval, then continue. An Elicitation hook can auto-respond if you want it unattended.
When it should be a hook instead
Having built against the hook system rather than as an MCP server, the split is clearer than the docs make it:
- MCP adds capability. New tools the agent can choose to call, new data it can read. The agent decides when.
- Hooks observe and gate lifecycle. They fire on events (turn started, tool about to run, turn ended, agent blocked) whether or not the model wants them to, and a
PreToolUsehook can veto. The agent does not choose.
So: "let Claude query our staging database" is MCP. "Never let a force-push run" or "tell me when the turn ends" is a hook, and no amount of MCP will make it reliable, because the model can always decline to call a tool. Channels blur the line slightly by letting a server push, but they push information; they cannot block anything.
Reliability facts worth knowing before they bite
- A long call backgrounds itself. In the main conversation, a tool call running past two minutes moves to the background, and you get the result as a task notification later. Tune with
CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS; subagent and non-interactive calls never background. - Timeouts are layered. A per-server
timeoutin.mcp.jsonis a hard wall-clock limit per call that progress notifications do not extend, and there is an idle timeout on top (five minutes for remote, thirty for stdio). - Reconnection is automatic but finite. A dropped remote server retries with exponential backoff up to five times, then shows as failed in
/mcp, where you can reconnect manually. /mcpis the status panel. Connected, needs authentication, failed, pending approval, disabled. When a tool is missing, that panel is the first stop, not the config file.
Security, briefly
An MCP server runs with your privileges and often fetches external content, which makes it a prompt injection path straight into your session. Anthropic reviews connectors for its directory against listing criteria but does not security-audit MCP servers, so the trust decision is yours. Prefer servers you or your organization wrote, keep secrets out of committed config, and remember that project-scope servers exist because someone put them in a repo. The wider threat model is in our security writeup.
Troubleshooting, most common first
- Stdio server will not start: missing
--before the command, or the binary is not onPATH. Use an absolute path. - "has a
urlbut notype": add"type": "http". - Server stuck pending approval: it came from a project
.mcp.jsonand needs an interactive session to show the trust dialog. - Tool not appearing: check server health in
/mcpfirst, then that no permission rule blocksmcp__server__tool. - claude.ai connectors missing: they load only when the active auth is a claude.ai login, so an
ANTHROPIC_API_KEYor a third-party provider in your environment suppresses them.
One more, in the other direction: claude mcp serve turns Claude Code itself into an MCP server, which is how you give another MCP client access to its tools.
The short version
- Use
--before stdio commands. Set"type"in hand-written config. - Local for experiments, project (
.mcp.json) for the team, user for your cross-project utilities. - OAuth first, environment expansion second,
headersHelperfor exotic auth. Never a literal token in committed config. - Every connected server is standing context cost. Connect what you use weekly; tool search helps but is not free.
- Prompts, resources, channels, and elicitation exist. Channels in particular are the only push channel in MCP.
- If it must happen regardless of what the model decides, it is a hook, not a server.
That last split is why our own integration went the hook route: we needed to know when a turn starts and ends every time, not when the model felt like reporting it. Unwait is built on that signal, and it is also why MCP channels caught our attention, since they are the first thing on the MCP side that can reach into a session rather than wait to be called.