Expand description
Test utilities for MCP servers.
This module provides TestClient, an ergonomic wrapper around McpRouter
for writing concise MCP server tests without manual JSON-RPC construction.
§Quick Start
use tower_mcp::{CallToolResult, McpRouter, ToolBuilder, TestClient};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Deserialize, JsonSchema)]
struct EchoInput {
message: String,
}
let echo = ToolBuilder::new("echo")
.description("Echo a message")
.handler(|input: EchoInput| async move {
Ok(CallToolResult::text(input.message))
})
.build();
let router = McpRouter::new()
.server_info("test-server", "1.0.0")
.tool(echo);
let mut client = TestClient::from_router(router);
client.initialize().await;
let result = client.call_tool("echo", json!({"message": "hello"})).await;
assert_eq!(result.all_text(), "hello");§Full Example
The following shows a complete test setup with tools, resources, and prompts:
use std::collections::HashMap;
use tower_mcp::{
CallToolResult, GetPromptResult, McpRouter, ReadResourceResult,
PromptBuilder, ResourceBuilder, TestClient, ToolBuilder,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Deserialize, JsonSchema)]
struct AddInput {
a: i64,
b: i64,
}
// -- Build the router ------------------------------------------------
let add = ToolBuilder::new("add")
.description("Add two numbers")
.handler(|input: AddInput| async move {
Ok(CallToolResult::text(format!("{}", input.a + input.b)))
})
.build();
let readme = ResourceBuilder::new("file:///README.md")
.name("README")
.description("Project readme")
.text("# My Project");
let greet = PromptBuilder::new("greet")
.description("Greet someone")
.required_arg("name", "Name to greet")
.handler(|args: HashMap<String, String>| async move {
let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
Ok(GetPromptResult::user_message(
format!("Please greet {} warmly.", name),
))
})
.build();
let router = McpRouter::new()
.server_info("test-server", "1.0.0")
.tool(add)
.resource(readme)
.prompt(greet);
// -- Create client and initialize ------------------------------------
let mut client = TestClient::from_router(router);
let init = client.initialize().await;
assert!(init.get("protocolVersion").is_some());
// -- Tools -----------------------------------------------------------
let tools = client.list_tools().await;
assert_eq!(tools.len(), 1);
let result = client.call_tool("add", json!({"a": 2, "b": 3})).await;
assert_eq!(result.all_text(), "5");
assert_eq!(result.first_text(), Some("5"));
assert!(!result.is_error);
// -- Resources -------------------------------------------------------
let resources = client.list_resources().await;
assert_eq!(resources.len(), 1);
let readme = client.read_resource("file:///README.md").await;
assert_eq!(readme.first_text(), Some("# My Project"));
assert_eq!(readme.first_uri(), Some("file:///README.md"));
// -- Prompts ---------------------------------------------------------
let prompts = client.list_prompts().await;
assert_eq!(prompts.len(), 1);
let mut args = HashMap::new();
args.insert("name".to_string(), "Alice".to_string());
let prompt = client.get_prompt("greet", args).await;
assert!(prompt.first_message_text().unwrap().contains("Alice"));
// -- Error handling --------------------------------------------------
// Expect a JSON-RPC error for a non-existent tool:
let error = client
.call_tool_expect_error("nonexistent", json!({}))
.await;
assert!(error.get("code").is_some());
// Expect a JSON-RPC error for an unknown method:
let error = client
.send_request_expect_error("unknown/method", None)
.await;
assert_eq!(error.get("code").and_then(|v| v.as_i64()), Some(-32601));
// -- Raw escape hatch ------------------------------------------------
// Use send_request for methods without typed helpers:
let pong = client.send_request("ping", None).await;
assert_eq!(pong, json!({}));Structs§
- Test
Client - An ergonomic test client for MCP servers.