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.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.mcp.disabled.<layer>.<id>), because a personal switch has no business in a file
you commit and share. Same approach as subagents.
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.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 MCPcall 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
approveToolsglob matches the tool name and the declaration comes from the system layer (~/.kova/mcp.json). AnapproveToolswritten 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 matchesread 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
ttlMswhen 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.
