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`).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues