From 4281a59d9b3aee071e1ecb4e881d277d5fc9dec1 Mon Sep 17 00:00:00 2001 From: BW Date: Tue, 20 Oct 2026 19:04:05 -0600 Subject: [PATCH] fix(mcp): advertise ttlMs and cacheScope on list responses Clients implementing the 2026-07-28 MCP spec (Claude Code 2.1.301) reject tools/list, prompts/list and resources/list results that lack ttlMs and cacheScope, which drops the whole server. Send a 60s private cache hint. Co-Authored-By: Claude Sonnet 5.5 --- src/mcp/rmcp_server.rs | 42 +++++++++++++++++++----------------- src/mcp/rmcp_server_tests.rs | 2 ++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/mcp/rmcp_server.rs b/src/mcp/rmcp_server.rs index d98ac026c..005fe3391 100644 --- a/src/mcp/rmcp_server.rs +++ b/src/mcp/rmcp_server.rs @@ -8,11 +8,11 @@ use lab_auth::AuthContext; use rmcp::{ ErrorData, RoleServer, ServerHandler, model::{ - Annotations, CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, - EmbeddedResource, GetPromptRequestParams, GetPromptResponse, GetPromptResult, + Annotations, CacheScope, CallToolRequestParams, CallToolResponse, CallToolResult, + ContentBlock, EmbeddedResource, GetPromptRequestParams, GetPromptResponse, GetPromptResult, Implementation, ListPromptsResult, ListResourcesResult, ListToolsResult, MetaObject, - PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResponse, - ReadResourceResult, Resource, ResourceContents, Role, ServerCapabilities, ServerInfo, Tool, + PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult, + Resource, ResourceContents, Role, ServerCapabilities, ServerInfo, Tool, }, service::RequestContext, transport::streamable_http_server::{ @@ -28,6 +28,11 @@ use super::actions; use super::prompts::{get_prompt, prompt_definitions}; use super::{AppState, AuthPolicy, schemas::tool_definitions, tools::execute_tool}; +/// Freshness advertised on tools/resources/prompts lists. The 2026-07-28 MCP +/// spec makes `ttlMs` and `cacheScope` mandatory; the lists only change when +/// the server binary does, and are auth-gated, so clients cache them privately. +const LIST_CACHE_TTL_MS: u64 = 60_000; + #[derive(Clone)] pub struct CortexRmcpServer { state: AppState, @@ -55,10 +60,9 @@ impl ServerHandler for CortexRmcpServer { let tools = rmcp_tool_definitions()?; tracing::info!(tool_count = tools.len(), "MCP tools listed"); - Ok(ListToolsResult { - tools, - ..Default::default() - }) + Ok(ListToolsResult::with_all_items(tools) + .with_ttl_ms(LIST_CACHE_TTL_MS) + .with_cache_scope(CacheScope::Private)) } async fn call_tool( @@ -180,14 +184,13 @@ impl ServerHandler for CortexRmcpServer { context: RequestContext, ) -> Result { require_auth_context(&self.state, &context)?; - Ok(ListResourcesResult { - resources: vec![ - schema_resource(), - prompt_output_schema_resource(), - query_widget_resource(), - ], - ..Default::default() - }) + Ok(ListResourcesResult::with_all_items(vec![ + schema_resource(), + prompt_output_schema_resource(), + query_widget_resource(), + ]) + .with_ttl_ms(LIST_CACHE_TTL_MS) + .with_cache_scope(CacheScope::Private)) } async fn read_resource( @@ -237,10 +240,9 @@ impl ServerHandler for CortexRmcpServer { require_auth_context(&self.state, &context)?; let prompts = prompt_definitions(); tracing::info!(prompt_count = prompts.len(), "MCP prompts listed"); - Ok(ListPromptsResult { - prompts, - ..Default::default() - }) + Ok(ListPromptsResult::with_all_items(prompts) + .with_ttl_ms(LIST_CACHE_TTL_MS) + .with_cache_scope(CacheScope::Private)) } async fn get_prompt( diff --git a/src/mcp/rmcp_server_tests.rs b/src/mcp/rmcp_server_tests.rs index 491d826ea..eba4a7373 100644 --- a/src/mcp/rmcp_server_tests.rs +++ b/src/mcp/rmcp_server_tests.rs @@ -1312,6 +1312,8 @@ async fn mounted_policy_with_auth_context_permits_tools_list() { tools[0]["name"], "cortex", "tools/list should return cortex tool; response: {response}" ); + assert_eq!(response["result"]["ttlMs"], 60_000, "{response}"); + assert_eq!(response["result"]["cacheScope"], "private", "{response}"); } #[tokio::test]