support-kb-server
by ketanongit
README.md
# support-kb-server
A role-based knowledge-base MCP server for a support bot. It serves FAQ answers
out of a local vector store and gates write operations behind an **admin** bearer
token, while a **read_only** token can only ask questions.
Everything runs locally — no API keys, no hosted services:
| Concern | Choice |
|--------------|----------------------------------------------------------------|
| MCP server | `mcp` Python SDK (**FastMCP**), over **streamable HTTP** |
| Vector store | **ChromaDB**, embedded persistent mode → `./chroma_data` |
| Embeddings | **sentence-transformers** `all-MiniLM-L6-v2` (fully local) |
| Auth | Two bearer tokens → two roles, in-memory `token → role` mapping |
## Tools & permissions
| Tool | admin | read_only | Notes |
|-----------------------------------|:-----:|:---------:|----------------------------------------------|
| `answer_question(query)` | ✅ | ✅ | Retrieves top FAQ match + source snippet |
| `add_knowledge(question, answer)` | ✅ | ❌ | Adds an FAQ entry (audit-logged) |
| `delete_knowledge(doc_id)` | ✅ | ❌ | Removes an FAQ entry by id (audit-logged) |
| `list_knowledge()` | ✅ | ❌ | Lists all entries with ids (for delete refs) |
Every tool call reads the caller's `Authorization: Bearer <token>` header, looks
up the role, and returns a clear **"Permission denied"** error if that role may
not call the tool.
## Setup
Requires Python 3.10+.
```bash
pip install -r requirements.txt
```
> The first run downloads the `all-MiniLM-L6-v2` model (~90 MB) from Hugging Face
> and caches it locally. Every run after that is fully offline.
## Run the server
```bash
python server.py
```
On startup the server:
1. Seeds the Chroma collection with 10 sample FAQ entries (first run only).
2. **Prints the two bearer tokens**, clearly labeled, to the console — this is
where you copy them from:
```
====================================================================
support-kb-server - BEARER TOKENS (copy these now)
====================================================================
ADMIN (full access) : admin-XXXXXXXXXXXXXXXXXXXX
READ_ONLY (answer_question) : readonly-XXXXXXXXXXXXXXXXXXXX
====================================================================
Endpoint: http://127.0.0.1:8000/mcp
====================================================================
```
3. Starts the MCP endpoint at **`http://127.0.0.1:8000/mcp`**.
Fresh random tokens are generated on each startup. To pin them (useful for
scripts/CI), set env vars before launching:
```bash
# PowerShell
$env:KB_ADMIN_TOKEN="admin-my-fixed-token"; $env:KB_READONLY_TOKEN="readonly-my-fixed-token"; python server.py
# bash
KB_ADMIN_TOKEN=admin-my-fixed-token KB_READONLY_TOKEN=readonly-my-fixed-token python server.py
```
Other env vars: `KB_HOST` (default `127.0.0.1`), `KB_PORT` (default `8000`).
The active tokens + endpoint are also written to `.mcp_tokens.json` so the test
script can authenticate without copy-pasting.
## Test it
With the server running in one terminal, run the test harness in another:
```bash
python test_local.py
```
It opens real MCP client sessions over HTTP with each token and verifies the full
matrix — `read_only` is allowed only on `answer_question` and denied on the other
three; `admin` succeeds on all four — printing clear `PASS`/`FAIL` per check:
```
--- read_only role ---
[PASS] read_only can call answer_question
[PASS] read_only is DENIED add_knowledge
[PASS] read_only is DENIED delete_knowledge
[PASS] read_only is DENIED list_knowledge
--- admin role ---
[PASS] admin can call answer_question
[PASS] admin can call add_knowledge
[PASS] admin can call list_knowledge (sees new entry)
[PASS] admin can call delete_knowledge
8/8 checks passed
```
## Audit log
Every **successful** `add_knowledge` / `delete_knowledge` call appends one JSON
line to `audit_log.jsonl`:
```json
{"timestamp": "2026-09-06T16:05:37.458146+00:00", "role": "admin", "tool": "add_knowledge", "arguments": {"question": "...", "answer": "...", "doc_id": "faq-7ee3b2da05e3"}}
```
Denied calls and read-only queries are **not** logged.
## Connect it to Claude Desktop / claude.ai as a custom connector
This server speaks streamable HTTP with bearer auth, so it plugs in as a **custom
connector**.
claude.ai (and Claude Desktop) require an **HTTPS** URL for custom connectors, so a
local `http://127.0.0.1:8000` URL is rejected. Expose the server over HTTPS with a
tunnel such as **ngrok**.
### 1. Start the server so it accepts the tunnel's hostname
The MCP server has built-in DNS-rebinding protection that, by default, only trusts
`localhost`. A tunnel forwards requests under its own public hostname, so tell the
server to accept it via `KB_ALLOWED_HOSTS`. Because every tool is already gated by a
bearer token, `*` (accept any host) is fine here:
```powershell
# PowerShell — pin tokens too so they survive restarts
$env:KB_ALLOWED_HOSTS="*"
$env:KB_ADMIN_TOKEN="admin-supportkb-8f3a2c17d9e4"
$env:KB_READONLY_TOKEN="readonly-supportkb-2b6d40af71c8"
python server.py
```
```bash
# bash
KB_ALLOWED_HOSTS='*' python server.py
```
(You can instead pin exact hosts: `KB_ALLOWED_HOSTS="myname.ngrok-free.app"`.)
### 2. Open the tunnel
```bash
ngrok http 8000
```
ngrok prints a forwarding URL like `https://eb41-49-238-50-150.ngrok-free.app`. Your
MCP endpoint is that URL **with `/mcp` appended**. (A free-tier ngrok subdomain
changes each time you restart ngrok — that's the reason for `KB_ALLOWED_HOSTS="*"`.)
### 3. Add it in Claude → Settings → Connectors → "Add custom connector"
1. Give it a name, e.g. `Support KB`.
2. **URL:** `https://<your-ngrok-subdomain>.ngrok-free.app/mcp`
3. **Authentication:** choose the bearer / access-token option and paste one of the
tokens the server printed:
- the **admin** token to enable all four tools, or
- the **read_only** token to expose only `answer_question`.
4. Save. Claude runs the MCP handshake and lists the tools your token is allowed to
use. Ask a support question and it will call `answer_question`.
> **Troubleshooting `421 Misdirected Request`:** the server didn't trust the tunnel
> hostname — start it with `KB_ALLOWED_HOSTS="*"` (or the exact ngrok host) as above.
> Keep both the server and `ngrok` processes running the whole time Claude is
> connected.
## Project layout
```
kb_store.py # Chroma + embeddings + seed data + query/add/delete/list helpers
server.py # FastMCP server: tools, auth/roles, audit logging, token printing
test_local.py # End-to-end role/permission test over HTTP
requirements.txt
chroma_data/ # persisted vector store (created on first run)
audit_log.jsonl # append-only audit trail (created on first admin write)
.mcp_tokens.json # active tokens+endpoint, written for the test script
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues