vtcode_mcp/
rmcp_transport.rs1use anyhow::{Context, Result};
6use rmcp::transport::StreamableHttpClientTransport;
7use rmcp::transport::TokioChildProcess;
8use rmcp_reqwest::header::HeaderMap;
9use std::ffi::OsString;
10use std::path::PathBuf;
11use tokio::process::Command;
12
13use vtcode_config::mcp::McpStdioServerConfig;
14
15pub type HttpTransport = StreamableHttpClientTransport<rmcp_reqwest::Client>;
17
18pub fn create_stdio_transport(
27 stdio_config: &McpStdioServerConfig,
28 env: &hashbrown::HashMap<OsString, OsString>,
29) -> Result<TokioChildProcess> {
30 let mut cmd = Command::new(&stdio_config.command);
31
32 let _command = cmd.args(&stdio_config.args);
34
35 if let Some(working_dir) = &stdio_config.working_directory {
37 let _command = cmd.current_dir(working_dir);
38 }
39
40 let _command = cmd.env_clear();
43
44 for (key, value) in env {
46 let _command = cmd.env(key, value);
47 }
48
49 TokioChildProcess::new(cmd).context("Failed to create child process for MCP server")
51}
52
53pub fn create_stdio_transport_with_stderr(
68 program: &OsString,
69 args: &[OsString],
70 working_dir: Option<&PathBuf>,
71 env: &hashbrown::HashMap<OsString, OsString>,
72) -> Result<(TokioChildProcess, Option<tokio::process::ChildStderr>)> {
73 let mut cmd = Command::new(program);
74
75 let _command = cmd
76 .kill_on_drop(true)
77 .stdin(std::process::Stdio::piped())
78 .stdout(std::process::Stdio::piped())
79 .stderr(std::process::Stdio::piped())
80 .env_clear();
81
82 for (key, value) in env {
84 let _command = cmd.env(key, value);
85 }
86
87 if let Some(dir) = working_dir {
89 let _command = cmd.current_dir(dir);
90 }
91
92 let _command = cmd.args(args);
94
95 let builder = TokioChildProcess::builder(cmd);
97 builder
98 .stderr(std::process::Stdio::piped())
99 .spawn()
100 .context("Failed to create stdio transport with stderr capture")
101}
102
103pub fn create_http_transport(
127 _endpoint: &str,
128 _bearer_token: Option<&str>,
129 _headers: &HeaderMap,
130) -> Result<HttpTransport> {
131 anyhow::bail!(
138 "HTTP transport creation requires rmcp's StreamableHttpClientTransport. \
139 Use RmcpClient::new_streamable_http_client() for full implementation."
140 )
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_transport_creation() {
149 }
152
153 #[tokio::test]
157 async fn stdio_transport_child_receives_only_the_resolved_environment() {
158 let dir = tempfile::tempdir().unwrap();
159 let dump = dir.path().join("child-env.txt");
160 let process_has_home = std::env::var_os("HOME").is_some();
161
162 let config = McpStdioServerConfig {
163 command: "/bin/sh".to_string(),
164 args: vec!["-c".to_string(), "env > \"$MCP_DUMP\"".to_string()],
165 working_directory: None,
166 };
167 let mut env = hashbrown::HashMap::new();
168 drop(env.insert(
169 OsString::from("PATH"),
170 std::env::var_os("PATH").unwrap_or_else(|| OsString::from("/usr/bin:/bin")),
171 ));
172 drop(env.insert(OsString::from("MCP_MARKER"), OsString::from("marker-value")));
173 drop(env.insert(OsString::from("MCP_DUMP"), dump.as_os_str().to_os_string()));
174 assert!(!env.contains_key(&OsString::from("HOME")));
177
178 let _transport = create_stdio_transport(&config, &env).unwrap();
179 let contents = child_env_dump(&dump).await;
180
181 assert!(contents.contains("MCP_MARKER=marker-value"), "resolved map not applied: {contents}");
182 assert!(contents.lines().any(|line| line.starts_with("PATH=")), "resolved PATH not applied: {contents}");
183 if process_has_home {
184 assert!(
185 !contents.lines().any(|line| line.starts_with("HOME=")),
186 "MCP stdio child inherited the process environment: {contents}"
187 );
188 }
189 }
190
191 async fn child_env_dump(path: &std::path::Path) -> String {
193 for _ in 0..100 {
194 if let Ok(contents) = tokio::fs::read_to_string(path).await {
195 return contents;
196 }
197 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
198 }
199 panic!("MCP stdio child never wrote {}", path.display());
200 }
201}