KB Platform MCP Server
by Syntax753
README.md
<div align="center">
# KB Platform
**A runnable reference architecture for governed enterprise AI.**
An agentic knowledge platform where every AI action is policy-checked, audited, and
traceable to a source document — 18 containers, one `make demo`, no cloud account required.
[](LICENSE)
[](https://github.com/Syntax753/synkb/actions/workflows/ci.yml)
[](https://www.python.org/)
[](https://nextjs.org/)
[](https://www.openpolicyagent.org/)
[](https://modelcontextprotocol.io/)
[Quick start](#quick-start) · [How this was built](#how-this-was-built) ·
[What makes it different](#what-makes-it-different) · [Product tour](#product-tour) ·
[Architecture](#architecture) · [Documentation](#documentation) ·
[Contact](#questions-adoption-and-contact)
</div>
---
Most enterprise-AI demos answer *"can an LLM read our documents?"* — a question that
was settled some time ago. The unsolved part is everything around it: **who is allowed
to ask, what the model is allowed to see, who approved the output, and what an auditor
sees six months later.**
KB Platform is a complete, working answer to that second question. It is a
**production-shaped MVP**: the architectural boundaries, policy enforcement points and
audit trail are the ones you would actually deploy, while the infrastructure underneath
is substituted for things that run on a laptop.
- **Docker Compose** stands in for Azure managed services
- **Qdrant** stands in for Azure AI Search
- **A mock SharePoint** stands in for the Microsoft Graph API
- **A seeded demo user** stands in for Entra SSO
The design intent throughout is that the demo **grows into production by replacing
components, not rewriting them** — and every one of those substitutions has a
decision-log entry naming its production analogue and the exact file where the swap
lands.

---
## How this was built
The platform is one half of what's on offer here. The other half is the method,
because it is the part that transfers.
Governance was not retrofitted onto this codebase, because the build process carried it
from the first commit. Four practices did the work:
- **Every feature is a numbered work package** with acceptance criteria written and
agreed *before* implementation — 47 of them, and each one is the contract for what
"done" means.
- **Larger features get an RFC first** — 15 of them, covering the marketplace, skills,
IaC, cost attribution and the rest.
- **Every decision the plan didn't already answer is recorded as it is taken**, in an
append-only log that now runs to 170 entries.
- **The whole plan is executed with [Claude Code](https://claude.com/claude-code)**
against those written criteria, so the agent has a contract to work to rather than a
prompt to interpret.
The decision log is the artefact worth stealing. It is why an early decision is still
legible months later, and why every demo shortcut in this repo can name its production
replacement and the file where the swap lands.
| Evidence | Where |
|---|---|
| The 47 work packages, with acceptance criteria | [design/implementation-plan.md](design/implementation-plan.md) |
| 170 decisions, newest first, append-only | [design/decision-log.md](design/decision-log.md) |
| 15 feature RFCs | [`design/wp-*-rfc.md`](design/) |
| 1,223 backend + 176 frontend tests, gated in CI | [.github/workflows/ci.yml](.github/workflows/ci.yml) |
None of that requires you to adopt this codebase. It is a way of building governed AI
systems fast without the governance being bolted on afterwards — which is the part most
teams get wrong, and the reason most AI pilots stall on the way to production.
> **Trying to get governed AI past pilot in your organisation?** The method above is the
> transferable part —
> [happy to talk it through](https://www.linkedin.com/in/peterturnerlondon).
---
## What makes it different
**🛡️ Policy is infrastructure, not `if` statements.**
Authorisation is a Rego policy bundle evaluated by
[Open Policy Agent](https://www.openpolicyagent.org/) running as an HTTP sidecar — not
`if` branches scattered through handlers. Every policy opens `default allow := false`,
and 108 Rego test rules cover 180 lines of rule code across 7 policy files
(`make opa-test`, gated in CI). The
decision point is the service that owns the resource rather than the gateway, so the
*same* rules govern the web UI and the MCP tools — a power user connecting Claude Code
gets no bypass. Today the rules are role-based (group membership per action);
resource-attribute rules such as document sensitivity are a documented next step, not a
current capability.
**📋 Built against ISO/IEC 27001:2022 from the start.**
[docs/iso27001-mapping.md](docs/iso27001-mapping.md) is a control-by-control Annex A
mapping with evidence pointers into the codebase — the seed of a real Statement of
Applicability. It is deliberately honest about gaps: partial and not-yet controls are
named as such, because fabricating coverage is worse than admitting work remains.
**🔌 A real MCP server with a real OAuth 2.1 flow.**
The [Model Context Protocol](https://modelcontextprotocol.io/) server exposes the same
tool registry the in-app agent uses, over OAuth-authenticated JSON-RPC. Not a bearer
token in a header — the full flow:
- S256 PKCE with constant-time verification
- Single-use authorization codes and exact-match redirect URIs
- RS256 JWTs signed over a real 2048-bit JWKS, with algorithm-downgrade rejection
- RFC 7591 dynamic client registration
- RFC 9728 protected-resource metadata
Point Claude Code or Claude Desktop at it and the knowledge base becomes available in an
ordinary Claude session. Only the user-authentication step is stubbed — there is no IdP
attached — and the JWT shape is already Entra-compatible.
**✍️ Human-in-the-loop by construction.**
AI output lands in a draft workbench, not in a shared drive. Drafts carry version
history, a conversation thread with Claude, an explicit submit → approve → export
lifecycle, and per-prompt edit modes — client-facing SOWs are *propose-only* so nothing
is silently rewritten, while lower-stakes documents auto-apply.
**🏗️ Genuinely decomposed, not a monolith in costume.**
Eighteen containers. Eleven of them are independently-built FastAPI services, each with
its own Dockerfile, lockfile, database and Alembic migrations, communicating only over
HTTP/JSON — no shared in-process state and no internal RPC framework. Plus a Next.js 15
frontend with a generated, drift-checked API client.
**🧪 Tested at five levels**, because unit tests that stub `fetch` cannot catch a broken
gateway proxy:
- 117 Python test files across 14 service suites
- 22 web component suites
- 7 Rego policy suites
- 7 Playwright journeys against the live stack
- An ABAC smoke suite driving three personas through the live OPA sidecar
---
## Product tour
The whole path, from a raw SharePoint estate to an approved, branded deliverable. This
is the same journey [demo/demo-script.md](demo/demo-script.md) walks in 14 steps, and
`make smoke` drives end to end in Playwright.
### 1. Curate — nothing is searchable until someone says so
The Library is a deliberately curated subset of the SharePoint estate, not a crawl.
Each document carries its type, practice, client and sensitivity, plus the exact
SharePoint path it came from. Everything not added here is invisible to search and to
the chatbot — which is what makes the blast radius of a prompt injection bounded.

### 2. Ask — every answer traces to the chunk that produced it
The Explore tab answers only from indexed content. Each claim carries a numbered
citation, and clicking one opens the **exact chunk the model saw** — with its
`chunk_id` and `library_item` id, not a plausible-looking reference to the whole
document.

### 3. Generate — pick a prompt, pin a template
Prompts are catalogued, versioned and parameterised. Where a prompt produces a
structured document, it requires a template, and the chosen **template version is
pinned to the draft** — so a template edit six months later cannot retroactively
change what was approved.
<table>
<tr>
<td width="50%"><img alt="The Prompts grid — one card per runnable prompt, with its document type and version" src="docs/images/04-prompts.png"></td>
<td width="50%"><img alt="The template selector listing active templates whose doc_type matches the selected prompt" src="docs/images/05-template-selector.png"></td>
</tr>
</table>
### 4. Watch — the agent loop runs inside declared bounds
Execution streams live over SSE. The tools the agent may call and the limits it runs
under are **displayed to the user before it starts** — here `search_kb`, capped at
`max 10 iterations / 180s wall / 100000 tokens`. An agent whose budget is a surprise
is an agent nobody can sign off.

### 5. Review — AI output lands in a workbench, not a shared drive
The draft opens with the rendered document on the left and a conversation with Claude
on the right. Note the **`Propose-only`** badge: this is a client-facing SOW, so Claude
suggests diffs you accept or reject rather than editing in place. Lower-stakes document
types auto-apply. Lineage, version history and the state-appropriate action bar
(Approve / Request changes / Reject) sit alongside.

### 6. Export — a frozen, attributed, versioned deliverable
Approval produces a real rendered `.docx` / `.pptx` / `.xlsx`, published back to
SharePoint and recorded as a first-class export: who authored it, **who approved it**,
which version, and the file itself, downloadable and immutable.

### And the whole thing is audited
Every step above — the curation decision, the OPA verdict, the LLM call, the approval,
the export — writes an audit row correlated by `X-Request-Id`. That is the difference
between a demo and something an ISMS lead will let near a client document.
<sub>The screenshots above are from a recent build. The sidebar has since gained
**Profile**, **Skills**, **Marketing Hub** and **Generators**, and running a prompt
moved to a **Generate** tab — the flows are otherwise current.
[docs/user-guide.md](docs/user-guide.md) is the up-to-date walkthrough.</sub>
---
## Quick start
**Prerequisites:** Docker Desktop (or Docker Engine + Compose v2) and a **bash shell**.
The Makefile uses bash idioms; on Windows run everything from **Git Bash** (ships with
[Git for Windows](https://git-scm.com/download/win)). macOS and Linux shells work as-is.
```bash
git clone https://github.com/Syntax753/synkb.git kb-platform && cd kb-platform
cp .env.example .env # then set ANTHROPIC_API_KEY (see below)
make demo # build + start + seed (first run ~10 min)
open http://localhost:3000 # login is automatic — single demo user
```
`make demo` is idempotent and safe to re-run: it brings the stack up if it is down,
(re-)seeds, and prints the next step. You land on a Library populated with 50 seeded
documents — a deliberately fictional consultancy serving farm-animal clients, so nothing
in the demo data resembles a real engagement.
Prefer npm? `npm run demo` is the same command (no `npm install` needed at the root).
See [all npm aliases](#npm-scripts).
> **First clone fails on missing `uv.lock` / `package-lock.json`?** That is the one-off
> [first-time setup](#first-time-setup-one-off) step.
### Supplying an LLM key
The chatbot, prompt execution and draft conversation call the Anthropic API. Everything
else — Library browsing, SharePoint subscriptions, drafts, approval, export — works
without one; LLM-shaped calls return a clear `502 llm_error`.
```bash
echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env
make demo # recreates the LLM-calling services with the new env
```
Services read the key at startup, so a recreate is required — `make demo` does that for
you. Two alternatives if you would rather not use an API key:
- **`make demo SUPPLY_PROXY=1`** — rides your existing Claude subscription through an
opt-in local proxy.
- **`ANTHROPIC_BASE_URL`** — point it at any Anthropic-compatible gateway (Bedrock,
Vertex, a corporate proxy).
See [.env.example](.env.example) for both.
### Take the guided tour
The fastest way to understand the platform is
[demo/demo-script.md](demo/demo-script.md) — a 14-step end-to-end walkthrough from
curating a document to exporting an approved deliverable. For audience-tailored talk
tracks (Sales, Marketing, CTO, Engineers) see
[demo/demo-playbook.md](demo/demo-playbook.md).
---
## Architecture
```mermaid
flowchart TB
subgraph Clients
WEB["Web App<br/>Next.js 15"]
CC["Claude Code / Desktop<br/>via MCP"]
end
subgraph Gateway["Entry point — the only policy enforcement point"]
API["core-api<br/>aggregation + proxy"]
OPA{{"OPA<br/>Rego / ABAC"}}
OAUTH["oauth-server<br/>OAuth 2.1 + JWKS"]
end
subgraph Services["Application services (FastAPI)"]
PROMPT["prompt-library"]
TPL["template-library"]
AGENT["agent-orchestrator"]
MCP["mcp-server<br/>tool registry"]
DRAFT["draft-service"]
LIB["library-service"]
INGEST["ingestion-worker"]
DOCGEN["doc-generation"]
end
subgraph Data
PG[("Postgres<br/>db per service")]
QD[("Qdrant<br/>vectors")]
SP[("mock-sharepoint")]
end
CLAUDE(["Anthropic API"])
WEB --> API
CC --> MCP
CC -.auth.-> OAUTH
MCP -.verify.-> OAUTH
API <--> OPA
MCP <--> OPA
API --> PROMPT & TPL & DRAFT & LIB
PROMPT --> AGENT
AGENT --> CLAUDE
AGENT <--> MCP
AGENT --> DRAFT
MCP --> LIB
LIB --> PG & QD
SP --> INGEST --> QD
DRAFT --> DOCGEN --> SP
PROMPT & TPL & DRAFT --> PG
```
**The request path in one paragraph.** A user request lands on **core-api**, which gates
it through **OPA**, then either aggregates from several downstream services or proxies
1:1. Retrieval goes through **library-service** (metadata) plus **Qdrant** (vectors).
Generating a draft runs **prompt-library-service** → **agent-orchestrator** (calling
**Anthropic** plus **mcp-server** for tools) → **draft-service**, which persists it. The
same MCP registry serves Claude Code and Desktop through the **oauth-server**. Documents
enter via **mock-sharepoint**, get parsed, chunked, embedded and indexed by
**ingestion-worker**, and exports travel back out through **doc-generation**.
Full component boundaries and the production-vs-demo mapping:
**[design/hld.md](design/hld.md)**.
### Services and ports
Ports are fixed and meaningful — every backend service exposes `/healthz` and
`/openapi.json`, and `docker compose ps` shows them all `(healthy)` after `make up`.
| URL | Service | Purpose |
|---|---|---|
| [:3000](http://localhost:3000) | **web** | Next.js 15 frontend — the whole UI |
| [:8080](http://localhost:8080) | **core-api** | API gateway — aggregation, proxies, OPA on entry |
| [:8081](http://localhost:8081) | **prompt-library-service** | Prompt catalogue + execution lifecycle |
| [:8082](http://localhost:8082) | **template-library-service** | Master document templates + packs |
| [:8083](http://localhost:8083) | **agent-orchestrator** | Anthropic agent loop for generation |
| [:8084](http://localhost:8084) | **mcp-server** | MCP tool registry — internal REST + JSON-RPC |
| [:8085](http://localhost:8085) | **draft-service** | Drafts, versions, conversation, approval, export |
| [:8086](http://localhost:8086) | **library-service** | Knowledge library: documents, chunks, lineage |
| [:8087](http://localhost:8087) | **ingestion-worker** | Parse → chunk → embed → index |
| [:8088](http://localhost:8088) | **doc-generation** | Render drafts to `.docx` / `.pptx` / `.xlsx` |
| [:8089](http://localhost:8089) | **oauth-server** | OAuth 2.1 authorization server |
| [:8090](http://localhost:8090) | **mock-sharepoint** | Stand-in for the Microsoft Graph API |
| [:8181](http://localhost:8181) | **opa** | Policy sidecar — Rego, enforced at the gateway |
| :5432 | postgres | One database per service |
| [:6333](http://localhost:6333) | qdrant | Vector store (production: Azure AI Search) |
### Tech stack
| Layer | Stack |
|---|---|
| **Backend** | Python 3.12 · FastAPI · Pydantic v2 · SQLAlchemy 2.x async · Alembic · uv · httpx · [anthropic](https://github.com/anthropics/anthropic-sdk-python) SDK · [mcp](https://github.com/modelcontextprotocol/python-sdk) SDK · unstructured.io · python-docx · python-pptx |
| **Frontend** | Next.js 15 (App Router) · TypeScript · Tailwind · shadcn/ui · TanStack Query |
| **Data & policy** | Postgres 16 · Qdrant · `all-MiniLM-L6-v2` embeddings (local, no API call) · Open Policy Agent |
| **Infra** | Docker Compose · Terraform (Azure implemented; AWS/GCP interfaces stubbed) · GitHub Actions |
---
## What the platform does
| Capability | What it means |
|---|---|
| **Knowledge Library** | Curate a subset of a SharePoint estate. Nothing is searchable until someone explicitly adds it — individually, or by subscribing to a folder that stays in sync. |
| **Explore (chatbot)** | Grounded Q&A over the curated corpus, with citations back to source documents. It declines to answer from anything it was not given. |
| **Generate** | Run a parameterised prompt against a selected template to produce a draft, streamed live over SSE. |
| **Skills** | A marketplace of reusable AI capabilities, activatable per user, with Personal / Company / Global sharing layers and enforcement. |
| **Draft Workbench** | Version history, a conversation with Claude about the draft, propose-only or auto-apply edit modes, and a submit → approve → export lifecycle. |
| **Generators & Marketing Hub** | Generators define document *types* (fields, constraints, AI guidance); the Marketing Hub holds the branded template files they render into. *Generators define, the Hub dresses, Generate runs.* |
| **Personal MCP endpoint** | Each user gets an MCP URL serving the KB tools plus their active skills — toggle a skill in the UI and connected Claude clients pick it up. |
| **Audit** | Every action, decision and LLM call is recorded with request-ID correlation, plus per-subscription usage and LLM cost attribution. |
New to the UI? [docs/user-guide.md](docs/user-guide.md) is the screenshot-led walkthrough
and the source of truth for labels and flows.
---
## Connecting Claude Code or Desktop
The MCP server exposes the same tools the in-app agent uses, at
`http://localhost:8084/mcp`:
- **Retrieval** — `search_kb`, `find_similar_past_work`, `get_document`
- **Catalogue** — `list_clients`, `list_capabilities`
- **Draft-side** — `propose_edit`, `check_against_template`, `get_section_sources`
> Seven further CRM-shaped tools (`get_pricing_band`, `get_engagement_details`,
> `get_consultant_profile` and friends) are **deliberate stubs** returning synthetic
> data — they exist to prove the tool-registry and policy path for systems the demo has
> no access to. Each is labelled `STUB:` in its description and docstring, so the model
> and the reader both know. See
> [`apps/mcp-server/tools/stub_tools.py`](apps/mcp-server/tools/stub_tools.py).
> **Prerequisite: the stack is running** (`make demo`), since the tools call downstream
> services. The OAuth consent screen auto-approves in demo mode, so it is a single click.
**Claude Code**
```bash
claude mcp add --transport http -s user kb-platform http://localhost:8084/mcp
# then, inside Claude Code:
/mcp # select kb-platform → Authenticate → approve in the browser tab
```
Drop `-s user` for a this-project-only entry. The OAuth handshake (dynamic registration
→ authorize → token) runs itself; tokens cache for an hour and `/mcp` re-auths in a click.
**Claude Desktop** has no native remote-HTTP MCP config, so use either **Settings →
Connectors → Add custom connector** with URL `http://localhost:8084/mcp`, or the
`mcp-remote` stdio bridge in `claude_desktop_config.json`:
```json
{
"mcpServers": {
"kb-platform": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:8084/mcp"]
}
}
}
```
Config lives at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS),
`%APPDATA%\Claude\claude_desktop_config.json` (Windows), or
`~/.config/Claude/claude_desktop_config.json` (Linux). Restart after editing.
**Hosting it beyond one laptop.** Three settings have to move together, because issuer
and audience are baked into tokens and discovery documents at startup:
- **Issuer** — set `ISSUER` on `oauth-server`, and `OAUTH_ISSUER` +
`MCP_RESOURCE_METADATA_URL` on `mcp-server`, to the deployed hostname.
- **Audience** — set `MCP_RESOURCE_IDENTIFIER` + `OAUTH_AUDIENCE` on `mcp-server` and
`AUDIENCE` on `oauth-server` all to `https://<host>/mcp`, in lockstep. The resource
identifier **must be the MCP server's own URL**: MCP clients follow RFC 9728 and
reject a non-URL identifier before the flow starts.
- **Internal auth** — set a shared `INTERNAL_API_TOKEN` across `core-api`,
`agent-orchestrator` and `mcp-server`. Outside `DEMO_MODE` the audit-ingest endpoint
requires it and fails closed without it.
---
## Documentation
The repo splits documentation by audience.
### 📘 `docs/` — using the platform
| Document | For |
|---|---|
| [user-guide.md](docs/user-guide.md) | End users — screenshot-led walkthrough of every tab, by persona |
| [templates.md](docs/templates.md) | Template authors — the definition-vs-export split: schema that a draft must satisfy, versus the branded file it renders into |
| [iso27001-mapping.md](docs/iso27001-mapping.md) | Auditors, ISMS and compliance — Annex A control mapping with evidence |
| [reference/seed-prompts.md](docs/reference/seed-prompts.md) | The seeded prompts and what each one does |
| [reference/template-structures.md](docs/reference/template-structures.md) | The master templates and their parsed structures |
### 🏗️ `design/` — building and extending it
| Document | For |
|---|---|
| [hld.md](design/hld.md) | Architects — High-Level Design, component boundaries, prod-vs-demo mapping |
| [implementation-plan.md](design/implementation-plan.md) | Engineers — numbered work packages (WP-01 … WP-49) with acceptance criteria |
| [decision-log.md](design/decision-log.md) | Anyone asking "why is it like that?" — append-only, newest first |
| [`wp-*-rfc.md`](design/) | Per-feature RFCs for the larger work packages (marketplace, skills, IaC, cost attribution) |
### 🎬 `demo/` — showing it
| Document | For |
|---|---|
| [demo-script.md](demo/demo-script.md) | The 14-step end-to-end live-demo script |
| [demo-playbook.md](demo/demo-playbook.md) | Audience-tailored talk tracks (Sales, Marketing, CTO, Engineers) |
| [demo-content.md](demo/demo-content.md) | What the seed loader plants in the demo database |
| [demo-mode-design.md](demo/demo-mode-design.md) | Why demo mode exists and what it substitutes |
| [demo-azure-deployment.md](demo/demo-azure-deployment.md) | Operator playbook for a shared Azure playtest deployment |
### ⚙️ `infra/` and `ops/` — running it
| Document | For |
|---|---|
| [infra/README.md](infra/README.md) | The Terraform IaC framework — cloud-portable, environment-aware |
| [ops/playtest/azure-runbook.md](ops/playtest/azure-runbook.md) | Standing up the playtest VM end to end |
Contributor-facing: **[CONTRIBUTING.md](CONTRIBUTING.md)** ·
**[SECURITY.md](SECURITY.md)** · **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)**
---
## Commands
### `make` targets
```bash
make demo # up + seed (idempotent; the everyday command)
make up # build + start everything (no seed)
make down # stop containers (volumes preserved)
make seed # 50 docs, 8 templates, 5 drafts, audit log
make smoke # Playwright suite — the full demo path against the live stack
make logs # tail logs from every service
make migrate # alembic upgrade head per service (rare; up already does this)
make reset # full reset: down -v + up + seed + smoke
make reset-soft # re-seed only (stack stays up — fastest between demos)
make types # regenerate apps/web/lib/api/schema.ts from core-api's OpenAPI
make test # per-service pytest suites (uv required on host)
make web-test # apps/web vitest suite (npm required on host)
make opa-test # rule-level Rego tests (no live stack needed)
make smoke-opa # ABAC three-persona end-to-end against the live OPA sidecar
make help # show the full list
```
### npm scripts
[package.json](package.json) aliases each common make target for contributors who
default to `npm run …`. The Makefile is the source of truth; these shell out to it, so
the **same bash requirement applies** (npm on Windows defaults to cmd.exe — run from Git
Bash).
| npm | make | | npm | make |
|---|---|---|---|---|
| `npm run demo` | `make demo` | | `npm test` | `make test` |
| `npm run dev` | `make up` | | `npm run test:web` | `make web-test` |
| `npm run down` | `make down` | | `npm run test:opa` | `make opa-test` |
| `npm run logs` | `make logs` | | `npm run smoke` | `make smoke` |
| `npm run seed` | `make seed` | | `npm run smoke:opa` | `make smoke-opa` |
| `npm run reset` | `make reset` | | `npm run migrate` | `make migrate` |
| `npm run reset:soft` | `make reset-soft` | | `npm run types` | `make types` |
### Testing
Five layers, each catching what the others cannot:
| Layer | Command | In CI? | Catches |
|---|---|---|---|
| Per-service pytest | `make test` | ✅ all 14 services | Handlers, repositories, schemas |
| Rego unit | `make opa-test` | ✅ | Policy rule logic against fixture inputs |
| Web vitest | `make web-test` | ❌ local only | Component logic, reducers, hooks |
| OPA smoke | `make smoke-opa` | ❌ needs a live stack | Three personas against the live sidecar |
| Playwright smoke | `make smoke` | ❌ needs a live stack | The full demo path — no mocks |
[CI](.github/workflows/ci.yml) runs the Rego tests and **every** service's pytest suite
on each push and PR, hermetically — no Postgres, Qdrant or Docker required. `make test`
runs the same fourteen suites locally, so local green and CI green mean the same thing.
The three stack-dependent layers are run by hand before a release; wiring them into CI
needs a compose-up job and is on the list, not done.
The Playwright suite is the safety net — it seeds first so journeys that consume seeded
state are repeatable, and three of the seven specs skip gracefully without
`ANTHROPIC_API_KEY`.
---
## Configuration
Every service reads config from environment variables via `pydantic-settings`; the
defaults in `docker-compose.demo.yaml` are the demo settings. Docker Compose reads `.env`
at the repo root automatically — copy [.env.example](.env.example), which documents every
option including the LLM proxy modes and the playtest deployment variables.
**Footgun: list-typed env vars need JSON-array syntax.** pydantic-settings parses them as
JSON, so a bare comma-separated list raises `SettingsError` at boot:
```bash
DEMO_USER_GROUPS='["kb-user","kb-prompt-user"]' # ✓
DEMO_USER_GROUPS=kb-user,kb-prompt-user # ✗
```
### Demo mode
Every Python service starts with `DEMO_MODE=true`. This is the substitute for production
auth: it short-circuits Entra SSO and assumes a single demo user holding every RBAC
group, so all panels are visible. **Code that branches on `DEMO_MODE` is deliberate, not
a bug** — production flips it off and real auth takes over.
Deliberately *not* built in the demo:
- Entra SSO
- Front Door / WAF
- Key Vault
- Sentinel
- TLS termination
- Email and Teams notifications — logged, not sent
---
## First-time setup (one-off)
`make up` fails until lockfiles exist — the Python Dockerfiles run `uv sync --frozen` and
the web Dockerfile runs `npm ci`, both of which refuse to build without one. Generate
them once and commit:
```bash
# Python services — one uv.lock per service
for svc in core-api prompt-library-service template-library-service \
agent-orchestrator mcp-server draft-service library-service \
ingestion-worker doc-generation mock-sharepoint oauth-server \
claude-proxy seed-loader; do
(cd apps/$svc && uv lock)
done
# Web
(cd apps/web && npm install)
```
Requires [`uv`](https://docs.astral.sh/uv/) and `npm` on the host. Equivalently:
`make lock`. Once the `uv.lock` files and `apps/web/package-lock.json` are committed,
subsequent clones skip this — locked builds are reproducible.
---
## Troubleshooting
<details>
<summary><b><code>'true' is not recognized…</code> or <code>CreateProcess(NULL, echo., …) failed</code></b></summary>
You are running make from cmd.exe or PowerShell. The Makefile is bash-only by design —
open **Git Bash** and re-run. Same applies to the `npm run …` aliases.
</details>
<details>
<summary><b><code>make up</code> says "no such file or directory: uv.lock"</b></summary>
That is the one-off [first-time setup](#first-time-setup-one-off).
</details>
<details>
<summary><b>Chatbot returns <code>502 llm_error</code></b></summary>
`ANTHROPIC_API_KEY` is not set in the container env. Add it to `.env` and re-run
`make demo` — the recreate picks it up. The service warns at startup:
`ANTHROPIC_API_KEY is not set; /api/chatbot/messages will return 502 llm_error`.
</details>
<details>
<summary><b>A service is restarting</b></summary>
`docker compose logs <service> --tail 80` answers this 95% of the time. Common causes:
`init.sql` missed a new database (`down -v && make up`), a failed Alembic migration, or
an unreachable peer (check the `depends_on` graph in `docker-compose.demo.yaml`).
</details>
<details>
<summary><b>Claude Code / Desktop OAuth flow 404s mid-handshake</b></summary>
The issuer is baked into the token at issuance. If you changed `ISSUER` on
`oauth-server`, update `OAUTH_ISSUER` and `MCP_RESOURCE_METADATA_URL` on `mcp-server` and
recreate both. The client discovers the auth server from `mcp-server`'s 401 challenge, so
a mismatch surfaces as a 404.
</details>
<details>
<summary><b>The web container ignores my source edits</b></summary>
By design — the frontend is baked into the image (`next build`). Rebuild:
`docker compose build web && docker compose up -d --no-deps web`.
</details>
<details>
<summary><b><code>make smoke</code> times out at the chat step</b></summary>
Claude can take 30–60s for a long generation. The specs have generous timeouts; if you
are hitting them, your network path to Anthropic is slow. Live progress streams over SSE
during the wait.
</details>
---
## Going to production
Every demo shortcut is paired with a decision-log entry naming its production analogue
and the file where the swap lands. The big ones, in order of effort:
| Demo | Production | Swap point |
|---|---|---|
| `DEMO_MODE=true` (single seeded user) | Entra OIDC | `shared.auth` middleware |
| Postgres container | Azure Database for PostgreSQL | Per-service `DATABASE_URL` |
| Qdrant | Azure AI Search | `library-service` retrieval interface |
| mock-sharepoint | Microsoft Graph API | `SharePointClient` interface |
| Local blob volumes | Azure Blob | `BlobStore` interface |
| In-memory OAuth keys | Key Vault-backed JWKS | `oauth-server.ISSUER` config |
| Local Docker Compose | Terraform-managed Azure | `infra/modules/`, `infra/envs/` |
Realistically 8–12 weeks of further work, depending on what a security review surfaces.
---
## Contributing
Contributions are welcome — see **[CONTRIBUTING.md](CONTRIBUTING.md)** for the
development workflow, service conventions, and how work is organised into numbered work
packages. Security issues: please follow **[SECURITY.md](SECURITY.md)** rather than
opening a public issue.
## Questions, adoption and contact
I am always happy to support adoption of my projects. If you are evaluating this
architecture, adapting it, or just want to talk through how the governance model works:
- 💬 **[GitHub Discussions](https://github.com/Syntax753/synkb/discussions)** — questions, ideas, show-and-tell
- 🐛 **[Issues](https://github.com/Syntax753/synkb/issues)** — bugs and feature requests
- 💼 **[LinkedIn: peterturnerlondon](https://www.linkedin.com/in/peterturnerlondon)** — the best route for direct questions, consulting enquiries, or a conversation about applying this in your organisation
If this is useful to you, a ⭐ helps other people find it.
## License
[Apache License 2.0](LICENSE) — permissive, with an express patent grant. Use it, fork
it, build on it commercially.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues