diff --git a/src/mcp/rmcp_server.rs b/src/mcp/rmcp_server.rs index d98ac026..005fe339 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 491d826e..eba4a737 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]