Lesson 03 The Python SDK Landscape
Theme:

Solutions

Lesson 03 · MCP Servers · Worked answers for the three homework tasks

These are reference solutions — not the only valid answers. Compare your writeup against the shape of the answer rather than the exact wording. If your tool names match and your explanations hit the same beats, you nailed the task.

Solution to Task 1 — mcp-server-everything tour

When you connect Inspector to npx -y @modelcontextprotocol/server-everything and open the Tools tab, you should see at least ten tools. The canonical list (which shifts a little between releases — don't panic if yours is slightly different) looks roughly like this:

The simplest call to verify the loop is end-to-end working is echo. In the Inspector Tools panel, select echo, fill the arguments field with:

{
  "message": "hi"
}

Hit Run. The response panel should show a single TextContent block:

{
  "content": [
    { "type": "text", "text": "Echo: hi" }
  ]
}

(Exact prefix may vary — the point is it echoes your message back as a text block.) That's the smallest possible round-trip through the protocol: arguments in, content blocks out.

Common mistake: trying to call sampleLLM standalone and getting a confusing error. sampleLLM is special — it asks the client to run an LLM completion on the server's behalf, which only works when the client advertises the sampling capability during initialise. Inspector and Claude Desktop do; an ad-hoc Python script using the bare client may not. If you want to play with that tool, do it from Inspector.

Solution to Task 2 — Side-by-side rewrite

Both versions below expose a tool named hello that takes one string argument name and returns a greeting. Drop each into its own file (say, hello_lowlevel.py and hello_fastmcp.py), run each under mcp dev, and confirm they're indistinguishable from the Inspector side.

Low-level Server version

from mcp.server import Server
from mcp.types import Tool, TextContent

server = Server(name="hello-srv")

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

@server.call_tool()
async def call_tool(name: str, args: dict) -> list[TextContent]:
    if name == "hello":
        return [TextContent(type="text", text=f"hello, {args['name']}")]
    raise ValueError(f"unknown tool: {name}")

FastMCP version

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("hello-srv")

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

When you run either under Inspector, both expose a single tool named hello that takes a name string and returns text. The FastMCP version is dramatically shorter because the SDK derives the JSON Schema from the type hints (the name: str annotation becomes {"type": "string"}, the missing default makes it required) and derives the tool description from the docstring. The low-level version has to spell all of that out by hand — the upside being that you get total control when your schema has shapes FastMCP's type-hint inference can't express cleanly (deeply nested unions, conditional required fields, custom $ref chains).

Common mistake: trying to mix decorators — attaching @server.list_tools() to a FastMCP instance, or hanging @mcp.tool() off a low-level Server. The two APIs are distinct objects with non-overlapping decorator surfaces; pick one per file. If you find yourself needing both styles in the same project, write two server modules and pick which one to launch at the CLI.

Solution to Task 3 — Reading a reference server

Model answer if you picked mcp-server-fetch:

“mcp-server-fetch exposes one tool, fetch, that takes a URL plus optional max_length and start_index arguments and returns the page's content (converted to Markdown). The design choice that surprised me: it doesn't just return raw HTML — it pipes through readabilipy and then markdownify, producing token-efficient Markdown that's friendlier to LLMs. The start_index + max_length pair lets the LLM paginate through large pages instead of blowing out the context window. The server is ~150 lines total — most of that is robots.txt handling, not the tool itself.”

If you picked sqlite instead, your writeup should hit similar beats but with a different tool list. Expect names like read_query, write_query, create_table, list_tables, and describe_table, plus one surprise — for many people that's the append_insight / business-insights resource the sqlite server maintains as a side channel, which isn't obvious from the tool list alone.

Common mistake: describing what a tool should do based on its name without reading the source. Names lie — fetch doesn't return raw HTML, and read_query is more restrictive than the SQL spec would suggest. Always git clone the repo and actually open the entry point before you write the description.
Lesson 04 (Plan B) puts the first real tool in pkb/server.py. Keep your venv active and the sample-server tree on PATH.