How to Build a Custom MCP Server for Claude Code: Step-by-Step Guide
If you want to build a custom MCP server so Claude Code can call your own tools — weather lookup, file summary, todo manager — this tutorial gives you a complete step-by-step guide with working Python code, JSON Schema tool definitions, and real API integration in under 45 minutes.
入门 · 45 分钟 · 2026年6月14日
TL;DR
If you're searching "how to build an MCP server for Claude Code," this tutorial walks you through building a custom MCP (Model Context Protocol) server in Python in under 45 minutes. You'll create a working server with three custom tools — weather lookup, file summary, and a todo manager — then connect it to Claude Code so the AI can call your tools directly from the terminal. No prior MCP experience needed.
What You'll Learn
- What MCP is and how the client-server protocol works
- How to set up a Python MCP server with the official SDK
- How to define custom tools with JSON Schema
- How to handle tool execution requests
- How to connect your server to Claude Code and test it live
- How to debug common MCP connection issues
Prerequisites
- Python 3.10+ installed on your machine
- Claude Code CLI installed (
npm install -g @anthropic-ai/claude-code) - Anthropic API key or Claude subscription (for Claude Code)
- Basic familiarity with Python and terminal
- A text editor (VS Code recommended)
Step 1: Understand the MCP Protocol (5 min)
MCP (Model Context Protocol) is an open standard that lets AI models talk to external tools through a client-server architecture. Think of it as a universal USB-C port for AI — once your server implements the MCP spec, any MCP-compatible client (Claude Code, Cursor, Continue.dev, Zed) can discover and use your tools.
How It Works
┌─────────────┐ stdio/HTTP ┌──────────────┐
│ Claude Code │ ◄──────────────────► │ Your MCP │
│ (Client) │ JSON-RPC 2.0 │ Server │
└─────────────┘ └──────┬───────┘
│
┌──────▼───────┐
│ Your Tools │
│ (Python fns) │
└──────────────┘The client (Claude Code) spawns your server as a subprocess and communicates via stdio (standard input/output) using JSON-RPC 2.0 messages. The server responds to three key request types:
| Request | Purpose |
|---|---|
tools/list | Return all available tools with their schemas |
tools/call | Execute a specific tool with arguments |
initialize | Handshake — server declares its capabilities |
Your job: write a Python script that reads JSON-RPC messages from stdin, processes them, and writes responses to stdout. The official mcp Python package handles all the protocol boilerplate.
Step 2: Set Up the Python MCP Server (10 min)
Create a new project directory and install the MCP SDK:
mkdir my-mcp-server && cd my-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install mcp httpxNow create server.py:
# !/usr/bin/env python3
"""Custom MCP Server with three tools: weather, file-summary, todo-manager."""
import json
import os
from datetime import datetime
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationCapabilities
from mcp.server.stdio import stdio_server
# In-memory todo store (for demo; use SQLite in production)
todos = []
next_id = 1
server = Server("my-tools-server")
# ── Tool 1: Weather Lookup ──────────────────────────────────
@server.list_tools()
async def list_tools() -> list:
return [
{
"name": "get_weather",
"description": "Get current weather for a city. Returns temperature, conditions, and humidity.",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'San Francisco' or 'Tokyo'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit (default: celsius)"
}
},
"required": ["city"]
}
},
{
"name": "summarize_file",
"description": "Summarize a text file's content — returns line count, word count, and first 500 characters.",
"inputSchema": {
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Absolute path to the file to summarize"
}
},
"required": ["filepath"]
}
},
{
"name": "todo_manager",
"description": "Manage a simple todo list. Supports 'add', 'list', 'done', and 'clear' actions.",
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["add", "list", "done", "clear"],
"description": "Action to perform on the todo list"
},
"text": {
"type": "string",
"description": "Todo item text (required for 'add' action)"
},
"id": {
"type": "integer",
"description": "Todo ID to mark as done (required for 'done' action)"
}
},
"required": ["action"]
}
}
]
# ── Tool Execution ──────────────────────────────────────────
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list:
global todos, next_id
if name == "get_weather":
city = arguments["city"]
units = arguments.get("units", "celsius")
# Simulated weather data (replace with real API call)
weather_data = {
"San Francisco": {"temp": 18, "condition": "Foggy", "humidity": 82},
"Tokyo": {"temp": 28, "condition": "Clear", "humidity": 65},
"London": {"temp": 15, "condition": "Rainy", "humidity": 78},
"New York": {"temp": 24, "condition": "Partly Cloudy", "humidity": 55},
}
data = weather_data.get(city, {"temp": 22, "condition": "Unknown", "humidity": 60})
return [{
"type": "text",
"text": f"📍 {city}: {data['temp']}°{'C' if units == 'celsius' else 'F'}, {data['condition']}, Humidity: {data['humidity']}%"
}]
elif name == "summarize_file":
filepath = arguments["filepath"]
if not os.path.exists(filepath):
return [{"type": "text", "text": f"❌ File not found: {filepath}"}]
if not os.path.isfile(filepath):
return [{"type": "text", "text": f"❌ Not a file: {filepath}"}]
try:
with open(filepath, "r") as f:
content = f.read()
lines = content.split("\n")
words = content.split()
preview = content[:500]
return [{
"type": "text",
"text": f"📄 {os.path.basename(filepath)}\nLines: {len(lines)}\nWords: {len(words)}\nChars: {len(content)}\n\n--- Preview (first 500 chars) ---\n{preview}"
}]
except Exception as e:
return [{"type": "text", "text": f"❌ Error reading file: {str(e)}"}]
elif name == "todo_manager":
action = arguments["action"]
if action == "add":
text = arguments.get("text", "")
if not text:
return [{"type": "text", "text": "❌ 'text' is required for add action"}]
todos.append({"id": next_id, "text": text, "done": False, "created": datetime.now().isoformat()})
next_id += 1
return [{"type": "text", "text": f"✅ Added todo #{next_id-1}: {text}"}]
elif action == "list":
if not todos:
return [{"type": "text", "text": "📋 No todos yet."}]
lines = ["📋 Todo List:"]
for t in todos:
status = "✅" if t["done"] else "⬜"
lines.append(f" {status} #{t['id']}: {t['text']}")
return [{"type": "text", "text": "\n".join(lines)}]
elif action == "done":
tid = arguments.get("id")
if tid is None:
return [{"type": "text", "text": "❌ 'id' is required for done action"}]
for t in todos:
if t["id"] == tid:
t["done"] = True
return [{"type": "text", "text": f"✅ Marked #{tid} as done: {t['text']}"}]
return [{"type": "text", "text": f"❌ Todo #{tid} not found"}]
elif action == "clear":
count = len(todos)
todos.clear()
return [{"type": "text", "text": f"🗑️ Cleared {count} todos"}]
return [{"type": "text", "text": f"Unknown tool: {name}"}]
# ── Main Entry ──────────────────────────────────────────────
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationCapabilities(
sampling={},
experimental={},
),
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())Test that your server starts without errors:
python3 server.py --help # Should not crash
# Press Ctrl+C to stop (it runs as a long-lived process)Step 3: Connect the Server to Claude Code (10 min)
Claude Code discovers MCP servers through a configuration file. Create or edit ~/.claude/claude_desktop_config.json (or .claude.json in your project root for project-level config):
{
"mcpServers": {
"my-tools": {
"command": "python3",
"args": ["/absolute/path/to/my-mcp-server/server.py"],
"env": {}
}
}
}⚠️ Always use absolute paths in the
argsarray. Relative paths are resolved from Claude Code's working directory, which may not be what you expect.
Now restart Claude Code (or start a new session):
claudeInside Claude Code, verify your server is connected:
> /mcp
You should see my-tools listed with 3 tools. If it says "No MCP servers connected," check the troubleshooting section below.
Try your tools:
> What's the weather in Tokyo?
> Summarize the file /Users/you/project/README.md
> Add "Buy groceries" to my todo listClaude Code will automatically discover your tools and call them when relevant to the user's request.
Step 4: Add a Real API Integration (15 min)
The weather tool above uses hardcoded data. Let's upgrade it to call a real API. We'll use OpenWeatherMap's free tier:
pip install httpxReplace the get_weather handler in server.py:
import httpx
# Add your API key (get one free at https://openweathermap.org/api)
OPENWEATHER_API_KEY = os.environ.get("OPENWEATHER_API_KEY", "your-key-here")
# ... inside call_tool, replace the get_weather block:
elif name == "get_weather":
city = arguments["city"]
units = arguments.get("units", "celsius")
unit_param = "metric" if units == "celsius" else "imperial"
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.openweathermap.org/data/2.5/weather",
params={"q": city, "appid": OPENWEATHER_API_KEY, "units": unit_param},
timeout=10.0
)
if resp.status_code == 404:
return [{"type": "text", "text": f"❌ City not found: {city}"}]
elif resp.status_code != 200:
return [{"type": "text", "text": f"❌ API error: {resp.status_code}"}]
data = resp.json()
temp = data["main"]["temp"]
condition = data["weather"][0]["description"]
humidity = data["main"]["humidity"]
symbol = "°C" if units == "celsius" else "°F"
return [{
"type": "text",
"text": f"📍 {data['name']}: {temp}{symbol}, {condition.title()}, Humidity: {humidity}%"
}]Add the API key to your Claude Code config:
{
"mcpServers": {
"my-tools": {
"command": "python3",
"args": ["/absolute/path/to/my-mcp-server/server.py"],
"env": {
"OPENWEATHER_API_KEY": "your-real-api-key"
}
}
}
}Restart Claude Code and test with any city worldwide.
Common Pitfalls
-
"No MCP servers connected": Check that the
commandin your config points to the correct Python binary. Runwhich python3and use that absolute path. Also verifyserver.pyruns without import errors:python3 server.py(it should hang waiting for stdin, not crash). -
"Tool not found" errors: Make sure your
@server.list_tools()decorator is spelled correctly and returns a list of tool definitions. Each tool needsname,description, andinputSchema. -
Arguments arriving as wrong type: MCP passes all arguments as their native JSON types, but Python sometimes receives
floatwhere you expectint. Cast explicitly:tid = int(arguments.get("id", 0)). -
Server crashes silently: Add logging to stderr (not stdout — stdout is the JSON-RPC channel). Use
import sys; print("DEBUG: got request", file=sys.stderr)to debug. -
Claude ignores your tools: Claude decides whether to call a tool based on the
descriptionfield. Write descriptions as if explaining the tool to a colleague: what it does, what it returns, and when to use it. Vague descriptions like "does stuff" will be ignored. -
Path issues with
summarize_file: The filepath argument must be an absolute path accessible from the server process. Claude Code runs the server as a subprocess, so relative paths resolve from wherever Claude Code was launched. -
Module not found: mcp: Make sure you installed the package in the same Python environment that Claude Code uses. Either activate the venv before running
claude, or use the venv's Python path in the config:"command": "/path/to/.venv/bin/python3".
Next Steps
-
Add more tools: Expose your project's internal APIs, database queries, or file operations as MCP tools. Anything you can write a Python function for, Claude Code can call.
-
Use HTTP transport: For remote servers or multi-client setups, switch from stdio to HTTP/SSE transport. The MCP Python SDK supports both.
-
Explore the MCP ecosystem: Check out mcp.so for community-built MCP servers — there are servers for GitHub, Slack, Notion, databases, and more.
-
Build for other clients: Your MCP server works with any compatible client: Cursor, Continue.dev, Zed, and the Claude desktop app all support MCP. Build once, use everywhere.
-
Try Claude Agent SDK: If you want Claude to orchestrate multiple tools in a workflow, the Claude Agent SDK extends MCP with multi-step reasoning and tool chaining.
This tutorial uses Claude Code, Python, and the MCP SDK. OpenWeatherMap provides the weather data. All code examples are MIT-licensed — use them freely in your projects.
相关推荐
How to Use OpenAI Codex CLI: Build & Deploy Apps from Terminal (2026)
If you want to build and deploy full-stack applications directly from your terminal without opening an IDE, this tutorial walks you through Codex CLI installation, multi-file editing, sandbox-safe execution, and one-command deployment — all in 30 minutes with real code examples.
Copilot vs Cursor vs Claude Code 2026: Which AI Coder Wins?
Real-world tests, pricing breakdown, and SWE-bench benchmarks for all three AI coding tools
主题中心
2026 AI 编程工具全景指南
从 Copilot 改版到 Claude Code / DeepSeek 低成本方案——把分散资讯收成可搜索、可对比的工具矩阵。
进入「2026 AI 编程工具全景指南」 →赚钱视角
这个趋势怎么赚钱?
WayToClawEarn 的差异在可验证的赚钱案例,而不只是资讯。从这些复盘开始:
浏览全部案例 →相关教程
相关资讯
- Anthropic Suspends Claude Fable 5 & Mythos 5 After US Export Control Directive
- Is Google Gemini CLI Shutting Down? June 18 Deadline and Antigravity Migration Guide
- Does Xcode 27 Have Built-in AI Coding Agents? Claude, Gemini, and GPT Integration Explained
- Copilot Code Review Gets 3 Enterprise Controls: Runner Config, Content Exclusion, and Unlimited Instructions