MCP — the Model Context Protocol — is Claude's standard interface for talking to external tools and data sources. If you've ever wired up a REST API wrapper by hand just so an LLM could query your database, MCP replaces that whole workflow with a single server registration. This tutorial walks you through the architecture, a minimal working implementation, and the production gotchas I hit along the way.
Quick answer
- MCP is a JSON-RPC 2.0 protocol that lets Claude call tools and read resources hosted on any server you register, without writing custom API glue for each integration.
- Register a server once in
~/.claude/settings.json; Claude immediately sees every tool and resource that server exposes. - Verify a new server is healthy with
/mcpin Claude Code, then confirm tool calls return the right shape by inspecting stderr logs — never mix stdout with log output or JSON-RPC parsing breaks.
Citation-ready summary
Verified on: 2026-06-04
Definition: MCP (Model Context Protocol) is an open protocol that standardizes how a Claude host communicates with external tool providers using JSON-RPC 2.0 over stdio, SSE, or HTTP transports.
Main answer: Registering an MCP server exposes its declared tools and resources to Claude without any additional integration code on the host side. The server's inputSchema field drives parameter validation, so the more precise the schema, the fewer malformed tool calls Claude produces.
Use condition: Applies to Claude Code and Claude.ai agent environments that support MCP; transport choice (stdio vs. SSE vs. HTTP) depends on whether the server runs locally or remotely.
Key terms
MCP server — a process that implements four core endpoints (tools/list, tools/call, resources/list, resources/read) and communicates with Claude over JSON-RPC 2.0. Think of it as a typed plugin your Claude session can call at any moment.
Transport layer — the physical channel used to move JSON-RPC messages. stdio means Claude forks the server process and talks through stdin/stdout. SSE (Server-Sent Events) is a one-way HTTP stream for remote servers. Streamable HTTP adds full bidirectionality over regular HTTP.
Tool vs. resource — a tool can have side effects (write, delete, execute); a resource is read-only context Claude can pull into its working memory automatically.
inputSchema — a JSON Schema object attached to each tool declaration. Claude uses this to validate its own arguments before calling the tool, which is why a vague schema produces unreliable calls.
1. Why this matters now
Before MCP, connecting Claude to a database or third-party API meant writing a dedicated wrapper for every integration — custom fetch logic, manual parameter marshalling, one-off error handling. That approach doesn't scale past two or three integrations.
MCP shifts that burden to a server-side contract. You write the server once, declare its tools with precise schemas, and every MCP-compatible Claude host can consume them with no additional glue. For an indie developer running multiple automations, that's the difference between maintaining five scripts and maintaining one protocol.
The practical pain this solves is tool drift: when your API wrapper and your LLM's expectations fall out of sync, calls silently fail or return garbage. MCP's schema-first design surfaces that mismatch at declaration time rather than at 2 a.m. when a production flow breaks.
2. The core idea
One registered server gives Claude access to every tool and resource that server exposes, immediately and without restart. The protocol layer sits between Claude (the host) and your external system (database, filesystem, third-party API), so the host never needs to know the implementation details of the external system.
The three-layer stack looks like this:
| Layer | Role | Transport options |
|---|---|---|
| Claude host | Initiates tool calls, reads resources | — |
| MCP server | Declares and executes tools/resources | stdio, SSE, HTTP |
| External system | Database, filesystem, API | server-specific |
The host speaks JSON-RPC 2.0 to the server. The server speaks whatever protocol the external system requires. Claude never crosses that boundary directly, which keeps the blast radius of a bad tool implementation contained to that one server.
3. How to implement it
Register the server
Add entries to ~/.claude/settings.json. A local stdio server and a remote SSE server look like this:
{
"mcpServers": {
"my-db": {
"command": "node",
"args": ["/path/to/mcp-db-server/index.js"],
"env": {
"DATABASE_URL": "postgres://user:pass@localhost/mydb"
}
},
"remote-api": {
"type": "sse",
"url": "https://mcp.example.com/sse",
"headers": {
"Authorization": "Bearer ${API_KEY}"
}
}
}
}
The command + args combination tells Claude Code to fork the process and communicate over stdio. The type: "sse" entry points at a remote server. Environment variables go in the env block or are injected with ${ENV_VAR} syntax from the shell environment — useful for keeping secrets out of the config file.
Write the minimal server
Here's a single-tool MCP server in TypeScript that queries a users table:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "my-db", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query_users",
description:
"Returns rows from the users table matching an optional name filter. " +
"Does not modify data.",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", default: 10 },
filter: { type: "string" },
},
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === "query_users") {
const { limit = 10, filter = "" } = req.params.arguments ?? {};
const rows = await db.query(
`SELECT * FROM users WHERE name LIKE $1 LIMIT $2`,
[`%${filter}%`, limit]
);
return {
content: [{ type: "text", text: JSON.stringify(rows) }],
};
}
throw new Error(`Unknown tool: ${req.params.name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
The description field is what Claude reads to decide whether to call this tool. Write it like a terse docstring: state what the tool does, what it returns, and — critically — whether it has side effects. That last part matters more than most people expect.
Verify the connection
In Claude Code, run:
/mcp
You should see my-db listed with a green connected status. If it shows error, run the server command directly in a terminal and read the stderr output — that's where all diagnostic messages live.
For a remote SSE server, test the handshake before registering:
curl -N -H "Accept: text/event-stream" https://mcp.example.com/sse
If the connection drops without an initialize message, the problem is almost always CORS or authentication on the server side, not Claude's configuration.
4. What to watch in production
Logging discipline
The most common stdio server failure I've seen is stdout pollution. The JSON-RPC channel runs over stdout, so anything you console.log into it silently corrupts the message stream. Always write diagnostics to stderr:
// Correct — goes to the diagnostic channel
process.stderr.write(`[debug] executing query: ${sql}\n`);
// Breaks the server — pollutes the JSON-RPC stream
console.log("executing query:", sql);
This is the single most disorienting failure mode because the server appears to start fine but every tool call returns a parse error.
SQL injection and allowlisting
Your MCP tool is as dangerous as whatever it can reach. If the tool accepts a table name from Claude's argument and passes it directly to a query, you've handed SQL injection surface directly to the Claude session:
// Dangerous — table name is unsanitized user input
const rows = await db.query(`SELECT * FROM ${req.params.table}`);
// Safe — validate against an explicit allowlist
const ALLOWED_TABLES = ["users", "products", "orders"];
if (!ALLOWED_TABLES.includes(req.params.table)) {
throw new Error("Unauthorized table access");
}
const rows = await db.query(`SELECT * FROM ${req.params.table}`);
Apply the same logic to file paths in filesystem servers. The MCP server runs inside the Claude session's execution context, so a vulnerable tool is a vulnerable session.
Choosing the right transport
| Scenario | Transport | Reason |
|---|---|---|
| Local dev tool, script, or DB | stdio | No network overhead; Claude forks and owns the process |
| Shared remote service, multi-user | SSE or HTTP | Server stays alive independently; auth via headers |
| Bidirectional streaming | Streamable HTTP | SSE is one-way; HTTP handles both directions |
After picking a transport, decide whether each operation should be a tool or a resource. File reads, configuration lookups, and read-only API responses belong as resources — Claude can inject them into context automatically. Writes, deletes, and mutations belong as tools, which require an explicit call. Mixing the two inverts Claude's trust model and leads to unexpected automatic reads of sensitive data.
Sources and checks
Verified on: 2026-06-04
| Claim | Evidence | How to verify | Limit |
|---|---|---|---|
| MCP uses JSON-RPC 2.0 | MCP SDK source; protocol spec | Inspect wire traffic with a stderr debug logger | Protocol version may change in future SDK releases |
console.log to stdout breaks stdio parsing |
Reproducible locally by adding a log and watching tool calls fail | Add one console.log, call the tool, observe parse error |
Only applies to stdio transport; SSE/HTTP unaffected |
| SSE handshake failure signals CORS or auth | Common failure pattern from remote server logs | Run curl -N -H "Accept: text/event-stream" <url> and check response headers |
Some servers fail for other reasons (rate limits, TLS) |
/mcp command shows server status |
Claude Code common workflows (code.claude.com) | Run /mcp and check listed server states |
Only available in Claude Code, not Claude.ai |
inputSchema drives parameter validation |
MCP SDK types; CallToolRequestSchema handler receives validated args |
Pass a wrong-type argument and confirm the rejection message | Schema validation strictness depends on SDK version |
If you cannot find the official MCP SDK release notes for your version, check the CHANGELOG.md in the @modelcontextprotocol/sdk npm package before assuming behavior matches this tutorial.
FAQ
When should I use MCP?
Use MCP when you want Claude to interact with an external system more than once, or across multiple sessions. If you're writing a one-off script that calls an API and you never need Claude to touch it again, a plain function call is simpler. The moment you want repeatability, multi-tool composition, or a shared service accessible to multiple Claude sessions, MCP pays for the setup cost immediately.
What should I check before applying MCP in a production environment?
Audit what your server can reach. List every database table, filesystem path, and API endpoint the server has credentials for, then ask whether Claude needs access to all of it. Apply the allowlist pattern to anything sensitive before the server goes live. Also confirm your logging writes to stderr, not stdout — a noisy log that looked harmless in testing will silently break production tool calls.
What is the easiest way to verify that a new MCP server is working correctly?
Register the server, run /mcp in Claude Code to confirm the status shows connected, then ask Claude to call the simplest tool with known inputs and check that the output matches what you'd get by running the query or API call directly. If there's a mismatch, run the server binary manually in a terminal and read its stderr — that channel contains everything the server wants to tell you.
Closing
Registering an MCP server once and getting clean, schema-validated tool calls across every Claude session is a materially different workflow from hand-rolled API wrappers — the protocol does the plumbing, you own the business logic.
Next step: if your server grows beyond a handful of tools, consider splitting it into domain-specific servers (one for the database, one for the filesystem, one for external APIs). Smaller servers are easier to audit, easier to test in isolation, and easier to restart without disrupting unrelated Claude sessions.
🐦 Faster updates on X: @baegseungh7061
📚 More in this series: All posts
💌 Subscribe: Follow on X or grab the RSS
댓글
댓글 쓰기