Skip to main content

xtap_core_lib/mcp/
mod.rs

1/*
2 * mcp/mod.rs
3 * Project: core_lib
4 * Description: 通用 MCP HTTP 服务框架(AI Shell 的“给 AI 用”这张脸)
5 *
6 * 设计(路由挂载模式抄 sts-x src/server/mod.rs:79-93):
7 * - McpServer 负责自动生成 /health、/tools、/ 三个“壳层”端点(工具发现 + 自解释)
8 * - app 只需 .tool(...) 声明工具 Schema,再 .merge(自有业务 Router) 挂载 /search 等 handler
9 * - 重新导出 axum / tokio,下游 crate 无需再直接依赖 HTTP 框架(去重,见白皮书 §2)
10 * - 全部在 `mcp` feature 下编译;不开该 feature 时 axum/tokio 完全不进依赖图
11 */
12
13pub mod instructions;
14
15// 重新导出,供下游 crate(如 sts-x)直接使用,避免各自再声明 axum/tokio 依赖。
16pub use axum;
17pub use tokio;
18
19use axum::{routing::get, Json, Router};
20use std::net::SocketAddr;
21use std::sync::Arc;
22
23/// 一个 MCP 工具的自描述(用于 GET /tools 工具发现)。
24///
25/// `input_schema` 使用 JSON Schema(object/properties/required),格式对齐
26/// sts-x src/server/mod.rs:271-354 的 handle_tools 输出。
27#[derive(Clone, serde::Serialize)]
28pub struct Tool {
29    pub name: String,
30    pub description: String,
31    pub input_schema: serde_json::Value,
32}
33
34impl Tool {
35    pub fn new(
36        name: impl Into<String>,
37        description: impl Into<String>,
38        input_schema: serde_json::Value,
39    ) -> Self {
40        Self {
41            name: name.into(),
42            description: description.into(),
43            input_schema,
44        }
45    }
46}
47
48/// 通用 MCP 服务框架。
49///
50/// 典型用法(app 侧):
51/// ```ignore
52/// let biz = Router::new()
53///     .route("/search", post(handle_search))
54///     .route("/file", post(handle_file))
55///     .with_state(state);            // 先把 state 解析成 Router<()>
56///
57/// McpServer::new("sts-x", env!("CARGO_PKG_VERSION"), HINT)
58///     .tool(Tool::new("search", "...", schema))
59///     .tool(Tool::new("file",   "...", schema))
60///     .merge(biz)                     // 挂载业务路由
61///     .serve("127.0.0.1", 9876)
62///     .await?;
63/// ```
64pub struct McpServer {
65    service: String,
66    version: String,
67    instructions: String,
68    tools: Vec<Tool>,
69    extra: Router,
70}
71
72impl McpServer {
73    pub fn new(
74        service: impl Into<String>,
75        version: impl Into<String>,
76        instructions: impl Into<String>,
77    ) -> Self {
78        Self {
79            service: service.into(),
80            version: version.into(),
81            instructions: instructions.into(),
82            tools: Vec::new(),
83            extra: Router::new(),
84        }
85    }
86
87    /// 声明一个工具(追加到 /tools 工具发现列表)。
88    pub fn tool(mut self, tool: Tool) -> Self {
89        self.tools.push(tool);
90        self
91    }
92
93    /// 批量声明工具。
94    pub fn tools(mut self, tools: impl IntoIterator<Item = Tool>) -> Self {
95        self.tools.extend(tools);
96        self
97    }
98
99    /// 合并 app 自有业务路由(如 /search /file)。
100    ///
101    /// 传入的 `Router` 需已 `.with_state(...)` 解析为 `Router<()>`。
102    pub fn merge(mut self, router: Router) -> Self {
103        self.extra = self.extra.merge(router);
104        self
105    }
106
107    /// 组装成最终 axum Router(自动挂载 /health、/tools、/ 后再合并业务路由)。
108    pub fn into_router(self) -> Router {
109        let health = Arc::new(serde_json::json!({
110            "status": "ok",
111            "version": self.version,
112            "service": self.service,
113        }));
114        let tools_val = Arc::new(serde_json::json!({ "tools": self.tools }));
115        let tool_names: Vec<&str> = self.tools.iter().map(|t| t.name.as_str()).collect();
116        let root = Arc::new(serde_json::json!({
117            "service": self.service,
118            "version": self.version,
119            "_ai_instructions": self.instructions,
120            "tools": tool_names,
121            "endpoints": {
122                "/health": "GET — service health",
123                "/tools": "GET — tool discovery (MCP schema)",
124            },
125        }));
126
127        Router::new()
128            .route(
129                "/health",
130                get({
131                    let v = health.clone();
132                    move || {
133                        let v = v.clone();
134                        async move { Json((*v).clone()) }
135                    }
136                }),
137            )
138            .route(
139                "/tools",
140                get({
141                    let v = tools_val.clone();
142                    move || {
143                        let v = v.clone();
144                        async move { Json((*v).clone()) }
145                    }
146                }),
147            )
148            .route(
149                "/",
150                get({
151                    let v = root.clone();
152                    move || {
153                        let v = v.clone();
154                        async move { Json((*v).clone()) }
155                    }
156                }),
157            )
158            .merge(self.extra)
159    }
160
161    /// 启动 HTTP 服务(调用方需已在 tokio runtime 内,如 `#[tokio::main]`)。
162    pub async fn serve(self, host: &str, port: u16) -> anyhow::Result<()> {
163        let app = self.into_router();
164        let addr: SocketAddr = format!("{host}:{port}").parse()?;
165        tracing::info!("MCP server listening on http://{}", addr);
166        let listener = tokio::net::TcpListener::bind(addr).await?;
167        axum::serve(listener, app).await?;
168        Ok(())
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use axum::body::Body;
176    use axum::http::Request;
177    use http_body_util::BodyExt;
178    use tower::ServiceExt;
179
180    #[test]
181    fn test_tool_new() {
182        let tool = Tool::new("search", "全库搜索", serde_json::json!({"type": "object"}));
183        assert_eq!(tool.name, "search");
184        assert_eq!(tool.description, "全库搜索");
185        assert_eq!(tool.input_schema["type"], "object");
186    }
187
188    #[tokio::test]
189    async fn test_into_router_tools_endpoint() {
190        let app = McpServer::new("test-svc", "0.1.0", "hint")
191            .tool(Tool::new(
192                "search",
193                "全库搜索",
194                serde_json::json!({"type": "object"}),
195            ))
196            .into_router();
197
198        let resp = app
199            .oneshot(
200                Request::builder()
201                    .uri("/tools")
202                    .body(Body::empty())
203                    .unwrap(),
204            )
205            .await
206            .expect("oneshot 请求应成功");
207
208        assert_eq!(resp.status(), axum::http::StatusCode::OK);
209        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
210        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
211        let tools = json["tools"].as_array().expect("tools 应为数组");
212        assert_eq!(tools.len(), 1);
213        assert_eq!(tools[0]["name"], "search");
214    }
215}