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
ToolSchemaobjects and handler functions, then callserver.register()to bind them to the server instance. -
Swap the transport layer: Replace the in-process dispatch with the
StreamableHTTPtransport defined inphases/19-capstone-projects/13-mcp-server-with-registry/code/ts/src/transport.tsto accept HTTP requests from external AI clients. -
Integrate real OAuth 2.1: Substitute the
Tokenclass with a JWT verification middleware that fetches JWKS from your authorization server, validates audience and issuer claims, and populates thescopesset from the JWT payload. -
Persist audit logs: Modify the
dispatchfunction to writeAuditEntryobjects 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-scratchcurriculum provides a complete MCP server scaffold inphases/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →