Skip to main content
Glama

šŸ”Œ MCP Learning Project

A complete educational project demonstrating the Model Context Protocol (MCP) end-to-end.

This project is designed for developers who are completely new to MCP and want to understand:

  • What an MCP Server is

  • What an MCP Client is

  • How they communicate

  • How tools are registered and discovered

  • How requests flow and responses return


šŸ“‹ Table of Contents


šŸŽÆ Project Overview

This project implements:

Component

Description

MCP Server

A FastMCP server exposing 5 tools via stdio transport

Interactive Client

A Rich-powered CLI that discovers and calls tools

Chat Interface

A natural language chatbot that maps queries to tools

Pydantic Validation

Type-safe input validation with friendly errors

Logging

Structured logs with INFO/WARNING/ERROR levels

Architecture Diagrams

Mermaid diagrams explaining every component

Tech Stack

  • Python 3.11+

  • uv package manager

  • Official MCP Python SDK (mcp[cli] with FastMCP)

  • asyncio

  • Pydantic v2

  • Rich library


šŸ—ļø Architecture

High-Level Architecture

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”          ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│     MCP Client      │   stdio  │     MCP Server      │
│                     │◄────────►│                     │
│  • Rich CLI/Chat    │  JSON-   │  • FastMCP Router   │
│  • ClientSession    │  RPC 2.0 │  • 5 Registered     │
│  • Tool Discovery   │          │    Tools            │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜          ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Request Flow

User
 ↓
Client UI (Rich terminal)
 ↓
ClientSession.call_tool(name, arguments)
 ↓
JSON-RPC 2.0 Request (serialized)
 ↓
stdio Transport (stdin pipe to server subprocess)
 ↓
FastMCP Server (deserializes, routes by tool name)
 ↓
@mcp.tool() Function → Pydantic validation → Business logic
 ↓
JSON Response (consistent format)
 ↓
stdio Transport (stdout pipe back to client)
 ↓
ClientSession (deserializes response)
 ↓
Rich UI (pretty-prints with syntax highlighting)
 ↓
User sees the result āœ…

Tool Discovery Flow

Client                          Server
  |                               |
  |──── initialize() ───────────►|
  |◄──── capabilities ──────────|
  |                               |
  |──── list_tools() ───────────►|
  |◄──── tool schemas ──────────|
  |       (name, description,     |
  |        inputSchema)           |
  |                               |
  |  Client now knows all tools!  |

šŸ“Œ Key MCP advantage: The client doesn't need hardcoded endpoints. It discovers tools dynamically!


šŸ“ Folder Structure

mcp-learning-project/
│
ā”œā”€ā”€ server/                    # MCP Server (the "backend")
│   ā”œā”€ā”€ __init__.py           # Package marker
│   ā”œā”€ā”€ server.py             # FastMCP setup + tool registration
│   ā”œā”€ā”€ tools.py              # Pure business logic (no MCP dependency)
│   ā”œā”€ā”€ models.py             # Pydantic validation models
│   └── quotes.py             # Hardcoded quote data
│
ā”œā”€ā”€ client/                    # MCP Client (the "frontend")
│   ā”œā”€ā”€ __init__.py           # Package marker
│   ā”œā”€ā”€ client.py             # Interactive CLI client
│   ā”œā”€ā”€ chat.py               # Natural language chat interface
│   └── ui.py                 # Rich terminal UI helpers
│
ā”œā”€ā”€ diagrams/                  # Documentation
│   └── architecture.md       # Mermaid architecture diagrams
│
ā”œā”€ā”€ logs/                      # Generated at runtime
│   └── app.log               # Structured application logs
│
ā”œā”€ā”€ screenshots/               # Screenshots placeholder
│
ā”œā”€ā”€ README.md                  # This file
ā”œā”€ā”€ pyproject.toml            # Project configuration
ā”œā”€ā”€ uv.lock                   # Dependency lock file (auto-generated)
└── .gitignore                # Git ignore rules

Why Each File Exists

File

Purpose

server/server.py

MCP registration layer — connects business logic to the MCP protocol using @mcp.tool() decorators

server/tools.py

Business logic — pure Python functions that can be tested without MCP

server/models.py

Validation — Pydantic models that enforce type safety at the input boundary

server/quotes.py

Data — separates data from logic for clean architecture

client/client.py

Interactive client — demonstrates the full MCP client workflow

client/chat.py

Chat interface — shows how natural language maps to tool calls

client/ui.py

Presentation — all Rich formatting in one place


šŸš€ Installation

Prerequisites

  • Python 3.11 or higher

  • uv package manager

Setup

# 1. Clone the repository
git clone <repository-url>
cd mcp-learning-project

# 2. Create a virtual environment and install dependencies
uv sync

# This will:
# - Create a .venv/ directory
# - Install mcp[cli], pydantic, and rich
# - Generate uv.lock for reproducibility

ā–¶ļø Running the Project

The client automatically starts the server as a subprocess — you only need one terminal:

uv run python -m client.client

Option 2: Chat Interface

uv run python -m client.chat

Option 3: Server Only (for debugging)

If you want to run the server independently (e.g., to test with MCP Inspector):

uv run python -m server.server

āš ļø The server uses stdio transport, so it reads from stdin and writes to stdout. Running it directly will wait for JSON-RPC input — this is expected behavior.

Testing with MCP Inspector

The MCP Inspector is a visual debugging tool:

npx -y @modelcontextprotocol/inspector uv run python -m server.server

🧰 Available Tools

1. current_time

Returns the current UTC time in ISO 8601 format.

Input

None

Output

{"success": true, "data": {"current_time": "2026-07-01T12:00:00Z"}}

2. calculator

Performs arithmetic operations on two numbers.

Input

{"a": 10, "b": 20, "operation": "add"}

Operations

add, subtract, multiply, divide

Output

{"success": true, "data": {"result": 30.0}}

Division by zero

{"success": false, "error": "Division by zero is not allowed"}

3. random_quote

Returns a random inspirational quote from an internal list of 25 quotes.

Input

None

Output

{"success": true, "data": {"quote": "Stay hungry, stay foolish. — Steve Jobs"}}

4. weather

Returns mock weather data for Indian cities.

Input

{"city": "Jaipur"}

Supported Cities

Jaipur, Delhi, Mumbai, Bangalore, Chennai, Kolkata, Hyderabad, Pune

Output

{"success": true, "data": {"city": "Jaipur", "temperature": "35°C", "condition": "Sunny"}}

Unknown city

{"success": false, "error": "City 'London' not found. Available cities: ..."}

5. uuid_generator

Generates a random UUID (version 4).

Input

None

Output

{"success": true, "data": {"uuid": "550e8400-e29b-41d4-a716-446655440000"}}


šŸ’» Example Execution

Interactive Client

╭─────────────────────────────────────────────╮
│ šŸ”Œ MCP Learning Client                     │
│    Connected to MCP Server via stdio        │
╰─────────────────────────────────────────────╯

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│            šŸ“‹ Available Tools                │
ā”œā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ #  │ Tool Name      │ Description            │
ā”œā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 1  │ current_time   │ Get the current UTC    │
│    │                │ time in ISO 8601       │
ā”œā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 2  │ calculator     │ Perform arithmetic     │
│    │                │ operations             │
ā”œā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 3  │ random_quote   │ Get a random           │
│    │                │ inspirational quote    │
ā”œā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 4  │ weather        │ Get mock weather data  │
ā”œā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 5  │ uuid_generator │ Generate a random UUID │
ā””ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

>>> 2
Enter calculator inputs:
  First number (a): 45
  Second number (b): 87
  Operation (add/subtract/multiply/divide): multiply
ā³ Calling calculator...
╭─── āœ… Success ───╮
│ {                │
│   "result": 3915 │
│ }                │
╰──────────────────╯
ā±  Completed in 0.023s

>>> info weather
šŸ“Œ weather

Get mock weather data for an Indian city.

šŸ“„ Input Schema:
{
  "type": "object",
  "properties": {
    "city": { "type": "string" }
  },
  "required": ["city"]
}

>>> exit
šŸ‘‹ Goodbye!

Chat Interface

šŸ¤– MCP Chat Interface
Ask me about weather, calculations, time, quotes, or UUIDs.
Type 'exit' to quit.

You: What is the weather in Jaipur?
ā³ Calling weather tool...
╭─── šŸ¤– Assistant ───╮
│ šŸŒ¤ļø  Weather in      │
│ Jaipur: 35°C, Sunny │
╰─────────────────────╯

You: Multiply 45 and 87
ā³ Calling calculator tool...
╭─── šŸ¤– Assistant ───╮
│ šŸ”¢ The answer is:   │
│ 3915.0               │
╰─────────────────────╯

You: Give me an inspirational quote
ā³ Calling random_quote tool...
╭───── šŸ¤– Assistant ─────╮
│ šŸ’¬ "Stay hungry, stay   │
│ foolish. — Steve Jobs"  │
╰─────────────────────────╯

Example Log Output (logs/app.log)

2026-07-08 10:30:15 | INFO    | mcp.server | FastMCP server instance created: 'MCP Learning Server'
2026-07-08 10:30:15 | INFO    | mcp.server | Starting MCP Learning Server (stdio transport)...
2026-07-08 10:30:16 | INFO    | mcp.client | Client starting — connecting to MCP server...
2026-07-08 10:30:16 | INFO    | mcp.client | stdio transport established
2026-07-08 10:30:16 | INFO    | mcp.client | MCP session initialized
2026-07-08 10:30:16 | INFO    | mcp.client | Discovered 5 tools: ['current_time', 'calculator', ...]
2026-07-08 10:30:20 | INFO    | mcp.client | Calling tool: calculator with args: {'a': 45, 'b': 87, 'operation': 'multiply'}
2026-07-08 10:30:20 | INFO    | mcp.server | Tool called: calculator(a=45.0, b=87.0, op=multiply)
2026-07-08 10:30:20 | INFO    | mcp.server | Tool result: calculator → {'success': True, 'data': {'result': 3915.0}}
2026-07-08 10:30:20 | INFO    | mcp.client | Tool calculator completed in 0.023s

šŸ“š MCP Learning Notes

What is MCP?

The Model Context Protocol (MCP) is a standardized protocol for connecting AI models to external tools and data sources. Think of it as a "USB-C for AI" — a universal interface that lets any AI assistant use any tool without custom integrations.

How MCP Registers Tools

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My Server")

@mcp.tool(
    name="calculator",
    description="Perform arithmetic operations"
)
def calculator(a: float, b: float, operation: str) -> str:
    # FastMCP auto-generates a JSON Schema from the function signature:
    # {
    #   "type": "object",
    #   "properties": {
    #     "a": {"type": "number"},
    #     "b": {"type": "number"},
    #     "operation": {"type": "string"}
    #   },
    #   "required": ["a", "b", "operation"]
    # }
    ...

When @mcp.tool() is called:

  1. FastMCP inspects the function's type hints

  2. Generates a JSON Schema for the inputs

  3. Stores the tool in an internal registry

  4. When list_tools() is called, returns all registered schemas

How the Client Discovers Tools

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# 1. Define how to launch the server
server_params = StdioServerParameters(
    command="uv",
    args=["run", "python", "-m", "server.server"]
)

# 2. Connect via stdio
async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        # 3. Initialize the session (handshake)
        await session.initialize()

        # 4. Discover all tools
        tools = await session.list_tools()
        # tools.tools → list of Tool objects with name, description, inputSchema

        # 5. Call a specific tool
        result = await session.call_tool("calculator", {"a": 10, "b": 20, "operation": "add"})

How Requests are Serialized

MCP uses JSON-RPC 2.0 over the stdio transport:

// Client → Server (request)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "calculator",
    "arguments": {"a": 10, "b": 20, "operation": "add"}
  }
}

// Server → Client (response)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"success\": true, \"data\": {\"result\": 30.0}}"
      }
    ]
  }
}

How Transport Works

This project uses stdio transport:

  • The client spawns the server as a subprocess

  • JSON-RPC messages flow through the subprocess's stdin (requests) and stdout (responses)

  • This is why we never print() to stdout in the server — it would corrupt the protocol!

Other MCP transports include:

  • SSE (Server-Sent Events): HTTP-based, good for remote servers

  • Streamable HTTP: Newer HTTP transport for production use

How MCP Differs from REST

Aspect

REST API

MCP

Discovery

You need documentation/OpenAPI spec

list_tools() returns schemas dynamically

Protocol

HTTP request/response

JSON-RPC 2.0 (bidirectional)

Schema

OpenAPI (optional)

JSON Schema (built-in)

Transport

Always HTTP

stdio, SSE, Streamable HTTP

Standardization

Varies per API

One protocol for all tools

AI Integration

Custom per model

Universal — any model, any tool

How to Add a New Tool

Adding a tool takes 3 steps:

  1. Business logic — Add a function to server/tools.py:

def my_tool(input_param: str) -> dict[str, Any]:
    """Do something useful."""
    return ToolResponse.ok(result="Hello!")
  1. Validation (optional) — Add a Pydantic model to server/models.py:

class MyToolInput(BaseModel):
    input_param: str = Field(..., min_length=1)
  1. Registration — Add a @mcp.tool() decorator in server/server.py:

@mcp.tool(name="my_tool", description="Does something useful")
def tool_my_tool(input_param: str) -> str:
    return json.dumps(my_tool(input_param))

That's it! The client will auto-discover the new tool on next connection.


āš ļø Common Errors

Error

Cause

Solution

Server script not found

Running from wrong directory

cd to project root

ModuleNotFoundError: server

Missing uv sync or wrong Python

Run uv sync first

ConnectionError

Server failed to start

Check logs/app.log for details

Invalid operation

Typo in operation name

Use: add, subtract, multiply, divide

Division by zero

b=0 with divide operation

Expected — handled gracefully

City not found

City not in mock data

Use: Jaipur, Delhi, Mumbai, Bangalore, Chennai, Kolkata

Broken pipe / garbled output

Printing to stdout in server

Always use logging or stderr


šŸ”® Future Improvements

  • Add LLM integration for intelligent intent parsing

  • Implement SSE transport for remote server access

  • Add tool caching and rate limiting

  • Build a web-based UI alongside the CLI

  • Add unit tests for each tool

  • Implement MCP Resources and Prompts (beyond Tools)

  • Add authentication and authorization

  • Create a tool that chains multiple other tools


šŸ“„ License

MIT License — Feel free to use this for learning and education.


Built as an educational project to understand the Model Context Protocol (MCP).