rmcp/handler/server/
common.rs1use std::{
4 any::TypeId,
5 collections::HashMap,
6 sync::{Arc, LazyLock},
7};
8
9use schemars::JsonSchema;
10
11use crate::{
12 RoleServer, model::JsonObject, schemars::generate::SchemaSettings, service::RequestContext,
13};
14
15pub fn schema_for_type<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
17 thread_local! {
18 static CACHE_FOR_TYPE: std::sync::RwLock<HashMap<TypeId, Arc<JsonObject>>> = Default::default();
19 };
20 CACHE_FOR_TYPE.with(|cache| {
21 if let Some(x) = cache
22 .read()
23 .expect("schema cache lock poisoned")
24 .get(&TypeId::of::<T>())
25 {
26 x.clone()
27 } else {
28 let settings = SchemaSettings::draft2020_12();
31 let generator = settings.into_generator();
35 let schema = generator.into_root_schema_for::<T>();
36 let object = serde_json::to_value(schema).expect("failed to serialize schema");
37 let serde_json::Value::Object(object) = object else {
38 panic!(
39 "Schema serialization produced non-object value: expected JSON object but got {object:?}"
40 );
41 };
42 let schema = Arc::new(object);
43 cache
44 .write()
45 .expect("schema cache lock poisoned")
46 .insert(TypeId::of::<T>(), schema.clone());
47
48 schema
49 }
50 })
51}
52
53fn validate_and_strip(raw: &Arc<JsonObject>, purpose: &str) -> Result<Arc<JsonObject>, String> {
56 match raw.get("type") {
57 Some(serde_json::Value::String(t)) if t == "object" => {
58 let mut object = raw.as_ref().clone();
59 object.remove("title");
60 object.remove("description");
61 Ok(Arc::new(object))
62 }
63 Some(serde_json::Value::String(t)) => Err(format!(
64 "MCP specification requires tool {purpose} to have root type 'object', but found '{t}'."
65 )),
66 None => Err(format!(
67 "Schema is missing 'type' field. MCP specification requires {purpose} to have root type 'object'."
68 )),
69 Some(other) => Err(format!(
70 "Schema 'type' field has unexpected format: {other:?}. Expected \"object\"."
71 )),
72 }
73}
74
75pub fn schema_for_input<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObject>, String> {
78 thread_local! {
79 static CACHE_FOR_INPUT: std::sync::RwLock<HashMap<TypeId, Result<Arc<JsonObject>, String>>> = Default::default();
80 };
81 CACHE_FOR_INPUT.with(|cache| {
82 if let Some(result) = cache
83 .read()
84 .expect("input schema cache lock poisoned")
85 .get(&TypeId::of::<T>())
86 {
87 return result.clone();
88 }
89 let result = validate_and_strip(&schema_for_type::<T>(), "inputSchema");
90 cache
91 .write()
92 .expect("input schema cache lock poisoned")
93 .insert(TypeId::of::<T>(), result.clone());
94 result
95 })
96}
97
98pub fn schema_for_empty_input() -> Arc<JsonObject> {
100 static EMPTY: LazyLock<Arc<JsonObject>> = LazyLock::new(|| {
101 let mut object = JsonObject::new();
102 object.insert("type".into(), serde_json::json!("object"));
103 object.insert("properties".into(), serde_json::json!({}));
104 Arc::new(object)
105 });
106 EMPTY.clone()
107}
108
109fn strip_output(raw: &Arc<JsonObject>) -> Arc<JsonObject> {
113 let mut object = raw.as_ref().clone();
114 object.remove("title");
115 object.remove("description");
116 Arc::new(object)
117}
118
119pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
122 thread_local! {
123 static CACHE_FOR_OUTPUT: std::sync::RwLock<HashMap<TypeId, Arc<JsonObject>>> = Default::default();
124 };
125
126 CACHE_FOR_OUTPUT.with(|cache| {
127 if let Some(schema) = cache
128 .read()
129 .expect("output schema cache lock poisoned")
130 .get(&TypeId::of::<T>())
131 {
132 return schema.clone();
133 }
134
135 let schema = strip_output(&schema_for_type::<T>());
136
137 cache
138 .write()
139 .expect("output schema cache lock poisoned")
140 .insert(TypeId::of::<T>(), schema.clone());
141
142 schema
143 })
144}
145
146pub trait FromContextPart<C>: Sized {
148 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData>;
149}
150
151impl<C> FromContextPart<C> for RequestContext<RoleServer>
153where
154 C: AsRequestContext,
155{
156 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
157 Ok(context.as_request_context().clone())
158 }
159}
160
161impl<C> FromContextPart<C> for tokio_util::sync::CancellationToken
162where
163 C: AsRequestContext,
164{
165 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
166 Ok(context.as_request_context().ct.clone())
167 }
168}
169
170impl<C> FromContextPart<C> for crate::model::Extensions
171where
172 C: AsRequestContext,
173{
174 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
175 Ok(context.as_request_context().extensions.clone())
176 }
177}
178
179#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
180pub struct Extension<T>(pub T);
181
182impl<C, T> FromContextPart<C> for Extension<T>
183where
184 C: AsRequestContext,
185 T: Send + Sync + 'static + Clone,
186{
187 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
188 let extension = context
189 .as_request_context()
190 .extensions
191 .get::<T>()
192 .cloned()
193 .ok_or_else(|| {
194 crate::ErrorData::invalid_params(
195 format!("missing extension {}", std::any::type_name::<T>()),
196 None,
197 )
198 })?;
199 Ok(Extension(extension))
200 }
201}
202
203impl<C> FromContextPart<C> for crate::Peer<RoleServer>
204where
205 C: AsRequestContext,
206{
207 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
208 Ok(context.as_request_context().peer.clone())
209 }
210}
211
212impl<C> FromContextPart<C> for crate::model::RequestMetaObject
213where
214 C: AsRequestContext,
215{
216 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
217 let request_context = context.as_request_context_mut();
218 let mut meta = crate::model::RequestMetaObject::default();
219 std::mem::swap(&mut meta, &mut request_context.meta);
220 Ok(meta)
221 }
222}
223
224#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
225pub struct RequestId(pub crate::model::RequestId);
226
227impl<C> FromContextPart<C> for RequestId
228where
229 C: AsRequestContext,
230{
231 fn from_context_part(context: &mut C) -> Result<Self, crate::ErrorData> {
232 Ok(RequestId(context.as_request_context().id.clone()))
233 }
234}
235
236pub trait AsRequestContext {
238 fn as_request_context(&self) -> &RequestContext<RoleServer>;
239 fn as_request_context_mut(&mut self) -> &mut RequestContext<RoleServer>;
240}
241
242#[cfg(test)]
243mod tests {
244 use rstest::rstest;
245
246 use super::*;
247
248 #[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
249 struct TestObject {
250 value: i32,
251 }
252
253 #[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
254 struct AnotherTestObject {
255 value: i32,
256 }
257
258 #[rstest]
259 #[case::primitive(schema_for_type::<i32>, "integer")]
260 #[case::array(schema_for_type::<Vec<i32>>, "array")]
261 #[case::struct_object(schema_for_type::<TestObject>, "object")]
262 fn schema_for_type_sets_expected_root_type(
263 #[case] schema_fn: fn() -> Arc<JsonObject>,
264 #[case] expected_type: &str,
265 ) {
266 let schema = schema_fn();
267
268 assert_eq!(schema.get("type"), Some(&serde_json::json!(expected_type)));
269 }
270
271 #[test]
272 fn schema_for_type_sets_array_item_type() {
273 let schema = schema_for_type::<Vec<i32>>();
274 let items = schema.get("items").and_then(|v| v.as_object()).unwrap();
275
276 assert_eq!(items.get("type"), Some(&serde_json::json!("integer")));
277 }
278
279 #[test]
280 fn schema_for_type_sets_struct_properties() {
281 let schema = schema_for_type::<TestObject>();
282 let properties = schema
283 .get("properties")
284 .and_then(|v| v.as_object())
285 .unwrap();
286
287 assert!(properties.contains_key("value"));
288 }
289
290 #[rstest]
291 #[case::primitive(schema_for_type::<i32>)]
292 #[case::struct_object(schema_for_type::<TestObject>)]
293 fn test_schema_for_type_caches_schemas(#[case] schema_fn: fn() -> Arc<JsonObject>) {
294 let schema1 = schema_fn();
295 let schema2 = schema_fn();
296
297 assert!(Arc::ptr_eq(&schema1, &schema2));
298 }
299
300 #[test]
301 fn test_schema_for_type_different_types_different_schemas() {
302 let schema1 = schema_for_type::<TestObject>();
303 let schema2 = schema_for_type::<AnotherTestObject>();
304
305 assert!(!Arc::ptr_eq(&schema1, &schema2));
306 }
307
308 #[test]
309 fn test_schema_for_type_arc_can_be_shared() {
310 let schema = schema_for_type::<TestObject>();
311 let cloned = schema.clone();
312
313 assert!(Arc::ptr_eq(&schema, &cloned));
314 }
315
316 #[test]
317 fn test_schema_for_output_accepts_primitive() {
318 let schema = schema_for_output::<i32>();
319 assert_eq!(schema.get("type"), Some(&serde_json::json!("integer")));
320 }
321
322 #[test]
323 fn test_schema_for_output_strips_description_for_primitive() {
324 let schema = schema_for_output::<i32>();
325 assert!(!schema.contains_key("description"));
326 }
327
328 #[test]
329 fn test_schema_for_output_accepts_composition() {
330 let schema = schema_for_output::<Option<String>>();
331 let schema_str = serde_json::to_string(&schema).unwrap();
332 assert!(
333 schema_str.contains("anyOf")
334 || schema_str.contains("oneOf")
335 || schema_str.contains("null"),
336 "Expected composition schema for Option<String>, got: {schema_str}"
337 );
338 }
339
340 #[test]
341 fn test_schema_for_output_caches_result() {
342 let schema1 = schema_for_output::<i32>();
343 let schema2 = schema_for_output::<i32>();
344 assert!(Arc::ptr_eq(&schema1, &schema2));
345 }
346
347 #[test]
348 fn test_schema_for_input_rejects_array() {
349 let result = schema_for_input::<Vec<i32>>();
350 assert!(result.is_err());
351 }
352
353 #[test]
354 fn test_schema_for_output_accepts_unit() {
355 let _schema = schema_for_output::<()>();
356 }
357
358 #[test]
359 fn test_schema_for_output_accepts_object() {
360 let schema = schema_for_output::<TestObject>();
361 assert_eq!(schema.get("type"), Some(&serde_json::json!("object")));
362 }
363
364 #[test]
365 fn test_schema_for_output_strips_top_level_title() {
366 let schema = schema_for_output::<TestObject>();
367 assert!(!schema.contains_key("title"));
368 }
369
370 #[test]
371 fn test_schema_for_output_strips_top_level_description() {
372 let schema = schema_for_output::<TestObject>();
373 assert!(!schema.contains_key("description"));
374 }
375
376 #[rstest]
377 #[case::input(schema_for_input::<i32>)]
378 fn test_schema_for_input_rejects_primitives(
379 #[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
380 ) {
381 let result = schema_fn();
382 assert!(result.is_err());
383 }
384
385 #[rstest]
386 #[case::input(schema_for_input::<TestObject>)]
387 fn test_schema_for_input_accepts_objects(
388 #[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
389 ) {
390 let result = schema_fn();
391 assert!(result.is_ok());
392 }
393
394 #[rstest]
395 #[case::input_title(schema_for_input::<TestObject>, "title")]
396 #[case::input_description(schema_for_input::<TestObject>, "description")]
397 fn test_schema_for_input_strips_top_level_metadata(
398 #[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
399 #[case] field: &str,
400 ) {
401 let schema = schema_fn().unwrap();
402 assert!(!schema.contains_key(field));
403 }
404}