use rmcp::{
RoleClient,
model::{CallToolRequestParam, CallToolResult, ClientInfo, InitializeResult},
service::{Peer, RunningService},
};
use tokio::time::{Duration, timeout};
pub mod error;
pub mod responses;
pub mod tools;
pub mod transports;
pub use error::ClientError;
pub use transports::{StdioClientBuilder, create_stdio_client, create_streamable_client};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct KodegenClient {
peer: Peer<RoleClient>,
default_timeout: Duration,
}
impl KodegenClient {
pub(crate) fn from_peer(peer: Peer<RoleClient>) -> Self {
Self {
peer,
default_timeout: DEFAULT_TIMEOUT,
}
}
#[must_use]
pub fn with_timeout(mut self, duration: Duration) -> Self {
self.default_timeout = duration;
self
}
#[must_use]
pub fn server_info(&self) -> Option<&InitializeResult> {
self.peer.peer_info()
}
pub async fn list_tools(&self) -> Result<Vec<rmcp::model::Tool>, ClientError> {
timeout(self.default_timeout, self.peer.list_all_tools())
.await
.map_err(|_| {
ClientError::Timeout(format!(
"list_tools timed out after {}s",
self.default_timeout.as_secs()
))
})?
.map_err(ClientError::from)
}
pub async fn call_tool(
&self,
name: &str,
arguments: serde_json::Value,
) -> Result<CallToolResult, ClientError> {
let call = self.peer.call_tool(CallToolRequestParam {
name: name.to_string().into(),
arguments: match arguments {
serde_json::Value::Object(map) => Some(map),
_ => None,
},
});
timeout(self.default_timeout, call)
.await
.map_err(|_| {
ClientError::Timeout(format!(
"Tool '{}' timed out after {}s",
name,
self.default_timeout.as_secs()
))
})?
.map_err(ClientError::from)
}
pub async fn call_tool_typed<T>(
&self,
name: &str,
arguments: serde_json::Value,
) -> Result<T, ClientError>
where
T: serde::de::DeserializeOwned,
{
let result = self.call_tool(name, arguments).await?;
let text_content = result
.content
.first()
.and_then(|c| c.as_text())
.ok_or_else(|| {
ClientError::ParseError(format!("No text content in response from tool '{name}'"))
})?;
serde_json::from_str(&text_content.text).map_err(|e| {
ClientError::ParseError(format!("Failed to parse response from tool '{name}': {e}"))
})
}
}
pub struct KodegenConnection {
service: RunningService<RoleClient, ClientInfo>,
}
impl KodegenConnection {
#[must_use]
pub fn from_service(service: RunningService<RoleClient, ClientInfo>) -> Self {
Self { service }
}
#[must_use]
pub fn client(&self) -> KodegenClient {
KodegenClient::from_peer(self.service.peer().clone())
}
pub async fn close(self) -> Result<(), ClientError> {
self.service
.cancel()
.await
.map(|_| ())
.map_err(ClientError::from)
}
pub async fn wait(self) -> Result<(), ClientError> {
self.service
.waiting()
.await
.map(|_| ())
.map_err(ClientError::from)
}
}