Skip to main content
Glama

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)│
                              └─────────────┘

Related MCP server: docusaurus-plugin-mcp

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.pyTrajectory 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.pyrun_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

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

# 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:

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:

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:

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:

{
  "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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Builds a keyword-searchable index from markdown documentation bundles and serves it to LLM agents via MCP, with retrieval logging and reporting for curation.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aditya0si/mcp-from-scratch'

If you have feedback or need assistance with the MCP directory API, please join our Discord server