Implementation Details of the Model Context Protocol (MCP) in AI Lessons: A Complete Technical Guide
The Model Context Protocol (MCP) implementation in the AI-Engineering-From-Scratch curriculum provides a lightweight JSON-RPC 2.0 server that exposes three primitives—Tools, Resources, and Prompts—enabling LLMs to discover and invoke capabilities through a standardized contract.
This deep dive explores the reference MCP implementation found in the ai-engineering-from-scratch repository, specifically within the Phase 11 LLM engineering lessons. Understanding these implementation details provides the foundation for building production-grade AI systems that separate model reasoning from tool execution.
MCP Protocol Overview and Core Primitives
The Model Context Protocol defines a client-server architecture where an LLM (client) communicates with a server offering three distinct service primitives. According to the source code in phases/11-llm-engineering/14-model-context-protocol/code/main.py, these primitives are:
- Tools: Pure functions that perform computation or side effects, exposed via
tools/listandtools/callmethods - Resources: Read-only data blobs accessible through
resources/listandresources/readmethods - Prompts: Template-driven messages that the LLM can render before processing, available via
prompts/listandprompts/get
Each primitive follows a JSON-RPC 2.0 request-response pattern, with the protocol version pinned to "2025-06-18" in the reference implementation.
Core Architecture and Data Structures
The implementation uses Python dataclasses to define the three core entities, separating static metadata from executable handlers.
Protocol Version and Entity Definitions
The server declares its protocol compliance at the module level:
PROTOCOL_VERSION = "2025-06-18"
The three entity dataclasses defined in phases/11-llm-engineering/14-model-context-protocol/code/main.py (lines 24-46) encapsulate both metadata and callable implementations:
@dataclass
class Tool:
name: str
description: str
input_schema: dict[str, Any]
handler: Callable[..., Any]
destructive: bool = False
@dataclass
class Resource:
uri: str
description: str
handler: Callable[[], str]
@dataclass
class Prompt:
name: str
description: str
arguments: list[str]
handler: Callable[..., str]
These structures enable type-safe registration while maintaining the flexibility to attach arbitrary callable logic.
Server Implementation and Registration
The MCPServer class manages three internal dictionaries (tools, resources, prompts) and provides decorator-based registration helpers that follow the curriculum's declarative pattern.
Decorator-Based Registration Pattern
The tool decorator (lines 59-63) captures function metadata and stores the implementation:
def tool(self, name: str, description: str, schema: dict[str, Any], *, destructive: bool = False):
def decorator(fn):
self.tools[name] = Tool(name, description, schema, fn, destructive)
return fn
return decorator
Equivalent registration helpers exist for resources (resource, lines 65-71) and prompts (prompt, lines 71-75). This pattern allows lesson authors to expose capabilities without boilerplate:
@server.tool(
name="translate",
description="Translate English text to French.",
schema={"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"]},
)
def translate(text: str) -> dict[str, str]:
return {"translation": f"[FAKE FR] {text}"}
JSON-RPC Dispatch Logic
The handle method (lines 77-124) serves as the central request router, extracting method, params, and id from incoming JSON-RPC messages:
def handle(self, message: dict[str, Any]) -> dict[str, Any]:
method = message.get("method")
params = message.get("params") or {}
request_id = message.get("id")
if method == "initialize":
result = {"protocolVersion": PROTOCOL_VERSION, "capabilities": {}}
elif method == "tools/list":
result = {"tools": [t.__dict__ for t in self.tools.values()]}
elif method == "tools/call":
tool = self.tools[params["name"]]
output = tool.handler(**params.get("arguments", {}))
result = {"content": [{"type": "text", "text": json.dumps(output)}]}
# ... additional method routing
else:
return {"jsonrpc": "2.0", "id": request_id,
"error": {"code": -32601, "message": f"unknown method: {method}"}}
return {"jsonrpc": "2.0", "id": request_id, "result": result}
Key implementation details include:
- Protocol Handshake: The
initializemethod returns the protocol version and server capabilities - Destructive Operation Hints: Tools marked with
destructive=Truetrigger the addition ofannotations.destructiveHint: truein the response (lines 96-98), allowing clients to warn users before execution - Standardized Error Codes: Missing parameters raise
-32602(Invalid Params), while unknown methods return-32601(Method Not Found), adhering strictly to the JSON-RPC 2.0 specification
Client-Side Communication
The MCPClient class provides an in-process simulation of a real remote client, composing JSON-RPC requests and unwrapping responses:
def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
message = {"jsonrpc": "2.0", "id": self._next_id(),
"method": method, "params": params or {}}
response = self.server.handle(message)
if "error" in response:
raise RuntimeError(response["error"]["message"])
return response["result"]
The client maintains monotonically increasing request IDs via _next_id() to satisfy JSON-RPC requirements for unique identifiers.
Complete Working Example
The reference implementation demonstrates a full lifecycle in main.py, starting with server instantiation and tool registration:
server = MCPServer("demo-server")
@server.tool(name="add", description="Add two integers", schema={"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}, "required": ["a", "b"]})
def add(a: int, b: int) -> dict[str, int]:
return {"sum": a + b}
@server.resource("config://app", "Application configuration")
def get_config() -> str:
return json.dumps({"debug": True, "version": "1.0.0"})
The client interaction pattern follows a strict discovery-then-execution flow:
client = MCPClient(server)
# Protocol initialization
init = client.request("initialize", {"protocolVersion": PROTOCOL_VERSION})
# Capability discovery
tools = client.request("tools/list")["tools"]
# Tool invocation
result = client.request("tools/call", {"name": "add", "arguments": {"a": 40, "b": 2}})
# Returns: {"content": [{"type": "text", "text": "{\"sum\": 42}"}]}
# Resource access
config = client.request("resources/read", {"uri": "config://app"})
Integration with the Broader Curriculum
This foundational implementation in phases/11-llm-engineering/14-model-context-protocol/code/main.py serves as the building block for advanced lessons in the repository:
- Phase 13 "Tools & Protocols": Extends the basic server with a registry and OPA-based policy gate (
phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py) - Phase 13 Lesson 18: Implements production-grade MCP authentication with JWT signing and JWKS caching (
phases/13-tools-and-protocols/18-mcp-auth-production/code/main.py) - Phase 16 "Multi-Agent & Swarms": Positions MCP as the vertical agent-to-tool channel, contrasting with A2A (Agent-to-Agent) for horizontal communication between agents
The progression from the in-process demo to stdio-based servers (phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py) and multi-server clients (phases/13-tools-and-protocols/08-building-an-mcp-client/code/main.py) illustrates the protocol's transport flexibility.
Summary
- The Model Context Protocol is implemented as a JSON-RPC 2.0 server in
phases/11-llm-engineering/14-model-context-protocol/code/main.py, supporting three primitives: Tools, Resources, and Prompts - Registration architecture uses Python decorators to bind callable handlers to metadata schemas, with special support for destructive operation annotations
- Request dispatch follows strict JSON-RPC semantics, returning standard error codes (
-32601,-32602) for invalid methods or parameters - Client implementation handles message ID generation and response unwrapping, simulating real-world transport over stdio or HTTP
- Curriculum progression moves from this basic in-process demo to authenticated, registry-based production systems in later phases
Frequently Asked Questions
What distinguishes Tools from Resources in the MCP implementation?
Tools are executable functions that perform computation or side effects and accept input parameters defined by JSON schemas, while Resources are read-only data blobs identified by URIs that return static content without arguments. According to the source code, tools are invoked via tools/call with an arguments payload, whereas resources are accessed via resources/read using only a URI identifier.
How does the MCP server handle potentially dangerous operations?
The implementation includes a destructive boolean flag in the Tool dataclass. When set to True during registration, the server adds annotations.destructiveHint: true to the tool's metadata in the tools/list response. This allows MCP clients to surface warnings to users before executing operations that might modify state or delete data.
What transport mechanisms does this MCP implementation support?
The reference implementation in Phase 11 uses in-process communication by directly calling server.handle() from the client. However, the curriculum evolves to support stdio transport in phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py, and production deployments typically extend this to HTTP or WebSocket transports while maintaining the same JSON-RPC message format.
How does the basic MCP server evolve into a production system?
Later lessons in the repository add three critical layers: a registry for managing multiple MCP server instances (phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py), OPA-based policy gates for authorization, and JWT-based authentication with JWKS caching (phases/13-tools-and-protocols/18-mcp-auth-production/code/main.py). These additions transform the simple JSON-RPC handler into a secure, multi-tenant service mesh component.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →