use pmcp::types::{
Content, JSONRPCRequest, ListResourcesResult, ReadResourceResult, RequestId, ResourceInfo,
};
use pmcp::{BatchRequest, BatchResponse, ResourceHandler, ServerBuilder, ToolHandler};
use serde_json::{json, Value};
use std::sync::Arc;
#[derive(Clone)]
struct TestTool;
#[async_trait::async_trait]
impl ToolHandler for TestTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
Ok(json!({
"result": "Tool executed",
"args": args
}))
}
}
#[derive(Clone)]
struct TestResourceHandler;
#[async_trait::async_trait]
impl ResourceHandler for TestResourceHandler {
async fn read(
&self,
uri: &str,
_extra: pmcp::RequestHandlerExtra,
) -> pmcp::Result<ReadResourceResult> {
Ok(ReadResourceResult::new(vec![Content::text(format!(
"Content of {}",
uri
))]))
}
async fn list(
&self,
_cursor: Option<String>,
_extra: pmcp::RequestHandlerExtra,
) -> pmcp::Result<ListResourcesResult> {
Ok(ListResourcesResult::new(vec![
ResourceInfo::new("file:///test1.txt", "test1.txt")
.with_description("Test file 1")
.with_mime_type("text/plain"),
ResourceInfo::new("file:///test2.txt", "test2.txt")
.with_description("Test file 2")
.with_mime_type("text/plain"),
]))
}
}
#[tokio::test]
async fn test_batch_multiple_requests() {
let server = Arc::new(
ServerBuilder::new()
.name("test-batch-server")
.version("1.0.0")
.tool("test-tool", TestTool)
.resources(TestResourceHandler)
.build()
.unwrap(),
);
let init_request = JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "initialize".to_string(),
params: Some(json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {"subscribe": true, "unsubscribe": true}
},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
})),
id: RequestId::from("init"),
};
let _init_response = server
.handle_batch_request(BatchRequest::Single(init_request))
.await
.unwrap();
let batch = BatchRequest::Batch(vec![
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "tools/list".to_string(),
params: Some(json!({})),
id: RequestId::from("batch-1"),
},
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "resources/list".to_string(),
params: Some(json!({})),
id: RequestId::from("batch-2"),
},
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "tools/call".to_string(),
params: Some(json!({
"name": "test-tool",
"arguments": {"value": 42}
})),
id: RequestId::from("batch-3"),
},
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "resources/read".to_string(),
params: Some(json!({
"uri": "file:///test1.txt"
})),
id: RequestId::from("batch-4"),
},
]);
let response = server.handle_batch_request(batch).await.unwrap();
let responses = response.into_responses();
assert_eq!(responses.len(), 4);
assert_eq!(responses[0].id, RequestId::from("batch-1"));
match &responses[0].payload {
pmcp::types::jsonrpc::ResponsePayload::Result(value) => {
assert!(value.get("tools").is_some(), "Response: {:?}", value);
},
pmcp::types::jsonrpc::ResponsePayload::Error(e) => {
panic!("Got error for tools/list: {:?}", e);
},
}
assert_eq!(responses[1].id, RequestId::from("batch-2"));
match &responses[1].payload {
pmcp::types::jsonrpc::ResponsePayload::Result(value) => {
assert!(value.get("resources").is_some());
},
pmcp::types::jsonrpc::ResponsePayload::Error(_) => {
panic!("Expected successful result for resources/list")
},
}
assert_eq!(responses[2].id, RequestId::from("batch-3"));
match &responses[2].payload {
pmcp::types::jsonrpc::ResponsePayload::Result(value) => {
assert!(value.get("content").is_some());
},
pmcp::types::jsonrpc::ResponsePayload::Error(_) => {
panic!("Expected successful result for tools/call")
},
}
assert_eq!(responses[3].id, RequestId::from("batch-4"));
match &responses[3].payload {
pmcp::types::jsonrpc::ResponsePayload::Result(value) => {
assert!(value.get("contents").is_some());
},
pmcp::types::jsonrpc::ResponsePayload::Error(_) => {
panic!("Expected successful result for resources/read")
},
}
}
#[tokio::test]
async fn test_batch_error_handling() {
let server = Arc::new(
ServerBuilder::new()
.name("test-batch-server")
.version("1.0.0")
.build()
.unwrap(),
);
let batch = BatchRequest::Batch(vec![
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "ping".to_string(),
params: None,
id: RequestId::from(1i64),
},
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "invalid/method".to_string(),
params: None,
id: RequestId::from(2i64),
},
JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "tools/call".to_string(),
params: Some(json!({
"name": "non-existent-tool",
"arguments": {}
})),
id: RequestId::from(3i64),
},
]);
let response = server.handle_batch_request(batch).await.unwrap();
let responses = response.into_responses();
assert_eq!(responses.len(), 3);
assert_eq!(responses[0].id, RequestId::from(1i64));
match &responses[0].payload {
pmcp::types::jsonrpc::ResponsePayload::Result(_) => {},
pmcp::types::jsonrpc::ResponsePayload::Error(_) => panic!("Expected successful ping"),
}
assert_eq!(responses[1].id, RequestId::from(2i64));
match &responses[1].payload {
pmcp::types::jsonrpc::ResponsePayload::Error(error) => {
assert_eq!(error.code, -32700, "Error: {:?}", error); },
pmcp::types::jsonrpc::ResponsePayload::Result(_) => {
panic!("Expected error for invalid method")
},
}
assert_eq!(responses[2].id, RequestId::from(3i64));
match &responses[2].payload {
pmcp::types::jsonrpc::ResponsePayload::Error(error) => {
assert_eq!(error.code, -32603); },
pmcp::types::jsonrpc::ResponsePayload::Result(_) => {
panic!("Expected error for non-existent tool")
},
}
}
#[tokio::test]
async fn test_batch_mixed_single_and_batch() {
let server = Arc::new(
ServerBuilder::new()
.name("test-batch-server")
.version("1.0.0")
.build()
.unwrap(),
);
let single = BatchRequest::Single(JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "ping".to_string(),
params: None,
id: RequestId::from("single"),
});
let response = server.handle_batch_request(single).await.unwrap();
match response {
BatchResponse::Single(resp) => {
assert_eq!(resp.id, RequestId::from("single"));
},
BatchResponse::Batch(_) => panic!("Expected single response"),
}
let batch_single = BatchRequest::Batch(vec![JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "ping".to_string(),
params: None,
id: RequestId::from("batch-single"),
}]);
let response = server.handle_batch_request(batch_single).await.unwrap();
match response {
BatchResponse::Single(resp) => {
assert_eq!(resp.id, RequestId::from("batch-single"));
},
BatchResponse::Batch(_) => panic!("Expected single response for batch with one item"),
}
}
#[tokio::test]
async fn test_batch_preserve_order() {
let server = Arc::new(
ServerBuilder::new()
.name("test-batch-server")
.version("1.0.0")
.build()
.unwrap(),
);
let ids: Vec<RequestId> = (1..=10).map(|i| RequestId::from(i64::from(i))).collect();
let requests: Vec<JSONRPCRequest> = ids
.iter()
.cloned()
.map(|id| JSONRPCRequest {
jsonrpc: "2.0".to_string(),
method: "ping".to_string(),
params: None,
id,
})
.collect();
let batch = BatchRequest::Batch(requests);
let response = server.handle_batch_request(batch).await.unwrap();
let responses = response.into_responses();
assert_eq!(responses.len(), 10);
for (i, response) in responses.iter().enumerate() {
assert_eq!(response.id, ids[i]);
}
}