mcp-from-scratch
by aditya0si
README.md
# mcp-from-scratch
A from-scratch implementation of the Model Context Protocol (MCP): raw JSON-RPC 2.0 server, HTTP and stdio transports, a minimal ReAct agent loop, and a trajectory-grading evaluation harness. Built with stdlib and FastAPI only — no LangChain, no MCP SDK, no third-party agent frameworks.
## Why this exists
Most "MCP" code on the internet is `npm install` and five lines of config. When my agent's tool call fails at 3 a.m., I want to be able to read the spec and fix it, not grep through four layers of SDK abstractions. This repo is the smallest version that teaches you what JSON-RPC actually looks like on the wire — and then gives you an eval harness to grade what your agent did with it.
```
┌──────────┐ HTTP/stdio ┌─────────────┐
│ Client │ ───────────────► │ MCP Server │
└──────────┘ │ (raw JSON- │
│ RPC 2.0) │
└──────┬──────┘
│ dispatch
┌──────┴──────┐
│ Tool/Resource│
│ Registry │
└──────┬──────┘
│
┌──────┴──────┐
│ search_docs │
│ (corpus.json)│
└─────────────┘
```
## What's in here
- **`src/mcp_from_scratch/jsonrpc.py`** — JSON-RPC 2.0 codec: `JsonRpcRequest`, `JsonRpcResponse`, `JsonRpcError`, encode/decode helpers, full error-code coverage (`-32700` parse, `-32600` invalid request, `-32601` method not found, `-32602` invalid params, `-32603` internal).
- **`src/mcp_from_scratch/transport_http.py`** — FastAPI HTTP transport, single `POST /rpc` endpoint, line-delimited JSON over `Content-Type: application/json`.
- **`src/mcp_from_scratch/transport_stdio.py`** — Stdio line-delimited JSON transport for piping agents.
- **`src/mcp_from_scratch/server.py`** — MCP server core: tool registry, resource registry, full dispatch over the five MCP methods (`initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`).
- **`src/mcp_from_scratch/tools/search_docs.py`** — A single tool: `search_docs(query) -> list[dict]` over the bundled corpus.
- **`src/mcp_from_scratch/resources/docs.py`** — A single resource: `docs://{slug}` returning doc content.
- **`src/mcp_from_scratch/agent_loop.py`** — A minimal ReAct agent loop as an explicit state machine. `Thought → Act → Observe`, with a deterministic stub LLM (keyword router) and a real LLM hook for production. `max_steps` budget.
- **`src/mcp_from_scratch/eval/trajectory.py`** — `Trajectory` and `Step` data structures.
- **`src/mcp_from_scratch/eval/graders.py`** — Grader suite: `exact_match`, `substring_match` (`contains`), `tool_called`, `step_count`, `llm_judge`.
- **`src/mcp_from_scratch/eval/harness.py`** — `run_eval(agent_fn, test_set, graders) -> EvalReport`, plus a CLI runner.
- **`data/eval_set.json`** — Four-task eval suite: Python dataclasses, JSON-RPC spec, MCP tools, async I/O.
- **`examples/run_eval.py`** — Demo runner that emits `eval_report.json`.
## Quickstart
```bash
git clone https://github.com/aditya0si/mcp-from-scratch
cd mcp-from-scratch
pip install -e ".[dev]"
# Run unit tests
pytest
# Run the eval harness against the bundled demo agent
python examples/run_eval.py
# → writes eval_report.json with per-task scores
```
## Try the MCP server
```bash
# Start the HTTP transport
python -m mcp_from_scratch.transport_http
# → listens on http://127.0.0.1:8765/rpc
```
A `curl` roundtrip — full MCP `initialize` then `tools/list`:
```bash
curl -s http://127.0.0.1:8765/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"client":"demo","version":"0.1.0"}}'
# {"jsonrpc":"2.0","id":1,"result":{"server":"mcp-from-scratch","version":"0.1.0","capabilities":{"tools":true,"resources":true}}}
curl -s http://127.0.0.1:8765/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# {"jsonrpc":"2.0","id":2,"result":[{"name":"search_docs","description":"...","inputSchema":{...}}]}
```
Then call the tool:
```bash
curl -s http://127.0.0.1:8765/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"python json-rpc"}}}'
```
## How the agent loop works
State machine, no LangChain. Each step is explicit:
```python
class AgentLoop:
def step(self, task: str) -> Step:
thought = self.llm.think(self.history, task)
action = self.router.parse(thought) # tool_call | final_answer
if action.type == "final_answer":
return Step(thought=thought, action=action, observation="")
observation = self.tools.call(action.tool, action.arguments)
return Step(thought=thought, action=action, observation=observation)
```
Loop terminates on `final_answer` or after `max_steps`. The LLM is an injected callable — for tests we use a deterministic stub (keyword router), for production we wire in any function-calling LLM.
## Eval harness — what's graded
Each test runs the agent against a task, captures the full `Trajectory`, then scores it with the grader list:
| Grader | What it checks |
|---|---|
| `exact_match` | Final answer equals expected (case/whitespace normalized) |
| `substring_match` (alias `contains`) | Final answer contains an expected substring |
| `tool_called` | A specific tool name appears in the trajectory |
| `step_count` | Total steps ≤ a maximum |
| `llm_judge` | An LLM rates the final answer 1-5 against the expected |
`run_eval(agent_fn, test_set, graders=[...])` returns:
```json
{
"total_tests": 4,
"passed_tests": 4,
"pass_rate": 1.0,
"per_test": [
{
"id": "eval-python-dataclass",
"task": "...",
"passed": true,
"final_answer": "...",
"trajectory": { "steps": [...], "tool_calls": [...] },
"grader_results": [
{"grader": "exact_match", "passed": true, "score": 1.0, "reason": "..."},
{"grader": "tool_called", "passed": true, "score": 1.0, "reason": "..."},
{"grader": "step_count", "passed": true, "score": 1.0, "reason": "..."}
]
}
],
"aggregate": { "avg_steps": 1.75, "avg_tool_calls": 0.75 }
}
```
Run `python examples/run_eval.py` to regenerate.
## Tests
29 unit tests across five files:
```
tests/test_jsonrpc.py 6 tests
tests/test_server.py 7 tests
tests/test_transport_http.py 6 tests
tests/test_agent_loop.py 4 tests
tests/test_eval_harness.py 6 tests
```
All passing on Python 3.11+. CI runs them on every PR.
## Project layout
```
mcp-from-scratch/
├── data/
│ ├── corpus.json # ~20 short docs the tool searches over
│ └── eval_set.json # 4-task evaluation set
├── examples/
│ └── run_eval.py # demo runner
├── src/mcp_from_scratch/
│ ├── agent_loop.py
│ ├── eval/
│ │ ├── graders.py
│ │ ├── harness.py
│ │ └── trajectory.py
│ ├── jsonrpc.py
│ ├── resources/
│ │ └── docs.py
│ ├── server.py
│ ├── tools/
│ │ └── search_docs.py
│ ├── transport_http.py
│ └── transport_stdio.py
└── tests/
├── test_agent_loop.py
├── test_eval_harness.py
├── test_jsonrpc.py
├── test_server.py
└── test_transport_http.py
```
## Why no LangChain / SDK
Because the point is to understand what's underneath. After reading this code, when you use the official MCP SDK or LangGraph you'll know exactly what JSON-RPC message is on the wire, why the tool call failed, and where the dispatch lives. You'll also be able to debug production agents without grepping four layers of abstraction.
## Roadmap
- [ ] Real LLM integration examples (OpenAI function calling, Anthropic, Groq)
- [ ] Streaming responses over SSE
- [ ] Resource subscriptions and notifications
- [ ] Pluggable tool registry (load from YAML)
- [ ] More graders: cost, latency, token-budget
- [ ] CI workflow with GitHub Actions
## License
MIT.This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues