Skip to main content
Glama
tahasiddiquii

mcp-knowledge-server

README.md
# mcp-knowledge-server

> **Connect an agent to your internal knowledge without leaking across permission
> boundaries.** A Model Context Protocol server that filters every retrieval by the
> caller's identity before anything reaches the model, answers only with grounded
> citations to documents the caller may actually see, and refuses when the answer
> lives behind a permission they lack. Fully offline, keyless, and leak-gated in CI.

[![ci](https://github.com/tahasiddiquii/mcp-knowledge-server/actions/workflows/ci.yml/badge.svg)](https://github.com/tahasiddiquii/mcp-knowledge-server/actions/workflows/ci.yml)
![python](https://img.shields.io/badge/python-3.11%2B-blue)
![license](https://img.shields.io/badge/license-MIT-green)
![offline](https://img.shields.io/badge/runs-offline%20%C2%B7%20zero--key-success)

"Connect our AI to our internal docs" is the request every enterprise makes, and the
naive version, dumping a folder into a vector database, quietly ignores who is
asking. An engineer must not retrieve compensation bands; a member of the public
must not see the revenue forecast. This server makes that structurally impossible:
the access filter runs before ranking, so an inaccessible document can never enter a
result, be cited, or shape an answer. Built from my RAG and evaluation work.

## What this demonstrates

| Enterprise concern | Where |
| --- | --- |
| Identity and role model, `public` implicit for everyone | [config.py](src/mcp_knowledge/config.py) |
| Per-document access control lists | [corpus.py](src/mcp_knowledge/corpus.py) |
| Access filter applied before ranking | [acl.py](src/mcp_knowledge/acl.py) · [retriever.py](src/mcp_knowledge/retriever.py) |
| Grounded, cited answers with an explicit refusal path | [answerer.py](src/mcp_knowledge/answerer.py) |
| Staleness signal on old sources | [corpus.py](src/mcp_knowledge/corpus.py) |
| ACL enforced at the MCP resource layer too | [server.py](src/mcp_knowledge/server.py) |
| Leak-proof and grounding CI gate | [evals.py](src/mcp_knowledge/evals.py) |

## Architecture

```mermaid
flowchart LR
    Q[agent query + caller identity] --> ACL{{access filter}}
    D[(documents + ACLs)] --> ACL
    ACL -->|accessible only| R[rank by relevance]
    R -->|clears the bar| G[grounded, cited answer]
    R -->|nothing relevant| X[refuse]
```

## Quickstart

```bash
make dev            # venv + install -e ".[dev]"

kb demo             # answers and refusals across roles
kb ask "how do I roll back a deployment" --roles engineering
kb ask "what are the salary bands" --roles engineering    # refused: HR-only
kb eval             # the leak-proof gate
kb serve --roles engineering                              # live MCP server over stdio
```

No keys, no network. Embeddings are a deterministic hashing vectorizer; set
`KB_EMBEDDER=openai` in production to swap in dense embeddings behind the same
interface, with the access-control logic unchanged.

## The gate that matters

`kb eval` runs labeled queries across roles ([report](reports/kb_report_example.md)):

| metric | value | gate |
| --- | --- | --- |
| **permission_leaks** | **0** | = 0 |
| **ungrounded_citations** | **0** | = 0 |
| recall_at_k | 1.000 | >= 0.80 |
| refusal_rate | 1.000 | >= 0.90 |

The two zeros are the contract. `permission_leaks` counts any answer that cites or
contains a document the caller cannot access, and any case where an inaccessible
gold document is even retrieved. `ungrounded_citations` counts any answer whose
cited snippet is not a verbatim span of its source. Both must be zero or CI fails.
Recall and refusal_rate confirm the server is still useful: it finds the right
document when the caller is entitled to it, and refuses cleanly when they are not.

## What it does

- **Answers within a permission.** An engineer asking how to roll back a deployment
  gets the runbook snippet with a citation.
- **Refuses across a permission.** The same engineer asking for salary bands is
  refused, and the HR content never appears, not even partially.
- **Grounds every claim.** Each answer carries a document id and a snippet copied
  verbatim from that document, so a reviewer can verify it.
- **Flags stale sources.** An answer sourced from a document over a year old is
  marked, so nobody acts on outdated terms.
- **Enforces at the resource layer.** Reading `kb://doc/{id}` directly is also
  access-checked, so the boundary holds no matter how the client reaches for data.

## Design decisions

- **Filter before rank, always.** Access control is not a post-filter on results;
  an inaccessible document is removed from consideration before scoring, so it can
  never influence what the model sees.
- **Refuse over guess.** When nothing accessible clears the relevance floor, the
  server says so. A wrong-but-confident answer and a leak are the two failure modes
  that matter, and refusal avoids both.
- **Citations are verbatim.** The snippet is a real substring of the source, not a
  paraphrase, which is what makes grounding checkable rather than aspirational.
- **Identity is explicit.** Roles come from the caller (`KB_ROLES`), and `public` is
  always included, so default access is the least-privilege set.

## Layout

```
src/mcp_knowledge/  config · corpus · embed · acl · retriever · answerer · server · evals · cli
data/  documents.jsonl · eval_queries.jsonl
reports/  kb_report_example.md
```

## Related repositories

Part of a portfolio on production ML and LLM engineering:

- [hybrid-graph-rag](https://github.com/tahasiddiquii/hybrid-graph-rag): hybrid and graph retrieval benchmark
- [llm-eval-observability](https://github.com/tahasiddiquii/llm-eval-observability): RAG evaluation and observability
- [mcp-guardrail-gateway](https://github.com/tahasiddiquii/mcp-guardrail-gateway): security gateway for MCP servers
- [ai-harness](https://github.com/tahasiddiquii/ai-harness): multi-stage agent harness
- **mcp-knowledge-server**: this repo.

## License

MIT (c) 2026 Taha Siddiqui