Skip to content

Commit f41700e

Browse files
Your Nameclaude
andcommitted
fix(mcp): set ttlMs/cacheScope on tools/list and prompts/list (SEP-2549)
Omitting these cache-hint fields (Phase 3 of the MCP 2026-07-28 upgrade, deferred in docs/plans/2026-08-04-mcp-2026-07-28-upgrade-plan.md) was assumed spec-safe, but at least one real client (Claude Code / Antigravity cc_version 2.1.232.43a) requires ttlMs/cacheScope as mandatory fields and rejects tools/list and prompts/list outright when they're absent -- silently registering zero tools even though the underlying MCP connection looks healthy. Explicitly setting ttl_ms=900_000 and cache_scope (Private for tools, since visibility is per-connection via preset/set_toolset; Public for prompts, which are a static list) fixes this for that client with no behavior change for clients that don't care about the fields. Verified live via a raw JSON-RPC probe through scripts/mcp-launcher.sh: the wire response now includes "ttlMs":900000,"cacheScope":"private" (tools) / "...,"cacheScope":"public" (prompts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent c88afa2 commit f41700e

1 file changed

Lines changed: 31 additions & 7 deletions

File tree

crates/calm-server/src/tools.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -784,18 +784,35 @@ impl rmcp::ServerHandler for CalmServer {
784784
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
785785
) -> Result<rmcp::model::ListToolsResult, rmcp::model::ErrorData> {
786786
let visible = self.current_visible_tool_names();
787-
// `with_all_items` leaves result_type/ttl_ms/cache_scope (SEP-2549) at
788-
// their no-cache-hint defaults -- Phase 3 of the MCP 2026-07-28 upgrade
789-
// (docs/plans/2026-08-04-mcp-2026-07-28-upgrade-plan.md) is where this
790-
// toolset already emits `tool_list_changed` on every `set_toolset`
791-
// narrowing, so that's the natural cache-bust signal to wire up then.
787+
// Phase 3 of the MCP 2026-07-28 upgrade (SEP-2549,
788+
// docs/plans/2026-08-04-mcp-2026-07-28-upgrade-plan.md): explicitly set
789+
// `ttl_ms`/`cache_scope` rather than leaving `with_all_items`'s
790+
// no-cache-hint defaults (both `None`, omitted from the wire). Live-
791+
// verified 2026-08-19: at least one MCP client (Claude Code / Antigravity
792+
// cc_version 2.1.232.43a, negotiating protocol 2026-07-28) treats these
793+
// as *required* fields on `tools/list` results — an omitted `ttlMs`/
794+
// `cacheScope` fails that client's response schema validation outright,
795+
// so `tools/list` never succeeds and zero tools register. The MCP spec
796+
// itself treats them as optional-with-default (rmcp's own doc comment:
797+
// "Defaults to CacheScope::Public when absent from the wire"), so this
798+
// is arguably a stricter-than-spec client, but sending them explicitly
799+
// costs nothing and unblocks every client either way.
800+
// `cache_scope` = `Private`: the visible set is per-connection (preset +
801+
// `set_toolset`), never global — see the Phase 3 plan note. `ttl_ms` is
802+
// a safety-net cap; the primary invalidation signal is already the
803+
// `tool_list_changed` notification `set_toolset` fires on every
804+
// narrowing (`enable_tool_list_changed()` above), so this doesn't need
805+
// to be short. Token-savings measurement (b4_token_efficiency) is still
806+
// open follow-up work, not done here.
792807
Ok(rmcp::model::ListToolsResult::with_all_items(
793808
self.tool_router
794809
.list_all()
795810
.into_iter()
796811
.filter(|t| visible.contains(t.name.as_ref()))
797812
.collect(),
798-
))
813+
)
814+
.with_ttl_ms(900_000)
815+
.with_cache_scope(rmcp::model::CacheScope::Private))
799816
}
800817

801818
async fn call_tool(
@@ -946,9 +963,16 @@ impl rmcp::ServerHandler for CalmServer {
946963
Output = Result<rmcp::model::ListPromptsResult, rmcp::model::ErrorData>,
947964
> + Send
948965
+ '_ {
966+
// See `list_tools`'s comment for why `ttl_ms`/`cache_scope` are set
967+
// explicitly (SEP-2549 client compatibility). `cache_scope` = `Public`
968+
// here (not `Private` like `list_tools`): `ci_prompts()` is a static,
969+
// connection-independent list, unlike the preset/`set_toolset`-scoped
970+
// tool set.
949971
std::future::ready(Ok(rmcp::model::ListPromptsResult::with_all_items(
950972
ci_prompts(),
951-
)))
973+
)
974+
.with_ttl_ms(900_000)
975+
.with_cache_scope(rmcp::model::CacheScope::Public)))
952976
}
953977

954978
fn get_prompt(

0 commit comments

Comments
 (0)