Skip to content

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:

bash
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:

text
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 Desktop

If 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:

python
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效果:

bash
mcp dev server.py
# Automatically launches MCP Inspector and opens http://localhost:5177 in the browser

In 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: int are 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:

python
@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 approachModel's判断 basis
"stats"Nearly zero; may never be正确 called
One sentence describing functionalityCan 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场景:

python
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:

  1. 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;
  2. Native async support: async def works直接; suitable for IO-intensive operations; the ctx parameter 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:

python
# 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:

CapabilityRegistrationTriggered byTypical Use
Tool@mcp.tool()Model自主 decisionExecute actions, query data
Resource@mcp.resource(uri)Application/user-side readExpose 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:

bash
# 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-http

Verify in order within the Inspector:

  1. Connect — observe whether the initialize handshake and capabilities negotiation succeed;
  2. 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;
  3. Resources / Prompts tabs — separately确认 resources are readable and templates可 expand;
  4. 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] provides mcp 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 ctx context 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 ​

  1. Add a read_note(note_name) tool to this chapter's Server: read a specified .md note from a固定 directory and return the content; pay attention to handling the "file not found" case.
  2. Register a template resource note://{name}; form a "tool queries, resource reads"组合 with the previous tool; verify both in Inspector.
  3. Write a @mcp.prompt() template weekly_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.