MCP Servers: Give Claude Code New Powers
Databases, browsers, design tools: plug external systems straight into your agent.
In Part 4 we made Claude Code obey our rules on every action. Hooks now lint our commits and block dangerous commands automatically. But our side project still lives in a bubble. Claude Code can read our files and run our scripts, yet it cannot touch our database, browse our docs, or check our deployment status. It knows only what we paste into the chat.
MCP servers change that. They are external programs that speak a standardized protocol, each one granting Claude Code a specific new capability. Connect a PostgreSQL server and Claude Code can query your schema. Connect a browser server and it can inspect rendered pages. Connect a GitHub server and it can open pull requests. Under the hood, all of this runs over JSON-RPC 2.0 messages shuttled across either a local subprocess pipe or an HTTP connection, with a strict lifecycle that negotiates capabilities before any tool gets invoked.
Here is the part that surprises most engineers: the LLM (large language model) never talks to these servers directly. Claude Code sits in the middle as the host. It discovers what each server can do, presents those capabilities to the model as function descriptions, and then routes the model's tool selections back to the right server. The model sees a menu of typed, schema-validated operations. It has no idea whether the database lives on localhost or in a cloud VPC. That separation is the entire point.
What exactly is the Model Context Protocol?
The Model Context Protocol is a wire standard for connecting AI applications to external tools and data sources. Think of it as USB-C for agents. Before MCP, every AI tool had to build bespoke integrations for every external system. Claude Code would need custom code to talk to PostgreSQL, different custom code for GitHub, and yet more for a file system. Each integration meant new authentication logic, new serialization formats, and new error handling.
MCP replaces that with one protocol that every server implements and every host understands. Write a server once, and any MCP-compatible host can use it. Claude Code, Claude Desktop, Cursor, and other tools all speak the same language.
The protocol defines three roles. The host is the application the human interacts with. In our case, that is Claude Code. Inside the host, one or more clients manage individual connections to servers. Each client handles exactly one server connection, maintaining the transport, the lifecycle, and the message serialization. The server is the program that exposes capabilities: tools for actions, resources for data, and prompts for reusable instruction templates. source
Claude Code acts as the host. When you add an MCP server, Claude Code creates a client for it, initializes the connection, discovers what the server offers, and makes those offerings available to the language model. You never write glue code. You configure a server, and Claude Code gains a new power.
How does Claude Code discover what an MCP server can do?
Every MCP connection begins with a handshake. The client sends an initialize request that declares its protocol version and its own capabilities. The server responds with its supported version and a capabilities object that lists exactly what it provides: whether it has tools, resources, prompts, or experimental features like tasks. source
Only after this handshake succeeds does the client ask for specifics. It sends tools/list to get every tool the server exposes, each with a name, a natural language description, and a JSON (JavaScript Object Notation) Schema for its parameters. It sends resources/list to discover read-only data the server can provide. It sends prompts/list to find reusable instruction templates. source
Claude Code takes all of this metadata and injects it into the model's system prompt. The model now sees something like: "You have access to a tool called run_sql_query that accepts a query string and returns rows. Use it when the user asks about the database." The model never sees the server. It sees function signatures with descriptions, exactly like the tool definitions you would write for any LLM function-calling API (application programming interface).
This discovery step is what makes MCP plug-and-play. Add a new server, restart Claude Code, and the model immediately knows about the new capabilities. No code changes. No prompt engineering.
What happens on the wire when Claude Code invokes a tool?
Let us trace a real call. You ask Claude Code: "Find all tables that reference the users table." The model decides to use the run_sql_query tool from your database MCP server. Claude Code constructs a JSON-RPC request. It looks like this:
This is standard JSON-RPC 2.0. Every message has a method name, an optional ID for request-response correlation, and a params object. source
The client serializes this to UTF-8 and sends it over the transport. If the server runs locally via stdio, the message gets written to the server's standard input, followed by a newline. The server reads it, parses it, validates the arguments against the tool's input schema, and executes the query. source
The server responds with a JSON-RPC result:
Claude Code receives this, correlates it to the original request by the id field, and injects the result back into the model's context as a tool result message. The model then continues reasoning with the new information. It might say: "The orders, profiles, and subscriptions tables reference users. Let me generate a migration to drop those foreign keys."
The entire loop is synchronous from the model's perspective. It selects a tool, waits for the result, and continues. But under the hood, the transport can be streaming. The Streamable HTTP transport supports Server-Sent Events for long-running operations, so a tool can send progress updates before the final result arrives. source
How do the two transports differ, and when does each matter?
MCP defines two standard transports: stdio and Streamable HTTP. They carry the same JSON-RPC messages. The difference is where the server lives and how the connection is established.
In stdio mode, Claude Code launches the server as a child process. The server reads JSON-RPC from stdin and writes responses to stdout. Stderr is reserved for logging. This is the simplest model. It works for local tools like a filesystem server that needs access to your project directory. Configuration might look like this in Claude Code:
Claude Code spawns the process, connects to its stdio pipes, and the protocol runs over those pipes. No network. No authentication beyond the OS process boundary. source
Streamable HTTP is for remote servers. The server runs as an independent process, often on another machine, and exposes an HTTP endpoint. Each JSON-RPC message is a separate HTTP POST to that endpoint. For server-to-client messages, the server can use standard HTTP responses or open an SSE stream for multiple messages over one connection. source
This is how Claude Code connects to third-party services. A GitHub MCP server might run as a cloud service. You configure it with a URL and an API key. Claude Code sends HTTP POST requests to that URL, authenticating with a bearer token in the headers. The protocol messages inside those HTTP bodies are identical to the stdio case.
For our side project, we will use stdio for a local SQLite inspector and Streamable HTTP for a hosted browser automation server. The model does not know or care which transport each server uses. That is the beauty of the abstraction.
What primitives can a server expose, and what is each one for?
MCP servers offer four kinds of primitives. Tools, resources, and prompts are the established ones. Tasks are experimental.
Tools are executable functions with side effects. They are model-controlled, meaning the LLM decides when to call them. Each tool has a name, a description, and an inputSchema written in JSON Schema. The server validates every invocation against that schema. Tools can return text or structured data in a content array. If something goes wrong at the application level, the server sets isError: true in the result rather than returning a protocol-level error. This lets the model see the error and potentially recover. source
Resources are read-only data. They represent things the model can read for context: files, database records, API responses, documentation pages. Resources are discovered with resources/list and fetched with resources/read. Unlike tools, they have no side effects. A server might expose every markdown file in your project docs as a resource. The model can pull them in when it needs context about your conventions. source
Prompts are reusable instruction templates. They are user-controlled, meaning the human explicitly selects them from a menu rather than the model deciding to invoke them. A prompt includes a name, a description, a list of arguments, and a sequence of messages that form the prompt content. Think of them as saved workflows. A server for a design tool might offer a prompt called "Audit accessibility" that includes a pre-written system message and parameterized user message asking the model to check color contrast and ARIA labels. source
Tasks wrap long-running operations. They are experimental and not yet widely deployed. A task lets a server start an operation, report progress, and deliver the result later. This matters for operations that take minutes or hours, like running a full test suite or training a small model. source
For our side project, we will lean on tools the most. A database server exposes tools for querying and migrating. A browser server exposes tools for navigating pages and taking screenshots. Resources will serve our project documentation. Prompts will capture our common workflows, like "Generate a new CRUD endpoint."
How does Claude Code handle permissions and safety?
MCP itself is deliberately agnostic about permissions. The protocol does not define an approval mechanism or a security policy language. That responsibility falls entirely on the host. source
Claude Code implements its own permission layer on top of MCP. When a tool invocation would modify state, delete data, or access sensitive resources, Claude Code prompts the human for approval before sending the tools/call request. This is not part of the protocol. It is a host-side gate that sits between the model's tool selection and the actual JSON-RPC call.
This design is intentional. The protocol's authors wanted hosts to have full control over their security model. A host aimed at enterprise use might integrate with SSO and audit logging. A host for personal use might simply ask "Allow this?" in a dialog. The server does not need to know which model is used. It just receives validated requests and executes them.
The practical implication for our side project: when Claude Code wants to run a destructive SQL query through our database MCP server, we will see an approval prompt. We can inspect the exact query before it runs. This is the same hook-like safety net we built in Part 4, now extended to external systems.
One common mistake is treating the server's inputSchema as a security boundary. It is not. The schema validates that arguments are well-formed, but it does not enforce authorization. A server that exposes a run_sql_query tool should still apply its own access controls, such as read-only database connections or row-level security. The schema says "this parameter must be a string." It does not say "this user may only query the public schema."
Why JSON-RPC instead of gRPC or REST?
The choice of JSON-RPC 2.0 is one of the most deliberate decisions in the MCP specification. gRPC would offer stronger typing and better performance. REST would fit existing HTTP infrastructure more naturally. The authors chose JSON-RPC for three reasons.
First, simplicity of implementation. A working MCP server in Python is about 50 lines of code. You read newline-delimited JSON from stdin, parse it, dispatch on the method name, and write JSON back to stdout. No protobuf compiler. No HTTP routing framework. No streaming infrastructure unless you want it. This low barrier to entry was essential for the ecosystem to grow quickly. source
Second, transport independence. JSON-RPC does not assume HTTP. The same message format works over stdio, over HTTP POST, over WebSockets, or over any future transport. The spec defines how to carry JSON-RPC over stdio and Streamable HTTP, but the message format stays the same. gRPC ties you to HTTP/2 and protobuf. MCP wanted to run inside a subprocess pipe as easily as over a network.
Third, the tool-calling ecosystem already speaks JSON Schema. LLM function-calling APIs from Anthropic, OpenAI, and others describe tools using JSON Schema for parameters. MCP tools use the same JSON Schema. The mapping from an MCP tool definition to a Claude tool definition is nearly one-to-one. Using gRPC would require translating protobuf schemas into JSON Schema, adding friction and potential mismatches.
The tradeoff is that JSON-RPC lacks built-in streaming, strong typing, and service discovery. MCP layers streaming on top with SSE for the HTTP transport and with notifications for server-to-client events like tools/list_changed. It layers discovery on top with the initialize handshake and the list methods. It layers typing on top with JSON Schema. These are pragmatic additions that keep the core simple while meeting the needs of agentic workflows.
How do I add an MCP server to my Claude Code project?
Claude Code provides two paths for adding MCP servers. The CLI (command-line interface) command is the fastest:
This registers a server named my-db that runs locally via stdio. Claude Code will spawn the process when it starts and maintain the connection for the session. source
The second path is configuration files. Claude Code reads MCP server definitions from .claude/mcp.json in your project root. This is the same pattern we used for hooks in Part 4. A project-scoped config keeps server definitions versioned alongside your code:
The command form uses stdio. The type: "http" form uses Streamable HTTP. Environment variables in the config get expanded at runtime, so you can keep secrets out of the committed file.
Once configured, restart Claude Code. The servers initialize on startup. The model immediately gains access to their tools, resources, and prompts. You can verify this by asking Claude Code: "What tools do you have available?" It will list everything from all connected servers.
For our side project, we will add a SQLite MCP server pointed at our development database. Claude Code will be able to inspect schemas, run read-only queries, and generate migrations without us ever leaving the terminal.
Quick Reference
| Property | Value |
|---|---|
| Protocol | JSON-RPC 2.0 |
| Standard transports | stdio, Streamable HTTP |
| Server primitives | tools, resources, prompts, tasks (experimental) |
| Initialization method | initialize with version and capability negotiation |
| Tool discovery | tools/list returns name, description, inputSchema |
| Tool invocation | tools/call with name and arguments matching inputSchema |
| Resource discovery | resources/list returns identifiers and metadata |
| Claude Code config path | .claude/mcp.json |
| CLI add command | claude mcp add <name> -- <command> [args...] |
| Permission model | Host-enforced, not protocol-enforced |
Frequently Asked Questions
Q: Can one MCP server expose multiple tools, or should I create one server per tool?
A single server can and should expose multiple related tools. A database server might expose run_query, list_tables, and describe_table as separate tools. Grouping related capabilities into one server reduces connection overhead and lets the server manage shared resources like connection pools internally. Create separate servers only when the capabilities belong to entirely different domains or require different authentication scopes.
Q: What happens if the MCP server crashes mid-session?
Claude Code detects the transport closure. For stdio servers, the subprocess exit triggers a connection loss. For HTTP servers, a connection error or timeout does the same. Claude Code surfaces the error to the user and marks the server as unavailable. The model will see that the tool is no longer available and must adapt. You can restart the server and reconnect without restarting Claude Code by using the /mcp command to re-enable it.
Q: Can I use MCP servers that require OAuth or API keys?
Yes. For Streamable HTTP servers, you pass authentication headers in the server configuration. The headers field in .claude/mcp.json accepts static values and environment variable references. For OAuth flows, MCP defines an authorization mechanism where the server can request tokens from the client. Claude Code supports this for configured servers, though the exact UX varies by server implementation. source
Q: Does the model see raw tool results, or does Claude Code process them first?
Claude Code passes tool results directly into the model's context. The content array from the server's response becomes part of the conversation. If the server returns structured data, that structure appears in the model's input. This is why tool descriptions and output schemas matter: the model needs to understand the shape of the data it will receive to reason about it effectively. Claude Code does not transform or summarize results unless a hook or middleware is configured to do so.
Q: Can Claude Code itself act as an MCP server for other tools?
Yes. Running claude mcp serve starts Claude Code in server mode. Other MCP-compatible hosts can connect to it and invoke its capabilities as tools. This means you can wrap Claude Code's code editing, searching, and reasoning abilities into a tool that another agent or application can call. It is a way to compose agents: one host might use Claude Code as a specialized code-generation server alongside a database server and a browser server. source
Test yourself
You are building an MCP server that wraps your company's internal issue tracker. The server exposes a tool called create_ticket that accepts a title, description, and priority. A teammate argues that the inputSchema should mark priority as an enum of ["low", "medium", "high", "critical"] with a default of "medium". Another teammate says JSON Schema validation is unnecessary because the LLM will always pass valid values. Who is right, and what else should you consider?
Answer: The first teammate is right. JSON Schema validation in the inputSchema serves two purposes. First, it gives the model a precise contract. When the schema declares priority as an enum, the model sees the exact allowed values and is far less likely to hallucinate something like "urgent" or "p1". Second, it protects the server from malformed input regardless of the source. Even if a future host has a bug, or a human crafts a raw tools/call, the server rejects invalid values before they reach your issue tracker API. The default value of "medium" is also good practice: it means the model can omit priority for routine tickets and still get a valid call. Beyond the schema, you should also enforce authorization in the server itself. The schema validates shape. Your server must still check that the caller is allowed to create tickets in the target project. Never confuse schema validation with access control.
If you want this kind of breakdown every week, how real systems actually work under the hood, from agent protocols to database internals, subscribe to Internals Decoded at internalsdecoded.com. Next in this series: we will wire Claude Code into CI/CD (continuous integration and continuous delivery), so every commit gets reviewed by an agent before it ever reaches a human.
Sources
- MCP Specification: Architecture
- MCP Specification: Lifecycle
- MCP Specification: Messages
- MCP Specification: Transports
- MCP Specification: Authorization
- MCP Specification: Tools
- MCP Specification: Resources
- MCP Specification: Prompts
- MCP Specification: Tasks
- Claude Code MCP Documentation
- spec.modelcontextprotocol.io · Transports
- spec.modelcontextprotocol.io · Transports
- spec.modelcontextprotocol.io · Architecture