pub mod instructions;
pub use axum;
pub use tokio;
use axum::{routing::get, Json, Router};
use std::net::SocketAddr;
use std::sync::Arc;
#[derive(Clone, serde::Serialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
impl Tool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
input_schema: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema,
}
}
}
pub struct McpServer {
service: String,
version: String,
instructions: String,
tools: Vec<Tool>,
extra: Router,
}
impl McpServer {
pub fn new(
service: impl Into<String>,
version: impl Into<String>,
instructions: impl Into<String>,
) -> Self {
Self {
service: service.into(),
version: version.into(),
instructions: instructions.into(),
tools: Vec::new(),
extra: Router::new(),
}
}
pub fn tool(mut self, tool: Tool) -> Self {
self.tools.push(tool);
self
}
pub fn tools(mut self, tools: impl IntoIterator<Item = Tool>) -> Self {
self.tools.extend(tools);
self
}
pub fn merge(mut self, router: Router) -> Self {
self.extra = self.extra.merge(router);
self
}
pub fn into_router(self) -> Router {
let health = Arc::new(serde_json::json!({
"status": "ok",
"version": self.version,
"service": self.service,
}));
let tools_val = Arc::new(serde_json::json!({ "tools": self.tools }));
let tool_names: Vec<&str> = self.tools.iter().map(|t| t.name.as_str()).collect();
let root = Arc::new(serde_json::json!({
"service": self.service,
"version": self.version,
"_ai_instructions": self.instructions,
"tools": tool_names,
"endpoints": {
"/health": "GET — service health",
"/tools": "GET — tool discovery (MCP schema)",
},
}));
Router::new()
.route(
"/health",
get({
let v = health.clone();
move || {
let v = v.clone();
async move { Json((*v).clone()) }
}
}),
)
.route(
"/tools",
get({
let v = tools_val.clone();
move || {
let v = v.clone();
async move { Json((*v).clone()) }
}
}),
)
.route(
"/",
get({
let v = root.clone();
move || {
let v = v.clone();
async move { Json((*v).clone()) }
}
}),
)
.merge(self.extra)
}
pub async fn serve(self, host: &str, port: u16) -> anyhow::Result<()> {
let app = self.into_router();
let addr: SocketAddr = format!("{host}:{port}").parse()?;
tracing::info!("MCP server listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use http_body_util::BodyExt;
use tower::ServiceExt;
#[test]
fn test_tool_new() {
let tool = Tool::new("search", "全库搜索", serde_json::json!({"type": "object"}));
assert_eq!(tool.name, "search");
assert_eq!(tool.description, "全库搜索");
assert_eq!(tool.input_schema["type"], "object");
}
#[tokio::test]
async fn test_into_router_tools_endpoint() {
let app = McpServer::new("test-svc", "0.1.0", "hint")
.tool(Tool::new(
"search",
"全库搜索",
serde_json::json!({"type": "object"}),
))
.into_router();
let resp = app
.oneshot(
Request::builder()
.uri("/tools")
.body(Body::empty())
.unwrap(),
)
.await
.expect("oneshot 请求应成功");
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let tools = json["tools"].as_array().expect("tools 应为数组");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0]["name"], "search");
}
}