MCP server¶
Requires the [mcp] extra (see the README's
installation section).
Start the TUI with korvid --mcp (or set mcp: {enabled: true} in
~/.config/korvid/config.yaml), then configure your external MCP host to run
korvid mcp stdio. The adapter connects to the already-running TUI's
authenticated loopback endpoint; it does not start another TUI, Kubernetes
client, embedded agent, or provider. The internal
Streamable HTTP MCP endpoint binds to
127.0.0.1:7878 (mcp: {port: N} to change).
Read and UI-drive tools are exposed; write tools are not: cluster mutations stay behind the in-TUI confirmation dialog. An opt-in proposal flow lets external agents queue writes for your review (see Propose a write) — even then, a proposal never executes without a fresh user keystroke in the TUI.
Evidence crosses a tool boundary¶
MCP tool calls never go through korvid's embedded-provider boundary. No
korvid-managed model call is involved, so OutboundPolicy — the sanitized
payload, the request caps, the text credential-pattern masking the embedded
agent applies — does not run on this
surface at all. Disclosure is tool-specific, and what an MCP client
receives is not necessarily the same snapshot a Direct keybinding or the
embedded Agent would show for the same object:
- Structured manifests (
get_resource) are recursively redacted where they are produced, by the samekorvid.core.redactionprimitive the embedded agent's boundary uses:Secretdata, thekubectl.kubernetes.io/last-applied-configurationannotation, and credential-named keys/values are masked structurally — by where they sit in the parsed document, not by how they look — before the document is bounded and returned. An MCP client sees the same redacted document the model would. - Compound workload diagnoses (
diagnose_workload) are a shaped text report rather than a parsed document, so the structural pass above never runs over them. What they do get is producer-side credential-pattern text redaction: korvid masks credential-shaped text in every shaped section of the report, and in each embedded pod diagnosis, before the report is compacted to its size cap — a cut taken first would split a credential across the boundary and leave the tail unclassifiable. Pattern matching is weaker than the structural guarantee above: a secret no pattern recognises is not masked here. - Service and PVC binding diagnoses (
diagnose_service,diagnose_pvc) are deterministic structured YAML: EndpointSlice or secondary-read RBAC denials become explicitgaps[]entries rather than an error, so the caller can reason about incomplete evidence instead of a hard failure. - Logs and events get producer-side credential-pattern text redaction after their tool-specific shaping and before the result size cap. A credential printed into a pod log or event message is masked before it reaches the client.
- Lists, single-pod diagnoses, and helm status get only their own tool-specific shaping and size caps. They are not credential-pattern masked.
The external client (and whatever model or data policy it applies) owns its own AI data boundary past this producer-side redaction; korvid's guarantee stops at what is exposed, how each tool shapes it, and that writes still require your keystroke — not what the connected client does with the data. See the threat model for the full boundary.
flowchart LR
CLIENT["External MCP client"]
READ["Bounded Kubernetes read tools"]
OBS_READ["Bounded observability tools<br/>activity note only"]
FOLLOW["Optional follow<br/>UI navigation only"]
PROPOSE["Opt-in write proposal"]
CONFIRM["In-TUI human confirmation"]
AUDIT["Fail-closed audit"]
KUBE[("Kubernetes API")]
OBS[("Prometheus / Loki")]
CLIENT --> READ --> KUBE
CLIENT --> OBS_READ --> OBS
READ -. successful read .-> FOLLOW
CLIENT -. disabled by default .-> PROPOSE --> CONFIRM --> AUDIT --> KUBE
style CONFIRM fill:#f5a623,color:#12151a,stroke:#ffcf6e
style AUDIT fill:#1b1f26,color:#e7e9ee,stroke:#f5a623
Follow never changes a tool result, and a proposal never becomes a mutation
until the TUI receives a fresh user keystroke and the audit append succeeds.
Prometheus and Loki queries never reach the Kubernetes API through this
surface — they route to their own backends and never touch KUBE above.
The live endpoint is also published to
$XDG_STATE_HOME/korvid/mcp-endpoint.json (defaulting to
~/.local/state/korvid/mcp-endpoint.json when XDG_STATE_HOME is unset)
while korvid runs. This private registry is keyed by process ID and contains
the internal credential. Do not copy it into MCP configuration, prompts,
logs, or issue reports. The stdio adapter reads it automatically.
Connect a client¶
| Host | Configuration |
|---|---|
| VS Code | .vscode/mcp.json: {"servers": {"korvid": {"type": "stdio", "command": "korvid", "args": ["mcp", "stdio"]}}} |
| Claude Code | claude mcp add --transport stdio korvid -- korvid mcp stdio |
| Cursor | .cursor/mcp.json: {"mcpServers": {"korvid": {"command": "korvid", "args": ["mcp", "stdio"]}}} |
| Zed | settings.json: {"context_servers": {"korvid": {"command": "korvid", "args": ["mcp", "stdio"]}}} |
The host must find the installed korvid executable on its PATH; use an
absolute executable path if a graphical editor has a different PATH.
Run the host as the same user as the TUI, with the same XDG_STATE_HOME.
One live instance is selected automatically. With multiple instances, append
"--instance", "12345" to the arguments (or run
korvid mcp stdio --instance 12345). Each TUI needs its own configured
mcp.port (for example, 7878 and 7879). No instance is selected
arbitrarily, and a missing selected PID never falls back to another instance.
Start the TUI and enable MCP before connecting. Missing, stale, invalid, or
unsafe registry data produces an actionable error on stderr, never protocol
noise on stdout. Restart the host's MCP connection after a TUI restart,
:mcp off/:mcp on, or a kube-context switch. Context switching deliberately
stops the old MCP run before changing clients and starts a fresh authenticated
run afterward: an existing stdio connection fails closed instead of silently
following it into another cluster. Reconnecting discovers the new endpoint
and credential. OAuth, remote access, and a
headless Kubernetes backend are not part of this local stdio flow.
Local transport authentication¶
Each server run generates a high-entropy capability token. The stdio adapter
reads it from the private registry and sends it only in the internal HTTP
Authorization: Bearer header. All MCP reads, UI actions, and opt-in proposals
require authentication before request parsing or dispatch. Tool arguments
never carry credentials; the legacy capability argument is rejected.
Do not paste a token into client configuration or ask a model to retrieve it.
The registry is atomically created with POSIX owner-only mode 0600;
macOS also sets an explicit non-inheriting ACL at creation.
On Windows it uses a protected DACL for the current user, SYSTEM, and
Administrators instead of relying on a mode argument. The adapter validates
file ownership and permissions, rejects symlinks, bounds registry reads,
accepts only the registered loopback URL, bypasses environment HTTP proxies,
and refuses redirects. Failed private publication prevents MCP startup.
This separates other unprivileged local accounts, not hostile processes running as the same user or an administrator. They remain in the local trust domain. Host/Origin checks remain in place; authentication does not replace them or the TUI approval gate. See the threat model.
Existing direct HTTP integrations: unauthenticated requests now receive 401, including read/UI calls. Migrate host configurations to stdio above. Deliberate low-level HTTP integrations must read the private registry and authenticate in headers; no token-in-URL or token-in-tool fallback exists. This is an internal local transport, not an OAuth-enabled remote service.
Read once or follow activity¶
External hosts overwhelmingly call the read tools, which by themselves
move nothing on screen. Follow mode mirrors those reads in the TUI so you
can watch the assistant work — off by default, since screen hijacking
mid-task is worse than invisibility. Enable at startup with
mcp: {follow: true}, or live with :mcp follow on|off (bare :mcp follow
toggles; bare :mcp reports both server and follow state).
With follow on, a successful Kubernetes read navigates the TUI:
list_resources selects that view, get_resource / get_events open the
describe pane, get_logs opens the log pane, and the diagnose_* tools open
the relevant describe pane too. The mirror is fire-and-forget UI navigation,
not a resumable feed; if it cannot land, korvid falls back to an activity
note.
With follow off, Kubernetes reads surface only as activity notes. Prometheus and Loki reads always use an activity note instead of navigating anywhere because no korvid screen can display them. An activity note does not make the read followable; it only makes the external read visible.
Mirroring never waits on, or fails with, the MCP response; a failed read is
never mirrored, and a mirror never opens over an approval dialog — approvals
are confirmed only by your keystrokes. With follow off, each external
read still surfaces as a transient toast (client-name: get_logs api-1 (ns
prod)), so nothing reads your cluster invisibly.
The landing clip of this flow is recorded from this repository alone: a real MCP SDK client making read-only requests over Streamable HTTP to a loopback korvid serving a synthetic cluster, with follow mirroring each answer.
Propose a write¶
Off by default. With
the MCP surface adds three opt-in tools — propose_write,
get_write_proposal, cancel_write_proposal — that let an external agent
propose a cluster write (delete, scale, rollout restart, in-place pod
resize). A proposal never mutates anything by itself:
propose_writeruns the same validation as the built-in agent write path (kind resolution, read-only mode, RBAC pre-check, target-UID capture, dry-run preview) and queues an immutable proposal; the caller pollsget_write_proposalfor the terminal outcome.- The TUI shows a persistent
⚑status-bar indicator naming the caller and target. Nothing auto-opens and focus is never stolen. :proposalsreviews pending proposals one at a time in the same confirmation dialog as every other write — including the typed-name and typed-context gates. Only your keystroke — the same fresh user keystroke every write path requires — can approve; MCP tools cannot focus, type, or confirm.- Before executing, korvid rechecks the kube context epoch, RBAC, and the
captured target UID, then writes through the same fail-closed
audit-before-mutation path with
source=external_mcpand the caller metadata recorded. - Deny, dismiss, TTL expiry (10 minutes), and caller cancel are distinct outcomes. A context switch, an MCP server restart, or TUI shutdown invalidates pending proposals.
Local callers are untrusted: authentication alone does not enable write
proposals. The TUI must opt in with write_proposals: true; read-only mode
and all approval, context, UID, and audit checks still apply. Proposals use
the same authenticated transport as reads, never a second credential in
tool arguments. MCP clientInfo is caller-supplied display metadata, never
treated as authenticated identity.
Representative tools¶
Not a full schema inventory — a compact map of what each family does and how it is disclosed:
| Family | Examples | Backend | Disclosure / follow |
|---|---|---|---|
| Recursively redacted manifests | get_resource |
Kubernetes API | Producer-side recursive document redaction (Secret data, last-applied annotation, credential-named keys); may emit a follow activity note |
| Pattern-masked compound diagnoses | diagnose_workload |
Kubernetes API | Producer-side credential-pattern text redaction of each shaped section, applied before compaction — not a recursive document pass; may emit a follow activity note |
| Pattern-masked log and event reads | get_logs, get_events |
Kubernetes API | Producer-side credential-pattern text redaction after shaping and before the result cap; may emit a follow activity note |
| Shaped Kubernetes reads | list_resources, diagnose_pod/_service/_pvc, helm_list_releases |
Kubernetes API | Tool-specific shaping and size caps only — not credential-pattern masked; may emit a follow activity note |
| Observability reads | Prometheus / Loki query tools | Prometheus / Loki | Activity note only — never followable navigation |
| Write proposals (opt-in) | propose_write, get_write_proposal, cancel_write_proposal |
Kubernetes API, gated | Inert until a fresh user keystroke in the TUI |