Skip to main content
Glama
deekshu05

MCP Runbook Search Server

by deekshu05
README.md
# MCP Runbook Search Server

A Model Context Protocol (MCP) server that exposes semantic search over a set of internal engineering runbooks as tools — so Claude Desktop, an MCP-compatible IDE, or a custom agent can ask "how do we handle a database failover?" and get back the right runbook, instead of someone grepping a wiki.

## Overview

MCP standardizes how an LLM client discovers and calls tools exposed by a separate server process, over stdio or HTTP. This server implements that side of the protocol for one concrete, realistic use case: making an internal knowledge base (runbooks, postmortems, playbooks) queryable by any MCP client without writing a custom integration per client.

The server exposes three tools:

- **`search_runbooks(query, top_k)`** — semantic search over the runbook corpus, ranked by cosine similarity.
- **`get_runbook(doc_id)`** — fetch one runbook's full text by id.
- **`list_runbooks()`** — list every indexed runbook's id and title.

## Key Features

- **Real MCP protocol, not a mock** — built on the official `mcp` Python SDK's `FastMCP` server, and verified end-to-end with a real `ClientSession` connecting over stdio (see Sample run below) — not just unit tests of the underlying functions.
- **Dependency-free semantic search** — a hashing embedder turns each document into a fixed-size vector with no external model, API key, or network call required, so the server runs entirely offline. Cosine similarity over those vectors ranks results by meaning, not just keyword overlap.
- **Tool logic decoupled from the transport** — `src/tools.py` holds plain functions over a `Corpus`, independently unit-tested; `src/server.py` only wires those functions into MCP tool decorators. Swapping stdio for HTTP transport, or the corpus for a real document store, doesn't touch the tool logic.
- **Clear error handling** — `get_runbook` on an unknown id returns a structured `{"error": ...}` payload instead of raising, so a client gets an actionable response either way.

## Architecture

```
MCP client (Claude Desktop, IDE, custom agent)
        │  stdio / JSON-RPC
        ▼
 FastMCP server (src/server.py)
        │  registers tools
        ▼
 tools.py  ──▶  Corpus (src/corpus.py)
                  │
                  ▼
          hashing embedder + cosine similarity
                  │
                  ▼
          5 sample engineering runbooks
```

## Tech Stack

| Layer | Tools |
|---|---|
| Language | Python |
| Protocol | Model Context Protocol (`mcp` Python SDK, `FastMCP`) |
| Search | Dependency-free hashing embedder + cosine similarity |
| CI/CD | GitHub Actions |

## Project Structure

```
.
├── src/
│   ├── corpus.py    # Hashing embedder, Corpus, sample runbook documents
│   ├── tools.py      # Pure tool functions (search / get / list)
│   └── server.py     # FastMCP server wiring tools.py into MCP tool decorators
├── tests/
│   ├── test_corpus.py
│   └── test_tools.py
├── .github/workflows/ci.yml
├── Dockerfile
├── requirements.txt
└── README.md
```

## Getting Started

### Prerequisites

- Python 3.10+

### Installation

```bash
git clone https://github.com/deekshu05/mcp-document-search-server.git
cd mcp-document-search-server
pip install -r requirements.txt
```

### Running the server

```bash
python -m src.server
```

This starts the server on stdio, waiting for an MCP client to connect.

### Connecting from Claude Desktop

Add this to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "runbook-search": {
      "command": "python",
      "args": ["-m", "src.server"],
      "cwd": "/path/to/mcp-document-search-server"
    }
  }
}
```

Restart Claude Desktop, and `search_runbooks`, `get_runbook`, and `list_runbooks` become tools Claude can call directly in conversation.

### Running with Docker

```bash
docker build -t mcp-runbook-server .
docker run -i mcp-runbook-server
```

## Sample run

Real output from a Python MCP client connecting to this server over stdio and calling its tools — not a simulated transcript:

```
Tools exposed: ['search_runbooks', 'get_runbook', 'list_runbooks']

search_runbooks('the primary database node is not responding'):
{
  "doc_id": "rb-001",
  "title": "Database failover procedure",
  "snippet": "Database failover procedure. When the primary Postgres node becomes
  unresponsive, promote the standby replica using the orchestrator's promote
  command, update the connection endpoint in the service config map, and verify",
  "score": 0.439
}
{
  "doc_id": "rb-003",
  "title": "Deploy rollback procedure",
  "snippet": "Deploy rollback procedure. If error rates exceed the alert
  threshold within ten minutes of a deploy, trigger the automated rollback to
  the previous stable image tag, confirm the health checks pass on all
  replicas, and po",
  "score": 0.3208
}

get_runbook('rb-001'):
{
  "doc_id": "rb-001",
  "title": "Database failover procedure",
  "text": "Database failover procedure. When the primary Postgres node becomes
  unresponsive, promote the standby replica using the orchestrator's promote
  command, update the connection endpoint in the service config map, and
  verify replication lag has dropped to zero on the new primary before
  resuming writes. Page the on-call DBA if promotion does not complete within
  five minutes."
}
```

The query never mentions "Postgres" or "failover" by name — it's a plain description of the symptom — and the search still ranks the correct runbook first by meaning, not keyword match, with a real second-ranked result (rollback procedure) that's genuinely the next-most-related runbook.

## Impact

A pattern like this turns an internal knowledge base that used to require someone to know which wiki page to search into something any MCP-compatible AI assistant can query directly and cite, cutting the time between "an incident starts" and "the right runbook is in front of the responder."

## Roadmap

- [ ] Swap the hashing embedder for a real embedding model when running against a larger corpus
- [ ] Streamable HTTP transport alongside stdio, for remote MCP clients
- [ ] Write-through indexing so new runbooks can be added without restarting the server
- [ ] Auth scoping so different MCP clients see different subsets of the corpus

## License

MIT