Multi-tenant knowledge base as a remote MCP server on one Cloudflare Worker — D1 + FTS5, exact cosine search, live custom tools in a QuickJS sandbox
read more show less
A multi-tenant, text-only knowledge base exposed as a remote MCP server, running as a single Cloudflare Worker. The one deploy is simultaneously an operator admin UI (tenants, collections, documents, tools, keys, activity, settings) and an MCP endpoint that serves many client applications, each scoped by its Bearer API key to its own tools and datasets. One MCP server to rule them all.
Storage is right-sized for the workload. D1 (SQLite) is the single source of truth: relational data, chunk text, embeddings as packed Float32 BLOBs, and an FTS5 keyword index. KV is a hot-path cache only (key hash to scope, global settings). The corpus is small enough to hold every vector in memory, so brute-force exact cosine replaces pgvector and Vectorize with no recall loss. No Postgres, Redis, Hyperdrive, R2, or Containers.
Search is a JS reimplementation of the old hybrid SQL query: FTS5 keyword ranking plus cosine similarity over the tenant-scoped candidate set, fused with Reciprocal Rank Fusion, deduped to the parent chunk, and hydrated with parent text. Search can be pinned to a single document without widening the collection scope. Per-tenant opt-in enhancers add HyDE, multi-query expansion, and Gemini reranking.
Ingestion uses topic-aware hierarchical chunking: parent boundaries follow explicit split markers, then headings, then sentence packing; ~128-token children are embedded with a document-and-section header prepended (contextual retrieval) while stored content stays clean. Embeddings and generation go through Cloudflare AI Gateway to Gemini, with Workers AI as a drop-in alternative.
Tools per key: rag_search, rag_list_collections, rag_get_chunk, rag_get_document, Brave web_search / news_search / image_search / place_search, wikipedia, calculate, and a tools_search meta-tool (BM25F-lite) for lazy tool discovery. Operators, or agents via create_custom_tool, write new tools as JS that runs live in a QuickJS WASM sandbox on the Worker with generate, tenant-scoped search, and opt-in fetch bridges. No redeploy: save and the next session sees the tool. Per-key server instructions steer the client.
Isolation: scope is resolved once per request from the key hash (KV-cached) and every repository read and write requires a tenantId argument, so cross-tenant leaks are prevented by type signatures rather than by remembering a WHERE clause. Operator surfaces sit behind Cloudflare Access with the JWT verified at the origin against the team JWKS; workers.dev is disabled so the custom domain is the only door.
Stack: SvelteKit + adapter-cloudflare, D1 + FTS5, KV, Drizzle ORM + migrations, AI Gateway to Gemini or Workers AI, @modelcontextprotocol/sdk (Streamable HTTP), quickjs-emscripten, jose, zod, vitest. First-run setup wizard stores provider keys in D1, so no secrets are needed to go live. Provisioning and deploy scripts create D1 and KV, patch wrangler.toml, apply remote migrations, and deploy in one command.
- sveltekit
- svelte-5
- typescript
- cloudflare-workers
- adapter-cloudflare
- d1
- sqlite-fts5
- workers-kv
- ai-gateway
- gemini-api
- workers-ai
- drizzle-orm
- model-context-protocol
- quickjs-wasm
- cloudflare-access
- jose
- zod
- vitest
- wrangler