//! Common utilities shared between tool and prompt handlers use std::{ any::TypeId, collections::HashMap, sync::{Arc, LazyLock}, }; use schemars::JsonSchema; use crate::{ RoleServer, model::JsonObject, schemars::generate::SchemaSettings, service::RequestContext, }; /// Generates a JSON schema for a type pub fn schema_for_type() -> Arc { thread_local! { static CACHE_FOR_TYPE: std::sync::RwLock>> = Default::default(); }; CACHE_FOR_TYPE.with(|cache| { if let Some(x) = cache .read() .expect("schema cache lock poisoned") .get(&TypeId::of::()) { x.clone() } else { // explicitly to align json schema version to official specifications. // refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/655 for details. let settings = SchemaSettings::draft2020_12(); // Note: AddNullable is intentionally NOT used here because the `nullable` keyword // is an OpenAPI 3.0 extension, not part of JSON Schema 2020-12. Using it would // cause validation failures with strict JSON Schema validators. let generator = settings.into_generator(); let schema = generator.into_root_schema_for::(); let object = serde_json::to_value(schema).expect("failed to serialize schema"); let serde_json::Value::Object(object) = object else { panic!( "Schema serialization produced non-object value: expected JSON object but got {object:?}" ); }; let schema = Arc::new(object); cache .write() .expect("schema cache lock poisoned") .insert(TypeId::of::(), schema.clone()); schema } }) } /// Validate that the schema root is `type: "object"` (per MCP spec) and strip top-level /// `title`/`description` (the wrapper type name and doc, which are noise to the LLM). fn validate_and_strip(raw: &Arc, purpose: &str) -> Result, String> { match raw.get("type") { Some(serde_json::Value::String(t)) if t == "object" => { let mut object = raw.as_ref().clone(); object.remove("title"); object.remove("description"); Ok(Arc::new(object)) } Some(serde_json::Value::String(t)) => Err(format!( "MCP specification requires tool {purpose} to have root type 'object', but found '{t}'." )), None => Err(format!( "Schema is missing 'type' field. MCP specification requires {purpose} to have root type 'object'." )), Some(other) => Err(format!( "Schema 'type' field has unexpected format: {other:?}. Expected \"object\"." )), } } /// Generate, validate, and strip a JSON schema for inputSchema (must have root type "object"; /// top-level "title" and "description" are removed). pub fn schema_for_input() -> Result, String> { thread_local! { static CACHE_FOR_INPUT: std::sync::RwLock, String>>> = Default::default(); }; CACHE_FOR_INPUT.with(|cache| { if let Some(result) = cache .read() .expect("input schema cache lock poisoned") .get(&TypeId::of::()) { return result.clone(); } let result = validate_and_strip(&schema_for_type::(), "inputSchema"); cache .write() .expect("input schema cache lock poisoned") .insert(TypeId::of::(), result.clone()); result }) } /// Schema used when input is empty. pub fn schema_for_empty_input() -> Arc { static EMPTY: LazyLock> = LazyLock::new(|| { let mut object = JsonObject::new(); object.insert("type".into(), serde_json::json!("object")); object.insert("properties".into(), serde_json::json!({})); Arc::new(object) }); EMPTY.clone() } /// Generate a JSON schema for outputSchema (must have root type "object"; top-level "title" and "description" are removed) pub fn schema_for_output() -> Result, String> { thread_local! { static CACHE_FOR_OUTPUT: std::sync::RwLock, String>>> = Default::default(); }; CACHE_FOR_OUTPUT.with(|cache| { // Try to get from cache first if let Some(result) = cache .read() .expect("output schema cache lock poisoned") .get(&TypeId::of::()) { return result.clone(); } // Generate, validate, and strip unnecessary top-level fields let result = validate_and_strip(&schema_for_type::(), "outputSchema"); // Cache the result (both success and error cases) cache .write() .expect("output schema cache lock poisoned") .insert(TypeId::of::(), result.clone()); result }) } /// Trait for extracting parts from a context, unifying tool and prompt extraction pub trait FromContextPart: Sized { fn from_context_part(context: &mut C) -> Result; } /// Common extractors that can be used by both tool and prompt handlers impl FromContextPart for RequestContext where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { Ok(context.as_request_context().clone()) } } impl FromContextPart for tokio_util::sync::CancellationToken where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { Ok(context.as_request_context().ct.clone()) } } impl FromContextPart for crate::model::Extensions where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { Ok(context.as_request_context().extensions.clone()) } } #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Extension(pub T); impl FromContextPart for Extension where C: AsRequestContext, T: Send + Sync + 'static + Clone, { fn from_context_part(context: &mut C) -> Result { let extension = context .as_request_context() .extensions .get::() .cloned() .ok_or_else(|| { crate::ErrorData::invalid_params( format!("missing extension {}", std::any::type_name::()), None, ) })?; Ok(Extension(extension)) } } impl FromContextPart for crate::Peer where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { Ok(context.as_request_context().peer.clone()) } } impl FromContextPart for crate::model::Meta where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { let request_context = context.as_request_context_mut(); let mut meta = crate::model::Meta::default(); std::mem::swap(&mut meta, &mut request_context.meta); Ok(meta) } } #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RequestId(pub crate::model::RequestId); impl FromContextPart for RequestId where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { Ok(RequestId(context.as_request_context().id.clone())) } } /// Trait for types that can provide access to RequestContext pub trait AsRequestContext { fn as_request_context(&self) -> &RequestContext; fn as_request_context_mut(&mut self) -> &mut RequestContext; } #[cfg(test)] mod tests { use super::*; #[derive(serde::Serialize, serde::Deserialize, JsonSchema)] struct TestObject { value: i32, } #[derive(serde::Serialize, serde::Deserialize, JsonSchema)] struct AnotherTestObject { value: i32, } #[test] fn test_schema_for_type_handles_primitive() { let schema = schema_for_type::(); assert_eq!(schema.get("type"), Some(&serde_json::json!("integer"))); } #[test] fn test_schema_for_type_handles_array() { let schema = schema_for_type::>(); assert_eq!(schema.get("type"), Some(&serde_json::json!("array"))); let items = schema.get("items").and_then(|v| v.as_object()); assert_eq!( items.unwrap().get("type"), Some(&serde_json::json!("integer")) ); } #[test] fn test_schema_for_type_handles_struct() { let schema = schema_for_type::(); assert_eq!(schema.get("type"), Some(&serde_json::json!("object"))); let properties = schema.get("properties").and_then(|v| v.as_object()); assert!(properties.unwrap().contains_key("value")); } #[test] fn test_schema_for_type_caches_primitive_types() { let schema1 = schema_for_type::(); let schema2 = schema_for_type::(); assert!(Arc::ptr_eq(&schema1, &schema2)); } #[test] fn test_schema_for_type_caches_struct_types() { let schema1 = schema_for_type::(); let schema2 = schema_for_type::(); assert!(Arc::ptr_eq(&schema1, &schema2)); } #[test] fn test_schema_for_type_different_types_different_schemas() { let schema1 = schema_for_type::(); let schema2 = schema_for_type::(); assert!(!Arc::ptr_eq(&schema1, &schema2)); } #[test] fn test_schema_for_type_arc_can_be_shared() { let schema = schema_for_type::(); let cloned = schema.clone(); assert!(Arc::ptr_eq(&schema, &cloned)); } #[test] fn test_schema_for_output_rejects_primitive() { let result = schema_for_output::(); assert!(result.is_err(),); } #[test] fn test_schema_for_output_accepts_object() { let result = schema_for_output::(); assert!(result.is_ok(),); } #[test] fn test_schema_for_output_strips_top_level_title() { let schema = schema_for_output::().unwrap(); assert!(!schema.contains_key("title")); } #[test] fn test_schema_for_output_strips_top_level_description() { let schema = schema_for_output::().unwrap(); assert!(!schema.contains_key("description")); } #[test] fn test_schema_for_input_rejects_primitive() { let result = schema_for_input::(); assert!(result.is_err()); } #[test] fn test_schema_for_input_accepts_object() { let result = schema_for_input::(); assert!(result.is_ok()); } #[test] fn test_schema_for_input_strips_top_level_title() { let schema = schema_for_input::().unwrap(); assert!(!schema.contains_key("title")); } #[test] fn test_schema_for_input_strips_top_level_description() { let schema = schema_for_input::().unwrap(); assert!(!schema.contains_key("description")); } }