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

> Learn to build a custom MCP server using Python and TypeScript with this comprehensive guide. Leverage curriculum outputs for a robust Model Context Protocol architecture.

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

---

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

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

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

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

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

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

```python
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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/src/protocol.ts) (core logic), [`src/transport.ts`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/src/transport.ts) (HTTP layer), and [`src/types.ts`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/src/types.ts) (definitions).

Register a tool in TypeScript:

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

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