soma_mcp_server/
protocol.rs1use std::{borrow::Cow, sync::Arc};
12
13use rmcp::{
14 model::{Prompt, Resource, Tool, ToolAnnotations},
15 ErrorData,
16};
17use serde_json::{Map, Value};
18
19pub fn tool_from_json_definition(value: Value) -> Result<Tool, ErrorData> {
28 let name = value
29 .get("name")
30 .and_then(Value::as_str)
31 .ok_or_else(|| ErrorData::internal_error("tool definition missing name", None))?;
32 let description = value
33 .get("description")
34 .and_then(Value::as_str)
35 .map(|d| Cow::Owned(d.to_string()));
36 let input_schema = value
37 .get("inputSchema")
38 .and_then(Value::as_object)
39 .cloned()
40 .ok_or_else(|| ErrorData::internal_error("tool definition missing inputSchema", None))?;
41 let mut tool = Tool::new_with_raw(
42 Cow::Owned(name.to_string()),
43 description,
44 Arc::new(input_schema),
45 );
46 if let Some(output_schema) = value.get("outputSchema") {
47 let output_schema = output_schema.as_object().cloned().ok_or_else(|| {
48 ErrorData::internal_error("tool outputSchema must be an object", None)
49 })?;
50 tool = tool.with_raw_output_schema(Arc::new(output_schema));
51 }
52 Ok(tool)
53}
54
55pub fn tool_from_descriptor(
64 name: impl Into<Cow<'static, str>>,
65 description: Option<String>,
66 input_schema: Option<Value>,
67 output_schema: Option<Value>,
68 destructive: bool,
69) -> Tool {
70 let mut tool = Tool::new_with_raw(
71 name.into(),
72 description.map(Cow::Owned),
73 schema_object(input_schema),
74 );
75 if let Some(output_schema) = schema_object_opt(output_schema) {
76 tool = tool.with_raw_output_schema(output_schema);
77 }
78 tool.with_annotations(ToolAnnotations::new().destructive(destructive))
79}
80
81pub fn resource_from_descriptor(uri: impl Into<String>, name: impl Into<String>) -> Resource {
83 Resource::new(uri.into(), name.into())
84}
85
86pub fn prompt_from_descriptor(name: impl Into<String>, description: Option<&str>) -> Prompt {
89 Prompt::new(name.into(), description, None)
90}
91
92fn schema_object(value: Option<Value>) -> Arc<Map<String, Value>> {
93 schema_object_opt(value).unwrap_or_else(|| {
94 Arc::new(Map::from_iter([(
95 "type".to_owned(),
96 Value::String("object".to_owned()),
97 )]))
98 })
99}
100
101fn schema_object_opt(value: Option<Value>) -> Option<Arc<Map<String, Value>>> {
102 match value {
103 Some(Value::Object(map)) => Some(Arc::new(map)),
104 Some(other) => {
105 tracing::warn!(
111 schema.type = %schema_type_name(&other),
112 "MCP tool schema is not a JSON object; dropping it"
113 );
114 None
115 }
116 None => None,
117 }
118}
119
120fn schema_type_name(value: &Value) -> &'static str {
121 match value {
122 Value::Null => "null",
123 Value::Bool(_) => "bool",
124 Value::Number(_) => "number",
125 Value::String(_) => "string",
126 Value::Array(_) => "array",
127 Value::Object(_) => "object",
128 }
129}
130
131#[cfg(test)]
132#[path = "protocol_tests.rs"]
133mod tests;