Files
qiming/qiming-mcp-proxy/mcp-sse-proxy/examples/test_mcp_server.rs
2026-06-01 13:03:20 +08:00

253 lines
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 用于集成测试的最小 MCP 服务器
//!
//! 支持 echo 和 counter 两个简单工具,用于测试连接、重连和工具调用功能。
//!
//! 运行方式:
//! ```bash
//! cargo run --example test_mcp_server -p mcp-sse-proxy
//! ```
use rmcp::{
ErrorData, RoleServer, ServerHandler, ServiceExt,
model::{
CallToolRequestParam, CallToolResult, Content, Implementation, JsonObject, ListToolsResult,
PaginatedRequestParam, ProtocolVersion, ServerCapabilities, ServerInfo, Tool,
},
service::RequestContext,
transport::stdio,
};
use std::sync::Arc;
use tokio::sync::Mutex;
/// 测试用 MCP 服务器
///
/// 提供简单工具:
/// - `echo`: 回显输入消息
/// - `increment`: 递增计数器并返回当前值
/// - `reset`: 重置计数器
/// - `get_counter`: 获取当前计数器值
#[derive(Clone)]
pub struct TestMcpServer {
counter: Arc<Mutex<i32>>,
}
impl TestMcpServer {
/// 创建新的测试服务器实例
pub fn new() -> Self {
Self {
counter: Arc::new(Mutex::new(0)),
}
}
/// 创建一个简单的空 schema
fn empty_schema() -> Arc<JsonObject> {
let mut schema = JsonObject::new();
schema.insert("type".to_string(), serde_json::json!("object"));
schema.insert("properties".to_string(), serde_json::json!({}));
Arc::new(schema)
}
/// 创建 echo 工具的 schema
fn echo_schema() -> Arc<JsonObject> {
let mut schema = JsonObject::new();
schema.insert("type".to_string(), serde_json::json!("object"));
schema.insert(
"properties".to_string(),
serde_json::json!({
"message": {
"type": "string",
"description": "Message to echo back"
}
}),
);
schema.insert("required".to_string(), serde_json::json!(["message"]));
Arc::new(schema)
}
/// 获取工具定义列表
fn get_tools() -> Vec<Tool> {
vec![
Tool::new("echo", "Echo back the input message", Self::echo_schema()),
Tool::new(
"increment",
"Increment the counter by 1 and return new value",
Self::empty_schema(),
),
Tool::new("reset", "Reset the counter to 0", Self::empty_schema()),
Tool::new(
"get_counter",
"Get current counter value without changing it",
Self::empty_schema(),
),
]
}
/// 处理 echo 工具调用
async fn handle_echo(&self, args: &serde_json::Value) -> CallToolResult {
let message = args
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("(no message)");
CallToolResult::success(vec![Content::text(format!("Echo: {}", message))])
}
/// 处理 increment 工具调用
async fn handle_increment(&self) -> CallToolResult {
let mut counter = self.counter.lock().await;
*counter += 1;
CallToolResult::success(vec![Content::text(format!("Counter: {}", *counter))])
}
/// 处理 reset 工具调用
async fn handle_reset(&self) -> CallToolResult {
let mut counter = self.counter.lock().await;
*counter = 0;
CallToolResult::success(vec![Content::text("Counter reset to 0".to_string())])
}
/// 处理 get_counter 工具调用
async fn handle_get_counter(&self) -> CallToolResult {
let counter = self.counter.lock().await;
CallToolResult::success(vec![Content::text(format!("Counter: {}", *counter))])
}
}
impl Default for TestMcpServer {
fn default() -> Self {
Self::new()
}
}
impl ServerHandler for TestMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::V_2024_11_05,
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation {
name: "test-mcp-server".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
title: Some("Test MCP Server".to_string()),
website_url: None,
icons: None,
},
instructions: Some(
"A minimal MCP server for integration testing. \
Provides echo, increment, reset, and get_counter tools."
.to_string(),
),
}
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParam>,
_context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, ErrorData> {
Ok(ListToolsResult {
tools: Self::get_tools(),
next_cursor: None,
})
}
async fn call_tool(
&self,
request: CallToolRequestParam,
_context: RequestContext<RoleServer>,
) -> Result<CallToolResult, ErrorData> {
let args = request
.arguments
.as_ref()
.map(|v| serde_json::Value::Object(v.clone()))
.unwrap_or(serde_json::Value::Object(Default::default()));
// 使用 &str 来进行匹配
let tool_name: &str = &request.name;
let result = match tool_name {
"echo" => self.handle_echo(&args).await,
"increment" => self.handle_increment().await,
"reset" => self.handle_reset().await,
"get_counter" => self.handle_get_counter().await,
_ => CallToolResult::error(vec![Content::text(format!(
"Unknown tool: {}",
request.name
))]),
};
Ok(result)
}
}
/// 独立运行时作为 stdio MCP 服务器
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 初始化日志(输出到 stderr避免干扰 stdio 通信)
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();
tracing::info!("Test MCP Server starting...");
let server = TestMcpServer::new();
let transport = stdio();
tracing::info!("Serving on stdio...");
let running = server.serve(transport).await?;
running.waiting().await?;
tracing::info!("Test MCP Server stopped.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_counter_increment() {
let server = TestMcpServer::new();
// 第一次递增
let result = server.handle_increment().await;
assert!(!result.is_error.unwrap_or(false));
// 第二次递增
let result = server.handle_increment().await;
assert!(!result.is_error.unwrap_or(false));
}
#[tokio::test]
async fn test_echo() {
let server = TestMcpServer::new();
let args = serde_json::json!({"message": "hello"});
let result = server.handle_echo(&args).await;
assert!(!result.is_error.unwrap_or(false));
}
#[tokio::test]
async fn test_reset() {
let server = TestMcpServer::new();
// 先递增几次
server.handle_increment().await;
server.handle_increment().await;
// 重置
let result = server.handle_reset().await;
assert!(!result.is_error.unwrap_or(false));
}
#[tokio::test]
async fn test_get_tools() {
let tools = TestMcpServer::get_tools();
assert_eq!(tools.len(), 4);
assert!(tools.iter().any(|t| &*t.name == "echo"));
assert!(tools.iter().any(|t| &*t.name == "increment"));
assert!(tools.iter().any(|t| &*t.name == "reset"));
assert!(tools.iter().any(|t| &*t.name == "get_counter"));
}
}