use anyhow::{Context, Result};
use rmcp::transport::StreamableHttpClientTransport;
use rmcp::transport::TokioChildProcess;
use rmcp_reqwest::header::HeaderMap;
use std::ffi::OsString;
use std::path::PathBuf;
use tokio::process::Command;
use vtcode_config::mcp::McpStdioServerConfig;
pub type HttpTransport = StreamableHttpClientTransport<rmcp_reqwest::Client>;
pub fn create_stdio_transport(
stdio_config: &McpStdioServerConfig,
env: &hashbrown::HashMap<OsString, OsString>,
) -> Result<TokioChildProcess> {
let mut cmd = Command::new(&stdio_config.command);
let _command = cmd.args(&stdio_config.args);
if let Some(working_dir) = &stdio_config.working_directory {
let _command = cmd.current_dir(working_dir);
}
let _command = cmd.env_clear();
for (key, value) in env {
let _command = cmd.env(key, value);
}
TokioChildProcess::new(cmd).context("Failed to create child process for MCP server")
}
pub fn create_stdio_transport_with_stderr(
program: &OsString,
args: &[OsString],
working_dir: Option<&PathBuf>,
env: &hashbrown::HashMap<OsString, OsString>,
) -> Result<(TokioChildProcess, Option<tokio::process::ChildStderr>)> {
let mut cmd = Command::new(program);
let _command = cmd
.kill_on_drop(true)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.env_clear();
for (key, value) in env {
let _command = cmd.env(key, value);
}
if let Some(dir) = working_dir {
let _command = cmd.current_dir(dir);
}
let _command = cmd.args(args);
let builder = TokioChildProcess::builder(cmd);
builder
.stderr(std::process::Stdio::piped())
.spawn()
.context("Failed to create stdio transport with stderr capture")
}
pub fn create_http_transport(
_endpoint: &str,
_bearer_token: Option<&str>,
_headers: &HeaderMap,
) -> Result<HttpTransport> {
anyhow::bail!(
"HTTP transport creation requires rmcp's StreamableHttpClientTransport. \
Use RmcpClient::new_streamable_http_client() for full implementation."
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_creation() {
}
#[tokio::test]
async fn stdio_transport_child_receives_only_the_resolved_environment() {
let dir = tempfile::tempdir().unwrap();
let dump = dir.path().join("child-env.txt");
let process_has_home = std::env::var_os("HOME").is_some();
let config = McpStdioServerConfig {
command: "/bin/sh".to_string(),
args: vec!["-c".to_string(), "env > \"$MCP_DUMP\"".to_string()],
working_directory: None,
};
let mut env = hashbrown::HashMap::new();
drop(env.insert(
OsString::from("PATH"),
std::env::var_os("PATH").unwrap_or_else(|| OsString::from("/usr/bin:/bin")),
));
drop(env.insert(OsString::from("MCP_MARKER"), OsString::from("marker-value")));
drop(env.insert(OsString::from("MCP_DUMP"), dump.as_os_str().to_os_string()));
assert!(!env.contains_key(&OsString::from("HOME")));
let _transport = create_stdio_transport(&config, &env).unwrap();
let contents = child_env_dump(&dump).await;
assert!(contents.contains("MCP_MARKER=marker-value"), "resolved map not applied: {contents}");
assert!(contents.lines().any(|line| line.starts_with("PATH=")), "resolved PATH not applied: {contents}");
if process_has_home {
assert!(
!contents.lines().any(|line| line.starts_with("HOME=")),
"MCP stdio child inherited the process environment: {contents}"
);
}
}
async fn child_env_dump(path: &std::path::Path) -> String {
for _ in 0..100 {
if let Ok(contents) = tokio::fs::read_to_string(path).await {
return contents;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
panic!("MCP stdio child never wrote {}", path.display());
}
}