Skip to main content
Glama
eagleffz

MCP Guidelines Server

by eagleffz
README.md
# MCP Guidelines Server

A remote **MCP server** that serves versioned Enterprise & Architecture
guidelines (security / architecture / compliance policies) to LLM clients
(Claude Desktop, IDE integrations, …) over **Streamable HTTP** behind
**static Bearer-token auth**. Guidelines are plain Markdown files with YAML
frontmatter, indexed in-memory with SQLite FTS5 for ranked full-text search and
hot-reloaded on change. Built on the official `mcp` SDK (`FastMCP`) and packaged
for Docker (e.g. a Synology NAS behind a reverse proxy / Tailscale).

## Features

- Five core MCP tools + `find_applicable` and two prompts (see [Tools](#tools-and-prompts)).
- Ranked full-text + tag search (SQLite FTS5, `bm25`) with highlighted snippets.
- Hot-reload: edits in the guidelines directory are picked up without a restart
  (watchdog), with a `POST /reload` fallback for filesystems where inotify/FSEvents
  doesn't fire (NAS shares).
- Schema validation: malformed frontmatter is logged and skipped — never crashes.
- Static Bearer-token auth; unauthenticated MCP calls get `401`. Scopes are
  modelled (data-model ready) but not enforced in Phase 1.
- Structured JSON audit logging per tool call; optional Prometheus `/metrics`.
- Per-token rate limiting and a `/health` endpoint for Docker/reverse-proxy.
- Read-only by design: the server never writes guidelines.

## Project layout

```
src/mcp_guidelines/
  server.py          # composition root: FastMCP, watcher lifecycle, ops routes, entrypoint
  loader.py          # read dir, parse frontmatter, validate, skip-on-error
  models.py          # Pydantic models (frontmatter contract + I/O shapes)
  index.py           # GuidelineIndex: in-memory cache + SQLite FTS5 search
  auth.py            # static bearer tokens, scope model, rate limiter
  tools.py           # MCP tool + prompt registrations
  config.py          # 12-factor env config
  metrics.py         # dependency-free Prometheus counters
  logging_setup.py   # JSON logging to stdout
guidelines/          # seed guidelines (security / architecture / compliance)
tests/               # loader, index/search, auth, tools (MCP protocol), HTTP (401)
Dockerfile  docker-compose.yml  .env.example  pyproject.toml
```

## Installation

Requires **Python ≥ 3.11**.

```bash
# editable install with dev/test extras
pip install -e ".[dev]"
# or, with uv
uv sync
```

## Running locally

```bash
AUTH_TOKENS=dev=secret GUIDELINES_PATH=guidelines mcp-guidelines
# equivalently: python -m mcp_guidelines
```

The MCP endpoint is served at `http://<host>:<port>/mcp` (Streamable HTTP).
Clients MUST send `Authorization: Bearer <token>`.

```bash
curl -s localhost:8000/health           # {"status":"ok","documents":4,"revision":"…"}
# unauthenticated MCP call is rejected:
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8000/mcp \
  -H 'content-type: application/json' -d '{}'        # -> 401
```

## Docker

```bash
# create your token(s) first (see "Token generation")
echo 'AUTH_TOKENS=team=PUT-A-REAL-TOKEN-HERE' > .env
docker compose up --build
curl localhost:8000/health               # -> 200, container reports "healthy"
```

`docker-compose.yml` mounts `./guidelines` **read-only** into the container,
passes config via env vars, defines a healthcheck against `/health`, and
restarts unless stopped. Put the container behind your reverse proxy
(Synology / Traefik / nginx) or expose it over Tailscale; terminate TLS there.

## Configuration (environment variables)

All configuration is via env vars (12-factor); a `.env` file is read when present.
See `.env.example`.

| Variable | Default | Description |
|---|---|---|
| `GUIDELINES_PATH` | `guidelines` | Directory of guideline `.md` files (read-only). |
| `AUTH_TOKENS` | _(empty)_ | Comma-separated `name=token` pairs. Empty ⇒ every MCP call is `401`. |
| `AUTH_TOKENS_FILE` | _(unset)_ | Path to a JSON secrets file (below); entries override `AUTH_TOKENS`. |
| `LOG_LEVEL` | `INFO` | `DEBUG` \| `INFO` \| `WARNING` \| `ERROR`. |
| `HOST` | `0.0.0.0` | Bind address. `0.0.0.0` disables the SDK's DNS-rebind guard (intended behind a proxy). |
| `PORT` | `8000` | Listen port. |
| `RATE_LIMIT` | `60` | Requests per minute per token (`0` disables). |
| `ISSUER_URL` | `http://localhost:8000` | OAuth issuer URL, used only for `WWW-Authenticate`/OAuth metadata. |
| `RESOURCE_SERVER_URL` | _(unset)_ | Optional protected-resource metadata URL. |
| `METRICS_ENABLED` | `true` | Expose Prometheus metrics at `/metrics`. |

### Token generation

Generate a strong random token and add it to `AUTH_TOKENS`:

```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
# AUTH_TOKENS=alice=<token1>,bob=<token2>
```

### Secrets file (`AUTH_TOKENS_FILE`)

For per-token scopes or to keep tokens out of the environment, point
`AUTH_TOKENS_FILE` at a JSON file. File entries override `AUTH_TOKENS`.

```json
{
  "tokens": [
    { "name": "alice", "token": "…", "scopes": ["read:all"] },
    { "name": "secaudit", "token": "…", "scopes": ["read:security"] }
  ]
}
```

> Scopes are recorded on the token and surfaced to the audit log. Phase 1 does
> **not** enforce them — any valid token may read everything. Enforcement is a
> later phase (set `required_scopes` in `build_auth_settings` and/or check
> `tok.scopes` in `tools._begin`). Swapping in real OAuth is a drop-in
> replacement of `StaticTokenVerifier` with an introspection verifier (same
> `TokenVerifier` protocol).

## Guideline frontmatter schema

Each guideline is a Markdown file with a YAML frontmatter block. Place files
under category subdirectories of `GUIDELINES_PATH` (the directory layout is for
humans; `category` comes from the frontmatter, not the path).

```markdown
---
id: arch-api-design            # required, unique, stable slug ([a-z0-9-])
title: API Design Guidelines   # required
category: architecture         # required, slug (e.g. security|architecture|compliance)
tags: [rest, versioning, http] # optional
version: 2.1.0                 # required, SemVer
status: active                 # required: draft | active | deprecated
owner: platform-team           # required
updated: 2026-06-01            # required, ISO date
applies_to: [backend, api]     # optional: scope/domains (drives find_applicable)
supersedes: arch-api-v1        # optional
---

# API Design Guidelines

… actual content …
```

- `id` and `category` must be slugs; `version` must be SemVer; `status` is one of
  the three literals. Files that fail validation (bad YAML or schema) are logged
  and skipped — the server keeps running.
- Unknown extra frontmatter keys are allowed and ignored.
- `status: deprecated` guidelines still appear in search, flagged with a
  `warning` (and `supersedes` when set).

## Tools and prompts

| Tool | Input | Output |
|---|---|---|
| `list_guidelines` | `category?`, `tag?`, `status?` | summaries (id, title, category, tags, version, status) |
| `get_guideline` | `id` | `{ metadata, content, path }` |
| `search_guidelines` | `query`, `category?`, `limit?` | ranked hits with `score`, `snippet`, deprecation `warning` |
| `list_categories` | – | categories with counts |
| `get_guideline_metadata` | `id` | frontmatter only (token-sparing) |
| `find_applicable` | `applies_to: [...]`, `category?` | active guidelines overlapping the context, ranked by overlap |

Prompts: `apply_guideline(id, code)` (check code against one guideline) and
`review_against_category(category, code)` (review against all active guidelines
in a category).

### Connecting a client

Use any MCP client that speaks Streamable HTTP. Point it at
`http://<host>:<port>/mcp` with header `Authorization: Bearer <token>`, e.g.:

```bash
npx -y @modelcontextprotocol/inspector
# URL: http://localhost:8000/mcp   Header: Authorization: Bearer <token>
```

## Operational endpoints

| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
| `/health` | GET | public | Liveness/readiness: `{status, documents, revision}` (Docker healthcheck). |
| `/metrics` | GET | public | Prometheus text exposition (when `METRICS_ENABLED`). |
| `/reload` | POST | Bearer | Force a full re-read of the guidelines directory (hot-reload fallback). |
| `/mcp` | POST | Bearer | The MCP Streamable HTTP endpoint. |

## Maintaining guidelines

Guidelines live in a versioned Git repo (keep `version`/`updated` current).
To add or change one:

1. Drop or edit a `.md` file under a category directory in `GUIDELINES_PATH`.
2. The file watcher applies the change within moments — no restart needed.
3. If your filesystem doesn't deliver watch events (some NAS shares), trigger a
   reload explicitly:

   ```bash
   curl -X POST -H 'Authorization: Bearer <token>' localhost:8000/reload
   ```

The server never writes guidelines; all maintenance is via Git/the filesystem.

## Development & tests

```bash
pip install -e ".[dev]"
pytest -q
```

Tests cover the loader (skip-on-error), the FTS5 index (ranked search, snippets,
category filter, deprecation flagging, hot-reload add/edit/delete), auth (token
verification, env+file principal merge, rate limiter), every tool over the real
in-memory MCP protocol, and the HTTP surface (`/health`, the `401` auth gate,
and `/reload`).