use rmcp::{
RoleClient,
model::{CallToolRequestParam, CallToolResult, ClientInfo, InitializeResult},
service::{Peer, RunningService},
};
use tokio::time::{Duration, timeout};
pub mod error;
pub mod headers;
pub mod responses;
pub mod transports;
pub mod validation;
pub use error::{ClientError, TransportType};
pub use headers::{X_KODEGEN_CONNECTION_ID, X_KODEGEN_GITROOT, X_KODEGEN_PWD};
pub use transports::{StdioClientBuilder, create_stdio_client, create_streamable_client};
fn json_type_name(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
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,
}
}
pub fn peer(&self) -> &Peer<RoleClient> {
&self.peer
}
#[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 {
operation: "list_tools".to_string(),
duration: self.default_timeout,
})?
.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),
serde_json::Value::Null => None,
other => {
return Err(ClientError::Protocol(format!(
"Tool arguments must be a JSON object or null, got {}",
json_type_name(&other)
)));
}
},
});
timeout(self.default_timeout, call)
.await
.map_err(|_| ClientError::Timeout {
operation: format!("Tool '{}'", name),
duration: self.default_timeout,
})?
.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
.iter()
.find_map(|c| c.as_text())
.ok_or_else(|| {
if result.content.is_empty() {
ClientError::Protocol(format!("Tool '{}' returned empty content array", name))
} else {
let content_types: Vec<_> = result
.content
.iter()
.map(|c| match c.raw {
rmcp::model::RawContent::Text(_) => "text",
rmcp::model::RawContent::Image(_) => "image",
rmcp::model::RawContent::Resource(_) => "resource",
rmcp::model::RawContent::Audio(_) => "audio",
rmcp::model::RawContent::ResourceLink(_) => "resource_link",
})
.collect();
ClientError::Protocol(format!(
"Tool '{}' returned {} content item(s) but none were text: [{}]",
name,
result.content.len(),
content_types.join(", ")
))
}
})?;
serde_json::from_str(&text_content.text).map_err(|e| ClientError::ParseError {
tool_name: name.to_string(),
source: e,
})
}
}
#[must_use = "Connection must be held to keep MCP service alive"]
pub struct KodegenConnection {
service: RunningService<RoleClient, ClientInfo>,
}
impl KodegenConnection {
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)
}
}