Courses/Introduction to Model Context Protocol (MCP)/Building a Server with the Python SDK
Building and TestingLesson 11 of 21

Building a Server with the Python SDK

From Concepts to Code

You've seen the primitives in isolation. Now we assemble a complete, runnable MCP server — the document-management server we've been sketching. The official Python SDK ships FastMCP, a high-level API that handles the protocol plumbing so you write plain functions. You install it with the MCP CLI tooling (commonly via uv: uv add "mcp[cli]").

A server needs just three things: create the FastMCP instance, define your primitives with decorators, and run it. Everything else — schemas, message routing, lifecycle — is handled for you.

pythonStep 1: create the server and an in-memory document store.
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
from pydantic import Field

mcp = FastMCP("DocumentMCP", log_level="ERROR")

docs = {
    "deposition.md": "This deposition covers the testimony of Angela Smith, P.E.",
    "report.pdf": "The report details the state of a 20m condenser tower.",
    "financials.docx": "These financials outline the project's budget.",
    "plan.md": "The plan outlines the steps for the project's implementation.",
}

Adding the Primitives

Now we register all three primitives on the same server: two tools (read, edit), two resources (list, fetch-by-id), and one prompt (format). This is the full primitive set working together on one server — exactly the pattern from the 'choosing a primitive' lesson, now in code.

pythonThe two tools (model-controlled actions).
@mcp.tool(name="read_doc_contents", description="Read the contents of a document and return it as a string.")
def read_document(doc_id: str = Field(description="Id of the document to read")):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

@mcp.tool(name="edit_document", description="Edit a document via exact-match find and replace.")
def edit_document(
    doc_id: str = Field(description="Id of the document that will be edited"),
    old_str: str = Field(description="Text to replace. Must match exactly."),
    new_str: str = Field(description="Replacement text."),
):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    docs[doc_id] = docs[doc_id].replace(old_str, new_str)
pythonThe two resources (app-controlled data): one direct, one templated.
@mcp.resource("docs://documents", mime_type="application/json")
def list_docs() -> list[str]:
    return list(docs.keys())

@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain")
def fetch_doc(doc_id: str) -> str:
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

Running the Server

Finally, run the server. For local development and use in Claude Desktop, that's the stdio transport. The same file can later run over Streamable HTTP by changing one argument — your primitives don't change.

pythonStep 3: run it. One line selects the transport.
if __name__ == "__main__":
    mcp.run(transport="stdio")

# Later, to serve remotely instead:
#   mcp.run(transport="streamable-http")
FastMCP("DocumentMCP")Tools: read_doc, edit_documentResources: list, fetch_docPrompt: format

One FastMCP server hosting all three primitive types — the complete document server.

Why the SDK Approach Wins

  • No manual JSON schema writing — type hints and Field descriptions generate it.
  • Type hints provide automatic validation of incoming arguments.
  • Clear parameter descriptions help Claude call tools correctly.
  • Error handling is just normal Python exceptions.
  • Registration is automatic via decorators — add a function, it's exposed.
ℹ️

Python and TypeScript both have official SDKs

We use Python here, but MCP provides official SDKs in multiple languages including TypeScript. The concepts are identical — create a server, decorate/register primitives, choose a transport. Pick the SDK that matches your stack.

Next: test before you wire it up

You have a complete server, but how do you know it works before building a whole app around it? The MCP Inspector lets you connect and test every tool, resource, and prompt in your browser. That's next.

Key Takeaways

  • A complete MCP server takes three steps: create FastMCP(), register primitives with decorators, and call mcp.run().
  • One server can host all three primitive types together — our document server has 2 tools, 2 resources, and 1 prompt.
  • mcp.run(transport="stdio") runs locally (for Claude Desktop); switching to "streamable-http" is a one-line change that leaves primitives untouched.
  • The SDK eliminates manual JSON schemas, validates arguments from type hints, and registers primitives automatically via decorators.
  • Errors are ordinary Python exceptions; descriptions double as guidance for the model.
  • Official SDKs exist for multiple languages (Python, TypeScript, …) with identical concepts — choose the one matching your stack.

Check Your Understanding

Test what you learned in this lesson.

Q1.What are the three essential steps to build a server with FastMCP?

Q2.Can a single MCP server host tools, resources, and prompts at the same time?

Q3.How do you change the document server from local to remote operation?

Q4.What does using the SDK's decorators and type hints replace?

Practice This Lesson