Skip to main content
Glama
mhamzanadeem

mcp-demo-server

by mhamzanadeem
README.md
# MCP Demo — Python Agent Tooling from the Ground Up

<p align="center">
  <img src="https://img.shields.io/badge/Python-3.10+-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python Version">
  <img src="https://img.shields.io/badge/MCP-SDK-orange?style=for-the-badge&logo=modelcontextprotocol&logoColor=white" alt="MCP SDK">
  <img src="https://img.shields.io/badge/Transport-stdio-green?style=for-the-badge" alt="Transport">
  <img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License">
</p>

<p align="center">
  <strong>A production-oriented, runnable example of the <a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> using Python, the official MCP Python SDK, stdio transport, Pydantic, and OpenAI function calling.</strong>
</p>

---

## What is MCP?

> **MCP (Model Context Protocol)** is a standardized protocol that lets AI applications discover and use external **tools, resources, and prompts** through one consistent interface.

Instead of every AI framework inventing a different integration for every database, API, filesystem, or internal service, an MCP host can connect to an MCP server and use the same protocol surface.

### Problems MCP Solves

| Problem | MCP Solution |
|---------|--------------|
| **Vendor lock-in** | Integrations expose capabilities through MCP rather than tying to one model provider or agent framework |
| **Inconsistent tool-calling** | Tools have machine-readable schemas and standardized discovery/call semantics |
| **No context persistence** | MCP separates context/tool providers from the model, enabling long-lived connections |
| **Dynamic data sources** | Databases, APIs, files, and internal systems wrapped as MCP resources/tools without embedding implementation into model runtime |

On the wire, MCP uses **JSON-RPC 2.0 messages** over transports such as **stdio** and HTTP-based transports (SSE/Streamable HTTP). This repository uses **stdio**: the client launches the server as a subprocess, sends protocol messages through stdin, and receives responses through stdout.

---

## Architecture

```mermaid
flowchart TD
    A[User: "What's the weather in London?"] --> B[AI Agent<br/>OpenAI Responses API]
    B --> C[1. Discovers MCP tools]
    B --> D[2. Decides whether to call]
    B --> E[3. Emits function call]
    E --> F[MCP Client<br/>ClientSession + stdio]
    F --> G[initialize]
    F --> H[tools/list]
    F --> I[tools/call]
    I --> J[JSON-RPC 2.0<br/>stdin/stdout]
    J --> K[MCP Server subprocess]
    K --> L[get_current_weather tool]
    K --> M[greeting://{name} resource]
```

### Why the Official SDK?

This repository uses the official Python MCP SDK instead of reimplementing the protocol. The SDK supplies:
- Protocol lifecycle & validation
- Transport abstraction (stdio, HTTP/SSE)
- Typed client/server APIs

The application code still makes the important MCP concepts explicit: server registration, tool schemas, `initialize`, `tools/list`, `tools/call`, resource reads, and stdio process management.

> The current SDK's stable v2 API uses `MCPServer` for server construction and `ClientSession`/`stdio_client` for stdio clients.

---

## Project Layout

```
mcp-demo/
├── README.md
├── requirements.txt
├── .env.example
├── pyproject.toml
├── src/
│   ├── mcp_server/
│   │   ├── __init__.py
│   │   ├── server.py      # MCP server entry point
│   │   ├── tools.py       # Tool implementations
│   │   ├── handlers.py    # Request handlers
│   │   └── utils.py       # Shared utilities
│   ├── mcp_client/
│   │   ├── __init__.py
│   │   ├── client.py      # MCP client wrapper
│   │   ├── agent.py       # OpenAI agent integration
│   │   └── runner.py      # Demo runner
│   └── shared/
│       ├── __init__.py
│       └── types.py       # Shared Pydantic models
├── tests/
│   ├── test_server.py
│   └── test_client.py
├── examples/
│   └── demo.ipynb
└── scripts/
    └── run_demo.sh
```

---

## Requirements

- **Python 3.10+**
- **OpenAI API key** (for the AI-agent demo)
- **No weather API key required** — the weather tool uses deterministic sample data so the MCP path works offline

---

## Quick Start

### 1. Create Virtual Environment

```bash
python -m venv .venv
source .venv/bin/activate        # Linux/macOS
.venv\Scripts\Activate.ps1       # Windows PowerShell
```

### 2. Install Dependencies

```bash
python -m pip install --upgrade pip
pip install -r requirements.txt
```

### 3. Configure OpenAI

```bash
cp .env.example .env
```

Edit `.env` with your credentials:

```env
OPENAI_API_KEY=your_api_key_here
OPENAI_MODEL=gpt-4.1-mini
```

> The server itself does **not** need the OpenAI key.

---

## Run the Demo

### From Repository Root

```bash
python src/mcp_client/runner.py
```

**What the runner does:**

| Step | Description |
|------|-------------|
| 1️⃣ | Launches `src/mcp_server/server.py` as a child process |
| 2️⃣ | Performs MCP initialization handshake |
| 3️⃣ | Calls `tools/list` |
| 4️⃣ | Converts discovered MCP schemas → OpenAI function tools |
| 5️⃣ | Asks model to answer a natural-language question |
| 6️⃣ | When model chooses `get_current_weather`, sends `tools/call` through MCP |
| 7️⃣ | Sends MCP result back to model |
| 8️⃣ | Prints final answer |
| 9️⃣ | Shuts down server cleanly |

### Alternative: Shell Wrapper

```bash
bash scripts/run_demo.sh
```

---

## Expected Output

Exact wording varies by model, but the log flow looks like:

```text
INFO mcp_client.client: -> MCP initialize
INFO mcp_client.client: <- MCP initialize: server=mcp-demo-server
INFO mcp_client.client: -> MCP tools/list
INFO mcp_client.client: <- MCP tools/list: ["get_current_weather"]
INFO mcp_client.agent: User: What's the weather in London?
INFO mcp_client.agent: OpenAI requested tool: get_current_weather {"city":"London","units":"metric"}
INFO mcp_client.client: -> MCP tools/call name=get_current_weather arguments={"city":"London","units":"metric"}
INFO mcp_server.tools: weather lookup city=London units=metric
INFO mcp_client.client: <- MCP tools/call result={"city":"London","temperature":18.0,...}
INFO mcp_client.agent: Final: London is 18°C and partly cloudy.
```

> The logs deliberately show MCP semantic messages at the application boundary. The SDK handles JSON-RPC framing internally.

---

## Run MCP Server Standalone

```bash
python src/mcp_server/server.py
```

A stdio MCP server appears to "hang" — this is **expected**. It waits for protocol messages on stdin. A host/client should launch it and own the stdio pipes.

### Interactive Protocol Inspection

```bash
pip install "mcp[cli]"
mcp dev src/mcp_server/server.py
```

---

## MCP Methods Demonstrated

The official SDK handles the JSON-RPC lifecycle:

| Method | Direction | Purpose |
|--------|-----------|---------|
| `initialize` | Client → Server | Handshake & capability negotiation |
| `tools/list` | Client → Server | Discover available tools |
| `tools/call` | Client → Server | Invoke a tool |
| `resources/list` | Client → Server | Discover available resources |
| `resources/read` | Client → Server | Read a resource |

The client explicitly calls `initialize()` before listing or invoking capabilities. The server's decorators generate tool/resource schemas from Python type annotations.

---

## Tool: `get_current_weather`

```python
get_current_weather(
    city: str,
    units: Literal["metric", "imperial"] = "metric"
) -> WeatherResponse
```

**Returns structured Pydantic-backed payload:**

```json
{
  "city": "London",
  "temperature": 18.0,
  "units": "metric",
  "condition": "partly cloudy",
  "humidity_percent": 72
}
```

> Unknown cities fail with a controlled MCP tool error rather than crashing the server.

---

## Agent Integration Flow

The agent uses **plain OpenAI function calling** (no extra framework) to keep the demo focused:

```mermaid
flowchart LR
    A[MCP Tool Schema] --> B[OpenAI Function Tool]
    B --> C[Model Chooses Function]
    C --> D[MCP ClientSession.call_tool]
    D --> E[MCP Server Executes Tool]
    E --> F[Function Call Output]
    F --> G[Final Model Answer]
```

This is the same pattern agent frameworks wrap: discover MCP tools → expose schemas to model → route selected calls back through MCP → feed results into next model turn.

---

## Testing

```bash
pytest -q
```

**Test suite covers:**

- ✅ Tool execution (metric weather)
- ✅ Tool execution (imperial weather)
- ✅ Validation/error behavior (unknown city)
- ✅ In-process MCP client discovery & tool invocation

Tests use the SDK's in-memory client where possible — avoids subprocess flakiness while exercising the real MCP protocol layer.

---

## Formatting & Linting

This project uses **[Ruff](https://docs.astral.sh/ruff/)**:

```bash
# Check
ruff check .
ruff format --check .

# Format
ruff format .
```

---

## Production Notes

This demo is deliberately small, but represents several production concerns:

| Concern | Implementation |
|---------|----------------|
| **stdout discipline** | Server never prints app logs to stdout (belongs to MCP); logs go to stderr via `logging` |
| **Typed I/O** | Pydantic models validate tool inputs/outputs at application boundary |
| **Controlled failures** | Tool exceptions → MCP error results (SDK), not process crashes |
| **Subprocess lifecycle** | SDK's stdio context manager owns process startup/shutdown |
| **Least-privilege env** | MCP stdio client explicitly passes env vars needed by child process |
| **Dynamic discovery** | Agent doesn't hard-code weather tool schema; discovers via `tools/list` |

> For real external data sources: replace deterministic weather with authenticated API/database calls, add timeouts, retries, rate limiting, observability, and secrets management.

---

## Protocol Mental Model

Simplified JSON-RPC sequence:

```json
// Client -> Server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}

// Server -> Client
{"jsonrpc":"2.0","id":1,"result":{...}}

// Client -> Server
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

// Server -> Client
{"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}

// Client -> Server
{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"get_current_weather","arguments":{"city":"London"}}}

// Server -> Client
{"jsonrpc":"2.0","id":3,"result":{"content":[...],"structuredContent":{...}}}
```

> The exact protocol schema is maintained by the [MCP specification](https://modelcontextprotocol.io/specification/) and the SDK. The above is intentionally simplified for teaching.

---

## References

- **Official MCP Python SDK**: https://py.sdk.modelcontextprotocol.io/
- **MCP Specification**: https://modelcontextprotocol.io/specification/
- **OpenAI Function Calling**: https://platform.openai.com/docs/guides/function-calling

---

<p align="center">
  Made with ❤️ for the MCP community
</p>