An MCP server that gives AI assistants safe, sandboxed hands on your machine — notes, tasks, web pages, and hybrid search (FTS5 + embeddings) over a personal document corpus. Built with TypeScript, SQLite, and a security-first design.
MCP (Model Context Protocol) is the open standard that lets AI clients like Claude Desktop call external tools. This server implements it twice from one codebase: over stdio for local clients and over HTTP for remote access.
Once connected to Claude Desktop, conversations like this just work:
You: Find my notes about the Postgres migration and add a task to finish it by Friday.
Claude: →
rag_search("postgres migration")— 3 matching chunks from your corpus →task_create("Finish Postgres migration", due: "2026-07-31")"Found your migration notes — the remaining step was the index rebuild. Task created for Friday."
Every step happens inside the sandbox you configured: Claude can only touch the notes directory you allowed, only fetch from hosts you allowlisted, and only through the tools below.
| Tool | What it does |
|---|---|
rag_search |
Search the corpus — keyword (FTS5), vector (embeddings), or hybrid (both, fused with Reciprocal Rank Fusion) |
rag_add_document |
Add or update a document: auto-chunked, FTS-indexed, embedded when vector search is configured |
rag_stats |
Corpus statistics: documents, chunks, embeddings, backend availability |
file_read / file_write / file_list |
Notes access — sandboxed to NOTES_DIR, allowlisted extensions only |
web_fetch |
Fetch a page from allowlisted hosts only, stripped to clean text (cheerio) |
task_create / task_list / task_complete |
Tasks stored as plain, human-editable markdown |
Hybrid search is the default when OPENAI_API_KEY is set: FTS5 and cosine-similarity results are merged with Reciprocal Rank Fusion — rank-based fusion that needs no score normalization between bm25 and cosine scales. If the vector backend fails mid-query, hybrid degrades gracefully to keyword results.
Local tool access for an LLM is a security problem before it is anything else. The interesting engineering here:
- Path sandboxing that survives the classic bypasses. Every path resolves against
NOTES_DIR; absolute paths,../traversal, and the sibling-prefix bypass (notesvsnotes-evil— a bug most naivestartsWithchecks have) are rejected. Extension allowlist is enforced on both read and write. - Web fetching is deny-by-default.
web_fetchrefuses any host not inWEB_ALLOWED_HOSTS. Subdomains of allowed hosts pass; lookalikes (example.com.evil.com) do not. HTTP(S) only. - The protocol channel stays clean. All logging goes to stderr — on a stdio MCP server, stdout belongs to JSON-RPC and a single stray
console.logcorrupts the stream. - Typed failure paths. The persistence layer returns neverthrow
Resulttypes instead of throwing; inputs are validated with zod.
All of this is pinned down by 45 unit tests targeting exactly these properties — traversal attempts, prefix bypasses, lookalike domains, protocol filtering, rank fusion, and registry dispatch — running in CI on Node 20 and 22.
Both transports consume one ToolRegistry — a single source of truth for tool definitions and dispatch, so the stdio and HTTP surfaces can never drift apart.
flowchart LR
CD[Claude Desktop] -- "stdio (JSON-RPC)" --> REG[ToolRegistry<br/>definitions + dispatch]
RC[Remote client] -- "HTTP :3333" --> REG
REG --> FS["FileService<br/>sandboxed notes"]
REG --> WS["WebService<br/>allowlisted fetch"]
REG --> TS["TaskService<br/>markdown store"]
REG --> RAG["RAGService<br/>keyword | vector | hybrid"]
RAG -- "FTS5 (bm25)" --> POOL["ConnectionPool"]
RAG -- "embeddings + cosine" --> VEC["VectorRAGService"]
VEC --> POOL
RAG -- "RRF fusion" --> RAG
POOL --> DB[("SQLite<br/>docs + chunks<br/>chunks_fts + chunk_vecs")]
src/
server.ts MCP entrypoint (SDK 1.x): wires services into the registry
tools/
registry.ts single source of truth: tool schemas + dispatch
transports/
http-transport.ts thin HTTP facade over the registry: /health, /tools, /call, /status
connectors/
file-service.ts sandboxed file access
web-service.ts allowlisted web fetching + HTML cleaning
task-service.ts markdown-backed task store
rag/
rag-service.ts search facade: keyword / vector / hybrid modes
fusion.ts Reciprocal Rank Fusion (pure, unit-tested)
sqlite-client.ts SQLite persistence: FTS5, chunking, migrations (neverthrow API)
vector-rag-service.ts vector search with OpenAI embeddings
embedding-service.ts embedding generation (text-embedding-3-small)
db/
connection-pool.ts SQLite connection pooling
git clone https://github.com/Galiusbro/ai-ops-hub.git && cd ai-ops-hub
npm install
cp .env.example .env # adjust paths and allowlist
npm run build
npm start # stdio only (for Claude Desktop)
npm run start:http # stdio + HTTP facade on :3333The HTTP facade is opt-in (--http flag or HTTP_ENABLED=1) so that MCP clients can spawn multiple server instances without port clashes.
{
"mcpServers": {
"ai-ops-hub": {
"command": "node",
"args": ["/absolute/path/to/dist/server.js"],
"env": {
"NOTES_DIR": "/path/to/your/notes",
"RAG_DB_PATH": "/path/to/your/rag.db"
}
}
}
}curl http://localhost:3333/health
curl http://localhost:3333/tools
curl -X POST http://localhost:3333/call \
-H "Content-Type: application/json" \
-d '{"name":"rag_search","arguments":{"query":"postgres migration"}}'| Variable | Default | Purpose |
|---|---|---|
NOTES_DIR |
./notes |
Directory the file tools are sandboxed to |
TASKS_FILE |
./tasks.md |
Markdown file behind the task tools |
RAG_DB_PATH |
./data/rag.db |
SQLite database for the corpus |
WEB_ALLOWED_HOSTS |
example.com,developer.mozilla.org |
Comma-separated allowlist for web_fetch |
HTTP_PORT |
3333 |
HTTP transport port |
OPENAI_API_KEY |
— | Enables vector + hybrid search (embeddings) |
npm run dev # run from source (tsx)
npm test # vitest unit suite
npm run type-check # tsc --noEmit
npm run lint- MCP server over stdio + HTTP
- Sandboxed file / web / task tools
- SQLite FTS5 corpus with trigger-synced index
- Unit tests for the security-critical paths + CI
- Shared tool registry between the two transports
- Vector search wired in: hybrid mode with Reciprocal Rank Fusion
-
@modelcontextprotocol/sdk1.x - Streamable HTTP transport from the SDK (replace the custom REST facade)
- Audit logging
- Local embedding backend as an alternative to OpenAI
I built this to understand MCP from the inside — the protocol, the transports, and what it actually takes to hand an LLM safe access to a real machine. It grew into a working local-first assistant backend: the FTS5 corpus, the sandboxing, and the test suite are the parts I'd reuse in production.