ep-mcp
by brianhearn
README.md
# ep-mcp
<!-- mcp-name: io.github.brianhearn/ep-mcp -->
**Expertise-as-a-Service over the Model Context Protocol.**
An MCP server that turns any ExpertPack into a live, queryable knowledge service. Any MCP-compatible agent (Claude Desktop, Cursor, Windsurf, Claude.ai, custom hosts) can connect and retrieve high-quality, provenance-rich domain expertise via standardized tools.
**[π expertpack.ai](https://expertpack.ai)** Β· **[π¦ ExpertPack Framework](https://github.com/brianhearn/expert-pack)** Β· **[π Architecture](ARCHITECTURE.md)**
97.6% retrieval hit rate / 0 full misses on a 22-question benchmark against a production v4.1 atomic-conceptual pack (658 chunks). Prior v4.0 baseline on the same pack and eval set: 65.1%.
## Features
- **Schema-aware hybrid retrieval**: BM25 + vector search (sqlite-vec), metadata boosting, MMR re-ranking, adaptive threshold filtering, length penalty
- **`requires:` expansion (schema v4.1 atomic-conceptual)**: When a top-K atom's frontmatter declares `requires: [B, C]`, the referenced atoms are auto-appended to results as additional `SearchResult`s flagged `requires_expanded=True`. Directional (AβB does not imply BβA), transitive up to 2 hops, capped at 3 atoms and 3,500 cumulative tokens, appended after the `max_results` slice so it never displaces top-K.
- **Intent-aware routing**: Automatic query classification (ENTITY/HOW/WHY/WHEN/GENERAL) adjusts vector/BM25 fusion weights per query β no LLM required
- **Query embedding cache**: SQLite-backed cache keyed by `(model, dimension, sha256(query))` β warm query latency drops from ~4s (live Gemini API) to ~1ms
- **Deep graph traversal**: Optional multi-hop BFS expansion over knowledge graph edges with per-hop score decay and drift prevention
- **Reserved-slot graph merge**: Configurable protected slots in the final top-K reserved for graph-expanded neighbors so related files surface even against dominant core results
- **BM25 fallback mode**: Embedding provider outage degrades gracefully to lexical-only search (coverage + density + path + length scoring) instead of a hard error
- **Cross-encoder reranker**: Optional second-pass precision reranker (disabled by default, ~70ms warm latency when enabled)
- **Provenance-first**: Every result includes `id`, `content_hash`, `verified_at`, `source_file`
- **Reconstruct mode (RFC-003)**: Opt-in `reconstruct=true` / `reconstruct: true` enriches results with `fragment_id`, `line_range`, `original_markdown`, span-level `content_hash`, `excerpt`, `stale`, and byte offsets so agents can prove exactly what source text grounded an answer
- **EP-native chunking**: Files are treated as atomic retrieval units (split at `##`/`###` only for oversized content). Split chunks keep the original span as `content`/`span_hash`, carry a computed `line_range`, and record a `heading_path` (0.7.0+)
- **Heading paths (0.7.0+)**: every hit carries `heading_path` (e.g. `["Routing", "Setup", "Ports"]`) so an H3 hit keeps its H2 parent; `ep_read(path, section=..., neighbors=true)` reads just that section plus its previous/next siblings
- **Freshness (0.7.0+, ExpertPack Core 4.2)**: hits and `ep_read` carry `verified_age_days`, `age_stale` (`verified_at` older than the file's `refresh` or the manifest `freshness.refresh_cycle`), `expired` and `expires_at` (volatile TTL). `stale` keeps its old meaning β **index drift** (file changed since indexing, reconstruct mode only). Optional down-rank via `retrieval.freshness_penalty` (default `0.0` = off)
- **Skills over MCP (0.7.0+, opt-in)**: set `mcp.skills.enabled: true` in a pack's `manifest.yaml` to declare the `io.modelcontextprotocol/skills` extension (SEP-2640). Each pack becomes one skill at `skill://{slug}/SKILL.md` (generated) with every non-`on_demand` file served verbatim at `skill://{slug}/{path}`; `skills/list` / `skills/get` publish the manifest with SHA-256 digests, sizes, `ttlMs` and `cacheScope`. Host support is still thin, so the `ep_*` tools remain the primary path
- **Frontmatter aware**: Strips metadata for embedding, extracts `type`, `tags`, `requires`, etc. for boosting/expansion/filtering
- **Incremental indexing**: Content-hash based staleness detection β only re-embeds changed files
- **Multi-pack support**: Path-based routing at `/packs/{slug}/mcp`
- **Graph-aware retrieval**: optional post-retrieval expansion via `_graph.yaml` adjacency, including accepted ontology/entity nodes when graph export provides them
- **`context_prefix` (RFC-004)**: sidecar situating strings are prepended at index time for embed + BM25 only; returned/reconstruct text stays the original atom body
- **Tier / navigation filter**: `retrieval_strategy: navigation`, `_index.md`, `source-coverage.md`, and `context.on_demand` files are excluded from the RAG pool (still readable via `ep_read` / resources)
- **Tools**:
- `ep_search` β primary hybrid retrieval tool
- `ep_read` β load a whole atom by path or provenance id (consume-loop step 2)
- `ep_list_topics` β browse pack structure and available content types
- `ep_graph_traverse`
- **Multi-pack stdio support**: All tools accept a `pack` parameter for routing when multiple packs are configured. Single-pack configs make `pack` optional (defaults to the only configured pack).
- **Resources**:
- `ep://{slug}/manifest`
- `ep://{slug}/authority`
- `ep://{slug}/files`
- `ep://{slug}/file/{+path}` (raw content with frontmatter)
- `skill://{slug}/SKILL.md`, `skill://{slug}/{+path}` (only when `mcp.skills.enabled: true`)
- **Transports**: Streamable HTTP (primary, cloud-ready), stdio (local dev)
- **stdio multi-pack support (0.6.0+)**: When using `--transport stdio` with multiple packs configured, all tools accept an optional `pack` parameter (slug string) to specify which pack to query. For single-pack configurations, the `pack` parameter is optional and defaults to the only configured pack. This allows MCP hosts (Cursor, Claude Desktop) to use a single stdio process for multiple packs instead of requiring one process per pack.
- **HTTP endpoint**: `GET /search` (for non-MCP HTTP clients)
- **Golden retrieval eval (0.7.0+)**: `tests/eval/` β YAML query sets (`expected` best-first, `must_not`, `category`) scored as recall@1/3/5, MRR and must_not violations per pack and category. Skipped in the default run; `pytest tests/eval -m golden --golden <dir> --packs-root <dir>` with `EP_EVAL_TODAY=2026-10-20` and optional `EP_EVAL_MIN="recall@5=0.9,mrr=0.7"`, or `python tests/eval/golden_eval.py --golden <dir> --today 2026-10-20`
- **Auth**: API key (Phase 1), designed for OAuth 2.1 (Phase 2)
## Quick Start
### Installation
```bash
git clone https://github.com/brianhearn/ep-mcp.git
cd ep-mcp
pip install -e .[dev]
```
### Configuration
Create `config.yaml` with at minimum:
```yaml
server:
host: "127.0.0.1"
port: 8000
log_level: "info"
# Required for public Host headers (SDK v2 DNS-rebinding protection).
# See DEPLOYMENT.md β omit and Streamable HTTP returns 421.
mcp_allowed_hosts:
- "localhost"
- "localhost:*"
- "127.0.0.1"
- "127.0.0.1:*"
packs:
- slug: "my-pack"
path: "/path/to/your/expertpack"
api_keys:
- "your-secret-key-here"
embedding:
provider: "gemini" # "gemini" (default) or "azure-openai"
model: "gemini-embedding-001"
```
For the full configuration reference β embedding providers, retrieval tuning (`mmr_lambda`, `min_score`, `default_max_results`, graph expansion, reranker, `requires:` expansion, etc.), production systemd setup, and deployment gotchas β see **[DEPLOYMENT.md](DEPLOYMENT.md)**.
### Running
```bash
ep-mcp serve --config config.yaml
```
The server builds/updates the SQLite index (FTS5 + sqlite-vec) on startup. **0.5.0 requires a rebuild** of existing indexes (`index_features=context_prefix_v1+nav_filter_v1`); startup does this automatically, or delete `<pack>/.ep-mcp/` first.
The GET `/search` HTTP endpoint is available at:
`GET /search?q=<query>&pack=<slug>&n=10`
Add `&reconstruct=true` to include original markdown spans and provenance blocks for verification.
with header `Authorization: Bearer <key>`.
## Environment Variables
Only the key for your configured embedding provider is required.
| Variable | Required | Description |
|---------------------------|----------|-------------|
| `GEMINI_API_KEY` | If `embedding.provider: gemini` | Gemini embedding API key |
| `AZURE_OPENAI_ENDPOINT` | If `embedding.provider: azure-openai` | Azure OpenAI resource endpoint |
| `AZURE_OPENAI_API_KEY` | If `embedding.provider: azure-openai` | Azure OpenAI API key |
| `EP_MCP_KEY_{SLUG}` | Optional | Per-pack API key override (uppercase slug, hyphensβunderscores) |
| `EP_MCP_REMOTE_HOST` | Optional | Deploy target for `scripts/deploy.sh` |
| `EP_MCP_REMOTE_SRC` | Optional | Remote source path for `deploy.sh` |
| `EP_MCP_REMOTE_SITE_PKG` | Optional | Remote site-packages path for `deploy.sh` |
## Tech Stack
- **Language**: Python 3.12+
- **MCP Framework**: official `mcp` SDK v2 (`MCPServer`, spec 2026-07-28, dual-era with 2025 clients)
- **Storage**: SQLite with FTS5 + [sqlite-vec](https://github.com/asg017/sqlite-vec)
- **Embeddings**: Gemini (default), configurable Azure OpenAI / OpenAI providers
- **Web**: Starlette + Uvicorn (for Streamable HTTP)
- **CLI**: Click
See `pyproject.toml` for exact dependencies.
## Project Structure
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed module breakdown, retrieval pipeline, indexing strategy, and design decisions.
## Related
- **[expertpack.ai](https://expertpack.ai)** β Framework website, community packs, and documentation
- **[ExpertPack](https://github.com/brianhearn/expert-pack)** β Schema framework, validation tools, Obsidian compatibility, community packs
- **[ExpertPacks](https://github.com/brianhearn/expert-packs)** β Published knowledge packs (private)
- **EasyTerritory MCP** (future) β Domain-specific layer built on top of this repo + `ezt-designer` pack
## License
Apache 2.0 Β© 2026 Brian Hearn
## Author
**Brian Hearn** β Built in 3 days (April 10β11, 2026).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues