Claude Code MCP servers: give your agent your tools
By Albert Nahas, 9 min read, updated
MCP (Model Context Protocol) servers give Claude Code tools it doesn't ship with: your issue tracker, your database, your error monitoring, an internal API. You add one with claude mcp add, choose whether it's just for you or shared with the repo, and from then on its tools show up in Claude's tool list as mcp__<server>__<tool>, where you can allow, deny or block them like any other tool.
Adding a server takes a minute. Deciding what it's allowed to do takes longer, and that's the part people skip. Every MCP server is code you didn't write, sending text into your agent's context and taking actions with your credentials. I treat each one like a new dependency with production access, because that's what it is.
How MCP fits into Claude Code
An MCP server is a small program that exposes three kinds of things to a client:
- Tools Claude can call, like
create_issueorrun_query. - Resources you can pull into a prompt with an @ mention, in the form
@server:protocol://resource/path. - Prompts that become slash commands, listed as
/servername:promptname (MCP)and also runnable as/mcp__servername__promptname.
Claude Code is the client. It starts or connects to each configured server at session start and passes the tools to the model. With tool search, which is on by default on current models, only tool names and server instructions load upfront, and full tool definitions load when Claude needs them. So adding a fifth server doesn't eat your context the way it used to. The ENABLE_TOOL_SEARCH environment variable controls this if you want everything loaded upfront (false) or a threshold mode (auto).
Adding a server: stdio vs HTTP
There are two transports you'll actually use.
HTTP is for remote servers, and it's what the docs recommend for anything hosted:
claude mcp add --transport http notion https://mcp.notion.com/mcp
claude mcp add --transport http secure-api https://api.example.com/mcp \
--header "Authorization: Bearer your-token"
stdio runs a local process and talks to it over stdin and stdout. Everything after -- goes to the server untouched, so the server's own flags don't get parsed as Claude Code's:
claude mcp add --env AIRTABLE_API_KEY=YOUR_KEY --transport stdio airtable \
-- npx -y airtable-mcp-server
Two details from that example bite people. --env can repeat, and you need another option (like --transport stdio) between the last --env and the server name. And without the --, a server flag like --port 8080 would be read as a Claude Code option.
SSE still works but is deprecated. If a server offers HTTP, use HTTP.
The rest of the management commands are what you'd guess:
claude mcp list
claude mcp get notion
claude mcp remove notion
claude mcp add-json example '{"command":"npx","args":["-y","@example/mcp-server"]}'
Inside a session, /mcp shows each server's status and handles login.
Scopes: local, project and user
The --scope flag (or -s) decides who gets the server and where it's stored:
| Scope | Loads in | Shared with the team | Stored in |
|---|---|---|---|
local (default) | This project only | No | ~/.claude.json |
project | This project only | Yes, through git | .mcp.json in the repo root |
user | All your projects | No | ~/.claude.json |
claude mcp add --transport http shared-server --scope project https://example.com/mcp
When the same server name is defined in several places, Claude Code uses one whole entry and doesn't merge fields. Local wins over project, project over user, then plugin servers, then claude.ai connectors. A server your organization pushes through managed settings beats all of them.
My placement rule matches what I do with skills. A server the whole team needs to work on this repo goes in project. Anything that carries my personal credentials or habits goes in local or user.
The .mcp.json file
Project scope writes to .mcp.json, which you commit:
{
"mcpServers": {
"api-server": {
"type": "http",
"url": "${API_BASE_URL:-https://api.example.com}/mcp",
"headers": {
"Authorization": "Bearer ${API_KEY}"
}
},
"db-tools": {
"command": "node",
"args": ["./tools/db-server.js"],
"env": { "DB_URL": "${DB_URL}" }
}
}
}
Variables expand in command, args, env, url and headers. ${VAR} takes the value and ${VAR:-default} falls back when it's unset. An unset variable with no default stays as literal text and you get a warning, which is how a missing secret usually shows up.
The gotcha I'd put on a sticky note: an entry with a url and no type is a configuration error, because Claude Code reads it as stdio. Always write "type": "http" for remote servers.
There's also a nice safety detail. In a remote server's url and headers, credential variables like ANTHROPIC_API_KEY read as empty, so a server named in someone else's .mcp.json can't quietly collect your Anthropic key. If you really want to pass one, copy it into a variable with your own name.
Authentication
Most hosted servers use OAuth. Add the server, run /mcp in a session and finish the login in the browser. From a shell, claude mcp login <name> does the same, and "Clear authentication" in /mcp revokes it.
For servers that need fixed OAuth settings there are --client-id, --client-secret (prompted with masked input, or MCP_CLIENT_SECRET in CI) and --callback-port. For short-lived tokens, a headersHelper command prints a JSON object of headers when Claude Code connects, so the token never sits in a config file.
Keep secrets out of .mcp.json. Reference them as ${VAR} and let each person's environment fill them in.
Permission rules for MCP tools
MCP tools follow the same allow, ask and deny rules as built-in tools, evaluated deny first, then ask, then allow. The names work like this:
mcp__githubmatches every tool from thegithubserver.mcp__github__*also matches every tool from that server.mcp__github__get_*matches only itsget_tools.mcp__github__create_issuematches one tool.
{
"permissions": {
"allow": ["mcp__github__get_*", "mcp__sentry__*"],
"ask": ["mcp__github__create_issue"],
"deny": ["mcp__github__delete_*", "mcp__postgres__execute_sql"]
}
}
A few rules that aren't obvious. Allow globs only work after a literal mcp__<server>__ prefix, so "mcp__*" as an allow rule is skipped and approves nothing, while as a deny rule it blocks every MCP tool. Dots and spaces in a server name become underscores, so a server called my.server is mcp__my_server__*. And you can't match MCP tool parameters in a settings file: an mcp__ rule with parentheses is dropped. To block a tool based on its arguments, use a PreToolUse hook with a matcher like mcp__postgres__.*, which reads the call as JSON and exits 2 to stop it. The hooks guide has the pattern.
If you're writing a server, you can also mark a tool as needing approval on every call by setting _meta["anthropic/requiresUserInteraction"] to true in its tools/list entry.
When to write your own small server
Most of the time you don't need one. If Claude can already do the job with a CLI through Bash (gh, psql, kubectl), a skill that documents the commands is cheaper and easier to audit than a server.
I write a server when one of these is true:
- The API has no good CLI, and wrapping it in a dozen curl recipes would bloat a skill.
- I want a narrower surface than the CLI gives. A server that exposes
get_order(id)andrefund_status(id)is safer than handing an agent a database connection string. The tool list is the permission boundary. - Several tools or teammates need the same integration. One server works in Claude Code, Claude Desktop and any other MCP client.
Keep it small: a handful of tools with clear names, descriptions that put the important words first (tool search reads the start), and outputs that fit. Claude Code warns when a tool returns more than 10,000 tokens and cuts output at 25,000 by default. MAX_MCP_OUTPUT_TOKENS raises that, but a tool that returns a page of results and a cursor is the better fix.
Security: least privilege and tool poisoning
The docs put it in one line: "Verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection risk."
Two risks are worth naming. The first is prompt injection through tool output. A server that reads issues, emails or web pages returns text written by strangers, and that text lands in Claude's context next to your instructions. The second is tool poisoning, a known MCP risk where a malicious or compromised server hides instructions in its tool descriptions or changes them after you approved it. Claude reads those descriptions to decide what to call, so they're effectively part of your prompt.
What I do about it:
- Least privilege at the credential. Give each server a token scoped to what it needs. A read-only database user, a GitHub token without admin rights. Permission rules are the second wall, not the first.
- Deny the destructive tools by name. Anything that deletes, sends or pays goes in
denyorask, even if you trust the server. - Don't mix untrusted input with powerful tools. A session that reads public issues shouldn't also have a tool that pushes to main.
- Watch headless runs. Interactive sessions ask before using project servers from
.mcp.json, butclaude -pruns and Agent SDK sessions load them without asking. In CI, pass--strict-mcp-configwith--mcp-configso only the servers you list load, or--bareto skip MCP entirely. The scheduling guide covers the rest of an unattended setup. - Pin and review. Pin the server package version and treat an upgrade like any dependency change. If someone else's repo ships a
.mcp.json, read it before approving.
What I'd do next
Start with one server you'd use daily, at local scope, with an allow list for its read tools and nothing else. Live with it for a week before you add a second. Then, if agents will run it unattended, put the deny rules and a guard hook in place first, and write down what the agent may never do in its charter.
Questions people ask
How do I add an MCP server to Claude Code?
Run claude mcp add. For a remote server: claude mcp add --transport http name https://example.com/mcp. For a local process: claude mcp add name -- npx -y some-mcp-server, where everything after -- is the server's own command. Check it with claude mcp list or /mcp inside a session.
What is the difference between local, project and user scope?
Local, the default, loads the server only in the current project and stores it privately in ~/.claude.json. Project stores it in .mcp.json in the repo so the team shares it. User loads it in every project on your machine, also from ~/.claude.json.
Where is .mcp.json and what goes in it?
It sits in the project root and holds an mcpServers object. Remote entries need type http and a url; local entries need a command, args and optional env. Use ${VAR} for secrets so nobody commits a token.
How do I allow or block a specific MCP tool?
Use permission rules with the mcp__server__tool name. mcp__github__* covers every tool from the github server, mcp__github__get_* covers its get tools, and a deny rule on mcp__github__delete_issue blocks one tool.
Should I use stdio or HTTP for an MCP server?
Use HTTP for hosted servers; it's the recommended transport for remote servers and SSE is deprecated. Use stdio for a server that runs as a local process, such as one that reads files or talks to a local database.
Are MCP servers safe to use with Claude Code?
Only as safe as the server and the credentials you give it. Tool output and tool descriptions both reach Claude's context, so untrusted content can carry prompt injection. Use scoped tokens, deny destructive tools, and restrict which servers load in headless runs.