Skip to main content

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}