Skip to main content
Glama
README.md
# mcp-tools-server — developer knowledge & utilities over MCP

A small, coherent **Model Context Protocol (MCP)** server that gives an LLM
client a useful toolbox: search a local docs knowledge base, summarize text,
do safe arithmetic, and convert units — plus knowledge-base **resources** and a
**prompt** template. It speaks the **stdio transport** that Cursor and Claude
Desktop use *and* an optional **HTTP/SSE transport** (great for `docker compose
up` and reaching it "like an API"), and it ships with a **local client harness**
so you can watch it work **without any host and with zero API keys**.

```bash
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
python -m mcp_tools.demo_client     # spins up the server and calls every tool
```

## What is MCP, and why it matters (2026)

Modern AI apps are only as good as the context and tools you can feed the model.
Before MCP, every tool integration was a bespoke, per-app, per-vendor affair. The
**Model Context Protocol** is the open standard that fixed that: you write one
server that advertises **tools** (functions the model can call), **resources**
(readable context addressed by URI), and **prompts** (reusable templates), and
*any* MCP-aware host — Cursor, Claude Desktop, and a growing list of others — can
use it. Capability is decoupled from model: a tool you write once works across
hosts and across model vendors. That is why, in 2026, MCP has become the common
integration layer for AI developer tooling.

Crucially, **MCP is a protocol, not an agent framework.** There is no LangGraph,
no orchestration loop, and no planner in this repo. The server just publishes
capabilities and answers JSON-RPC requests; the *client's* model decides when to
call them. (See [Design decisions](#design-decisions--tradeoffs).)

## Architecture

```
                            JSON-RPC 2.0 over stdio
 ┌───────────────────────┐  ───── request  ─▶ stdin ─┐   ┌──────────────────────────────┐
 │   MCP host + client   │                           └──▶│      dev-knowledge-tools       │
 │                       │                               │          MCP server            │
 │  Cursor / Claude      │                               │      (mcp.server.fastmcp)      │
 │  Desktop, or the      │◀─┐                            │                                │
 │  bundled demo_client  │  └── response ◀─ stdout ◀─────│  tools                         │
 └───────────────────────┘                               │   • search_docs ─▶ IDF search  │
                                                         │   • summarize   ─▶ LLM layer   │
   The host launches the server as a                     │   • calculate   (safe AST)     │
   subprocess. Requests go to its stdin,                 │   • convert_units              │
   responses come back on its stdout, one                │  resources                     │
   JSON object per line. The server logs                 │   • kb://index                 │
   to stderr so the protocol stream stays                │   • kb://doc/{name}            │
   clean.                                                 │  prompt                        │
                                                         │   • summarize_document         │
                                                         └───────────────┬────────────────┘
                                                                         │
                                            LLM layer (summarize):  mock ▸ openai/anthropic/ollama
                                            knowledge base:         data/kb/*.md
```

## Repository layout

```
mcp_tools/
├── server.py        # FastMCP server: registers tools/resources/prompt, runs over stdio  ← entry point
├── tools.py         # the capabilities as plain, tested functions (no protocol here)     ← the core
├── search.py        # CorpusSearch: deterministic IDF keyword search over the KB
├── config.py        # env-driven settings; resolves paths relative to the package
├── demo_client.py   # local MCP client that drives the server over stdio                 ← "see it work"
└── llm/             # tiny provider-agnostic ChatModel layer (mock | openai | anthropic | ollama)
data/kb/*.md         # the sample "developer knowledge" knowledge base (6 docs)
tests/               # hermetic pytest suite (imports the functions directly)
```

## Capability catalog

**Tools**

| Tool | Signature | What it does |
| --- | --- | --- |
| `search_docs` | `(query: str, k: int = 4)` | Ranked snippets from the local KB via IDF keyword scoring. |
| `summarize` | `(text: str, max_sentences: int = 3)` | Condenses text through the LLM layer (offline mock by default). |
| `calculate` | `(expression: str)` | Safe arithmetic (`+ - * / // % **`, parens) via AST allow-list — no `eval`. |
| `convert_units` | `(value, from_unit, to_unit)` | Length, mass, time, volume, data, and temperature conversions. |

**Resources**

| URI | Kind | Contents |
| --- | --- | --- |
| `kb://index` | static | Markdown list of every knowledge-base document. |
| `kb://doc/{name}` | template | The full markdown of one document, by file name. |

**Prompt** — `summarize_document(name)`: a ready-made instruction to summarize a KB
document into a few bullet points.

> Note: `from` is a Python keyword, so `convert_units` uses `from_unit` / `to_unit`.

## Run it

With the Makefile (creates `./.venv`):

```bash
make install    # venv + runtime + dev deps
make demo       # start the server, list capabilities, call every tool  ← best first run
make test       # hermetic pytest suite
make run        # start the server on stdio (it waits for a client; Ctrl-C to stop)
make run-http   # start the server on HTTP/SSE at http://0.0.0.0:8084/sse
make demo-http  # exercise a running HTTP server (see "Run over HTTP / Docker")
```

Or directly, inside an activated virtualenv:

```bash
pip install -r requirements-dev.txt
python -m mcp_tools.demo_client     # the harness
python -m mcp_tools.server          # the raw server (stdio)
pytest
```

`make demo` connects over stdio, prints the server's tools/resources/prompts,
calls each tool (including a deliberately **rejected** unsafe `calculate` to show
the guard), and reads the resources — all offline.

## Use it from Cursor or Claude Desktop

The server is launched by the host as a subprocess. Point the host at this repo's
venv Python and set `PYTHONPATH` to the project root (so the package imports from
any working directory — the knowledge base is resolved relative to the package,
not the cwd). Replace `/ABSOLUTE/PATH/TO/mcp-tools-server` with your checkout path.

**Cursor** — add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per-project):

```json
{
  "mcpServers": {
    "dev-knowledge-tools": {
      "command": "/ABSOLUTE/PATH/TO/mcp-tools-server/.venv/bin/python",
      "args": ["-m", "mcp_tools.server"],
      "env": { "PYTHONPATH": "/ABSOLUTE/PATH/TO/mcp-tools-server" }
    }
  }
}
```

**Claude Desktop** — add to `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "dev-knowledge-tools": {
      "command": "/ABSOLUTE/PATH/TO/mcp-tools-server/.venv/bin/python",
      "args": ["-m", "mcp_tools.server"],
      "env": { "PYTHONPATH": "/ABSOLUTE/PATH/TO/mcp-tools-server" }
    }
  }
}
```

Prefer [`uv`](https://docs.astral.sh/uv/)? `--directory` sets the working
directory, so no `PYTHONPATH` is needed:

```json
{
  "mcpServers": {
    "dev-knowledge-tools": {
      "command": "uv",
      "args": ["--directory", "/ABSOLUTE/PATH/TO/mcp-tools-server", "run", "python", "-m", "mcp_tools.server"]
    }
  }
}
```

Restart the host, and the four tools plus the `kb://` resources appear.

## Run over HTTP / Docker

The same server can serve over **HTTP + Server-Sent Events (SSE)** instead of
stdio, so you can run it in a container and reach it "like an API". The transport
is chosen by `MCP_TRANSPORT` (default `stdio`, so desktop hosts are unaffected).

**With Docker (zero setup):**

```bash
docker compose up          # builds the image, serves at http://localhost:8084/sse
# in another terminal — watch it work over HTTP, no Cursor/Claude needed:
make demo-http             # or: python -m mcp_tools.demo_client --http
docker compose down        # stop and remove
```

**Without Docker:**

```bash
make run-http              # MCP_TRANSPORT=sse MCP_HOST=0.0.0.0 MCP_PORT=8084 python -m mcp_tools.server
make demo-http             # in another terminal
```

**Register the HTTP server with an MCP client.** Point the client at the SSE URL
(no `command`/`args` — the server is already running):

```json
{
  "mcpServers": {
    "dev-knowledge-tools-http": {
      "url": "http://localhost:8084/sse"
    }
  }
}
```

**It speaks JSON-RPC, not plain REST.** An MCP HTTP endpoint is *not* a REST API
and has no browsable webpage. A bare `curl` to the SSE URL opens an event stream
and hands you the JSON-RPC message endpoint — it does not return HTML:

```console
$ curl -N http://localhost:8084/sse
event: endpoint
data: /messages/?session_id=…
```

The full exchange (initialize → list tools → call tool, each a JSON-RPC method)
is what an MCP client does for you; `make demo-http` is exactly that client. Use
stdio for desktop hosts and HTTP when the server runs elsewhere or behind a port.

## A real summarizer (optional)

`summarize` runs fully offline by default via a deterministic **extractive mock**
(it returns the leading sentences, never inventing facts). Point it at a real
model with environment variables — no code changes — via the provider-agnostic
`ChatModel` layer:

```bash
export LLM_PROVIDER=ollama   OLLAMA_MODEL=llama3.1          # local & free
export LLM_PROVIDER=openai   OPENAI_API_KEY=sk-...          # hosted
export LLM_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-ant-...  # hosted
```

All settings live in [`.env.example`](.env.example) and load automatically from a
`.env`. Only the chosen provider's key is ever required.

## Testing

```bash
pip install -r requirements-dev.txt
pytest
```

The suite is **hermetic and offline**: it imports the tool functions and asserts
behaviour directly — search returns the relevant hit, the calculator is correct
and rejects unsafe input, unit conversions are exact, `summarize` works against
the mock, and the KB resources list/read correctly with path-traversal blocked.
One test lists the server's registered tools/resources/prompt in-process. No
network, no subprocess, deterministic.

## Design decisions & tradeoffs

- **Tools are plain functions; the server is a thin adapter.** Every capability
  lives in `tools.py` as an ordinary typed function with a docstring (which
  doubles as the LLM-facing description). `server.py` just registers them with
  the SDK. That keeps the logic unit-testable with no protocol in the loop and
  makes the tool surface obvious.
- **Pinned to the stable FastMCP API (`mcp>=1.12,<2`).** The SDK's 2.0 release
  replaced `mcp.server.fastmcp.FastMCP` with a new `MCPServer`/`Client` API. This
  project deliberately targets the widely-adopted 1.x FastMCP interface that
  every current Cursor/Claude Desktop tutorial uses — maximum compatibility and
  familiarity — and documents the boundary so migrating to 2.0 is a conscious
  choice, not a surprise. (This is exactly the case SemVer's `<2` constraint is
  for.)
- **stdio by default, HTTP/SSE on demand.** For a local server, stdio needs no
  ports, sockets, or auth — the OS process boundary is the trust boundary — which
  is why desktop hosts use it, and why it stays the default. Because tool logic is
  transport-agnostic, HTTP/SSE (for Docker or "reach it like an API") is a pure
  `MCP_TRANSPORT` switch that changes only the one `mcp.run(transport=...)` call —
  the tools, tests, and knowledge base are untouched.
- **Safe calculator without `eval`.** `calculate` parses to an AST and walks a
  strict allow-list of numeric operations, so `__import__('os').system(...)` is
  rejected as an unsupported node rather than executed. A power-exponent cap
  guards against resource-exhaustion (`2 ** 10_000_000`).
- **Deterministic IDF search, not embeddings.** Keyword/IDF scoring is
  dependency-free, instant, and reproducible — ideal for a tool an LLM calls and
  for hermetic tests. The `search(query, k) -> results` surface is the seam:
  swap in embeddings + a vector DB (or a hosted web search) without touching the
  MCP tool.
- **Offline mock LLM behind a provider-agnostic seam.** `summarize` is genuinely
  useful with zero keys and stays deterministic in tests, while a real provider
  is a pure `LLM_PROVIDER` switch. The adapters speak each vendor's HTTP API via
  `httpx` — no heavy vendor SDKs.
- **Package-relative paths.** A host launches the server from an arbitrary
  working directory, so `config.py` resolves the knowledge base relative to the
  package (`__file__`), not the cwd. The server runs the same everywhere.
- **No agent framework.** MCP is the integration layer, not an agent. There is no
  LangGraph or planner here by design; orchestration is the *client's* job.

## Not included (by design)

Authentication and multi-tenant concerns (the HTTP mode binds an unauthenticated
listener meant for localhost/Docker, not the public internet), TLS termination,
streaming tool output, embeddings/vector search, and write tools (everything here
is read-only and deterministic). Each is a deliberate, well-scoped extension
point rather than scope creep — this repo is a focused, legible demonstration of
a clean MCP server.