use crate::{ClientError, KodegenClient, KodegenConnection};
use rmcp::{
ServiceExt,
model::{ClientCapabilities, ClientInfo, Implementation},
transport::TokioChildProcess,
};
use std::{collections::HashMap, path::PathBuf};
use tokio::{process::Command, time::Duration};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct StdioClientBuilder {
command: String,
args: Vec<String>,
envs: HashMap<String, String>,
current_dir: Option<PathBuf>,
timeout: Duration,
client_name: Option<String>,
}
impl StdioClientBuilder {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
args: Vec::new(),
envs: HashMap::new(),
current_dir: None,
timeout: DEFAULT_TIMEOUT,
client_name: None,
}
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args.extend(args.into_iter().map(Into::into));
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.envs.insert(key.into(), value.into());
self
}
#[must_use]
pub fn envs(mut self, envs: HashMap<String, String>) -> Self {
self.envs.extend(envs);
self
}
#[must_use]
pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.current_dir = Some(dir.into());
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn client_name(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
self
}
pub async fn build(self) -> Result<(KodegenClient, KodegenConnection), ClientError> {
let mut cmd = Command::new(&self.command);
cmd.args(&self.args);
if !self.envs.is_empty() {
cmd.envs(&self.envs);
}
if let Some(dir) = &self.current_dir {
cmd.current_dir(dir);
}
let transport = TokioChildProcess::new(cmd).map_err(|e| {
ClientError::Connection(format!("Failed to spawn process '{}': {}", self.command, e))
})?;
let client_info = ClientInfo {
protocol_version: Default::default(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: self
.client_name
.unwrap_or_else(|| "kodegen-stdio-client".to_string()),
title: None,
version: env!("CARGO_PKG_VERSION").to_string(),
website_url: None,
icons: None,
},
};
let service = client_info
.serve(transport)
.await
.map_err(ClientError::InitError)?;
let connection = KodegenConnection::from_service(service);
let client = connection.client().with_timeout(self.timeout);
Ok((client, connection))
}
}
pub async fn create_stdio_client(
command: &str,
args: &[&str],
) -> Result<(KodegenClient, KodegenConnection), ClientError> {
StdioClientBuilder::new(command)
.args(args.iter().copied())
.build()
.await
}