1use std::borrow::Cow;
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10use rmcp::ErrorData;
11use rmcp::handler::server::ServerHandler;
12use rmcp::model::{
13 CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, Implementation,
14 ListToolsResult, MetaObject, PaginatedRequestParams, ProtocolVersion, ServerCapabilities,
15 ServerConfig, Tool, ToolAnnotations,
16};
17use rmcp::service::{RequestContext, RoleServer};
18
19use crate::ctx::Ctx;
20use crate::errors;
21use crate::mcp::{Options, protocol_version};
22use crate::registry::Registry;
23use crate::spec::entry::Entry;
24
25#[derive(Clone)]
26pub struct XyzServer {
27 impl_info: Implementation,
28 instructions: Option<String>,
29 tools: Vec<Tool>,
30 by_name: Arc<BTreeMap<String, Arc<Entry>>>,
31 pinned: Option<Vec<ProtocolVersion>>,
32 allowed: Option<std::collections::BTreeSet<String>>,
33 info_version: ProtocolVersion,
36 pub(crate) ctx: Arc<Ctx>,
37 defaults: std::collections::HashMap<String, String>,
38 meta_ctx: Option<MetaCtx>,
40}
41
42#[derive(Clone)]
44struct MetaCtx {
45 app_name: String,
46 app_version: String,
47 headers: Vec<(String, String)>,
48}
49
50pub fn build(reg: &Registry, opts: &Options, ctx: Arc<Ctx>) -> errors::Result<XyzServer> {
53 for v in &opts.versions {
54 if v.is_empty() {
55 return Err(errors::Error::new(
56 errors::Kind::Internal,
57 "mcp: empty protocol version in --versions".to_string(),
58 ));
59 }
60 if protocol_version(v).is_none() {
61 return Err(errors::Error::new(
62 errors::Kind::Internal,
63 format!(
64 "mcp: unknown protocol version {v:?} (known: {})",
65 crate::mcp::DEFAULT_VERSIONS.join(", ")
66 ),
67 ));
68 }
69 }
70 let pinned: Option<Vec<ProtocolVersion>> = if opts.versions.is_empty() {
71 None
72 } else {
73 Some(
74 opts.versions
75 .iter()
76 .filter_map(|v| protocol_version(v))
77 .collect(),
78 )
79 };
80 let allowed: Option<std::collections::BTreeSet<String>> = if opts.versions.is_empty() {
81 None
82 } else {
83 Some(opts.versions.iter().cloned().collect())
84 };
85 let info_version = pinned
89 .as_ref()
90 .and_then(|p| p.iter().max_by(|a, b| a.as_str().cmp(b.as_str())).cloned())
91 .unwrap_or(ProtocolVersion::LATEST_WITH_INITIALIZE);
92 let (name, version) = impl_name(opts);
93 let (meta_name, meta_version) = (name.clone(), version.clone());
94 let impl_info = Implementation::new(name, version);
95
96 let mut tools = Vec::new();
97 let mut by_name = BTreeMap::new();
98 for e in reg.all() {
99 if e.mcp.skip || e.cli.daemon {
100 continue; }
102 let input_schema = Arc::new(
103 crate::spec::schema::schema_to_value(&e.input_schema)
104 .as_object()
105 .cloned()
106 .unwrap_or_default(),
107 );
108 let output_schema = e
112 .output_schema
113 .as_ref()
114 .filter(|s| s.r#type.as_deref() == Some("object"))
115 .map(|s| {
116 Arc::new(
117 crate::spec::schema::schema_to_value(s)
118 .as_object()
119 .cloned()
120 .unwrap_or_default(),
121 )
122 });
123 let desc = if e.mcp.description.is_empty() {
127 tool_description(&e)
128 } else {
129 e.mcp.description.clone()
130 };
131 let mut tool = Tool::new_with_raw(
132 tool_name(&e),
133 Some(std::borrow::Cow::Owned(desc)),
134 input_schema,
135 );
136 if let Some(out) = output_schema {
137 tool = tool.with_raw_output_schema(out);
138 }
139 match parse_annotations(&e) {
140 Some(mut ann) => {
141 if !e.mcp.title.is_empty() {
142 ann.title = Some(e.mcp.title.clone());
143 }
144 tool = tool.with_annotations(ann);
145 }
146 None => {
147 if !e.mcp.title.is_empty() {
148 let mut ann = ToolAnnotations::default();
149 ann.title = Some(e.mcp.title.clone());
150 tool = tool.with_annotations(ann);
151 }
152 }
153 }
154 if !e.mcp.meta.is_empty() {
155 tool.meta = Some(MetaObject(e.mcp.meta.clone()));
156 }
157 tools.push(tool);
158 by_name.insert(tool_name(&e), e);
159 }
160
161 let meta_ctx = if opts.no_server_meta {
162 None
163 } else {
164 Some(MetaCtx {
167 app_name: meta_name,
168 app_version: meta_version,
169 headers: opts.response_headers.clone(),
170 })
171 };
172 Ok(XyzServer {
173 impl_info,
174 instructions: if opts.instructions.is_empty() {
175 None
176 } else {
177 Some(opts.instructions.clone())
178 },
179 tools,
180 by_name: Arc::new(by_name),
181 pinned,
182 allowed,
183 info_version,
184 ctx,
185 defaults: opts.defaults.clone(),
186 meta_ctx,
187 })
188}
189
190impl ServerHandler for XyzServer {
191 fn get_info(&self) -> ServerConfig {
192 let mut info = ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
196 .with_server_info(self.impl_info.clone())
197 .with_protocol_version(self.info_version.clone());
198 if let Some(ins) = &self.instructions {
199 info = info.with_instructions(ins.clone());
200 }
201 info
202 }
203
204 fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
205 match &self.pinned {
206 Some(p) => Cow::Owned(p.clone()),
207 None => Cow::Borrowed(ProtocolVersion::KNOWN_VERSIONS),
208 }
209 }
210
211 async fn list_tools(
212 &self,
213 _request: Option<PaginatedRequestParams>,
214 context: RequestContext<RoleServer>,
215 ) -> Result<ListToolsResult, ErrorData> {
216 Ok(ListToolsResult::with_all_items(shape_tools(
217 self.tools.clone(),
218 context.protocol_version().as_ref(),
219 )))
220 }
221
222 fn get_tool(&self, name: &str) -> Option<Tool> {
223 self.tools
224 .iter()
225 .find(|t| t.name == name && self.by_name.contains_key(name))
226 .cloned()
227 }
228
229 async fn call_tool(
230 &self,
231 request: CallToolRequestParams,
232 context: RequestContext<RoleServer>,
233 ) -> Result<CallToolResponse, ErrorData> {
234 let pv = context.protocol_version().map(|v| v.as_str().to_string());
237 if let (Some(allowed), Some(pv)) = (&self.allowed, pv)
238 && !allowed.contains(&pv)
239 {
240 let e = self
241 .by_name
242 .get(request.name.as_ref())
243 .map(|e| e.name.clone())
244 .unwrap_or_else(|| request.name.to_string());
245 return Ok(CallToolResponse::Complete(CallToolResult::error(vec![
246 ContentBlock::text(format!(
247 "tool {e:?}: protocol version {pv:?} is not enabled on this server"
248 )),
249 ])));
250 }
251 let Some(entry) = self.by_name.get(request.name.as_ref()) else {
252 return Err(ErrorData::method_not_found::<
253 rmcp::model::CallToolRequestMethod,
254 >());
255 };
256 let mut arguments: crate::spec::JsonMap = request.arguments.unwrap_or_default();
257 for (k, v) in entry.mcp_defaults() {
260 if !arguments.contains_key(&k) {
261 arguments.insert(k, v);
262 }
263 }
264 for (k, v) in &self.defaults {
266 if !arguments.contains_key(k) {
267 arguments.insert(k.clone(), serde_json::Value::String(v.clone()));
268 }
269 }
270 let started = std::time::Instant::now();
271 match (entry.invoke)(&self.ctx, &arguments) {
272 Ok(out) => {
273 let meta = self.meta_for(&entry.name, started.elapsed(), None);
274 let structured = supports_structured(context.protocol_version().as_ref());
275 Ok(success_response(out, meta, structured))
276 }
277 Err(e) => {
278 let msg = match e.cause() {
279 Some(c) => c.to_string(),
280 None => e.to_string(),
281 };
282 let body = errors::error_body(&e);
284 let meta = self.meta_for(&entry.name, started.elapsed(), Some(&body));
285 let mut result = CallToolResult::error(vec![ContentBlock::text(msg)]);
286 result.meta = meta;
287 Ok(CallToolResponse::Complete(result))
288 }
289 }
290 }
291}
292
293impl XyzServer {
294 fn meta_for(
296 &self,
297 command: &str,
298 duration: std::time::Duration,
299 err: Option<&errors::Body>,
300 ) -> Option<MetaObject> {
301 let ctx = self.meta_ctx.as_ref()?;
302 Some(result_meta(
303 &ctx.app_name,
304 &ctx.app_version,
305 &ctx.headers,
306 command,
307 duration.as_millis(),
308 err,
309 ))
310 }
311}
312
313pub(super) fn result_meta(
317 app_name: &str,
318 app_version: &str,
319 headers: &[(String, String)],
320 command: &str,
321 duration_ms: u128,
322 err: Option<&errors::Body>,
323) -> MetaObject {
324 let mut xyz = serde_json::Map::new();
325 xyz.insert("app_name".into(), serde_json::json!(app_name));
326 xyz.insert("app_version".into(), serde_json::json!(app_version));
327 xyz.insert(
328 "sdk_version".into(),
329 serde_json::json!(crate::version::SDK_VERSION),
330 );
331 xyz.insert("command".into(), serde_json::json!(command));
332 xyz.insert("duration_ms".into(), serde_json::json!(duration_ms));
333 if !headers.is_empty() {
334 let mut hs = serde_json::Map::new();
335 for (k, v) in headers {
336 hs.insert(k.clone(), serde_json::Value::String(v.clone()));
337 }
338 xyz.insert("headers".into(), serde_json::Value::Object(hs));
339 }
340 if let Some(b) = err {
341 let mut em = serde_json::Map::new();
342 em.insert("kind".into(), serde_json::json!(b.kind.as_str()));
343 if let Some(code) = &b.code {
344 em.insert("code".into(), serde_json::json!(code));
345 }
346 if let Some(detail) = &b.detail {
347 em.insert("detail".into(), serde_json::Value::Object(detail.clone()));
348 }
349 xyz.insert("error".into(), serde_json::Value::Object(em));
350 }
351 let mut root = serde_json::Map::new();
352 root.insert("xyz".into(), serde_json::Value::Object(xyz));
353 MetaObject(root)
354}
355
356pub(super) fn success_response(
359 out: serde_json::Value,
360 meta: Option<MetaObject>,
361 structured: bool,
362) -> CallToolResponse {
363 let structured = structured && out.is_object();
367 if let Some(blocks) = crate::blocks::extract(&out) {
368 let mut result = CallToolResult::error(to_rmcp_blocks(&blocks));
369 result.is_error = None;
370 result.structured_content = structured.then_some(out);
371 result.meta = meta;
372 return CallToolResponse::Complete(result);
373 }
374 let text = render_text(&out);
375 let mut result = CallToolResult::error(vec![ContentBlock::text(text)]);
376 result.is_error = None;
377 result.structured_content = structured.then_some(out);
378 result.meta = meta;
379 CallToolResponse::Complete(result)
380}
381
382pub(super) fn supports_structured(client: Option<&ProtocolVersion>) -> bool {
385 client.is_none_or(|v| *v >= ProtocolVersion::V_2025_06_18)
386}
387
388pub(super) fn shape_tools(mut tools: Vec<Tool>, client: Option<&ProtocolVersion>) -> Vec<Tool> {
391 if !supports_structured(client) {
392 for t in &mut tools {
393 t.output_schema = None;
394 }
395 }
396 tools
397}
398
399fn render_text(v: &serde_json::Value) -> String {
400 let mut buf = Vec::new();
401 if crate::cli::render::render_value(&mut buf, v).is_err() {
402 return v.to_string();
403 }
404 String::from_utf8_lossy(&buf).trim_end().to_string()
405}
406
407fn to_rmcp_blocks(blocks: &[crate::blocks::Block]) -> Vec<ContentBlock> {
409 blocks
410 .iter()
411 .map(|b| match b {
412 crate::blocks::Block::Text { text } => ContentBlock::text(text.clone()),
413 crate::blocks::Block::Image { mime_type, data } => {
414 ContentBlock::image(data.clone(), mime_type.clone())
415 }
416 crate::blocks::Block::Audio { mime_type, data } => {
417 ContentBlock::audio(data.clone(), mime_type.clone())
418 }
419 })
420 .collect()
421}
422
423fn tool_name(e: &Entry) -> String {
425 if e.mcp.name.is_empty() {
426 e.name.clone()
427 } else {
428 e.mcp.name.clone()
429 }
430}
431
432fn tool_description(e: &Entry) -> String {
433 if !e.summary.is_empty() && !e.description.is_empty() {
434 format!("{}\n\n{}", e.summary, e.description)
435 } else if !e.description.is_empty() {
436 e.description.clone()
437 } else {
438 e.summary.clone()
439 }
440}
441
442fn parse_annotations(e: &Entry) -> Option<ToolAnnotations> {
443 if e.mcp.annotations.is_empty() {
444 return None;
445 }
446 let mut ann = ToolAnnotations::default();
447 for a in &e.mcp.annotations {
448 let (key, val) = match a.split_once(':') {
449 Some((k, v)) => (k, v.trim()),
450 None => (a.as_str(), ""),
451 };
452 match key.trim().to_ascii_lowercase().as_str() {
453 "read" => ann.read_only_hint = Some(true),
454 "write" => ann.read_only_hint = Some(false),
455 "destructive" => ann.destructive_hint = Some(true),
456 "idempotent" => ann.idempotent_hint = Some(true),
457 "openworld" => ann.open_world_hint = Some(true),
458 "title" => ann.title = Some(val.to_string()),
459 _ => {}
460 }
461 }
462 Some(ann)
463}
464
465fn impl_name(opts: &Options) -> (String, String) {
466 let name = if opts.name.is_empty() {
467 crate::cli::app::bin_name()
468 } else {
469 opts.name.clone()
470 };
471 let version = if opts.version.is_empty() {
472 "0.0.0".to_string() } else {
474 opts.version.clone()
475 };
476 (name, version)
477}