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:
- 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.
- 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:
-
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 tohttps://chat.printing.cauda.dev/api/v1/retrieval/process/query(already operator-reachable, already TLS-fronted by Cilium GW). -
Tools exposed. Exactly two, both read-only:
rag_query(question: str, k: int = 6) -> list[chunk]— returns the top-k retrieved chunks (text + source path + score) from thelusoris-k8sknowledge base.-
rag_list_sources() -> list[str]— returns the set of source paths currently indexed, so the agent can decide whether a follow-upReadon disk is worth doing. -
Authentication. Bearer token from the same
OPEN_WEBUI_JWTsealed secret thatcluster-rag-indexeralready 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. -
Authorisation scope. The token is bound to a dedicated Open WebUI user (
mcp-shim) whose group membership grants read access to thelusoris-k8sknowledge base only. No chat history, no uploads, no admin endpoints. -
No cluster API access. The shim does not import
kubernetes, does not shell tokubectl, does not read a kubeconfig. Any "what's running where" question is answered indirectly via whatSTATE.md/ runbooks /.workingdir2/already say in the index. -
Surface lives in
scripts/cluster-mcp/. Singleserver.py+README.md+pyproject.toml(uv-managed). Noargocd-apps/entry, no Helm chart, no ConfigMap. The README documents the Zed and Claude Code wiring snippets and the env-var contract. -
Update README + state.
apps/ai/open-webui/README.md"Open follow-ups" gets the MCP shim row marked shipped with a link toscripts/cluster-mcp/.STATE.mdgets 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
Deploymentin theainamespace, callable from open-webui's pod, withget,list, watchon 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
curlthe 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.localbehind 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-indexerrun. Agents asking about brand-new files on a feature branch still need to fall back toRead. Mitigated byrag_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.pyis 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 sameOPEN_WEBUI_JWTsecret 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'srag_list_sourcesmirrors 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.