#![allow(dead_code)]
#![cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
use async_trait::async_trait;
use pmcp::server::core::ProtocolHandler;
use pmcp::shared::{Transport, TransportMessage};
use pmcp::types::{CallToolResult, ClientCapabilities};
use pmcp::{Client, Error, Result, Server};
use serde_json::Value;
use tokio::sync::mpsc;
#[cfg(feature = "testing")]
use pmcp::testing::META_PROTOCOL_VERSION;
#[cfg(feature = "testing")]
use pmcp::types::jsonrpc::{JSONRPCResponse, RequestId, ResponsePayload};
#[cfg(feature = "testing")]
use pmcp::types::protocol::{
Era, ProtocolVersion, LATEST_PROTOCOL_VERSION, PROTOCOL_VERSION_2026_07_28,
};
#[cfg(feature = "testing")]
use pmcp::types::{ClientRequest, Request, RequestMeta};
#[cfg(feature = "testing")]
use serde_json::json;
#[derive(Debug)]
pub struct DuplexTransport {
tx: mpsc::UnboundedSender<TransportMessage>,
rx: mpsc::UnboundedReceiver<TransportMessage>,
connected: bool,
}
impl DuplexTransport {
pub fn pair() -> (Self, Self) {
let (client_tx, server_rx) = mpsc::unbounded_channel();
let (server_tx, client_rx) = mpsc::unbounded_channel();
(
Self {
tx: client_tx,
rx: client_rx,
connected: true,
},
Self {
tx: server_tx,
rx: server_rx,
connected: true,
},
)
}
}
#[async_trait]
impl Transport for DuplexTransport {
async fn send(&mut self, message: TransportMessage) -> Result<()> {
self.tx
.send(message)
.map_err(|_| Error::internal("duplex peer dropped"))
}
async fn receive(&mut self) -> Result<TransportMessage> {
self.rx
.recv()
.await
.ok_or_else(|| Error::internal("duplex peer closed"))
}
async fn close(&mut self) -> Result<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn transport_type(&self) -> &'static str {
"in-process-duplex"
}
}
pub async fn call_via_server(server: Server, name: &str, args: Value) -> CallToolResult {
let (client_t, server_t) = DuplexTransport::pair();
tokio::spawn(async move {
let _ = server.run(server_t).await;
});
let mut client = Client::new(client_t);
client
.initialize(ClientCapabilities::default())
.await
.expect("client initializes against server");
client
.call_tool(name.to_string(), args)
.await
.expect("tools/call succeeds against server")
}
pub async fn call_via_core(
core: Arc<dyn ProtocolHandler>,
name: &str,
args: Value,
) -> CallToolResult {
let (client_t, mut server_t) = DuplexTransport::pair();
tokio::spawn(async move {
while let Ok(message) = server_t.receive().await {
if let TransportMessage::Request { id, request } = message {
let response = core.handle_request(id, request, None).await;
if server_t
.send(TransportMessage::Response(response))
.await
.is_err()
{
break;
}
}
}
});
let mut client = Client::new(client_t);
client
.initialize(ClientCapabilities::default())
.await
.expect("client initializes against core");
client
.call_tool(name.to_string(), args)
.await
.expect("tools/call succeeds against core")
}
#[cfg(feature = "testing")]
const REQUEST_META_KEY: &str = "_meta";
#[cfg(feature = "testing")]
const RESULT_TYPE_KEY: &str = "resultType";
#[cfg(feature = "testing")]
pub fn v2_accept_list() -> Vec<ProtocolVersion> {
vec![
ProtocolVersion(LATEST_PROTOCOL_VERSION.to_string()),
ProtocolVersion(PROTOCOL_VERSION_2026_07_28.to_string()),
]
}
#[cfg(feature = "testing")]
pub fn call_tool_request(name: &str, args: Value, era: Era) -> Request {
let mut params = serde_json::Map::new();
params.insert("name".to_string(), Value::String(name.to_string()));
params.insert("arguments".to_string(), args);
era_signalling_request("tools/call", params, era)
}
#[cfg(feature = "testing")]
fn era_signalling_request(
method: &str,
mut params: serde_json::Map<String, Value>,
era: Era,
) -> Request {
if matches!(era, Era::V2) {
let meta =
RequestMeta::new().with_meta(META_PROTOCOL_VERSION, json!(PROTOCOL_VERSION_2026_07_28));
let meta = serde_json::to_value(&meta).expect("request meta serializes");
params.insert(REQUEST_META_KEY.to_string(), meta);
}
let mut envelope = serde_json::Map::new();
envelope.insert("method".to_string(), Value::String(method.to_string()));
envelope.insert("params".to_string(), Value::Object(params));
let client_request: ClientRequest = serde_json::from_value(Value::Object(envelope))
.unwrap_or_else(|e| panic!("`{method}` request deserializes into ClientRequest ({e})"));
Request::Client(Box::new(client_request))
}
#[cfg(feature = "testing")]
pub fn read_resource_request(uri: &str, era: Era) -> Request {
let mut params = serde_json::Map::new();
params.insert("uri".to_string(), Value::String(uri.to_string()));
era_signalling_request("resources/read", params, era)
}
#[cfg(feature = "testing")]
pub async fn raw_via_core(core: Arc<dyn ProtocolHandler>, request: Request) -> JSONRPCResponse {
core.handle_request(RequestId::from(1i64), request, None)
.await
}
#[cfg(feature = "testing")]
pub async fn initialize_via_core(core: &Arc<dyn ProtocolHandler>) -> JSONRPCResponse {
let request: ClientRequest = serde_json::from_value(json!({
"method": "initialize",
"params": {
"protocolVersion": LATEST_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": { "name": "duplex-harness", "version": "0.0.0" },
},
}))
.expect("initialize request deserializes into ClientRequest");
core.handle_request(
RequestId::from(0i64),
Request::Client(Box::new(request)),
None,
)
.await
}
#[cfg(feature = "testing")]
pub async fn raw_via_server(server: Server, request: Request) -> JSONRPCResponse {
let (mut client_t, server_t) = DuplexTransport::pair();
tokio::spawn(async move {
let _ = server.run(server_t).await;
});
client_t
.send(TransportMessage::Request {
id: RequestId::from(1i64),
request,
})
.await
.expect("client half sends the request");
loop {
if let TransportMessage::Response(response) = client_t
.receive()
.await
.expect("server answers the request")
{
return response;
}
}
}
#[cfg(feature = "testing")]
pub fn result_object(response: &JSONRPCResponse) -> &serde_json::Map<String, Value> {
match &response.payload {
ResponsePayload::Result(value) => value
.as_object()
.unwrap_or_else(|| panic!("expected an object result, got: {value}")),
ResponsePayload::Error(error) => {
panic!("expected a Result payload, got error: {error:?}")
},
}
}
#[cfg(feature = "testing")]
pub fn assert_v2_witness(response: &JSONRPCResponse, ctx: &str) {
let result = result_object(response);
assert!(
result.contains_key(RESULT_TYPE_KEY),
"{ctx}: no `{RESULT_TYPE_KEY}` in the result, so the dispatcher did NOT resolve Era::V2 \
for this request — the server is probably not opted in via \
`with_supported_protocol_versions(v2_accept_list())`, or the request carries no \
`_meta` protocol-version signal. Result was: {result:?}"
);
}
#[cfg(feature = "testing")]
pub fn assert_no_v2_witness(response: &JSONRPCResponse, ctx: &str) {
let result = result_object(response);
assert!(
!result.contains_key(RESULT_TYPE_KEY),
"{ctx}: found `{RESULT_TYPE_KEY}` in the result, so the dispatcher resolved Era::V2 for \
a request that was supposed to be served as v1. Result was: {result:?}"
);
}
#[cfg(feature = "testing")]
pub fn call_tool_result_of(response: &JSONRPCResponse) -> CallToolResult {
let result = Value::Object(result_object(response).clone());
serde_json::from_value(result.clone())
.unwrap_or_else(|e| panic!("result deserializes into CallToolResult ({e}): {result}"))
}