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:
echo— returns whatever message you send.add— adds two numbers.printEnv— dumps the server's environment variables.longRunningOperation— demonstrates progress notifications over time.sampleLLM— asks the client to sample its own LLM (server-initiated sampling).getTinyImage— returns a base64-encoded PNG to exercise the image content type.annotatedMessage— returns content with annotation metadata attached.getResourceReference— returns a content block that points at a resource URI.getResourceLinks— returns a list of resource link references.structuredContent— returns a typed JSON object (not just a TextContent blob).
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.
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).
@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-fetchexposes one tool,fetch, that takes a URL plus optionalmax_lengthandstart_indexarguments and returns the page's content (converted to Markdown). The design choice that surprised me: it doesn't just return raw HTML — it pipes throughreadabilipyand thenmarkdownify, producing token-efficient Markdown that's friendlier to LLMs. Thestart_index+max_lengthpair 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.
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.
pkb/server.py. Keep your venv active and the
sample-server tree on PATH.