RBAC-enforced MCP documentation RAG, end to end: Python MCP server with 4-layer security over live Confluence + Next.js console to experience it in the browser. Live demo, $0/month.
A complete, production-deployed Model Context Protocol system built around one idea: the same question must yield different answers depending on who is asking — and nothing else must ever leak. The umbrella repo combines two submodules. The first is a Python MCP server exposing a retrieval-augmented knowledge base built from a live Atlassian Confluence instance, implementing a zero-trust four-layer security model: bearer-token authentication, ACL filters pushed inside the ChromaDB vector query, a LangGraph context-verification node, and a post-generation leak scanner — all gated by an automated 8-scenario evaluation suite targeting 0.00% leakage. The bearer presented to the server can be either a pre-shared role token (for anonymous demos) or an OpenID Connect access token from a self-hosted identity provider, verified by the server against the provider's JWKS and mapped to a role by its claims — so the same RBAC model is driven by static demo credentials and by real federated identity interchangeably. The second is a server-first Next.js 16 console that makes the access-control story tangible in a browser: a live health and Prometheus metrics dashboard, and an RBAC playground that runs one question through two genuine MCP sessions holding different bearer tokens, in two modes: a retrieval mode that shows side by side which document chunks each authorization level may retrieve, and an answer mode that runs the server-side LangGraph agent and contrasts the grounded answer each role receives. A third submodule is a Tauri desktop spotlight written in Rust: a global hotkey summons a frameless command bar that calls the server's agent through the official Rust MCP SDK and returns a grounded answer with links straight to the source Confluence pages, with the bearer token held in the trusted Rust process and never exposed to the webview; a released build signs in directly against the self-hosted OIDC identity provider over an Authorization-Code + PKCE deep-link round trip (receiving access and refresh tokens whose role is fixed by the user's account, with no role picker), while a development build falls back to the console's hardcoded-persona demo sign-in — and the distributed binary holds no secret either way. The desktop app is downloadable for macOS, Windows, and Linux and keeps itself up to date through a signed in-app updater that pulls new releases from GitHub and verifies their signatures before installing. The whole system runs 24/7 on Hugging Face Spaces and Vercel at $0/month infrastructure cost.
Sub-second retrieval with CPU-local all-MiniLM-L6-v2 embeddings and sub-10ms embedded ChromaDB queries; the console streams results over Partial Prerendering so the static shell paints instantly while both MCP role sessions resolve in parallel.
Both console routes build as Partial Prerender (static HTML shell, server-streamed dynamic content) with the React Compiler owning all memoization; total client-side JavaScript beyond the framework is a single useFormStatus submit button, and the RBAC playground is fully functional with JavaScript disabled.
$0/month for a 24/7 public system: Hugging Face Spaces (CPU Basic) for the MCP server, Vercel free tier for the console, local embedding computation, Gemini API free tier with tiered model fallback, and Upstash free tier for rate limiting.
Designed a secure Four-Layer Enforcement Security Model (bearer token validation -> ACL query filter pushdown -> LangGraph context verifier node -> post-generation register leak scanner) ensuring complete authorization separation where a compromised retriever or a prompt injection embedded in the corpus cannot leak unauthorized content.
Implemented ACL Pushdown in ChromaDB using metadata filters evaluated natively inside the vector query, preventing unauthorized document chunks from ever entering the retrieval candidate set regardless of calling-code behavior.
Architected a Structure-Preserving Layout Chunker that parses Confluence XHTML into clean Markdown while keeping scientific tables atomic and carrying active heading context down through all generated sub-chunks.
Developed a Three-Phase LangGraph State Graph (router -> retrieve -> integrate -> verify -> generate/refuse), decoupling context verification from generation and rendering the safety pipeline as an inspectable execution graph.
Exhaustive situation-action-result breakdowns showcasing problem-solving and architectural execution.
Traditional RAG pipelines trust the vector database output, leaving them vulnerable to buggy retriever logic or prompt injections hidden inside the corpus (a permanent 'SYSTEM OVERRIDE: ignore all previous instructions...' fixture lives in the live Confluence space to prove the point).
Implemented a strict four-layer security boundary: ACL filters pushed directly into the Chroma query, a dedicated LangGraph verify node auditing chunk-level permissions after retrieval, injection-hardened prompting, and a post-generation regex leak scanner checking register addresses in the LLM output against the authorized context.
Low-level component relationships, system boundaries, and runtime flows.
The Python server syncs Atlassian Confluence page version histories, parses XHTML into clean Markdown, and indexes chunks into ChromaDB with role-permission metadata. Any MCP client — Claude Desktop, Claude Code, the Next.js console, or the Rust desktop spotlight — connects over streamable HTTP with a bearer token that ASGI middleware maps to a role context; that bearer is either a pre-shared role token or an OpenID Connect access token verified against the identity provider's JWKS and mapped by its roles claim, after which role filters are pushed inside the vector query, a LangGraph verify node audits the retrieved context, an injection-hardened prompt drives the tiered Gemini fallback group, and a leak scanner checks the final output, with every decision recorded in Prometheus metrics. The console is the system's public window: server components fetch /health and parse the Prometheus exposition into semantic cards, while the RBAC playground — a GET form whose URL is the state — fans one query out to both role sessions in parallel through the official MCP TypeScript SDK — comparing either the raw chunks each role retrieves or, in answer mode, the grounded answer the server-side agent returns to each — renders the two result panes side by side with restricted content highlighted, and is itself protected by per-IP and global rate limits so the demo cannot be weaponized against the free-tier infrastructure it showcases. The desktop spotlight closes the loop: in production it authenticates the user directly against the self-hosted OIDC provider over a PKCE deep-link round trip handled entirely by the Rust process, then drives the same RBAC-governed agent with the resulting access token.
Layered, zero-trust security boundaries are the only way to build safe RAG agents — a single vector-store filter is insufficient once prompt injections live inside the corpus. The same philosophy applies one level up: the web console that demonstrates the security story must itself be designed as part of the attack surface, which is why it ended up with no JSON API, server-only credentials, and fail-closed rate limiting rather than being treated as 'just a frontend'. And federating identity late taught a third lesson — accepting both a static demo token and a verified OIDC access token through one middleware seam kept real sign-in and account-free demos alive at the same time, instead of forcing a choice between them.
Rendered live in real-time. Direct URL: https://confluence-bot.hy13dev.com
Exposed two MCP deployment patterns from one server under identical RBAC: capable clients (Claude Desktop, Claude Code) call the raw retrieval tools and reason for themselves, while thin or untrusted clients call a single ask tool that runs the full LangGraph agent server-side — so the context-verifier node and post-generation leak scanner (layers 3-4) are enforced regardless of how much the caller can be trusted.
Built a dual-role RBAC playground as a server-first Next.js 16 app: one query fans out to two real MCP sessions via the official TypeScript SDK over streamable HTTP, in two modes — a retrieval mode that highlights the chunks withheld from the lower-privileged role, and an answer mode that runs the server-side LangGraph agent and contrasts each role's grounded answer — and the entire flow is a GET form driven by URL search params that works with JavaScript disabled, the only client component being a pending-state submit button.
Eliminated the console's JSON API surface entirely: bearer tokens are server-only, the browser receives rendered HTML and RSC payloads, and the single path to the MCP server is a rate-limited server render (per-IP sliding window plus a global daily budget, fail-closed in production).
Federated the MCP server's bearer authentication to a self-hosted OpenID Connect identity provider without losing the demo path: the ASGI middleware first checks a pre-shared token registry (anonymous demos), then verifies the bearer as an OIDC access token against the provider's JWKS (issuer, audience and RS256 signature) and maps its roles claim onto the same role context — failing closed when no recognized role is present — so static demo credentials and real federated identity drive one RBAC model interchangeably and the feature is dark unless an issuer and audience are configured.
Built a Tauri desktop spotlight in Rust that drives the same RBAC-governed server from a third language: a global-hotkey, frameless, always-on-top command bar calls the agent through the official rmcp SDK over streamable HTTP and renders the grounded answer with links to the source Confluence pages, while the bearer token stays inside the Rust process and the webview only ever receives rendered text — the desktop analog of the console's server-only tokens and the server's own STDIO_ROLE model, where the client never sends its own role; a released build signs in directly against the OIDC provider over an Authorization-Code + PKCE deep-link round trip (redeemed by the Rust process, never the webview) and renews its access token in place via the refresh token, while a development build uses the console's hardcoded-persona demo sign-in, so the shipped binary holds no secret either way; the app is distributed as unsigned macOS, Windows, and Linux installers built by a GitHub Actions tauri-action matrix, with the macOS-only NSPanel overlay cfg-gated behind a portable always-on-top-window fallback so one codebase builds on all three OSes; and installed copies keep themselves current with a Rust-driven self-updater (tauri-plugin-updater) that reads the GitHub releases feed and refuses to install any downloaded build whose signature does not verify against a public key baked into the app, with the private signing key held only in CI.
Created a zero-dependency Prometheus pipeline on both ends: a custom Python metrics exporter exposing tool-call rates, latency, sync state, and layer-specific RBAC denials, plus a dependency-free TypeScript exposition parser rendering them as semantic dashboard cards.
Engineered a Two-Speed CI/CD Pipeline in GitHub Actions running static analysis and offline evaluation gates in seconds per push, combined with nightly jobs executing full semantic evaluations, adversarial probe suites, Trivy scans, and self-healing sync against the deployed Space.
Achieved a 0.00% RBAC violation/leakage rate with zero adversarial probes leaked, verified continuously by the gated evaluation suite — the live LLM quotes the embedded injection as data and refuses to obey it.
Exposing a public, anonymous web playground over the MCP server meant handing real bearer-token sessions to unknown visitors — risking token exposure, quota-draining abuse of the free-tier Space, and a needlessly large attack surface from conventional JSON API routes.
Rebuilt the console server-first: tokens guarded by server-only imports, all three API routes deleted in favor of server actions and a next/form GET flow so the browser only ever receives rendered HTML, and Upstash rate limiting keyed on platform-set IP headers (unspoofable, unlike cookie-based identity) with a per-IP sliding window plus a global daily budget that fails closed in production.
Anonymous visitors can run real dual-role MCP comparisons against the live index while tokens never reach the client, the only path to the upstream server is a rate-limited server render, and the playground degrades gracefully (including working with JavaScript disabled).
The demo needed real federated sign-in for the desktop app — production users authenticating as themselves and receiving a role from their account — without breaking the anonymous, account-free demos the public playground and recruiters rely on.
Stood up a self-hosted OpenID Connect provider and made the desktop app sign in directly against it over Authorization-Code + PKCE (public client, no secret in the binary), receiving access and refresh tokens; taught the MCP server to accept either a pre-shared demo token or an OIDC access token, verifying the latter against the provider's JWKS and mapping its roles claim onto the existing role context, failing closed and staying dark unless an issuer and audience are configured.
One RBAC model is now driven interchangeably by static demo credentials and by real federated identity; production Spotlight users get a role fixed by their account with no role picker and silent token refresh, while anonymous demos keep working unchanged.
Standard HTML-to-Markdown libraries split or corrupt complex data tables, destroying the semantic meaning of diagnostic register parameters when chunked across sliding windows.
Built a custom ConfluenceSanitizationEngine that extracts XHTML tables, collapses inner newlines, normalizes row-column counts, and pre-renders Markdown tables wrapped in blank-line boundaries so the chunker treats them atomically.
Passed 100% of table-parsing integrity checks in the evaluation suite, preserving structure and data accuracy across all document pages.
Free-tier Gemini endpoints frequently rate-limit or exhaust quota under heavy evaluation runs, causing generation to drop and CI to fail; the free-tier Space additionally sleeps when idle, so a visitor's first request often hits a cold upstream.
Engineered a tiered LLM fallback hierarchy (prioritized Gemini models, Ollama for offline execution, a deterministic stub for CI gating) and made the console treat the sleeping Space as an explicit 'waking up' state with independent per-role failure handling rather than an error page.
Maintained uninterrupted RAG generation across CI and production under rate-limit constraints, and the first click after a quiet week shows a friendly wake-up state instead of a failure.