Skip to main content

xyz_rust/mcp/
handler.rs

1// 服务器实现:rmcp 3.1.x 的 ServerHandler trait。每条注册命令一个工具;
2// 版本「钉定」经 supported_protocol_versions 交给 SDK 的协商管线(它同时
3// 约束 discover、initialize 与每次请求的版本校验——比 Go 版 handler 内
4// 逐请求检查更彻底,语义等价)。
5
6use 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    /// initialize 结果声明的协议版本:钉定集内最新,否则
34    /// LATEST_WITH_INITIALIZE(见 build 内说明)。
35    info_version: ProtocolVersion,
36    pub(crate) ctx: Arc<Ctx>,
37    defaults: std::collections::HashMap<String, String>,
38    /// 结果 _meta.xyz 的上下文(§12.8):None = 关闭(no_server_meta)。
39    meta_ctx: Option<MetaCtx>,
40}
41
42/// _meta.xyz 的静态身份分量(每个结果复用)。
43#[derive(Clone)]
44struct MetaCtx {
45    app_name: String,
46    app_version: String,
47    headers: Vec<(String, String)>,
48}
49
50/// 构建服务器实现。未登记的协议版本在注册期报错(与「注册期即报错」
51/// 一致)。
52pub 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    // initialize 结果只能命名「仍带 initialize 握手」的版本,且不应超出
86    // --versions 钉定集——rmcp 的协商兜底直接采用该值、不核对成员资格,
87    // 钉定集外的值会「漏」给客户端(钉 2024-11-05 仍答 2025-11-25 的根因)。
88    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; // 通道层面整体移除;daemon 只属于 CLI
101        }
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        // outputSchema 顶层必须是 object(MCP:给了 schema 就必须提供合规的
109        // structuredContent,而数组/标量 schema 不可能存在合规结果)——
110        // 非 object 的结果类型不发射 schema,结果只走 textContent(ddc#9)。
111        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        // spec §12.4:描述可经 MCPHints.description 覆写,否则 summary+
124        // description 合并;title 经 hints 或 title:… 注解写入
125        // annotations.title;hints.meta 并入工具的保留 _meta。
126        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        // serverInfo 与 _meta 用同一身份(§12.6/§12.8;impl_name 已解析
165        // basename / 默认版本)。
166        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        // MCP 要求支持 tools 的服务器 MUST 声明 tools 能力,否则按能力协商
193        // 的客户端不发 tools/list、工具面显示为 0;工具集构建期固定且不发
194        // list_changed 通知,故不启用 enable_tool_list_changed。
195        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        // 版本门(对齐 Go handler 内的 allowed 检查;SDK 侧已先校验,
235        // 这里兜自定义子集)。
236        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        // 接口默认值只补「客户端未提供」的键;显式入参优先(与 CLI/HTTP
258        // 一致),不覆盖调用方传来的值。
259        for (k, v) in entry.mcp_defaults() {
260            if !arguments.contains_key(&k) {
261                arguments.insert(k, v);
262            }
263        }
264        // 通道级默认参数(--default k=v):只补缺席键。
265        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                // §8.6 + §12.8:失败结果带共享错误体投影的 _meta.xyz.error。
283                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    /// 单次调用结果的 _meta.xyz(§12.8);关闭时 None。
295    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
313/// 构造 _meta.xyz 服务器上下文(spec §12.8,形状对齐 HTTP §11.6 头):
314/// 应用身份 + xyz 库版本 + 命令 + 耗时 + 可选自定义头与错误上下文
315/// (kind/code/detail,让 MCP 客户端无需解析文本即可分支)。
316pub(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
356/// 成功结果 → CallToolResponse:保留块信封原样透传 Content 块
357/// (spec §12.7);其余走 §12.5 的双内容(文本 + structuredContent)。
358pub(super) fn success_response(
359    out: serde_json::Value,
360    meta: Option<MetaObject>,
361    structured: bool,
362) -> CallToolResponse {
363    // structuredContent 必须是 JSON object(MCP 规范),且该字段与
364    // outputSchema 自 2025-06-18 修订才入规——不满足时只走 textContent
365    // (ddc#9:数组/标量结果此前发裸值,严格客户端直接拒绝)。
366    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
382/// outputSchema / structuredContent 自 2025-06-18 修订入规;更早的协商版本
383/// 一律不发射(`--versions` 钉旧版因此是有效的逃生门)。
384pub(super) fn supports_structured(client: Option<&ProtocolVersion>) -> bool {
385    client.is_none_or(|v| *v >= ProtocolVersion::V_2025_06_18)
386}
387
388/// tools/list 整形:客户端协商版本 < 2025-06-18 时逐请求剥离
389/// outputSchema(工具表构建期只带合规 schema)。
390pub(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
407/// 块信封 → rmcp Content:协议级内容块保真透传(spec §12.7)。
408fn 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
423/// MCP 工具名:MCPHints.name 覆写优先(spec §12.4a),否则点分注册名。
424fn 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() // Go impl 同款默认
473    } else {
474        opts.version.clone()
475    };
476    (name, version)
477}