use std::time::Duration;
use grex_mcp::{GrexMcpServer, ServerState, VERBS_EXPOSED};
use rmcp::{
model::{ClientJsonRpcMessage, ServerJsonRpcMessage, ServerResult},
transport::IntoTransport,
};
fn raw(s: &str) -> ClientJsonRpcMessage {
serde_json::from_str(s).unwrap()
}
fn init() -> ClientJsonRpcMessage {
raw(
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"x","version":"0.0.1"}}}"#,
)
}
fn list_tools() -> ClientJsonRpcMessage {
raw(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#)
}
fn initialized() -> ClientJsonRpcMessage {
raw(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#)
}
#[tokio::test]
async fn tools_list_returns_eleven_with_annotations() {
let list = drive_list_tools().await;
assert_count_and_annotations(list);
}
async fn drive_list_tools() -> rmcp::model::ListToolsResult {
let (server_io, client_io) = tokio::io::duplex(8192);
let server = GrexMcpServer::new(ServerState::for_tests());
let _server = tokio::spawn(async move { server.run(server_io).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_io);
use rmcp::transport::Transport;
client.send(init()).await.unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), client.receive()).await.unwrap().unwrap();
client.send(initialized()).await.unwrap();
client.send(list_tools()).await.unwrap();
loop {
let msg = tokio::time::timeout(Duration::from_secs(2), client.receive())
.await
.expect("response within 2s")
.expect("response not None");
if let ServerJsonRpcMessage::Response(r) = msg {
if let ServerResult::ListToolsResult(list) = r.result {
return list;
}
}
}
}
fn assert_count_and_annotations(list: rmcp::model::ListToolsResult) {
assert_eq!(
list.tools.len(),
VERBS_EXPOSED.len(),
"tools/list must advertise exactly {} tools, got {} ({:?})",
VERBS_EXPOSED.len(),
list.tools.len(),
list.tools.iter().map(|t| &t.name).collect::<Vec<_>>(),
);
for t in &list.tools {
let a = t
.annotations
.as_ref()
.unwrap_or_else(|| panic!("tool `{}` over the wire missing annotations", t.name));
assert!(a.read_only_hint.is_some(), "wire: `{}` lacks read_only_hint", t.name);
assert!(a.destructive_hint.is_some(), "wire: `{}` lacks destructive_hint", t.name);
}
}