Skip to main content
Glama
mtmultiservicesllc-jpg

mcp-toolkit

README.md
# mcp-toolkit

[![Tests](https://github.com/<your-username>/mcp-toolkit/actions/workflows/tests.yml/badge.svg)](https://github.com/<your-username>/mcp-toolkit/actions/workflows/tests.yml)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)

A hands-on exploration of the **Model Context Protocol (MCP)** — the open
protocol (created by Anthropic) that standardizes how AI applications
connect to external tools and data sources. This repo implements both
sides of the protocol: an MCP **client** and an MCP **server**, plus a
test suite and CI.

## Why this project

MCP is becoming the standard way AI assistants (Claude, and increasingly
others) discover and call external tools — think of it as a plugin
interface for LLMs. Rather than just reading about it, I built:

1. A client that connects to Anthropic's official filesystem MCP server
2. A custom MCP server exposing my own tools
3. A client that talks to that custom server
4. Unit + integration tests, and CI to run them on every push

## Architecture

```
                         MCP protocol (JSON-RPC over stdio)
┌──────────────┐        ┌──────────────────────────────┐
│  client.py    │ <────> │ @modelcontextprotocol/        │  (official,
│              │        │ server-filesystem (npx)       │   Anthropic)
└──────────────┘        └──────────────────────────────┘

┌──────────────┐        ┌──────────────────────────────┐
│client_local.py│ <────> │  server.py                    │  (custom,
│              │        │  - calculate(expression)      │   this repo)
│              │        │  - get_weather(city)          │
└──────────────┘        └──────────────────────────────┘
```

Both clients follow the same MCP lifecycle:

1. **Spawn** the server as a subprocess (stdio transport)
2. **`initialize`** — protocol handshake
3. **`list_tools`** — discover what the server can do, dynamically (no
   hardcoded knowledge of the server's capabilities)
4. **`call_tool`** — invoke a tool by name with typed arguments

This is exactly what Claude Desktop does under the hood when you connect
an MCP server.

## Repo structure

```
mcp-toolkit/
├── server.py                # custom MCP server (FastMCP) — calculate & get_weather
├── client.py                 # MCP client -> official filesystem server
├── client_local.py           # MCP client -> server.py
├── sandbox_files/             # sample files used by client.py's demo
├── tests/
│   ├── test_calculate.py      # unit tests: correctness + security
│   └── test_server.py         # integration tests: tool registration & schemas
├── .github/workflows/tests.yml   # CI: pytest on Python 3.10 & 3.12
├── requirements.txt
└── LICENSE
```

## Getting started

```bash
git clone https://github.com/<your-username>/mcp-toolkit.git
cd mcp-toolkit
pip install -r requirements.txt
```

Run the client against Anthropic's official filesystem server:

```bash
python3 client.py
```

Run the client against the custom server built in this repo:

```bash
python3 client_local.py
```

Run the test suite:

```bash
pytest -v
```

## Example output

```
$ python3 client_local.py
Connexion au serveur MCP maison (server.py)

✅ Session MCP initialisée

🔧 Outils disponibles :
  - get_weather: Donne la météo actuelle (température, vent) pour une ville donnée.
  - calculate: Évalue une expression arithmétique (+, -, *, /, %, **).

🧮 Appel de calculate(expression='12 * (3 + 4)') :
12 * (3 + 4) = 84
```

```
$ pytest -v
tests/test_calculate.py::TestCalculateNominal::test_addition PASSED
tests/test_calculate.py::TestCalculateNominal::test_power PASSED
tests/test_calculate.py::TestCalculateSecurity::test_rejects_function_calls PASSED
tests/test_calculate.py::TestCalculateSecurity::test_rejects_attribute_access PASSED
tests/test_server.py::test_registered_tools PASSED
...
21 passed in 0.49s
```

## Engineering notes

A few things worth calling out for anyone reviewing this code:

- **Security-conscious tool design.** `calculate` doesn't use `eval()`.
  It parses the expression into a Python AST and walks a restricted
  whitelist of node types (`ast.BinOp`, `ast.UnaryOp`, numeric
  constants), so arbitrary code execution isn't possible even though the
  input is a raw string. Covered by dedicated security tests in
  `tests/test_calculate.py` (rejecting function calls, attribute access,
  name lookups, etc.).

- **Default environment isolation.** The MCP SDK does *not* forward the
  parent process's full environment to spawned servers by default — only
  a minimal safe subset (`get_default_environment()`). Servers that need
  network access (like `get_weather`, via `httpx`) need `env` passed
  explicitly. This tripped up the first version of this project and is
  documented here so it doesn't trip up the next person.

- **Tests don't require a live subprocess.** `tests/test_server.py`
  exercises the server's tool registration and schemas directly through
  the `FastMCP` instance (`mcp.list_tools()`), not by spawning a real
  stdio subprocess — faster and more deterministic for CI.

## Connecting to Claude Desktop

To use `server.py` as a real MCP server inside Claude Desktop, add it to
your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "mcp-toolkit-demo": {
      "command": "python3",
      "args": ["/absolute/path/to/mcp-toolkit/server.py"]
    }
  }
}
```

Restart Claude Desktop and the `calculate` / `get_weather` tools become
available in conversation.

## License

MIT — see [LICENSE](LICENSE).