How to Build an MCP Server with Claude Code: A Step-by-Step Guide (2026)
Building an MCP server with Claude Code means writing a small service that exposes tools/resources over the Model Context Protocol, then plugging it straight into Claude Code with claude mcp add. The path has three steps: (1) design and write your tool in Python or TypeScript, (2) pick a transport (stdio is the default for local), and (3) wire it into Claude Code and test. There are two ways to do it: write it by hand to understand what is really happening, or let Claude Code scaffold it for you. This guide walks through both.
· By Jasmine - a dev who uses Claude Code daily and has hand-written and shipped a few internal MCP servers for my team.
What is an MCP server? (quick recap)
An MCP server is a small process that exposes three kinds of capability - tools (functions an agent can call), resources (data it can read), and prompts (reusable prompt templates) - to an LLM over the Model Context Protocol. Put another way, it is a standardized "adapter" between Claude Code and the outside world: an internal API, a database, the filesystem, or any service you want Claude to reach in a controlled way.
The nice thing about MCP is that it is an open standard: write the server once and use it across many hosts (Claude Code, Claude Desktop, and other clients). This guide will not deep-dive the concept - that belongs to the what is MCP article. Here we focus on writing a server yourself and wiring it straight into Claude Code, the part the official docs usually leave disconnected.
When do you actually need to write your own MCP server?
Before you type a single line of code, ask: has someone already written this server? A lot of common needs already have first-party or community servers - GitHub, Playwright, Sentry, filesystem, and more. Plugging in an existing one is always faster than building new.
| Situation | What to do |
|---|---|
| Work with GitHub, drive a browser, read Sentry errors | Use an existing server - just claude mcp add |
| Your company's internal API, not wrapped by anyone yet | Write your own MCP server |
| A private database with a specific schema | Write your own (control the queries and permissions) |
| A multi-step workflow spanning several systems | Write your own, packaged as a workflow |
| Just need to read a few local files | Use the existing filesystem server |
My rule of thumb: write your own MCP server when the data or logic is yours and no standard adapter exists yet. Do not rewrite something other people already do well.
Before you start
A short checklist so you do not stumble halfway through:
- Claude Code installed and signed in (a Pro/Max plan or an API key both work).
- Runtime: Node.js 18+ (for TypeScript) or Python 3.10+ (for Python).
- Pick an SDK:
FastMCP/ the Python SDK if you are comfortable in Python; the TypeScript SDK (@modelcontextprotocol/sdk) if you live in Node. - A concrete goal: for example, "a tool that looks up an order from our internal API." Do not start with a generic, do-everything server.
If you are new to Claude Code, read what is Claude Code first to understand how to run a session and grant permissions.
Option 1 - Write the MCP server by hand
Doing it manually once helps you understand what actually happens when Claude calls a tool. After that, you can automate freely. The five steps below go from design to a working test.
Step 1 - Design tools around workflows, not around endpoints
The most common mistake is mapping each REST endpoint one-to-one to a tool. The result is an agent that has to call five tools to do one thing, and it easily loses the thread. Design agent-centric instead:
- Consolidate operations by intent: a single
get_order_summarytool returns everything at once instead of forcing the agent to stitch togetherget_order+get_customer+get_items. - Return output a human or an agent can read: clear field names, not internal codes.
- Write error messages that "teach" the agent: report the error with a hint for fixing it, not just a stack trace.
These principles come from the best practices baked into the ak-mcp-builder skill - the part many generic tutorials skip.
Step 2 - Scaffold the project and install the SDK
Name things clearly so they are obvious later: Python uses {service}_mcp, TypeScript uses {service}-mcp-server.
Python (FastMCP):
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "mcp[cli]" # or: pip install fastmcp
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders_mcp")
if __name__ == "__main__":
mcp.run() # defaults to the stdio transport
TypeScript (MCP SDK):
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({ name: "orders-mcp-server", version: "1.0.0" });
const transport = new StdioServerTransport();
await server.connect(transport);
Step 3 - Write your first tool (with a runnable example)
A good tool has three parts: a tight input schema, a clear description (the agent reads it to know when to call the tool), and tool annotations that describe behavior. Here is an order-lookup tool:
Python:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders_mcp")
@mcp.tool(
annotations={
"readOnlyHint": True, # read-only, does not change data
"idempotentHint": True, # same result when called again
"openWorldHint": True, # calls an external system
}
)
def get_order_summary(order_id: str) -> str:
"""Look up a summary of one order by its order ID.
Use when the user asks about the status/total/customer of a specific order."""
order = fetch_order(order_id) # calls your internal API
if order is None:
return f"Order '{order_id}' not found. Double-check the ID (format ORD-xxxxx)."
return (
f"Order {order['id']} | Customer: {order['customer']} | "
f"Status: {order['status']} | Total: ${order['total']:,}"
)
TypeScript (using Zod for the input schema):
import { z } from "zod";
server.registerTool(
"get_order_summary",
{
description: "Look up a summary of one order by its order ID.",
inputSchema: { order_id: z.string().describe("Order ID, format ORD-xxxxx") },
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
},
async ({ order_id }) => {
const order = await fetchOrder(order_id);
const text = order
? `Order ${order.id} | Customer: ${order.customer} | Status: ${order.status}`
: `Order '${order_id}' not found.`;
return { content: [{ type: "text", text }] };
}
);
Sample output when Claude calls the tool: Order ORD-10231 | Customer: Alex Nguyen | Status: In transit | Total: $540. One caveat: annotations are only hints for the host, not a security mechanism - do not rely on readOnlyHint to block writes.
Step 4 - Choose a transport (stdio / HTTP / SSE)
The transport decides how the host talks to your server. Choose based on the situation rather than defaulting blindly:
| Transport | Use when | Pros / cons |
|---|---|---|
| stdio | Server runs locally, one client (Claude Code on your machine) | Simplest, no network needed · single client only |
| HTTP (streamable) | Remote server, many clients, deployed on your own infrastructure | Shareable, scalable · you must handle auth/OAuth |
| SSE | You need to push real-time events (gradually being replaced by streamable HTTP) | Good streaming · the older approach |
Default for local + Claude Code: use stdio. Only move to HTTP when you need to share the server with several people or deploy it remotely.
Step 5 - Run and test it the right way
This is where a lot of people trip up - and few tutorials warn about it:
- The server is a long-running process. Running
python server.pydirectly will seem to "hang" the terminal because it is waiting for input over stdio - that is normal, not a bug. For a quick check without getting stuck, usetimeout 5s python server.py, run it in tmux / a separate pane, or use an eval harness. - With stdio: never log to stdout. stdout is the protocol channel - a stray
print()there breaks the JSON-RPC stream and the server "dies" in a confusing way. Log to stderr instead (Python:print(..., file=sys.stderr)or theloggingmodule). - Check that it compiles first: Python
python -m py_compile server.py; TypeScriptnpm run build.
The most reliable way to test is eval-driven: write a script that calls each tool with sample input and asserts on the output, running it in a child process with a timeout. That way you catch schema and protocol bugs immediately, instead of waiting until you plug into Claude Code and discover a tool that silently returns nothing.
Option 2 - Let Claude Code write the MCP server for you
Once you understand the structure from Option 1, you do not need to retype boilerplate every time. This is the fun "meta-angle": use Claude Code itself to write an MCP server for Claude Code.
A prompt pattern I like, broken into pieces so the agent stays on task:
Write a Python MCP server named orders_mcp using FastMCP.
- Tool get_order_summary(order_id) calls our internal API at BASE_URL (read from env).
- Tight input schema, clear description, readOnlyHint/idempotentHint annotations.
- Log to stderr, NOT stdout. Transport stdio.
Then write a test script that uses timeout so it does not hang, and explain how to wire it into Claude Code.
Claude Code will scaffold the project, write the tool + schema, and, if you ask, write the test step too. Your main job is to review it - the agent handles the repetitive parts. Tip: ask the agent to state its assumptions (paths, env var names) before writing, and have it run py_compile / npm run build itself to confirm the code compiles right there in the session. That way you get a server that has already passed a basic check instead of a pile of untested code.
If you build a lot of servers, it is worth using a ready-made skill instead of remembering every best practice yourself. The Engineer Kit from AgentKit (the ak-mcp-builder skill) ships a multi-phase MCP-server build process (research -> implement -> review -> eval) that packages the tool-design best practices and an eval harness for testing - so you do not have to keep all the boilerplate and the stdio / long-running traps above in your head. For details, see what's inside AgentKit's Engineer Kit.
One clarification to avoid confusion: "AgentKit" here is the kit of skills for Claude Code (agentkit.best, the ak CLI), which is different from OpenAI's "AgentKit" product. My approach: do it by hand once to understand it, then use a skill for speed.
Wiring the MCP server into Claude Code
Now that you have a server, plug it into Claude Code. The core command is claude mcp add.
Local stdio server - pass the command after the -- separator:
claude mcp add orders -- python /path/to/server.py
# or a built TypeScript server:
claude mcp add orders -- node /path/to/dist/index.js
Remote server (HTTP):
claude mcp add orders --transport http https://mcp.company.com/orders
Check the connection:
claude mcp list # shows: orders ✔ Connected
claude mcp get orders # view a server's full configuration
Scope decides where the server is available: local (just you, this project only), project (committed for the whole team), user (all of your projects). For a shared team server, hand-write a .mcp.json at the repo root and commit it:
{
"mcpServers": {
"orders": {
"command": "python",
"args": ["server.py"],
"env": { "BASE_URL": "https://api.internal.company.com" }
}
}
}
After editing .mcp.json, remember to restart the Claude Code session so it reloads.
Common problems and how to fix them
| Symptom | Common cause | Fix |
|---|---|---|
Failed to connect | Wrong command/path/URL | Run claude mcp get <name> to inspect; test the command standalone |
| Server "dies" right at startup | Logging to stdout, breaking the stdio protocol | Move all logging to stderr |
| Tools do not show up in Claude | Missing env var / API key | Pass via --env KEY=value or declare it in .mcp.json |
| Timeout on first run (npx downloading a package) | Dependency download takes longer than the default timeout | Set MCP_TIMEOUT=60000 at startup |
Edited .mcp.json but nothing changed | Session has not reloaded the config | Restart the Claude Code session |
Best practices for writing MCP servers
Wrapping up what is worth remembering (much of it drawn from ak-mcp-builder):
- Name tools with the
{service}_{action}_{resource}pattern (snake_case), with a service prefix to avoid collisions when several servers are plugged in. - Support both JSON and Markdown for output - agents read structured data well, humans read the Markdown version more easily.
- Paginate tools that return a lot of data: use
limit,has_more,next_offsetinstead of returning thousands of rows. - Cap output length (rule of thumb ~25,000 characters) and truncate with guidance ("N more results, use offset...").
- Make error messages actionable: say exactly what went wrong and how to fix it.
- Security: validate input, keep API keys in env (never hardcode), and do not leak internal errors / stack traces to the agent. Remember that annotations are only hints, not a substitute for real access control.
Want to automate more of your dev workflow? See how to create a custom skill for Claude Code and use subagents in Claude Code to split the build/test work.
Frequently asked questions (FAQ)
What language should I write an MCP server in?
The most common choices are Python (FastMCP / Python SDK) and TypeScript (@modelcontextprotocol/sdk). MCP has SDKs for other languages too, but for Claude Code, Python or TS are the fastest options with the most examples.
How is this different from connecting to an existing server?
Connecting to an existing server just needs claude mcp add pointed at one someone else already wrote. Writing your own server is for when you need to expose your own logic/data (an internal API, a DB) that nobody has built an adapter for yet.
Can Claude Code write an MCP server itself?
Yes. Describe the tool, schema, and transport you want, and Claude Code will scaffold the project, write the tool, and even a test script. You just review it. The ak-mcp-builder skill packages this process together with best practices.
How do I deploy a remote (HTTP) server?
Run the server with the streamable HTTP transport on your own infrastructure, then claude mcp add <name> --transport http <url>. A remote server needs to handle authentication (OAuth/token) too - unlike a local stdio server, which trusts your machine.
Do I need Claude Code Pro?
No specific plan is required. MCP works with Claude Code once you are signed in - a Pro/Max plan or an API key both work. Writing a server costs nothing beyond the model usage you are already paying for.
What is ak-mcp-builder, and is it required?
ak-mcp-builder is a skill in AgentKit's Engineer Kit that builds MCP servers through a multi-phase process with an eval harness. It is not required - you can hand-write everything as in Option 1. It just makes things faster and helps you avoid best-practice traps when building many servers.
Conclusion and next steps
To recap, there are two ways to create an MCP server with Claude Code: write it by hand (design tools around workflows -> install the SDK -> write the tool -> pick a transport -> test with timeout/stderr) to understand it deeply, and let Claude Code write it for you when you need speed. Either way, the crux is wiring it into Claude Code with claude mcp add or .mcp.json and confirming ✔ Connected. My advice: do it by hand once to understand it, then automate.
As a next step, try turning this build process into your own custom skill. And if you want a standardized, eval-ready MCP-server build process out of the box, take a look at AgentKit's Engineer Kit — 20% off, now $79.20 (the ak-mcp-builder skill).
Sources: Model Context Protocol - spec & build-a-server guide (versioned 2026-07-28); Claude Code docs - MCP & claude mcp add (v2.1.219). Verify the exact commands before use, since the CLI updates quickly.