1pub mod instructions;
14
15pub use axum;
17pub use tokio;
18
19use axum::{routing::get, Json, Router};
20use std::net::SocketAddr;
21use std::sync::Arc;
22
23#[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
48pub 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 pub fn tool(mut self, tool: Tool) -> Self {
89 self.tools.push(tool);
90 self
91 }
92
93 pub fn tools(mut self, tools: impl IntoIterator<Item = Tool>) -> Self {
95 self.tools.extend(tools);
96 self
97 }
98
99 pub fn merge(mut self, router: Router) -> Self {
103 self.extra = self.extra.merge(router);
104 self
105 }
106
107 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 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}