Skip to main content
The MCP (Model Context Protocol) ecosystem already has a lot of ready-made capability: GitHub, databases, browsers, all kinds of SaaS APIs. Kova can connect to them and let the agent use the tools those servers provide. This page covers how connections work, how to write the configuration, and the tradeoffs you should know about.

Why MCP tools are not registered directly

The industry has two approaches. Kova takes the second one, and the reason is prompt caching.

Full registration (not used)

Register every MCP tool as its own tool for the model. Best ergonomics, but a single server can bring 10k+ tokens of schema, and adding or removing a server rewrites the tool table.

Proxy gateway (what Kova does)

Register one gateway tool of roughly 200 tokens. The model calls search to discover, then call to invoke. Metadata caching keeps search working even while disconnected.
The reason is firm: Kova’s system prompt and tool schemas stay byte-identical across sessions, which is what lets OpenAI prefix caching and Anthropic tool blocks hit. MCP servers are runtime configuration — dynamic registration would destroy that guarantee. So the model sees one tool called mcp with four actions: The internal name format is mcp__<server>__<tool>. search returns those names; describe and call accept them. The convention is search first, then call. This is stated in the system prompt and the model follows it.

Configuration: three layers, merged

MCP server config lives in three layers, lowest to highest: Merged by server id — a higher layer replaces the whole entry for a matching id. Same id with a different transport counts as a different definition.
The workspace standard layer uses .mcp.json (the ecosystem format) but only honours command / args / env / url / headers / type. Kova-specific fields placed there are ignored — put them in .kova/mcp.json to take effect.
Enable/disable toggles never go into files. They live in SQLite (mcp.disabled.<layer>.<id>), because a personal switch has no business in a file you commit and share. Same approach as subagents.
Servers that come from the workspace are disabled by default (system and plugin layers stay enabled). A server’s command is spawned on its first use, before any approval — so “the repo ships a .mcp.json” must not mean “this machine starts that process on its own”. Enable it once in Settings → MCP to run it; that record lives in the local SQLite and is the only authorization state this machine has.

Writing a config

There is no settings layer and no global approval switch: approveTools is a per-server field, and the output guard is always on. Enabling and disabling happens in Settings only (stored in the local SQLite) — a disabled key in the file is not read, and neither is anything else not listed above.
Changing the URL drops the auth fields. Suspected credential entries in headers / env are bound to the URL — so that sharing a config cannot point the server at someone else’s address while carrying your credentials along.

Limits

After merging: ≤ 32 servers across system + workspace, ≤ 64 tools per server, ≤ 16 concurrent connections. Validation is tight: ids allow only letters, digits, underscore and hyphen; stdio and http fields are mutually exclusive; command may not contain ..; urls must be absolute.

Approval

Every MCP call goes through the existing per-tool approval flow — the same one used for writing files and running bash. No separate mechanism is invented. When approval is denied, the model receives a blocked tool result, consistent with every other denied tool. Two routes skip approval — both must be decisions made on your own machine; a file that travels with the repository cannot authorize anything:
  • The server’s approveTools glob matches the tool name and the declaration comes from the system layer (~/.kova/mcp.json). An approveTools written in a workspace layer (.mcp.json / .kova/mcp.json) has no effect — it only shows up on the approval card as “this project requests X to skip approval”. Enabling the server does not change that: enabling means “I am willing to run it”, not “I agree it never asks”.
  • The machine-local list allowMcpTools: “remember this tool” on the approval card writes into <workspace>/.kova/permissions.local.json, matching the full tool name exactly (remembering one does not open up the rest of that server).
search / describe / status never trigger approval — they only read the local cache and connection state.

Output guard

MCP servers can return a lot. Kova’s approach matches read and grep: bounded, but resumable. Nothing is lost — it is delivered differently. The model gets a bounded result plus a pointer it can follow.

Search still works while disconnected

The metadata cache (mcp-cache.ts) keeps search and describe available even when a server cannot be reached:
  • Entries are keyed by configHash (covering command/args/env/url/headers), so a config change invalidates them automatically
  • Fully refreshed after every successful handshake; default TTL 7 days, or the server’s own ttlMs when declared
  • On a cache hit the old value is returned immediately while a refresh runs in the background — the foreground never waits on the network

Connection management

Connections are a process-wide singleton with a pool shared across sessions (lazy connect plus idle disconnect). They are not isolated per session — MCP servers have no notion of a session. A server is in one of four states: idle / connecting / ready / failed (with backoff). Each row in Settings shows a state dot and a “Test connection” button that forces a fresh handshake. Configuration changes hot-reload: the manager diffs the pool against the new config, disconnecting changed or disabled servers and releasing deleted ones. The system prompt and tool table need no re-injection — under proxy mode the tool table never changed in the first place.

Settings page

Settings → MCP. The skeleton matches the subagents page: two layer groups (system / workspace), and per row a transport icon, name, enable toggle, status badge (ready · N tools / connecting / failed with the reason), plus a row menu for testing the connection and deleting. Plain HTTP pointing at a non-loopback host shows a warning bar.

Known tradeoffs

args travel as a JSON string

call parameters are a JSON string rather than an object, because some models are unstable with nested union schemas. If testing shows mainstream models handle objects fine, this will be relaxed to accept both.

No regex or paging in search

Search is field-weighted matching (name 12 / server 8 / description 5; exact > prefix > substring, with a bonus for full-name hits), returning 12 by default and 40 at most. Regex and paging come later.

Orphan processes on Windows

stdio servers are spawned directly by the sidecar. The normal path kills them, but an abnormal exit (especially on Windows) can leave a child behind; cleanup walks the process tree. This still needs validation under real usage.

No OAuth yet

Authentication is currently static, via headers or env in the config. OAuth, resources, sampling, and elicitation are on the way at a later pace.

Next

Modes and permissions

How MCP approval relates to the permission levels.

Agent engine

The built-in tool list and where the proxy gateway tool fits.