# How to Build MCP Servers Using the 17-Lesson Route: A Complete Implementation Guide

> Learn to build production-ready Model Context Protocol MCP servers with Python standard library. Explore stateless architecture, metadata, runtime discovery, governance, and audit logging.

- Repository: [Rohit Ghumare/ai-engineering-from-scratch](https://github.com/rohitg00/ai-engineering-from-scratch)
- Tags: how-to-guide
- Published: 2026-08-31

---

**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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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.json`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/server.json) as a registry metadata document that describes the server's name, version, and remote transport (e.g., *streamable-http*).
- **Runtime Layer**: Uses the `server/discover` RPC 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:

```python
@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:

```python
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:

1. Validates incoming request metadata
2. Invokes `policy_decide` to check authorization
3. Executes the tool handler
4. Records an `AuditEntry` with 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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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:

```python
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:

```python
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:

```python
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:

```python
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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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.