Chapter 19 · Writing Your First MCP Server
Chapter goals: Understand the minimal structure of an MCP Server, write a complete Server containing tools, resources, and prompt templates using the official Python SDK's decorator style, and learn to debug it with MCP Inspector.
19.1 Environment Setup: Installing the Official Python SDK
Model Context Protocol (MCP), led by Anthropic, provides an official Python SDK with package name mcp. The recommended installation includes the CLI extension:
pip install "mcp[cli]"
# Or using uv:
uv add "mcp[cli]"The [cli] extra installs the mcp command-line tool on top of the SDK, providing three high-frequency subcommands:
mcp dev server.py # Launch MCP Inspector debug server (dev mode)
mcp run server.py # Run the server directly
mcp install server.py # One-click install the server into Claude DesktopIf you only want to run it once temporarily without polluting your environment, you can also use uv's one-shot run: uv run --with "mcp[cli]" mcp dev server.py.
Version Note
Earlier tutorials普遍 use from mcp.server.fastmcp import FastMCP. The current official mainline API has unified to MCPServer; this tutorial strictly follows the latest official README. If you see FastMCP online, that's the旧版 integration — the concepts are完全相通.
19.2 A Complete Server in Fifteen Lines
Create server.py:
from mcp.server import MCPServer
# Create server instance; the argument is the name shown to clients
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"This is a complete, runnable MCP Server: one tool, one templated resource. Launch the debug interface to see the效果:
mcp dev server.py
# Automatically launches MCP Inspector and opens http://localhost:5177 in the browserIn the Inspector's Tools tab, call add(a=1, b=2) and you'll get 3 — a standard JSON-RPC response completes the full protocol流程.
19.3 Type Annotations Are Schema, Docstrings Are Descriptions
Looking back at the code above, what you didn't write is more important than what you did:
- ❌ No JSON Schema — the type annotations
a: int, b: intare the schema; - ❌ No request parsing — the protocol layer is handled by the SDK;
- ❌ No parameter validation code — Pydantic自动完成s at the底层;
- ❌ No protocol handshake — initialize / capabilities negotiation is全自动.
The function's docstring becomes the tool description (description), going directly into the model's context — the model relies on it to judge "when to call this tool." Therefore, follow two disciplines when writing tools:
@mcp.tool()
def word_stats(path: str) -> dict:
"""Count the number of lines, words, and characters in a text file.
Use this tool when the user wants to know the size or length info of a text file.
path must be an absolute path to a file readable on this machine.
"""
from pathlib import Path
text = Path(path).read_text(encoding="utf-8")
lines = text.count("\n") + 1
words = len(text.split())
return {"lines": lines, "words": words, "chars": len(text)}Comparing the信息量 the model gets from两种 docstring approaches:
| docstring approach | Model's判断 basis |
|---|---|
"stats" | Nearly zero; may never be正确 called |
| One sentence describing functionality | Can judge "what it does" but not the边界 |
| Functionality + when to use + parameter constraints (as above) | All three要素齐全; reliable triggering |
Keep parameter types simple
Prioritize str, int, float, bool, and lists/dicts of these types. Complex nested objects are supported, but the more complex the schema, the higher the model's parameter-passing error rate.
19.4 Two More Practical Tools: Default Values and Error Handling
An example closer to real-world场景:
import os
from mcp.server import MCPServer
mcp = MCPServer("FileTools")
@mcp.tool()
def find_markdown(directory: str, keyword: str = "") -> list[str]:
"""Find Markdown files in a directory; can filter by keyword in filenames.
Use when the user wants to locate .md docs in a project.
directory is the path to search; keyword is an optional substring to match in filenames.
"""
if not os.path.isdir(directory):
# Return the failure reason as a result instead of raising a裸 exception
return [f"Error: directory does not exist {directory}"]
results = []
for root, _dirs, files in os.walk(directory):
for f in files:
if f.endswith(".md") and keyword.lower() in f.lower():
results.append(os.path.join(root, f))
if len(results) >= 20: # Limit result count to protect context
results.append("...(results truncated)")
return results
return results or ["No matching files found"]
@mcp.tool()
async def disk_usage(path: str, ctx) -> str:
"""Show a disk space summary for a directory.
Use when system status needs to be read; path is the target directory.
"""
total = sum(
f.stat().st_size
for f in __import__("pathlib").Path(path).rglob("*")
if f.is_file()
)
# ctx is the context object injected by the SDK; can log execution progress
await ctx.info(f"Scanned {path}")
return f"{path} uses {total / 1024:.1f} KB in total"Two details worth learning:
- Return errors as normal results: when the directory doesn't exist, return an explanatory sentence instead of crashing the server; the model can explain to the user or修正参数 after receiving the text;
- Native async support:
async defworks直接; suitable for IO-intensive operations; thectxparameter is auto-injected by the SDK (not in the model-visible parameter list) and用于 logs and进度 reporting.
19.5 Registering Resources and Prompt Templates
Beyond tools, MCP has two more Server capabilities. Continue extending server.py:
# Resource: expose data to the client for reading via URI
@mcp.resource("file://changelog")
def get_changelog() -> str:
"""Return the project's CHANGELOG content."""
with open("CHANGELOG.md", encoding="utf-8") as f:
return f.read()
# Prompt template: pre-built reusable conversation starters; the client user主动 selects
@mcp.prompt()
def review_commit(hash: str) -> str:
"""Generate a starting prompt for a code review."""
return f"""Please help me review commit {hash}:
1. First use git show {hash} to查看 the changes;
2. Give opinions item by item from correctness, readability, and test coverage perspectives;
3. Finally give a conclusion of "recommend merging" or "needs modification."
"""The division of labor among the three must be clear in a table:
| Capability | Registration | Triggered by | Typical Use |
|---|---|---|---|
| Tool | @mcp.tool() | Model自主 decision | Execute actions, query data |
| Resource | @mcp.resource(uri) | Application/user-side read | Expose files, DB rows, and other context |
| Prompt template | @mcp.prompt() | User主动 selection | 固化 high-quality workflow entry points |
Resource URIs support template parameters (e.g. greeting://{name}); the client first请求s the template list, then取值 by actual parameters.
19.6 Systematic Debugging with MCP Inspector
The Inspector opened by mcp dev is the standard tool for debugging MCP. Core workflow:
# Option 1: Dev mode (auto-restart + Inspector)
mcp dev server.py
# Option 2: Run with Streamable HTTP transport (simulate production形态)
mcp run server.py --transport streamable-httpVerify in order within the Inspector:
- Connect — observe whether the initialize handshake and capabilities negotiation succeed;
- Tools tab — List Tools to check whether the schema matches expectations (重点 look at parameter names and required markers); then use the form to调用 and验证 returns;
- Resources / Prompts tabs — separately确认 resources are readable and templates可 expand;
- Deliberately pass错误 parameters (e.g. non-existent paths); confirm the return is friendly text, not a stack trace.
After调试 passes, you can directly mcp install server.py to装 it into Claude Desktop, or接入 any client using the manual configuration方式 from the next chapter.
Chapter Summary
- The official Python SDK installation command is
pip install "mcp[cli]";[cli]providesmcp dev / run / install; - The current API is
MCPServer:@mcp.tool()defines tools,@mcp.resource(uri)defines resources,@mcp.prompt()defines prompt templates; - Type annotations auto-generate JSON Schema; docstrings become tool descriptions — both jointly决定 the model's calling quality;
- Errors should be returned as text results to the model; the
ctxcontext object can be used for logs and进度; - Tools are自主 called by the model; Resources are read by the application; Prompts are selected by the user — three different responsibilities;
mcp dev+ MCP Inspector是 the standard debug闭环: handshake → list tools → call →异常 paths.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. In the official Python SDK, where do tool parameter JSON Schemas come from?
2. Regarding a tool's docstring, which statement is correct?
3. Who triggers Tools, Resources, and Prompts respectively?
4. For a business error like "directory not found" during tool execution, what is the best approach?
🛠️ Hands-on Practice
- Add a
read_note(note_name)tool to this chapter's Server: read a specified.mdnote from a固定 directory and return the content; pay attention to handling the "file not found" case. - Register a template resource
note://{name}; form a "tool queries, resource reads"组合 with the previous tool; verify both in Inspector. - Write a
@mcp.prompt()templateweekly_report(days: int)that generates a weekly report writing prompt, requiring the template body to have explicit requirements for output format.
After completing the exercises, proceed to Next chapter: MCP Client Integration in Practice to接入 this Server to a real AI client.