Skip to content

0018. cluster-mcp shim — read-only Open WebUI RAG surface for Zed/Claude

  • Status: proposed
  • Date: 2026-05-29
  • Deciders: lusoris
  • Related: Issue #665, PR (this ADR)

Context

The cluster-rag-indexer CronJob (ADR-adjacent, shipped via argocd-apps/ai/cluster-rag-indexer.yaml + scripts/cluster-rag-indexer/) periodically scrapes high-signal lusoris/k8s documents — STATE.md, ADRs, runbooks, the AGENTS.md tree, .workingdir2/ planning dossiers, CLAUDE.md and PRINCIPLES.md — into an Open WebUI knowledge base named lusoris-k8s. Open WebUI exposes that corpus to logged-in browser users via its retrieval endpoint at /api/v1/retrieval/process/query, which performs embedding lookup + re-rank and returns the matching chunks.

Per the operator's direction in issue #665, Zed and Claude Code agent sessions should be able to query the same corpus without scraping the files from disk on every session start. That solves two recurring problems:

  1. Session-time context is finite. When an agent has to read STATE.md tail, the bug index, three runbooks, and the relevant ADR before it can answer a question, a non-trivial fraction of the context window is spent re-discovering state that the RAG index already has in embedded form.
  2. The local filesystem view drifts. The RAG index is rebuilt on a schedule against main; an agent reading from a feature branch's working tree may answer from a stale or in-flight state. Routing queries through the index gives a consistent server-side view.

There is no in-tree component yet that bridges Zed/Claude (MCP clients) to the Open WebUI retrieval API. Issue #665 tracks the follow-up that previously sat in apps/ai/open-webui/README.md:60; it was surfaced during the 2026-05-29 TODO sweep (PR #667), but no implementation has shipped.

Out of scope for this ADR: cluster mutation, kubectl access, helm access, or any path that would let an agent change cluster state from a chat session. Cluster mutations remain GitOps-only per CLAUDE.md and PRINCIPLES.md.

Decision

Ship scripts/cluster-mcp/ as a local-process MCP server that the operator launches alongside Zed or Claude Code, configured via each agent's standard MCP server registry. The shim exposes a small, fixed tool surface and proxies retrieval calls to the in-cluster Open WebUI:

  1. Transport. stdio MCP server (Python, single file, no external runtime deps beyond mcp + requests). No in-cluster Deployment, no Service, no HTTPRoute. The shim runs on the operator workstation; the only network call it makes is HTTPS to https://chat.printing.cauda.dev/api/v1/retrieval/process/query (already operator-reachable, already TLS-fronted by Cilium GW).

  2. Tools exposed. Exactly two, both read-only:

  3. rag_query(question: str, k: int = 6) -> list[chunk] — returns the top-k retrieved chunks (text + source path + score) from the lusoris-k8s knowledge base.
  4. rag_list_sources() -> list[str] — returns the set of source paths currently indexed, so the agent can decide whether a follow-up Read on disk is worth doing.

  5. Authentication. Bearer token from the same OPEN_WEBUI_JWT sealed secret that cluster-rag-indexer already uses for the push side. The operator exports it into the shim's process environment; it does not land in Zed/Claude config files. No new credential is minted for this surface.

  6. Authorisation scope. The token is bound to a dedicated Open WebUI user (mcp-shim) whose group membership grants read access to the lusoris-k8s knowledge base only. No chat history, no uploads, no admin endpoints.

  7. No cluster API access. The shim does not import kubernetes, does not shell to kubectl, does not read a kubeconfig. Any "what's running where" question is answered indirectly via what STATE.md / runbooks / .workingdir2/ already say in the index.

  8. Surface lives in scripts/cluster-mcp/. Single server.py + README.md + pyproject.toml (uv-managed). No argocd-apps/ entry, no Helm chart, no ConfigMap. The README documents the Zed and Claude Code wiring snippets and the env-var contract.

  9. Update README + state. apps/ai/open-webui/README.md "Open follow-ups" gets the MCP shim row marked shipped with a link to scripts/cluster-mcp/. STATE.md gets a one-line entry.

Alternatives considered

  • In-cluster MCP server with kubectl read-only RBAC. The operator brief that requested this ADR floated this shape: a Deployment in the ai namespace, callable from open-webui's pod, with get,list, watch on Pods/Deployments/Services/Events in a fixed namespace allow-list. Rejected as a misread of the actual request — issue #665 asks for Zed/Claude → Open WebUI RAG, not Open WebUI → kube-api. The kube-api direction is also strictly more dangerous (a chat-room prompt-injection that asks the LLM to "describe everything in namespace X" now hits the apiserver), buys no win the rag-indexer cannot already deliver via documents, and would require a NetworkPolicy, RBAC, and audit story that this ADR's scope explicitly avoids.

  • Direct kubectl / kubectl-ai in the open-webui pod. Rejected for the same reason plus: it bypasses GitOps, it would grant a chat surface a service-account token, and there is no audit trail that ties a chat message to a kube-api call.

  • Broad RBAC on open-webui's ServiceAccount. Rejected outright. The open-webui SA is for talking to Postgres, Ollama, and the Longhorn PVC; it has no business holding any kube-api permission.

  • Skip the shim; have agents curl the retrieval endpoint directly. Workable for a human, but every agent session would need to re-discover the endpoint shape, the auth header, and the response format. A typed MCP tool surface is the point — it's the same argument as "why MCP instead of REST" for every other MCP server.

  • In-cluster MCP server proxying RAG (instead of local). Same RAG scope as the chosen decision, but exposed at mcp.ai.svc.cluster.local behind Authentik. Deferred: the local-stdio shape ships in one PR with no Cilium GW listener, no HTTPRoute, no Authentik provider, no NetworkPolicy. If a non-workstation MCP client (e.g. a remote agent runner) ever needs the same surface, promote the shim to a Deployment in a follow-up ADR; the tool contract defined here stays unchanged.

Consequences

Positive:

  • Closes #665 with a concrete contract a future implementer can land in one PR — single file under scripts/cluster-mcp/, no cluster manifests, no new sealed secrets.
  • Zed/Claude sessions save context by querying the embedded corpus instead of re-reading the same dossiers each session.
  • No new attack surface inside the cluster: the shim is a local process, the cluster side is the existing Open WebUI retrieval endpoint already exposed at chat.printing.cauda.dev.
  • Reaffirms the GitOps-only mutation invariant — this ADR explicitly rules the kube-api direction out, so the next time someone asks "can we just let the chat run kubectl?" the answer is linkable.

Negative / costs:

  • The corpus is only as fresh as the last cluster-rag-indexer run. Agents asking about brand-new files on a feature branch still need to fall back to Read. Mitigated by rag_list_sources() so the agent can see what's indexed and route around gaps.
  • The shim has to be relaunched per workstation. Mitigated by the single-file shape — uv run scripts/cluster-mcp/server.py is the whole story.
  • If the operator later wants the surface remotely (away from a LAN workstation), this ADR will need a successor that promotes the shim to an in-cluster Deployment with Authentik in front.

References

  • Issue #665 — "open-webui: MCP shim for Zed/Claude knowledge-base querying" — verbatim acceptance criteria:

    MCP server exposed locally that proxies Open WebUI's /api/v1/retrieval/process/query (or successor) endpoint. Configurable auth via the same OPEN_WEBUI_JWT secret cluster-rag-indexer uses. README updated to document Zed/Claude wiring.

  • apps/ai/open-webui/README.md — "Open follow-ups" section that previously held the TODO replaced by #665.
  • argocd-apps/ai/cluster-rag-indexer.yaml — push side; this ADR's shim is the pull side of the same corpus.
  • apps/ai/cluster-rag-indexer/files/index.py — document-set definition; the MCP shim's rag_list_sources mirrors this.
  • PR #667 — chore: link or resolve stale TODOs across repo — surfaced the TODO that became #665.
  • PRINCIPLES.md — GitOps-only mutation invariant that the kube-api alternative would violate.