How to Build MCP Servers Using the 17-Lesson Route: A Complete Implementation Guide
The 17-lesson route demonstrates how to build production-ready Model Context Protocol servers using only Python's standard library, implementing stateless architecture with separate registry metadata and runtime discovery layers, plus policy-driven governance and comprehensive audit logging.
The Model Context Protocol (MCP) enables AI systems to securely interact with external tools and data sources. In the rohitg00/ai-engineering-from-scratch repository, Phase 19 (Capstone 13) provides a reference implementation showing exactly how to build MCP servers from scratch without external dependencies. This lesson implements a stateless server architecture that separates publication contracts from runtime capabilities while enforcing security through policy decisions and approval records.
Understanding the Stateless MCP Server Architecture
The implementation in phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py treats the server as an immutable, stateless entity. Each request carries its own metadata, and the server never stores session state between invocations.
The Two-Layer Discovery Model
The design separates concerns into two distinct layers:
- Publication Layer: Uses
server.jsonas a registry metadata document that describes the server's name, version, and remote transport (e.g., streamable-http). - Runtime Layer: Uses the
server/discoverRPC endpoint to announce the protocol revision, capabilities, and server identity that a live instance actually supports.
This separation allows the registry to validate both the static configuration and the live runtime behavior before deployment.
Frozen Dataclass Implementation
The server model uses a frozen dataclass to ensure immutability and thread safety:
@dataclass(frozen=True)
class MCPServer:
name: str
title: str
description: str
version: str
url: str
trusted_issuer: str
tools: dict[str, ToolSchema] = field(default_factory=dict)
handlers: dict[str, Handler] = field(default_factory=dict)
The frozen=True parameter prevents accidental state mutation, while the tools dictionary maps tool names to their schemas and handler functions. Each request must include its own _meta block, as seen in lines 33-41 of the reference implementation.
Implementing the Core Protocol Primitives
The server implements three essential MCP protocol primitives that handle discovery, capability advertisement, and execution.
Server Definition and Tool Registration
Tools are registered using a ToolSchema that defines required scopes, descriptive annotations, and JSON Schema validation for arguments. Here is how read-only and destructive tools are registered:
server.register(
ToolSchema(
"postgres.readonly",
"postgres:query:readonly",
False,
"Run an approved read-only query.",
{"type":"object","properties":{"sql":{"type":"string"}},"required":["sql"],"additionalProperties":False},
),
lambda args: {"rows": [[1]], "sql": args["sql"]},
)
The register method binds the schema to a handler callable. Read-only tools like postgres.readonly and s3.list require different approval workflows than destructive tools like jira.create.
The discover Endpoint
The discover method validates request metadata and returns the server's supported protocol versions, capabilities, TTL, and cache scope. It ensures the client and server speak compatible protocol revisions, returning error code -32022 for unsupported versions.
The tools_list Endpoint
This endpoint returns a deterministic, cache-aware list of registered tools sorted by name. The deterministic ordering allows clients to cache capabilities efficiently while detecting schema updates.
The dispatch Handler
The dispatch function serves as the central execution engine:
- Validates incoming request metadata
- Invokes
policy_decideto check authorization - Executes the tool handler
- Records an
AuditEntrywith redacted payloads
The implementation follows MCP's JSON-RPC error codes, returning -32602 for invalid parameters and other standard codes for protocol violations.
Registry Validation and Governance
A lightweight Registry class manages the relationship between published metadata and live server instances.
Registry Class and Document Validation
The registry stores both the static server.json document and live discovery results. The validate_registry_document method ensures:
- Required fields are present (name, version, URL)
- Name formatting follows conventions
- Version strings are concrete (no wildcards)
- Remote profile shapes match expected schemas
Runtime Alignment Checks
The validate_runtime_alignment method compares the live serverInfo from the discover endpoint against the published server.json metadata. This catches configuration drift before deployment, ensuring that the registry advertisement matches the actual runtime capabilities.
Security, Policy, and Audit Controls
The implementation includes comprehensive security controls that govern tool execution without external dependencies.
Policy Decisions and Token Validation
The policy_decide function validates:
- Token issuer and audience claims
- Expiration timestamps
- Required OAuth scopes for the requested tool
- Payload size limits
Each tool schema declares its required scopes, and the policy engine enforces these requirements before execution.
Approval Records for Destructive Operations
Destructive tools require an explicit ApprovalRecord that binds an authorization to a specific actor, tool, argument digest, target URL, and expiry time. This prevents replay attacks and ensures temporal limitations on sensitive operations:
approval = ApprovalRecord.for_action(
actor="bob",
tool="jira.create",
args={"title": "Urgent bug"},
target=destructive.url,
expires_at=time.time() + 900,
)
Audit Logging with Data Redaction
Every successful or denied call creates an immutable AuditEntry. The redact helper function automatically removes sensitive data (emails, SSNs, credit card numbers) before persistence, ensuring compliance with privacy regulations while maintaining forensic value.
End-to-End Implementation Example
Here is how to build a read-only server, register it, and inspect the registry metadata:
from main import build_readonly_server, Registry
readonly = build_readonly_server()
registry = Registry()
registry.register(readonly) # validates server.json and live discovery
print(registry.entries[readonly.name]) # <-- published metadata
print(registry.runtime_discovery[readonly.name]) # <-- live discovery result
To dispatch a tool call with policy enforcement:
from main import request_meta, dispatch, Token
import time
token = Token(
user="alice",
issuer=readonly.trusted_issuer,
audience=readonly.url,
scopes=frozenset({"postgres:query:readonly"}),
expires_at=time.time() + 3600,
)
audit_log = []
result = dispatch(
server=readonly,
token=token,
tool="postgres.readonly",
args={"sql": "SELECT 1"},
meta=request_meta(),
audit=audit_log,
)
print(result) # → complete result with rows
print(audit_log[0]) # → audit entry (redacted)
For destructive operations requiring approval:
from main import build_destructive_server, ApprovalRecord, request_meta, dispatch, Token
destructive = build_destructive_server()
approval = ApprovalRecord.for_action(
actor="bob",
tool="jira.create",
args={"title": "Urgent bug"},
target=destructive.url,
expires_at=time.time() + 900,
)
token = Token(
user="bob",
issuer=destructive.trusted_issuer,
audience=destructive.url,
scopes=frozenset({"jira:write"}),
expires_at=time.time() + 3600,
)
audit = []
result = dispatch(
server=destructive,
token=token,
tool="jira.create",
args={"title": "Urgent bug"},
meta=request_meta(),
audit=audit,
approval=approval,
)
print(result["structuredContent"]["created"]) # → True
Summary
- Stateless architecture prevents session storage vulnerabilities by requiring each request to carry complete metadata and using frozen dataclasses for server definitions.
- Two-layer discovery separates static registry metadata (
server.json) from live runtime capabilities (server/discover), enabling validation of deployment alignment. - Policy engine validates JWT tokens, OAuth scopes, payload sizes, and expiration times before executing any tool handler.
- Approval records provide cryptographic binding of authorization to specific actions, preventing replay attacks on destructive operations like
jira.create. - Audit logging with automatic PII redaction ensures forensic accountability while protecting sensitive user data.
- Zero-dependency implementation uses only Python's standard library (dataclasses, json, typing), making the reference model portable to TypeScript and other languages.
Frequently Asked Questions
What makes this MCP server implementation "stateless"?
The server uses a frozen dataclass (MCPServer with frozen=True) that cannot be modified after creation, and it stores no session state between requests. Each JSON-RPC request must include a complete _meta block containing all necessary context, while the server validates tokens, checks policies, and executes handlers without maintaining server-side session storage.
How does the registry prevent deployment drift?
The Registry class runs validate_runtime_alignment to compare the live serverInfo returned by the discover endpoint against the static server.json document. This catches discrepancies between the published metadata (name, version, capabilities) and the actual runtime behavior before the server is allowed to serve production traffic.
What security controls protect destructive tool operations?
Destructive tools require an explicit ApprovalRecord that cryptographically binds an actor, tool name, argument digest, target URL, and expiration time. The policy_decide function validates this approval exists and matches the current request before executing destructive handlers, preventing unauthorized or replayed operations.
Can this implementation be ported to other programming languages?
Yes, the reference implementation uses only standard library features available in most languages. The code/ts/ directory contains a TypeScript reference model exposing the same JSON-RPC contracts over stdio, demonstrating that the core MCP contract logic remains pure and testable across language boundaries.
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 →