Build a ServerIntermediate8 min08 / 10

Build a Python Server

Build the same sales connector in Python with FastMCP, where decorators and type hints turn plain functions into a typed MCP surface — proving the concept is identical across SDKs, only the syntax changes.

You've built this connector before in Node/TypeScript. Now let's build the exact same thing in Python with FastMCP — the ergonomic server class from the official mcp package. Same demo shop, same three tools, same one resource. The point of this lesson isn't a new idea; it's to show you that MCP is a protocol, not a library. The concept stays fixed across every SDK. Only the syntax changes.

#Set up the project

The Python SDK ships as mcp[cli]. The recommended way to install it is with `uv`, a fast Python package manager. Initialize a project, add the dependency, and you're ready to run a server over stdio:

terminal
# using uv (recommended)
uv init sales-connector && cd sales-connector
uv add "mcp[cli]"

# run your server
uv run server.py
Think of it like

Decorators are the whole trick

In FastMCP, @mcp.tool() and @mcp.resource(...) turn plain Python functions into a typed MCP surface. Here's the magic: your type hints become the schema the agent reads to know how to call you. Write metric: str and limit: int = 5, and FastMCP generates the JSON schema that gets advertised over tools/list. The docstring becomes the tool's description. You never hand-write a schema — you just write a normal, well-typed function.

#The server, end to end

Here's a complete, runnable FastMCP server. Read it top to bottom — the four numbered comments walk the same arc as the Node version: create the server → expose tools → expose a resource → run over stdio.

server.py
from mcp.server.fastmcp import FastMCP
import sqlite3

db = sqlite3.connect("shop.db", check_same_thread=False)

# 1. Create the server — your MCP "connector".
mcp = FastMCP("sales-connector")


# 2. Expose a TOOL. The decorator + type hints become the schema
#    the agent reads to know how to call this function.
@mcp.tool()
def query_sales(metric: str, group_by: str = "customer", limit: int = 5) -> list[dict]:
    """Aggregate revenue by customer or by month."""
    if group_by == "month":
        sql = ("SELECT strftime('%Y-%m', created_at) AS month, "
               "SUM(amount) AS revenue FROM orders "
               "GROUP BY month ORDER BY month LIMIT ?")
    else:
        sql = ("SELECT c.name AS customer, SUM(o.amount) AS total "
               "FROM orders o JOIN customers c ON c.id = o.customer_id "
               "GROUP BY c.name ORDER BY total DESC LIMIT ?")

    cur = db.execute(sql, (limit,))
    cols = [d[0] for d in cur.description]
    return [dict(zip(cols, row)) for row in cur.fetchall()]


# 3. Expose a RESOURCE — read-only context the agent can pull in.
@mcp.resource("schema://shop")
def schema() -> str:
    """The database schema the model can reason over."""
    return "orders(id, customer_id, amount, created_at)"


# 4. Run over stdio so a host like Claude can launch and talk to it.
if __name__ == "__main__":
    mcp.run()

#Reading the four steps

Step 1 — Create the server. FastMCP("sales-connector") gives your connector a name. That name is how a host identifies this server among all the others it's connected to.

Step 2 — Expose a tool. @mcp.tool() registers query_sales. Because metric: str, group_by: str = "customer", and limit: int = 5 are type-hinted, FastMCP knows the parameter names, their types, and which are optional — and turns that into the input schema. The docstring """Aggregate revenue by customer or by month.""" becomes the description the agent reads when deciding whether to call it.

Step 3 — Expose a resource. @mcp.resource("schema://shop") publishes read-only context under a URI the agent can fetch. Here it hands back the schema so the model can reason about the shape of the data.

Step 4 — Run over stdio. mcp.run() starts the server speaking MCP over standard input/output, so a host can launch the process and talk to it.

Quick check

In the FastMCP server, where does the input schema for `query_sales` come from?

#Same concept, different syntax

Put the Python and Node servers side by side and the shape is identical — create, expose tools, expose a resource, connect over stdio. Only the surface details differ:

  • Schema. Node uses a zod schema passed explicitly (z.enum([...]), z.number().int()). Python uses type hints that FastMCP reads automatically.
  • Registration. Node calls server.registerTool(...) and server.registerResource(...). Python uses the @mcp.tool() and @mcp.resource(...) decorators.
  • Transport. Node wires up a StdioServerTransport and calls server.connect(transport). Python just calls mcp.run().

An agent talking to either server can't tell the difference — it sees the same tool names, the same schemas, the same JSON-RPC over the wire. That's the promise of a protocol: learn MCP once, and every SDK is just a dialect.

Note

The server is the boundary

Notice the SQL never leaves the server. The agent picks a tool and passes structured arguments (metric, group_by, limit); your Python function decides exactly which query runs and against which database. The agent never sees shop.db. Whoever writes the connector owns the boundary — the agent can only ever call the tools you chose to expose.

Quick check

What line makes the FastMCP server actually start listening so a host can talk to it?

Key takeaways

  • FastMCP (from the official `mcp` package) lets you build an MCP server out of plain, type-hinted Python functions.
  • `@mcp.tool()` and `@mcp.resource(...)` turn functions into a typed MCP surface — your type hints become the schema, the docstring becomes the description.
  • The four steps are the same in every SDK: create the server, expose tools, expose resources, then run over stdio so a host can launch and talk to it.
  • The concept is identical to the Node server; only the syntax changes (type hints vs zod, decorators vs registerTool, mcp.run() vs StdioServerTransport).
  • The server owns the database connection and the SQL — the agent can only call the tools you chose to expose.
Practice challenges
Test yourself · earn XP
0/4
Reorder the lines#1

Order the four steps of building the FastMCP server as they appear in server.py, top to bottom.

1
Expose a resource: decorate schema() with @mcp.resource("schema://shop")
2
Create the server: mcp = FastMCP("sales-connector")
3
Expose a tool: decorate query_sales with @mcp.tool()
4
Run over stdio: call mcp.run() inside if __name__ == "__main__":
Fill in the blank#2

Complete the FastMCP tool registration. The decorator plus the function's type hints become the schema the agent reads.

@mcp.()
def query_sales(metric: str, group_by: str = "customer", limit:  = 5) -> list[dict]:
    """Aggregate revenue by customer or by month."""
    ...
Predict the output#3

An agent connects to this FastMCP server and asks for the tool list. For the query_sales tool, where does the input schema (parameter names, types, which are optional) come from?

predict-output
@mcp.tool()
def query_sales(metric: str, group_by: str = "customer", limit: int = 5) -> list[dict]:
    """Aggregate revenue by customer or by month."""
    ...
Fix the bug#4

This FastMCP tool is meant to be exposed to the agent, but the host reports zero tools available. What's wrong?

fix-bug
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("sales-connector")

# missing something right here
def query_sales(metric: str, limit: int = 5) -> list[dict]:
    """Aggregate revenue by customer or by month."""
    ...

if __name__ == "__main__":
    mcp.run()
Your turn
Practice exercise

Extend the FastMCP server.py with a fourth tool, inventory_lookup, that lists products whose stock is below a threshold. Write the decorated Python function: give it one type-hinted parameter below: int = 25, a one-line docstring, and have it run a read-only SELECT against a products(id, name, stock, reorder_level) table returning products where stock < below ordered by stock ascending. Then, in one or two sentences, explain what schema the agent will see for your new tool and where that schema came from.

Try it yourself — a starting point to build on:

starter.py
# Write your solution here