Table of Contents
1621 words
8 minutes
Build Your Own MCP Server: A Practical Guide from Zero to One

1. Introduction#

MCP (Model Context Protocol) is a standard protocol open-sourced by Anthropic at the end of 2024, designed to enable large language models (LLMs) to access external tools and data sources in a unified way. If the LLM is the brain, MCP is the nervous system—it gives the model the ability to “do things,” not just “talk.”

In 2026, the MCP ecosystem has matured considerably: mainstream AI tools like Claude Desktop, Claude Code, Cursor, and Continue all have built-in MCP support. But more importantly, you can write your own MCP Server—exposing your company’s internal APIs, database queries, document retrieval, or even smart home control capabilities to AI in a standardized way.

This article will take you from zero to building a complete MCP Server in Python.

2. MCP Architecture at a Glance#

Before diving in, let’s understand the core architecture of MCP:

┌──────────────┐ MCP Protocol ┌──────────────┐
│ MCP Client │ ◄──────────────────► │ MCP Server │
│ (Claude) │ (stdio / SSE) │ (your code) │
└──────────────┘ └──────────────┘
│
┌────┴────┐
│ Tools │ ← Functions the LLM can call
│Resources│ ← Data the LLM can read
│ Prompts │ ← Preset prompt templates
└─────────┘

An MCP Server provides three primitives:

  • Tools: Functions the model can call, similar to OpenAI Function Calling
  • Resources: Read-only data the model can read, similar to REST API GET endpoints
  • Prompts: Predefined prompt templates that help users get started quickly

There are two communication methods:

  • stdio: Via standard input/output, suitable for local inter-process communication
  • SSE (Server-Sent Events): Via HTTP, suitable for remote services

This article will use stdio mode, which is the most common and simplest integration method.

3. Environment Setup#

3.1 Installing Dependencies#

Terminal window
mkdir my-mcp-server && cd my-mcp-server
python3 -m venv venv
source venv/bin/activate
pip install mcp

The only core dependency you need is one package: mcp, the official Python SDK provided by Anthropic.

3.2 Project Structure#

my-mcp-server/
├── server.py # MCP Server main entry point
├── tools/
│ ├── __init__.py
│ ├── weather.py # Weather query tool
│ └── database.py # Database query tool
├── resources/
│ ├── __init__.py
│ └── docs.py # Document resources
└── pyproject.toml

4. Writing Your First MCP Server#

4.1 Minimal Example#

Let’s start with a minimal MCP Server that exposes just one tool:

server.py
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
# Create Server instance
server = Server("my-first-mcp-server")
@server.tool()
async def get_current_time() -> str:
"""Get the current system time"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())

What this code does:

  1. Server("my-first-mcp-server") creates an MCP Server instance
  2. The @server.tool() decorator registers a Python function as an MCP Tool
  3. The function docstring """获取当前系统时间""" automatically becomes the tool description—writing a good docstring is crucial, because the LLM uses it to decide when to call this tool
  4. stdio_server() communicates with the client via standard input/output

4.2 Configuring Claude Desktop#

To make Claude Desktop recognize your MCP Server, edit the Claude Desktop configuration file:

%APPDATA%\Claude\claude_desktop_config.json
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"my-first-server": {
"command": "python3",
"args": ["/path/to/my-mcp-server/server.py"]
}
}
}

After restarting Claude Desktop, click the 🔌 icon next to the input box, and you’ll see the get_current_time tool is now available.

4.3 Using in Claude Code#

Claude Code’s configuration is even simpler—just edit the .mcp.json in the project root directory:

{
"mcpServers": {
"my-first-server": {
"type": "stdio",
"command": "python3",
"args": ["server.py"],
"env": {
"API_KEY": "${MY_API_KEY}"
}
}
}
}

Once configured, Claude Code will automatically connect to your MCP Server on startup.

5. Hands-On: Building a Weather Query MCP Server#

Now let’s look at a more realistic example—building an MCP Server that supports weather queries for multiple cities.

5.1 Complete Code#

server.py
import asyncio
import httpx
from typing import Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
server = Server("weather-mcp-server")
# Simulated weather data (replace with real API calls in actual projects)
WEATHER_DATA = {
"Beijing": {"temp": 32, "humidity": 65, "condition": "Sunny"},
"Shanghai": {"temp": 35, "humidity": 70, "condition": "Cloudy"},
"Shenzhen": {"temp": 30, "humidity": 80, "condition": "Thunderstorms"},
"Tokyo": {"temp": 28, "humidity": 60, "condition": "Overcast"},
}
@server.tool()
async def get_weather(city: str, unit: Optional[str] = "celsius") -> str:
"""Query weather information for a specified city.
Args:
city: City name, such as "Beijing", "Shanghai", "Shenzhen"
unit: Temperature unit, celsius or fahrenheit
"""
data = WEATHER_DATA.get(city)
if not data:
return f"No weather data for “{city}”. Supported cities: {', '.join(WEATHER_DATA.keys())}"
temp = data["temp"]
if unit == "fahrenheit":
temp = temp * 9 / 5 + 32
unit_str = "°F"
else:
unit_str = "°C"
return (
f"🌍 Weather report for {city}\n"
f"━━━━━━━━━━━━━━\n"
f"🌡️ Temperature: {temp:.1f}{unit_str}\n"
f"💧 Humidity: {data['humidity']}%\n"
f"☁️ Weather: {data['condition']}\n"
)
@server.tool()
async def compare_weather(city_a: str, city_b: str) -> str:
"""Compare the weather conditions of two cities.
Args:
city_a: Name of the first city
city_b: Name of the second city
"""
data_a = WEATHER_DATA.get(city_a)
data_b = WEATHER_DATA.get(city_b)
if not data_a or not data_b:
missing = city_a if not data_a else city_b
return f"No weather data for “{missing}”"
comparison = f"📊 Weather comparison: {city_a} vs {city_b}\n"
comparison += f"━━━━━━━━━━━━━━━━━━━━\n"
comparison += f"🌡️ {city_a}: {data_a['temp']}°C | {city_b}: {data_b['temp']}°C\n"
comparison += f"💧 {city_a}: {data_a['humidity']}% | {city_b}: {data_b['humidity']}%\n"
comparison += f"☁️ {city_a}: {data_a['condition']} | {city_b}: {data_b['condition']}\n"
cooler = city_a if data_a['temp'] < data_b['temp'] else city_b
comparison += f"\n💡 {cooler} is cooler, great for going out!"
return comparison
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())

5.2 Key Design Points#

1. Type Annotations Determine the Parameter Schema

The MCP Python SDK automatically extracts the parameter schema from the function signature. city: str → string parameter, unit: Optional[str] = "celsius" → optional string parameter with a default value. No need to hand-write JSON Schema—but only if the type annotations are accurate.

2. Docstrings Are Prompt Engineering

A tool’s docstring is sent directly to the LLM as the basis for it to decide “when to call this tool.” Following the Google style (Args/Returns) keeps descriptions structured and clear.

3. Return Values Are Plain Text

An MCP Tool’s return value is of type str. You can use emoji, formatted text, or even Markdown in the return value—the LLM will display it to the user as-is. This makes the output both informative and readable.

6. Advanced: Adding Resources#

Besides callable Tools, an MCP Server can also expose Resources—read-only data that the model can proactively read:

from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("docs-mcp-server")
@server.resource("docs://readme")
async def get_readme() -> str:
"""Project README documentation"""
return open("README.md", "r").read()
@server.resource("docs://api/{endpoint}")
async def get_api_doc(endpoint: str) -> str:
"""Get documentation for a specified API endpoint"""
docs = {
"auth": "POST /api/auth - User authentication endpoint...",
"users": "GET /api/users - Get user list...",
"weather": "GET /api/weather - Query weather data...",
}
return docs.get(endpoint, f"No documentation for endpoint “{endpoint}”")
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)

Once configured, when Claude needs to read the project README, it will automatically call the docs://readme resource. Note that {endpoint} in docs://api/{endpoint} is a path parameter—MCP will extract it and pass it into the function.

7. Best Practices#

Through building the above examples, I’ve summarized five best practices for MCP Server development:

7.1 One Server, One Responsibility#

Don’t cram weather queries, database operations, and file management all into one Server. Keep each MCP Server focused on a single responsibility——

{
"mcpServers": {
"weather": { "command": "python3", "args": ["servers/weather.py"] },
"database": { "command": "python3", "args": ["servers/database.py"] },
"filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] }
}
}

This makes it easier to pinpoint issues when they arise, and easier to reuse.

7.2 Handle Errors Well#

The LLM may pass strange parameters when calling tools. Do proper boundary checks:

@server.tool()
async def query_database(sql: str) -> str:
"""Execute SQL queries on a read-only replica"""
# Security check: only allow SELECT
if not sql.strip().upper().startswith("SELECT"):
return "❌ Error: Only SELECT queries are allowed"
# Security check: prohibit dangerous keywords
dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER"]
for keyword in dangerous:
if keyword in sql.upper():
return f"❌ Error: Query contains forbidden keyword {keyword}"
try:
result = execute_readonly_query(sql)
return format_table(result)
except Exception as e:
return f"❌ Query failed: {str(e)}"

7.3 Tool Descriptions Should Be Specific#

Compare the following two types of descriptions:

❌ Bad description✅ Good description
”Query data""Execute a SELECT query on a MySQL read replica. Supports standard SQL syntax, returns the first 100 rows."
"Get weather""Query real-time weather for a specified city, including temperature (°C/°F), humidity, and weather conditions. See the parameter description for the list of supported cities.”

Detailed descriptions help the LLM accurately determine when to call your tool—this directly determines the user experience.

7.4 Use Environment Variables to Manage Sensitive Information#

import os
@server.tool()
async def search_knowledge_base(query: str) -> str:
"""Search the company's internal knowledge base"""
api_key = os.environ["KB_API_KEY"]
base_url = os.environ.get("KB_BASE_URL", "https://kb.internal.example.com")
# ... call API

Inject environment variables in .mcp.json to avoid hardcoding:

{
"mcpServers": {
"kb-search": {
"type": "stdio",
"command": "python3",
"args": ["servers/kb_search.py"],
"env": {
"KB_API_KEY": "${KB_API_KEY}",
"KB_BASE_URL": "https://kb.mycompany.com"
}
}
}
}

7.5 Write Tests#

An MCP Server is essentially a collection of async functions, so it can absolutely be unit tested:

import pytest
from server import get_weather
@pytest.mark.asyncio
async def test_get_weather_beijing():
result = await get_weather("Beijing")
assert "32" in result
assert "°C" in result
@pytest.mark.asyncio
async def test_get_weather_unknown_city():
result = await get_weather("Mars")
assert "Not found" in result

Once the tool logic is decoupled from the MCP protocol layer, testing becomes as simple as testing ordinary Python functions.

8. Conclusion#

MCP is becoming the infrastructure for AI application development—just as HTTP is to the Web, and SQL is to databases. Mastering MCP Server development means you can make AI truly “get things done”—whether it’s checking the weather, reading documents, or operating a company’s internal business systems.

Following the approach in this article, you can start building your own MCP Server today. It’s recommended to start with a simple tool (such as fetching Git commit records or querying Jira Issues), then gradually expand to more complex scenarios.


References

Build Your Own MCP Server: A Practical Guide from Zero to One
https://www.hehonglei.com/posts/build-your-own-mcp-server/
Author
HHLBlog
Published at
2026-08-06
License
CC BY-NC-SA 4.0