#![cfg(unix)]
use std::path::PathBuf;
use std::time::Duration;
use protocol_droid::adapters::secondary::mcp::{
CallToolRequest, ClientCapabilities, ContentBlock, Implementation, InitializeRequest,
InitializeResult, ListToolsResult, McpStdioPeer, SpawnSpec,
};
const PINNED_REVISION: &str = "2025-06-18";
fn reference_server_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("mcp_reference_server.mjs")
}
fn find_node() -> Option<String> {
let output = std::process::Command::new("node")
.arg("--version")
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some("node".to_owned())
}
#[tokio::test]
async fn initialize_tools_list_and_call_round_trip() -> Result<(), String> {
let Some(node) = find_node() else {
eprintln!("skipping: `node` is not on PATH");
return Ok(());
};
let spec = SpawnSpec::new(
node,
vec![reference_server_path().to_string_lossy().into_owned()],
)
.with_timeout(Duration::from_secs(10));
let peer = McpStdioPeer::spawn(&spec).map_err(|error| format!("spawning the peer: {error}"))?;
let initialize_request = InitializeRequest {
protocol_version: PINNED_REVISION.to_owned(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "protocol-droid-mcp-client".to_owned(),
title: Some("protocol-droid MCP client".to_owned()),
version: "0.1.0".to_owned(),
},
};
let initialize: InitializeResult = peer
.send_typed_request("initialize", initialize_request)
.await
.map_err(|error| format!("initialize reply: {error}"))?;
assert_eq!(initialize.protocol_version, PINNED_REVISION);
assert_eq!(initialize.server_info.name, "mcp-reference-server");
assert!(
initialize.capabilities.tools.is_some(),
"the server must advertise the `tools` capability",
);
let tools: ListToolsResult = peer
.send_typed_request("tools/list", serde_json::json!({}))
.await
.map_err(|error| format!("tools/list reply: {error}"))?;
assert_eq!(tools.tools.len(), 1, "the fixture advertises one tool");
let Some(echo) = tools.tools.first() else {
return Err("the fixture must advertise at least one tool".to_owned());
};
assert_eq!(echo.name, "echo");
assert_eq!(
echo.description.as_deref(),
Some("Echoes the call arguments back as text content."),
);
assert!(echo.input_schema.get("properties").is_some());
let call = CallToolRequest {
name: "echo".to_owned(),
arguments: serde_json::json!({ "message": "hello" }),
};
let result: protocol_droid::adapters::secondary::mcp::CallToolResult = peer
.send_typed_request("tools/call", call)
.await
.map_err(|error| format!("tools/call reply: {error}"))?;
assert!(!result.is_error);
assert_eq!(result.content.len(), 1);
let Some(first) = result.content.first() else {
return Err("the call result must contain at least one content block".to_owned());
};
match first {
ContentBlock::Text { text } => assert_eq!(text, "echo: hello"),
other => return Err(format!("expected a `Text` content block, got: {other:?}")),
}
Ok(())
}
#[tokio::test]
async fn ping_round_trips() -> Result<(), String> {
let Some(node) = find_node() else {
eprintln!("skipping: `node` is not on PATH");
return Ok(());
};
let spec = SpawnSpec::new(
node,
vec![reference_server_path().to_string_lossy().into_owned()],
)
.with_timeout(Duration::from_secs(10));
let peer = McpStdioPeer::spawn(&spec).map_err(|error| format!("spawning the peer: {error}"))?;
let _initialize: InitializeResult = peer
.send_typed_request(
"initialize",
InitializeRequest {
protocol_version: PINNED_REVISION.to_owned(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "protocol-droid-mcp-client".to_owned(),
title: None,
version: "0.1.0".to_owned(),
},
},
)
.await
.map_err(|error| format!("initialize reply: {error}"))?;
let ping: serde_json::Value = peer
.send_typed_request("ping", serde_json::json!({}))
.await
.map_err(|error| format!("ping reply: {error}"))?;
assert_eq!(ping, serde_json::json!({}));
Ok(())
}
#[tokio::test]
async fn an_unknown_method_returns_a_json_rpc_error() -> Result<(), String> {
let Some(node) = find_node() else {
eprintln!("skipping: `node` is not on PATH");
return Ok(());
};
let spec = SpawnSpec::new(
node,
vec![reference_server_path().to_string_lossy().into_owned()],
)
.with_timeout(Duration::from_secs(10));
let peer = McpStdioPeer::spawn(&spec).map_err(|error| format!("spawning the peer: {error}"))?;
let result: Result<serde_json::Value, _> = peer
.send_typed_request("definitely/not/a/real/method", serde_json::json!({}))
.await;
let error = result
.err()
.ok_or_else(|| "the unknown method should have errored".to_owned())?;
match error {
protocol_droid::adapters::secondary::mcp::McpPeerError::JsonRpc { code, .. } => {
assert_eq!(code, -32601, "the server should return `Method not found`");
}
other => return Err(format!("expected a `JsonRpc` error, got: {other:?}")),
}
Ok(())
}