Skip to main content
Glama
README.md
# šŸ”Œ 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](#-project-overview)
- [Architecture](#-architecture)
- [Folder Structure](#-folder-structure)
- [Installation](#-installation)
- [Running the Project](#-running-the-project)
- [Available Tools](#-available-tools)
- [Example Execution](#-example-execution)
- [MCP Learning Notes](#-mcp-learning-notes)
- [Common Errors](#-common-errors)
- [Future Improvements](#-future-improvements)

---

## šŸŽÆ 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](https://docs.astral.sh/uv/getting-started/installation/) package manager

### Setup

```bash
# 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

### Option 1: Interactive Client (Recommended)

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

```bash
uv run python -m client.client
```

### Option 2: Chat Interface

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

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

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

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

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

```json
// 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`:
```python
def my_tool(input_param: str) -> dict[str, Any]:
    """Do something useful."""
    return ToolResponse.ok(result="Hello!")
```

2. **Validation** (optional) — Add a Pydantic model to `server/models.py`:
```python
class MyToolInput(BaseModel):
    input_param: str = Field(..., min_length=1)
```

3. **Registration** — Add a `@mcp.tool()` decorator in `server/server.py`:
```python
@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).*

TDQS

A4.4/5.0

Scored across 5 tools

Disambiguation5/5

Each tool serves a distinct purpose: calculator for arithmetic, current_time for time, random_quote for quotes, uuid_generator for UUIDs, and weather for mock weather data. There is no overlap or ambiguity.

Naming Consistency5/5

All tool names are lowercase, descriptive, and use underscores where needed. They follow a consistent pattern of terse but clear naming.

Tool Count5/5

With 5 tools, the set is well-scoped for a learning/demo server. Each tool provides a distinct utility without being overwhelming or too sparse.

Completeness4/5

The set covers several typical utility functions (math, time, random, identification, weather). A minor gap could be string manipulation or unit conversion, but for a learning server it is adequately complete.

Maintenance

ActivityInactive
ResponsivenessNo issues