mod common;
use anyhow::Result;
use common::McpTestClient;
use serde_json::json;
use std::io::Write;
#[test]
fn test_initialize() -> Result<()> {
let mut client = McpTestClient::new()?;
let response = client.call(
"initialize",
json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "1.0" }
}),
1,
)?;
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["id"], 1);
assert_eq!(response["result"]["protocolVersion"], "2024-11-05");
assert!(response["result"]["serverInfo"]["name"].is_string());
let capabilities = response["result"]["capabilities"]
.as_object()
.expect("capabilities object");
assert!(
capabilities.get("tools").is_some(),
"capabilities should include tools"
);
assert!(
capabilities.get("prompts").is_some(),
"capabilities should include prompts"
);
let notification = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
});
writeln!(client.stdin, "{notification}")?;
client.stdin.flush()?;
Ok(())
}
#[test]
fn test_tools_list() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
let response = client.call("tools/list", json!({}), 2)?;
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["id"], 2);
let tools = response["result"]["tools"].as_array().unwrap();
assert!(!tools.is_empty());
let tool_names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
assert!(tool_names.contains(&"semantic_search"));
assert!(tool_names.contains(&"relation_query"));
assert!(tool_names.contains(&"explain_code"));
assert!(tool_names.contains(&"search_similar"));
assert!(tool_names.contains(&"show_dependencies"));
assert!(tool_names.contains(&"get_index_status"));
assert!(tool_names.contains(&"export_graph"));
assert!(tool_names.contains(&"cross_language_edges"));
for tool in tools {
assert!(tool["name"].is_string());
assert!(tool["description"].is_string());
assert!(tool["inputSchema"]["type"].is_string());
assert!(tool["inputSchema"]["properties"].is_object());
let schema_obj = tool["inputSchema"]
.as_object()
.expect("inputSchema must be an object");
assert!(
!schema_obj.contains_key("oneOf"),
"inputSchema must not include oneOf at the top level (Claude API restriction)"
);
assert!(
!schema_obj.contains_key("anyOf"),
"inputSchema must not include anyOf at the top level (Claude API restriction)"
);
assert!(
!schema_obj.contains_key("allOf"),
"inputSchema must not include allOf at the top level (Claude API restriction)"
);
}
Ok(())
}
#[test]
fn test_unknown_method() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
client.send_request("unknown/method", json!({}), 3)?;
let error_resp = client.read_response()?;
assert_eq!(error_resp["jsonrpc"], "2.0");
assert_eq!(error_resp["id"], 3);
assert_eq!(error_resp["error"]["code"], -32601);
let response = client.call("tools/list", json!({}), 30)?;
assert_eq!(response["id"], 30);
Ok(())
}
#[test]
fn test_invalid_jsonrpc() -> Result<()> {
let mut client = McpTestClient::new()?;
let request = json!({
"jsonrpc": "1.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "1.0" }
},
"id": 4
});
writeln!(client.stdin, "{request}")?;
client.stdin.flush()?;
let response = client.read_response();
match response {
Ok(value) => {
assert_eq!(value["jsonrpc"], "2.0");
assert_eq!(value["id"], 4);
assert!(value["error"].is_object());
assert_eq!(value["error"]["code"], -32600); }
Err(err) => {
let message = err.to_string();
assert!(
message.contains("EOF while parsing a value"),
"unexpected error: {message}"
);
}
}
Ok(())
}
#[test]
fn test_parse_error() -> Result<()> {
let mut client = McpTestClient::new()?;
writeln!(client.stdin, "{{invalid json}}")?;
client.stdin.flush()?;
let response = client.read_response();
match response {
Ok(value) => {
assert_eq!(value["jsonrpc"], "2.0");
assert!(value["error"].is_object());
assert_eq!(value["error"]["code"], -32700); assert!(value["id"].is_null());
}
Err(err) => {
let message = err.to_string();
assert!(
message.contains("EOF while parsing a value"),
"unexpected error: {message}"
);
}
}
Ok(())
}
#[test]
fn test_request_id_propagation() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
let response1 = client.call("tools/list", json!({}), 100)?;
assert_eq!(response1["id"], 100);
let request = json!({
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": "test-id-123"
});
writeln!(client.stdin, "{request}")?;
client.stdin.flush()?;
let response2 = client.read_response()?;
assert_eq!(response2["id"], "test-id-123");
Ok(())
}
#[test]
fn test_missing_tool_name() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
client.send_request(
"tools/call",
json!({
"arguments": {}
}),
5,
)?;
let error_resp = client.read_response()?;
assert_eq!(error_resp["id"], 5);
assert!(error_resp["error"].is_object());
let response = client.call("tools/list", json!({}), 100)?;
assert_eq!(response["id"], 100);
Ok(())
}
#[test]
fn test_unknown_tool() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
let response = client.call(
"tools/call",
json!({
"name": "nonexistent_tool",
"arguments": {}
}),
6,
)?;
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["id"], 6);
assert!(response["error"].is_object());
assert_eq!(response["error"]["code"], -32602);
let message = response["error"]["message"].as_str().unwrap_or("");
assert!(message.contains("tool") && message.contains("not"));
Ok(())
}
#[test]
fn test_notification_no_response() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
let notification = json!({
"jsonrpc": "2.0",
"method": "notifications/test",
"params": {}
});
writeln!(client.stdin, "{notification}")?;
client.stdin.flush()?;
let response = client.call("tools/list", json!({}), 100)?;
assert_eq!(response["id"], 100);
assert!(response["result"]["tools"].is_array());
Ok(())
}
#[test]
fn test_notification_known_method() -> Result<()> {
let mut client = McpTestClient::new_initialized()?;
let notification = json!({
"jsonrpc": "2.0",
"method": "tools/list",
"params": {}
});
writeln!(client.stdin, "{notification}")?;
client.stdin.flush()?;
let response = client.call("tools/list", json!({}), 101)?;
assert_eq!(response["id"], 101);
Ok(())
}
#[test]
fn test_multi_request_session() -> Result<()> {
let mut client = McpTestClient::new()?;
let resp1 = client.call(
"initialize",
json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"}
}),
1,
)?;
assert_eq!(resp1["id"], 1);
assert!(resp1["result"]["protocolVersion"].is_string());
let notification = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
});
writeln!(client.stdin, "{notification}")?;
client.stdin.flush()?;
let resp2 = client.call("tools/list", json!({}), 2)?;
assert_eq!(resp2["id"], 2);
assert!(resp2["result"]["tools"].as_array().unwrap().len() >= 5);
let resp3 = client.call(
"tools/call",
json!({
"name": "semantic_search",
"arguments": {
"query": "kind:function",
"path": ".",
"max_results": 5
}
}),
3,
)?;
assert_eq!(resp3["id"], 3);
if resp3.get("result").is_none() {
assert_eq!(resp3["error"]["code"], -32603);
}
client.send_request("unknown/method", json!({}), 4)?;
let err4 = client.read_response()?;
assert_eq!(err4["id"], 4);
assert_eq!(err4["error"]["code"], -32601);
let resp4 = client.call("tools/list", json!({}), 5)?;
assert_eq!(resp4["id"], 5);
Ok(())
}