How to Build a Custom MCP Server from Curriculum Outputs: Complete Python & TypeScript Guide

The curriculum's capstone project provides a ready-to-run scaffold that implements the full Model Context Protocol (MCP) architecture including tool schemas, OAuth-style authorization, policy gates, and audit logging.

The rohitg00/ai-engineering-from-scratch repository delivers a production-grade foundation for building Model Context Protocol servers through its "MCP Server with Registry" capstone lesson. This scaffold demonstrates how to build a custom MCP server that exposes tools to AI agents while enforcing security policies and maintaining comprehensive audit trails. By leveraging the provided Python and TypeScript implementations, you can assemble a compliant MCP service that handles everything from tool discovery to human-in-the-loop approval for destructive operations.

Understanding the MCP Server Architecture

The scaffold implements a complete MCP ecosystem contained primarily in phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py. At its core, the architecture separates concerns between tool definition, authorization, policy enforcement, and execution dispatching.

The system uses a declarative tool schema approach where each capability advertises its required OAuth 2.1 scopes, destructiveness flag, and JSON Schema validation rules. This allows the server to expose metadata to discovery registries while enforcing security boundaries at runtime.

Step-by-Step Implementation Guide

Define Tool Schemas with Required Scopes

Begin by creating a ToolSchema dataclass that declares the tool's contract. According to the source code in main.py (lines 25-32), each schema must specify the tool name, required OAuth scope, destructiveness boolean, human-readable description, and a JSON Schema for input validation.

from dataclasses import dataclass

@dataclass
class ToolSchema:
    name: str
    required_scope: str
    destructive: bool
    description: str
    input_schema: dict

Instantiate the MCPServer Class

The MCPServer class (lines 37-44) acts as the container for your tools and configuration. Initialize it with a server name and base URL to create the registry container:

from main import MCPServer

server = MCPServer(name="production-mcp", url="https://api.example.com/mcp")

This object maintains an internal registry mapping ToolSchema objects to their corresponding Python callables.

Register Tool Handlers

Use the register method (lines 71-84) to bind schemas to implementation functions. The curriculum demo registers read-only tools (Postgres queries, S3 lists) on one server instance and destructive tools (Jira creation) on another to demonstrate isolation patterns:

from main import ToolSchema

def echo_handler(args: dict) -> dict:
    return {"echo": args}

server.register(
    ToolSchema(
        name="util.echo",
        required_scope="util:echo",
        destructive=False,
        description="Echo back the supplied JSON payload",
        input_schema={"type": "object", "additionalProperties": True},
    ),
    echo_handler,
)

Implement OAuth-Style Token Handling

The Token class (lines 68-78) carries user identity, granted scopes, and an optional "approved:by:human" timestamp required for destructive operations:

from main import Token

token = Token(
    user="alice", 
    scopes={"util:echo", "jira:read"},
    approved_by_human="2024-01-15T10:30:00Z"  # Required for destructive tools

)

Apply OPA-Style Policy Decisions

The policy_decide function (lines 85-97) implements a four-layer authorization gate, returning a boolean decision and message. It verifies tool existence, validates required scopes, checks human approval timestamps for destructive operations, and enforces payload size limits before permitting execution.

Dispatch Calls with Audit Logging

The dispatch function (lines 122-143) orchestrates execution while generating tamper-evident audit trails:

from main import dispatch, AuditEntry

audit_log: list[AuditEntry] = []
result = dispatch(server, token, "util.echo", {"msg": "hello"}, audit_log)

Each invocation produces an AuditEntry (lines 102-119) containing timestamps, user identity, outcomes, and redacted arguments. The redaction logic automatically removes PII patterns including email addresses, SSNs, and phone numbers from logs before storage.

Set Up the Registry for Discovery

The Registry class (lines 146-165) enables cross-server tool discovery and search capabilities:

from main import Registry

registry = Registry()
registry.register(server)
matches = registry.search("echo")  # Returns [('production-mcp', 'util.echo')]

Customizing the MCP Server for Production

To adapt the scaffold for real-world deployment, modify these specific components:

  • Add new tools: Define additional ToolSchema objects and handler functions, then call server.register() to bind them to the server instance.

  • Swap the transport layer: Replace the in-process dispatch with the StreamableHTTP transport defined in phases/19-capstone-projects/13-mcp-server-with-registry/code/ts/src/transport.ts to accept HTTP requests from external AI clients.

  • Integrate real OAuth 2.1: Substitute the Token class with a JWT verification middleware that fetches JWKS from your authorization server, validates audience and issuer claims, and populates the scopes set from the JWT payload.

  • Persist audit logs: Modify the dispatch function to write AuditEntry objects to PostgreSQL, a SIEM, or append-only files rather than keeping them in an in-memory list.

TypeScript Implementation

The curriculum provides a parallel TypeScript implementation in phases/19-capstone-projects/13-mcp-server-with-registry/code/ts/. Key files include src/protocol.ts (core logic), src/transport.ts (HTTP layer), and src/types.ts (definitions).

Register a tool in TypeScript:

import { MCPServer, ToolSchema } from "./src/protocol";

const server = new MCPServer("my-ts-mcp", "https://myhost/mcp");
server.register(
    new ToolSchema({
        name: "util.echo",
        requiredScope: "util:echo",
        destructive: false,
        description: "Echo back the supplied JSON payload",
        inputSchema: { type: "object", additionalProperties: true },
    }),
    async (args) => ({ echo: args })
);

Dispatch using the transport layer:

import { dispatch } from "./src/transport";

const token = { user: "alice", scopes: new Set(["util:echo"]) };
const result = await dispatch(server, token, "util.echo", { msg: "hi" });

Summary

  • The ai-engineering-from-scratch curriculum provides a complete MCP server scaffold in phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py.
  • ToolSchema and MCPServer classes handle tool declaration and registration (lines 25-44).
  • Token objects enforce OAuth-style scoping and human-in-the-loop approval for destructive operations (lines 68-78).
  • policy_decide implements OPA-style authorization checks before execution (lines 85-97).
  • dispatch executes tools while generating redacted AuditEntry logs for compliance (lines 102-143).
  • The Registry class enables cross-server tool discovery (lines 146-165).
  • TypeScript implementations in code/ts/ provide Node.js compatibility with HTTP transport layers.

Frequently Asked Questions

What is the Model Context Protocol (MCP)?

The Model Context Protocol is a specification that allows AI agents to discover and invoke tools exposed by external services through a standardized interface. It defines how tools advertise their capabilities via JSON Schema, how clients authenticate requests using OAuth-scoped tokens, and how servers enforce policy decisions before executing destructive operations.

How do I add real authentication instead of the demo Token class?

Replace the Token dataclass with a JWT verification middleware that validates tokens against your authorization server's JWKS endpoint. Extract the scopes claim from the JWT payload to populate the scopes set, and ensure the middleware validates audience and issuer claims before the request reaches the dispatch function.

Can this MCP server integrate with Claude Desktop or other AI clients?

Yes, by implementing the StreamableHTTP transport defined in src/transport.ts, your server communicates over HTTP according to the MCP specification. Configure Claude Desktop or compatible clients to point to your server's base URL exposed through this transport layer, ensuring your tool schemas follow the expected JSON Schema format.

How do I persist audit logs instead of storing them in memory?

Modify the dispatch function in main.py (lines 122-143) to write AuditEntry objects to a persistent store. Insert records into PostgreSQL, send them to a log aggregation service like Splunk or ELK, or write to append-only files. Ensure the redaction logic—which removes PII like emails and SSNs—executes before persistence to maintain compliance with privacy regulations.

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 →