use std::sync::atomic::{AtomicI64, Ordering};
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use tokio_util::sync::CancellationToken;
use crate::context::TransportContext;
use crate::server::Server;
use crate::spec::{self, methods};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClientError {
#[error("JSON-RPC error {code}: {message}")]
Rpc {
code: i64,
message: String,
data: Option<Value>,
},
#[error("the server sent no response")]
NoResponse,
#[error("invalid response: {0}")]
Decode(#[from] serde_json::Error),
}
#[derive(Debug)]
pub struct TestClient {
server: Server,
next_id: AtomicI64,
meta: Value,
}
impl TestClient {
#[must_use]
pub fn new(server: Server) -> Self {
let meta = json!({
"io.modelcontextprotocol/protocolVersion": spec::PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": { "name": "flymcprust-test-client", "version": env!("CARGO_PKG_VERSION") },
});
Self {
server,
next_id: AtomicI64::new(1),
meta,
}
}
#[must_use]
pub fn with_meta(mut self, meta: Value) -> Self {
self.meta = meta;
self
}
pub async fn discover(&self) -> Result<spec::DiscoverResult, ClientError> {
self.request(methods::SERVER_DISCOVER, json!({})).await
}
pub async fn list_tools(&self) -> Result<spec::ListToolsResult, ClientError> {
self.request(methods::TOOLS_LIST, json!({})).await
}
pub async fn call_tool(
&self,
name: &str,
arguments: Value,
) -> Result<spec::CallToolResult, ClientError> {
self.request(
methods::TOOLS_CALL,
json!({ "name": name, "arguments": arguments }),
)
.await
}
pub async fn list_resources(&self) -> Result<spec::ListResourcesResult, ClientError> {
self.request(methods::RESOURCES_LIST, json!({})).await
}
pub async fn list_resource_templates(
&self,
) -> Result<spec::ListResourceTemplatesResult, ClientError> {
self.request(methods::RESOURCES_TEMPLATES_LIST, json!({}))
.await
}
pub async fn read_resource(&self, uri: &str) -> Result<spec::ReadResourceResult, ClientError> {
self.request(methods::RESOURCES_READ, json!({ "uri": uri }))
.await
}
pub async fn list_prompts(&self) -> Result<spec::ListPromptsResult, ClientError> {
self.request(methods::PROMPTS_LIST, json!({})).await
}
pub async fn get_prompt(
&self,
name: &str,
arguments: Value,
) -> Result<spec::GetPromptResult, ClientError> {
self.request(
methods::PROMPTS_GET,
json!({ "name": name, "arguments": arguments }),
)
.await
}
pub async fn complete_prompt(
&self,
prompt: &str,
argument: &str,
value: &str,
) -> Result<spec::CompleteResult, ClientError> {
self.request(
methods::COMPLETION_COMPLETE,
json!({
"ref": { "type": "ref/prompt", "name": prompt },
"argument": { "name": argument, "value": value },
}),
)
.await
}
pub async fn complete_resource(
&self,
uri_template: &str,
variable: &str,
value: &str,
) -> Result<spec::CompleteResult, ClientError> {
self.request(
methods::COMPLETION_COMPLETE,
json!({
"ref": { "type": "ref/resource", "uri": uri_template },
"argument": { "name": variable, "value": value },
}),
)
.await
}
pub async fn request<T: DeserializeOwned>(
&self,
method: &str,
mut params: Value,
) -> Result<T, ClientError> {
if let Value::Object(map) = &mut params {
map.entry("_meta").or_insert_with(|| self.meta.clone());
}
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let message = json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params });
let bytes = serde_json::to_vec(&message)?;
let response = self
.server
.handle_bytes(
&bytes,
TransportContext::default(),
CancellationToken::new(),
)
.await
.ok_or(ClientError::NoResponse)?;
let mut envelope: Value = serde_json::from_slice(&response.to_vec())?;
if let Some(error) = envelope.get_mut("error").map(Value::take) {
let error: spec::Error = serde_json::from_value(error)?;
return Err(ClientError::Rpc {
code: error.code,
message: error.message,
data: error.data,
});
}
let result = envelope
.get_mut("result")
.map(Value::take)
.unwrap_or_default();
Ok(serde_json::from_value(result)?)
}
}