How to Set Up an MCP Server for AI Projects: A Complete Guide

Setting up a Model Context Protocol (MCP) server requires implementing a JSON-RPC 2.0 dispatch loop that exposes tools, resources, and prompts via stdio or HTTP transport, starting with the initialize handshake and following three strict rules for message handling.

The Model Context Protocol (MCP) has emerged as the de-facto standard for connecting AI systems to external tools and data sources. This guide walks through the complete implementation found in the rohitg00/ai-engineering-from-scratch repository, providing both the foundational stdio server and production-ready graduation paths using FastMCP and HTTP transports.

Core Architecture of an MCP Server

An MCP server acts as a bridge between LLM clients and external capabilities. According to the source code in phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py, the architecture revolves around a strict JSON-RPC message protocol and three specialized registries.

The Dispatch Loop

The heart of every MCP server is the dispatch loop defined in docs/en.md. This loop reads newline-delimited JSON-RPC messages from stdin, routes them to appropriate handlers, and writes responses to stdout. The implementation follows three inviolable rules:

  1. Only JSON-RPC envelopes may be printed to stdout; all debugging and logging must go to stderr.
  2. Every request must be answered with a response carrying the same id as the incoming message.
  3. Notifications (messages without an id) receive no response.

These rules ensure that any MCP-aware client—from Claude Code to the OpenAI SDK—can safely interoperate with your server without parsing ambiguity.

The Initialize Handshake

Before serving tools or resources, the server must respond to the initialize method. In main.py, the handle_initialize function returns the protocol version and capability flags:

def handle_initialize(params: dict) -> dict:
    return {
        "protocolVersion": "2025-11-25",
        "capabilities": {
            "tools": {"listChanged": False},
            "resources": {"listChanged": False, "subscribe": False},
            "prompts": {"listChanged": False}
        },
        "serverInfo": {"name": "notes-lesson-07", "version": "1.0.0"},
    }

This handshake informs the client which features the server supports (tools, resources, prompts) and establishes the protocol version contract.

The Three Registries

After initialization, the server exposes capabilities through three distinct registries located in main.py:

  • Tool Registry (TOOLS list, lines 36-73): Declares executable functions with JSON Schema input definitions and annotations. Tools can perform side effects or read-only queries.
  • Resource Registry (resources list, lines 55-61): Exposes read-only data via URI schemes like file:// or notes://.
  • Prompt Registry (prompts list, lines 76-84): Supplies slash-command templates that clients can inject into LLM prompts.

Step-by-Step MCP Server Setup

Follow these steps to set up your own MCP server using the reference implementation from the ai-engineering-from-scratch curriculum.

1. Clone the Repository and Navigate to the Lesson

Start by cloning the repository and moving into the specific lesson directory:

git clone https://github.com/rohitg00/ai-engineering-from-scratch.git
cd ai-engineering-from-scratch/phases/13-tools-and-protocols/07-building-an-mcp-server/

2. Run the Minimal stdio Implementation

The lesson requires only Python 3.11+ standard library dependencies. Verify the server works by running the built-in demo:

python code/main.py --demo

This command sends a sequence of initialize, tools/list, and tools/call messages to demonstrate the JSON-RPC flow.

3. Test the Server Manually

Drive the server manually using echo and pipes to verify custom payloads:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python code/main.py

The server should return a JSON-RPC response with matching id and the initialization payload.

4. Extend with Custom Tools

To add functionality, modify code/main.py by extending the TOOLS list with a new entry containing name, description, inputSchema, and annotations. Then map the tool name to a Python function in the TOOL_EXECUTORS dictionary. The server automatically exposes the new capability through the tools/list and tools/call endpoints.

5. Graduate to FastMCP for Production

For production environments, collapse the 180-line stdlib implementation into less than 80 lines using the FastMCP Python SDK:

from fastmcp import FastMCP

app = FastMCP("my-server")

@app.tool()
def my_tool(arg: str) -> list[dict]:
    return [{"type": "text", "text": f"Result for {arg}"}]

FastMCP handles the JSON-RPC framing, transport negotiation, and error handling automatically while maintaining full protocol compliance.

6. Deploy via HTTP for Horizontal Scaling

When you need stateless horizontal scaling behind a load balancer, replace the stdio transport with FastMCP's StreamableHTTP server. The capstone lesson in phases/19-capstone-projects/13-mcp-server-with-registry/outputs/skill-mcp-server.md provides a production-grade scaffold that includes OAuth 2.1 scope matrices and OPA policy gates for destructive tools.

Deploy the HTTP server using Uvicorn:

uvicorn your_fastmcp_module:app --host 0.0.0.0 --port 8000

Key Implementation Details from the Source Code

The reference implementation in code/main.py demonstrates the strict message handling required by the 2025-11-25 specification. The dispatch loop parses incoming JSON-RPC envelopes, validates the method field, and routes to handlers like handle_initialize or handle_tools_call. Each handler returns a dictionary that the loop wraps in the proper JSON-RPC response format with the original request id.

The TOOLS list defines the contract between your Python functions and the LLM client. Each tool entry includes an inputSchema following JSON Schema Draft 7, allowing clients to validate arguments before invoking the server. The annotations field provides hints about whether a tool is read-only or destructive, enabling client-side permission prompts.

Summary

  • MCP servers use a JSON-RPC 2.0 protocol over stdio or HTTP to expose tools, resources, and prompts to LLMs.
  • Three strict rules govern the dispatch loop: stdout-only for JSON-RPC, matching request/response IDs, and no responses for notifications.
  • Initialize handshake establishes capabilities and protocol version compliance before any tool execution.
  • FastMCP SDK reduces boilerplate from 180 lines to under 80 while maintaining full specification adherence.
  • Production deployment uses StreamableHTTP transport with OAuth 2.1 and OPA policies as detailed in Phase 19 of the curriculum.

Frequently Asked Questions

What is the difference between stdio and HTTP transport for MCP servers?

The stdio transport runs the server as a subprocess communicating via standard input and output, ideal for local desktop applications like Claude Code or Cursor. HTTP transport, specifically StreamableHTTP, exposes the server as a stateless web endpoint suitable for horizontal scaling and remote access behind load balancers.

Do I need external dependencies to run the basic MCP server?

No. The lesson implementation in code/main.py uses only Python 3.11+ standard library modules. However, graduating to FastMCP requires installing the fastmcp package from PyPI, and HTTP deployment requires uvicorn or another ASGI server.

How do I add authentication to my MCP server?

For stdio servers, authentication typically happens at the host application level. For HTTP deployments, the Phase 19 capstone in phases/19-capstone-projects/13-mcp-server-with-registry/ demonstrates OAuth 2.1 scope validation and OPA (Open Policy Agent) gates for controlling access to destructive tools.

Can an MCP server handle multiple concurrent connections?

The stdio implementation handles sequential messages from a single client. For concurrent connections, use the StreamableHTTP transport with FastMCP, which runs on ASGI servers like Uvicorn that handle concurrency through async workers or multiple processes.

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 →