feat(tui): cache MCP tool schemas for instant advertisement - #2
feat(tui): cache MCP tool schemas for instant advertisement#2undivisible wants to merge 1 commit into
Conversation
Cache each MCP server's discovered tool list (names, descriptions, input schemas) at ~/.telekinesis/mcp-cache.json, keyed by server name plus a stable FNV-1a hash of its config entry. On startup, cached servers' tools register before any network I/O, so the model's tool set — and with it the prompt-cache prefix — is stable from the first turn. Discovery still runs in the background: it seeds the lazy connections behind cached tools, refreshes the cache for the next startup, prunes entries for servers no longer configured, and surfaces a system message when the live tool list drifts from the cached set. It deliberately never hot-swaps the registry mid-session for cached servers, because replacing the registry invalidates the model's prompt cache. Servers without a valid cache entry keep the old behavior: live discovery, then one all-or-nothing registry swap, then a cache write. Editing a server's transport, command, args, url, or headers changes the config hash and invalidates its entry. Corrupt or unreadable cache files are treated as empty and fall back to live discovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_52e9735c-d09b-460f-8e77-1b1ba383cf69) |
|
Review note: substantial, well-described feature (MCP schema disk cache + lazy client + no mid-session registry swap). Tests look real. Holding merge until a closer read of ui/tui/src/main.rs (334-line change) and confirmation the cache path/fingerprint cannot advertise tools from a stale server config. The isolated cursor-agent on this repo is also reviewing. |
undivisible
left a comment
There was a problem hiding this comment.
Review vs current main (12 commits ahead of this branch). Do not merge: mergeable: CONFLICTING.
The cache + lazy client + no-hot-swap-for-fully-cached sessions is the right shape. Blockers after rebase:
- Mixed cache still hot-swaps the whole registry when any uncached server appears — the cache-hostile swap this PR exists to avoid.
OnceCell::setduring in-flightget_or_try_initdrops the live discovery client.fs::writeis not atomic; a tornmcp-cache.jsonloads empty and the next refresh can wipe schemas for servers that are down this run.
Missing tests: mixed cache vs swap, seed-vs-in-flight get, atomic save, empty-config prune.
| if outcome.new_specs.is_empty() { | ||
| return; | ||
| } | ||
| specs.extend(outcome.new_specs); |
There was a problem hiding this comment.
L2731: 🔴 bug: mixed cache still hot-swaps whole registry. If any server was pre-advertised, skip set_tools; cache + Notice like drift. Swap only when mcp_cached_specs was empty.
| } | ||
|
|
||
| fn seed(&self, client: Arc<rx4::McpClient>) { | ||
| let _ = self.client.set(client); |
There was a problem hiding this comment.
L3327: 🔴 bug: OnceCell::set returns InitializingError if get() is connecting; let _ = drops the live discovery client. Use Mutex/watch so seed wins, or retry set after failed init. Retry-on-err itself is fine (get_or_try_init leaves cell empty).
| std::fs::create_dir_all(parent)?; | ||
| } | ||
| let json = serde_json::to_string_pretty(cache).map_err(std::io::Error::other)?; | ||
| std::fs::write(path, json) |
There was a problem hiding this comment.
L68: 🔴 bug: fs::write truncates then writes. Crash/concurrent TUI → torn JSON → load-as-empty → refresh keeps only servers that connect this run, wiping down servers' schemas. Write mcp-cache.json.tmp + rename, same as session JSONL.
|
Triage: since this opened, main was refactored — monolithic ui/tui/src/main.rs was split into app.rs/tui.rs/providers.rs/exec.rs/host.rs/etc (PR #9/#10). This patch no longer applies (git apply fails @ main.rs). Feature is real and not landed on main, but needs porting onto the new module layout: [addressing here]. Left OPEN for rebase rather than blind-merge/c close. |
Motivation
MCP discovery currently blocks tool advertisement on the network: every startup probes all configured servers (10s per-server budget), and the registry is swapped only after discovery finishes. Two costs follow:
build_tool_registry_with_profilecomment).This adopts the idea behind jcode's MCP pre-advertisement: remember what each server advertised last time and advertise it again immediately, before any connection exists.
What changed
ui/tui/src/mcp_cache.rs— disk cache at~/.telekinesis/mcp-cache.jsonstoring each server's tool list (name, description, input schema) plus a config fingerprint. The hash is a pinned FNV-1a over name/transport/command/args/url/headers (notDefaultHasher, which is not stable across Rust releases), so editing a server's config invalidates its entry. Corrupt or unreadable cache files load as empty — never fatal.McpToolClient(tokioOnceCell): they connect on first call, and a failed connect degrades to a tool error and retries on the next call.AppEvent::Notice) when the live tool list drifts from the advertised set. It deliberately does not hot-swap the registry mid-session.Testing
From
ui/tui(matching.github/workflows/ci.yml):cargo fmt --check— cleancargo clippy --locked --all-features -- -D warnings— cleancargo test --locked— 135 passed, 0 failed (8 new: serde/disk roundtrip, corrupt-file tolerance, config-hash stability and per-field sensitivity, hash-mismatch lookup rejection, cached-spec registration without a live server, config-change invalidation, empty-refresh no-op, lazy-client soft failure on unreachable server)cargo build --locked— clean🤖 Generated with Claude Code
Note
Medium Risk
Changes MCP tool registration timing and session-stable tool advertisement; wrong or stale cache could expose outdated schemas until refresh/restart, but config hashing and no mid-session hot-swap limit blast radius.
Overview
Adds MCP pre-advertisement so the model’s tool set (and prompt-cache prefix) can be stable from the first turn instead of waiting on concurrent server probes.
A new
mcp_cachemodule persists per-server tool schemas at~/.telekinesis/mcp-cache.json, keyed by server name and a stable config hash (invalidates on command/args/url/headers changes). Corrupt cache loads as empty. At startup, valid entries register synchronously;McpToolClientwraps connections in a lazyOnceCellso cached tools connect on first call and failed connects retry later.Background
refresh_mcp_toolsreplacesdiscover_mcp_tools: it still probes servers in parallel, updates the cache for the next run, seeds connections for cached servers, and only swaps the tool registry for servers that had no cache (fully cached sessions skip the swap). If live tools drift from what was advertised, a newAppEvent::Noticesystem message tells the user to restart rather than hot-swapping mid-session.Uncached servers keep the prior all-or-nothing discovery + registry swap path. Tests cover cache roundtrip, hash invalidation, registration without a live server, and lazy client soft failure.
Reviewed by Cursor Bugbot for commit dd366f1. Configure here.