Lesson 03 The Python SDK Landscape
Theme:
Lesson 03 · Slide 1

Lesson 03 — The Python SDK Landscape

Picking your tools and scaffolding the PKB project.

MCP Servers Module 1

Lesson 03 · Slide 2

Two ways to build a Python MCP server

The official mcp Python package ships two layered APIs. Pick the layer that matches how much control you actually need.

Official low-level (mcp.server.Server)

Full control. You hand-roll every capability handler — list_tools, call_tool, list_resources, and so on — and own the registration plumbing yourself.

Verbose, but nothing is hidden. You see every request shape the protocol gives you.

FastMCP (mcp.server.fastmcp.FastMCP)

Decorator-based and opinionated. Slap @mcp.tool() on a function and the SDK derives the schema, registers the handler, and wires it into the protocol for you.

Less boilerplate, fewer foot-guns, faster iteration.

Lesson 03 · Slide 3

Side-by-side: a hello tool

The same one-argument tool, written both ways:

Low-level (mcp.server.Server)FastMCP
from mcp.server import Server
from mcp.types import Tool, TextContent

server = Server("pkb")

@server.list_tools()
async def list_tools():
    return [Tool(
        name="hello",
        description="Say hi",
        inputSchema={
          "type":"object",
          "properties":{"name":{"type":"string"}},
          "required":["name"],
        },
    )]

@server.call_tool()
async def call_tool(name, arguments):
    return [TextContent(
        type="text",
        text=f"hi {arguments['name']}",
    )]
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("pkb")

@mcp.tool()
def hello(name: str) -> str:
    """Say hi."""
    return f"hi {name}"

Same wire behaviour. Same Inspector view. Wildly different amounts of code.

Lesson 03 · Slide 4

When to pick which

NeedPick
Quick prototype, ergonomic decoratorsFastMCP
Fine-grained capability control / custom transportsLow-level
Teaching the protocolLow-level (this course uses both — FastMCP from L05)
Notes: Lessons 03–04 stay on the low-level API so you see the protocol mechanics. From Lesson 05 onwards we switch to FastMCP and don't look back.
Lesson 03 · Slide 5

Project layout

The shape we'll use for the PKB server across the whole course:

pkb/
  pyproject.toml
  pkb/
    __init__.py
    server.py        ← entry point
    tools.py         ← tool implementations
    resources.py     ← resource handlers
    prompts.py       ← prompt templates
  tests/
    test_tools.py

One package, one entry point, one file per MCP primitive. Boring on purpose — it stays easy to grep as the server grows.

Lesson 03 · Slide 6

pyproject.toml essentials

[project]
name = "pkb"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["mcp[cli]>=1.0.0", "pydantic>=2.0.0"]

[project.scripts]
pkb = "pkb.server:main"

Enough to pip install -e . from the project root and then run pkb straight from your shell — that's the script entry point doing its job.

Lesson 03 · Slide 7

The mcp dev workflow

mcp dev pkb/server.py

One command launches your server and the Inspector pointed at it. Your inner loop becomes: edit a tool, save, restart, re-run the call in the Inspector, read the JSON, repeat. No client config, no Claude Desktop reload — just the protocol and your code.

Lesson 03 · Slide 8

Tool schemas via Pydantic

from pydantic import BaseModel

class SearchArgs(BaseModel):
    query: str
    limit: int = 10

FastMCP introspects Pydantic models (and plain type hints) to generate the JSON Schema that goes on the wire for tool arguments. Type errors get caught before the LLM ever sees them — the SDK refuses bad input at the protocol boundary.

Lesson 03 · Slide 9

The official reference servers

The modelcontextprotocol/servers repo is the canonical collection. Each server lives in its own small package, with one clear job:

Reading their source is the fastest way to learn idiomatic MCP — everything you'll build in this course is a variation on a pattern that lives there.

Lesson 03 · Slide 10

60-second walk through mcp-server-fetch

The smallest interesting reference server. Tool registration is just this much:

server = Server("mcp-fetch")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(name="fetch", description="...", inputSchema=Fetch.model_json_schema())]

@server.call_tool()
async def call_tool(name, arguments):
    url = arguments["url"]
    content, prefix = await fetch_url(url, ...)
    return [TextContent(type="text", text=prefix + content)]

Source: github.com/modelcontextprotocol/servers/tree/main/src/fetch. One list_tools, one call_tool, a Pydantic model for the args. That's the whole shape.

Lesson 03 · Slide 11

Testing patterns

import pytest
from mcp.shared.memory import create_connected_server_and_client_session

@pytest.mark.asyncio
async def test_search_returns_results():
    async with create_connected_server_and_client_session(server) as (_, client):
        result = await client.call_tool("pkb.search", {"query": "hello"})
        assert result.content

The SDK ships an in-memory transport that wires a client and server through a pair of queues. No subprocesses, no sockets, no real I/O — but real protocol semantics (initialize, capability negotiation, request/response). Fast, deterministic, CI-friendly.

Lesson 03 · Slide 12

Setting up the PKB project

In this lesson's lab you scaffold the project from scratch — but a reference copy already lives in the repo at mcp-servers/sample-server/python/. It ships:

Lesson 04 puts the first real tool into server.py end-to-end. This lesson is about getting the runway built.

Lesson 03 · Slide 13

Recap & what's next

Coming up: Lesson 04 — your first tool, end-to-end (covered in Plan B).
1 / ?