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/list and tools/call methods
  • Resources: Read-only data blobs accessible through resources/list and resources/read methods
  • Prompts: Template-driven messages that the LLM can render before processing, available via prompts/list and prompts/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 initialize method returns the protocol version and server capabilities
  • Destructive Operation Hints: Tools marked with destructive=True trigger the addition of annotations.destructiveHint: true in 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:

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →