yggdrasil/mcp/server.rs
1//! MCP 服务器:rmcp `ServerHandler` 实现,组合所有工具组。
2//!
3//! 装配模式(rmcp `tool_router` 宏支持的「命名路由 + 组合」):
4//! - 每个工具组在各自文件(`tools/<x>.rs`)里用
5//! `#[tool_router(router = <x>_router, vis = "pub")] impl YggMcpServer { ... }`
6//! 生成一个返回 `ToolRouter<YggMcpServer>` 的公开函数。
7//! - 本文件定义 `YggMcpServer` 结构体,并在 `#[tool_handler(router = ...)]`
8//! 里把所有路由用 `+` 合并成单一 `ServerHandler`。
9//!
10//! 所有工具都在 `YggMcpServer` 上 `impl`,故 `ToolRouter` 的类型参数一致,
11//! 可用 `+`(`Add` for `ToolRouter<S>`)合并。鉴权与作用域校验在各工具内部
12//! 经 `Extension<http::request::Parts>` 读取 `McpPrincipal` 完成。
13
14use rmcp::{tool_handler, ServerHandler};
15
16/// MCP 服务器实例(无状态:每个请求由 service_factory 新建一份)。
17///
18/// 共享状态(DB 连接等)通过 `get_conn()` 全局池获取,无需在实例里持有。
19/// 工具方法分散在各 `tools/*.rs` 的 `impl YggMcpServer` 块里。
20#[derive(Clone, Default)]
21pub struct YggMcpServer;
22
23impl YggMcpServer {
24 /// 组合所有工具组的路由表。新增工具组时在此追加一行。
25 fn combined_router() -> rmcp::handler::server::router::tool::ToolRouter<Self> {
26 YggMcpServer::read_router()
27 + YggMcpServer::posts_router()
28 + YggMcpServer::notes_router()
29 + YggMcpServer::comments_router()
30 + YggMcpServer::tags_router()
31 + YggMcpServer::media_router()
32 + YggMcpServer::settings_router()
33 + YggMcpServer::runner_router()
34 }
35}
36
37/// 单一 `ServerHandler`:工具调度委托给合并后的路由表。
38/// `router = Self::combined_router()` 让 `tool_handler` 宏生成的
39/// `call_tool`/`list_tools`/`get_tool` 全部走组合路由。
40#[tool_handler(router = Self::combined_router())]
41impl ServerHandler for YggMcpServer {
42 // get_info 由宏自动生成(name/version 来自宏的默认或属性)。
43 // 这里不手写 get_info,让宏用默认 ServerInfo。
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 /// OpenAI 兼容上游(MCA 等)要求 object schema 必须带 `properties` 键。
51 /// 零字段参数结构体在 schemars 1.x 下默认只生成 `{"type":"object"}`,
52 /// 会让上游把整个请求判为非法工具 schema(流式路径甚至只回 502)。
53 #[test]
54 fn every_tool_schema_declares_object_properties() {
55 for tool in YggMcpServer::combined_router().list_all() {
56 let schema = serde_json::to_value(&tool.input_schema).unwrap();
57 assert_eq!(
58 schema.get("type").and_then(|t| t.as_str()),
59 Some("object"),
60 "tool `{}` should have an object input schema: {schema}",
61 tool.name
62 );
63 assert!(
64 schema.get("properties").is_some_and(|p| p.is_object()),
65 "tool `{}` input schema must declare `properties`: {schema}",
66 tool.name
67 );
68 }
69 }
70}