@@ -18,7 +18,7 @@ use crate::{
1818 model:: {
1919 ClientCapabilities , ClientJsonRpcMessage , ClientNotification , ClientRequest , ErrorData ,
2020 GetExtensions , Implementation , InitializeRequest , InitializeRequestParams ,
21- InitializedNotification , JsonRpcError , ProtocolVersion , RequestId ,
21+ InitializedNotification , JsonObject , JsonRpcError , ProtocolVersion , RequestId ,
2222 } ,
2323 serve_server,
2424 service:: serve_directly,
@@ -274,19 +274,20 @@ fn header_mismatch_jsonrpc_response(
274274 . expect ( "valid response" )
275275}
276276
277- /// Validates SEP-2243 `Mcp-Method` / `Mcp-Name` headers against the request body.
277+ /// Validates SEP-2243 `Mcp-Method` / `Mcp-Name` / `Mcp-Param-*` headers against the body.
278278///
279279/// Only enforced when the request declares a protocol version `>= STANDARD_HEADERS`.
280280/// The `initialize` handshake is exempt: clients emit these headers only after the
281- /// version has been negotiated. `Mcp-Param-*` validation is not enforced here because
282- /// the transport layer has no synchronous access to tool input schemas .
281+ /// version has been negotiated. `tool_schema` supplies the called tool's input schema
282+ /// so annotated `Mcp-Param-*` headers can be checked (no schema => those are skipped) .
283283#[ expect(
284284 clippy:: result_large_err,
285285 reason = "BoxResponse is intentionally large; matches other handlers in this file"
286286) ]
287287fn validate_standard_headers (
288288 headers : & HeaderMap ,
289289 message : & ClientJsonRpcMessage ,
290+ tool_schema : impl Fn ( & str ) -> Option < Arc < JsonObject > > ,
290291) -> Result < ( ) , BoxResponse > {
291292 let version_requires_headers = headers
292293 . get ( HEADER_MCP_PROTOCOL_VERSION )
@@ -310,7 +311,16 @@ fn validate_standard_headers(
310311 let Ok ( value) = serde_json:: to_value ( message) else {
311312 return Ok ( ( ) ) ;
312313 } ;
313- if let Err ( reason) = mcp_headers:: validate_request_headers ( headers, & value, None ) {
314+ // For tools/call, look up the tool schema so Mcp-Param-* headers are validated.
315+ let schema = value
316+ . get ( "method" )
317+ . and_then ( |method| method. as_str ( ) )
318+ . filter ( |method| * method == "tools/call" )
319+ . and_then ( |_| value. get ( "params" ) )
320+ . and_then ( |params| params. get ( "name" ) )
321+ . and_then ( |name| name. as_str ( ) )
322+ . and_then ( tool_schema) ;
323+ if let Err ( reason) = mcp_headers:: validate_request_headers ( headers, & value, schema. as_deref ( ) ) {
314324 return Err ( header_mismatch_jsonrpc_response ( request_id, reason) ) ;
315325 }
316326 Ok ( ( ) )
@@ -611,6 +621,10 @@ pub struct StreamableHttpService<S, M> {
611621 pending_restores : Option <
612622 Arc < tokio:: sync:: RwLock < HashMap < SessionId , tokio:: sync:: watch:: Sender < Option < bool > > > > > ,
613623 > ,
624+ /// Caches tool input schemas by name for SEP-2243 `Mcp-Param-*` validation.
625+ /// Populated lazily via `get_tool` so the service factory runs at most once
626+ /// per tool name. `None` value means the tool exposes no schema.
627+ tool_schemas : Arc < std:: sync:: RwLock < HashMap < String , Option < Arc < JsonObject > > > > > ,
614628}
615629
616630impl < S , M > Clone for StreamableHttpService < S , M > {
@@ -620,14 +634,15 @@ impl<S, M> Clone for StreamableHttpService<S, M> {
620634 session_manager : self . session_manager . clone ( ) ,
621635 service_factory : self . service_factory . clone ( ) ,
622636 pending_restores : self . pending_restores . clone ( ) ,
637+ tool_schemas : self . tool_schemas . clone ( ) ,
623638 }
624639 }
625640}
626641
627642impl < RequestBody , S , M > tower_service:: Service < Request < RequestBody > > for StreamableHttpService < S , M >
628643where
629644 RequestBody : Body + Send + ' static ,
630- S : crate :: Service < RoleServer > + Send + ' static ,
645+ S : crate :: ServerHandler + Send + ' static ,
631646 M : SessionManager ,
632647 RequestBody :: Error : Display ,
633648 RequestBody :: Data : Send + ' static ,
@@ -681,7 +696,7 @@ impl Drop for PendingRestoreGuard {
681696
682697impl < S , M > StreamableHttpService < S , M >
683698where
684- S : crate :: Service < RoleServer > + Send + ' static ,
699+ S : crate :: ServerHandler + Send + ' static ,
685700 M : SessionManager ,
686701{
687702 pub fn new (
@@ -700,12 +715,33 @@ where
700715 session_manager,
701716 service_factory : Arc :: new ( service_factory) ,
702717 pending_restores,
718+ tool_schemas : Arc :: new ( std:: sync:: RwLock :: new ( HashMap :: new ( ) ) ) ,
703719 }
704720 }
705721 fn get_service ( & self ) -> Result < S , std:: io:: Error > {
706722 ( self . service_factory ) ( )
707723 }
708724
725+ /// Returns the cached input schema for `name`, constructing a service once
726+ /// per name to read its `ServerHandler::get_tool` definition. Used to
727+ /// validate SEP-2243 `Mcp-Param-*` headers against the request body.
728+ fn tool_schema ( & self , name : & str ) -> Option < Arc < JsonObject > > {
729+ if let Ok ( cache) = self . tool_schemas . read ( ) {
730+ if let Some ( schema) = cache. get ( name) {
731+ return schema. clone ( ) ;
732+ }
733+ }
734+ let schema = self
735+ . get_service ( )
736+ . ok ( )
737+ . and_then ( |service| service. get_tool ( name) )
738+ . map ( |tool| tool. input_schema ) ;
739+ if let Ok ( mut cache) = self . tool_schemas . write ( ) {
740+ cache. insert ( name. to_owned ( ) , schema. clone ( ) ) ;
741+ }
742+ schema
743+ }
744+
709745 /// Spawn a task that runs `serve_server` for the given session, waits for
710746 /// it to finish, and then calls `close_session`.
711747 ///
@@ -720,7 +756,7 @@ where
720756 transport : M :: Transport ,
721757 init_done_tx : Option < tokio:: sync:: oneshot:: Sender < ( ) > > ,
722758 ) where
723- S : crate :: Service < RoleServer > + Send + ' static ,
759+ S : crate :: ServerHandler + Send + ' static ,
724760 M : SessionManager ,
725761 {
726762 tokio:: spawn ( async move {
@@ -763,7 +799,7 @@ where
763799 parts : & http:: request:: Parts ,
764800 ) -> Result < bool , std:: io:: Error >
765801 where
766- S : crate :: Service < RoleServer > + Send + ' static ,
802+ S : crate :: ServerHandler + Send + ' static ,
767803 M : SessionManager ,
768804 {
769805 // Both fields are Some iff a session store is configured.
@@ -1140,7 +1176,7 @@ where
11401176 // Validate MCP-Protocol-Version header (per 2025-06-18 spec)
11411177 validate_protocol_version_header ( & part. headers ) ?;
11421178 // Validate SEP-2243 standard headers against the body
1143- validate_standard_headers ( & part. headers , & message) ?;
1179+ validate_standard_headers ( & part. headers , & message, |name| self . tool_schema ( name ) ) ?;
11441180
11451181 // inject request part to extensions
11461182 match & mut message {
@@ -1294,7 +1330,7 @@ where
12941330 }
12951331 }
12961332 // Validate SEP-2243 standard headers against the body
1297- validate_standard_headers ( & part. headers , & message) ?;
1333+ validate_standard_headers ( & part. headers , & message, |name| self . tool_schema ( name ) ) ?;
12981334 let service = self
12991335 . get_service ( )
13001336 . map_err ( internal_error_response ( "get service" ) ) ?;
0 commit comments