Skip to main content

vtcode_mcp/
rmcp_transport.rs

1/// RMCP transport layer wrappers for VT Code
2///
3/// This module provides wrappers around rmcp's transport types to integrate
4/// with VT Code's configuration and error handling.
5use 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
15/// Type alias for HTTP transport
16pub type HttpTransport = StreamableHttpClientTransport<rmcp_reqwest::Client>;
17
18/// Create a stdio-based transport from configuration
19///
20/// # Arguments
21/// * `stdio_config` - Stdio server configuration with command and args
22/// * `env` - Environment variables to pass to the process
23///
24/// # Returns
25/// A TokioChildProcess transport ready to use with RMCP client
26pub 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    // Add arguments
33    let _command = cmd.args(&stdio_config.args);
34
35    // Set working directory if specified
36    if let Some(working_dir) = &stdio_config.working_directory {
37        let _command = cmd.current_dir(working_dir);
38    }
39
40    // Match `create_stdio_transport_with_stderr`: the child receives the
41    // caller-resolved environment only, never vtcode's full process environment.
42    let _command = cmd.env_clear();
43
44    // Configure environment variables
45    for (key, value) in env {
46        let _command = cmd.env(key, value);
47    }
48
49    // Create the child process transport
50    TokioChildProcess::new(cmd).context("Failed to create child process for MCP server")
51}
52
53/// Create a stdio transport from individual parameters (Phase 2 integration)
54///
55/// This is a convenience wrapper for use within RmcpClient where transport
56/// parameters come from different configuration sources.
57///
58/// # Arguments
59/// * `program` - Path to the executable
60/// * `args` - Command arguments
61/// * `working_dir` - Working directory (optional)
62/// * `env` - Environment variables to pass
63///
64/// # Returns
65/// A tuple of (TokioChildProcess transport, stderr reader)
66/// The stderr reader can be passed to async logging tasks
67pub 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    // Add all environment variables
83    for (key, value) in env {
84        let _command = cmd.env(key, value);
85    }
86
87    // Set working directory if provided
88    if let Some(dir) = working_dir {
89        let _command = cmd.current_dir(dir);
90    }
91
92    // Add command arguments
93    let _command = cmd.args(args);
94
95    // Create transport with stderr capture for logging
96    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
103/// Create an HTTP-based transport from endpoint URL (Phase 3.2)
104///
105/// # Arguments
106/// * `endpoint` - HTTP endpoint URL (e.g., `https://api.example.com/mcp`)
107/// * `bearer_token` - Optional bearer token for authentication
108/// * `headers` - Custom HTTP headers to include in requests
109///
110/// # Returns
111/// A StreamableHttpClientTransport ready to use with RMCP client
112///
113/// # Note
114/// This is a convenience wrapper for HTTP transport creation. The actual
115/// transport construction delegates to rmcp's StreamableHttpClientTransport
116/// following the pattern used in RmcpClient::new_streamable_http_client().
117///
118/// # Example
119/// ```ignore
120/// let transport = create_http_transport(
121///     "https://api.example.com/mcp",
122///     Some("auth_token"),
123///     &HeaderMap::new()
124/// )?;
125/// ```
126pub fn create_http_transport(
127    _endpoint: &str,
128    _bearer_token: Option<&str>,
129    _headers: &HeaderMap,
130) -> Result<HttpTransport> {
131    // Phase 3.2: HTTP transport wrapper
132    // NOTE: Full implementation requires direct use of rmcp APIs
133    // See RmcpClient::new_streamable_http_client() for reference implementation
134    // This function provides the interface; actual HTTP transport is created via:
135    // StreamableHttpClientTransport::with_client(http_client, config)
136
137    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        // Test transport creation with configuration
150        // Detailed tests in integration tests
151    }
152
153    /// MCP stdio children must receive the caller-resolved environment only. Before
154    /// this was pinned, this transport inherited vtcode's whole process environment
155    /// while its `_with_stderr` sibling already cleared it.
156    #[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        // HOME is present in this process but deliberately absent from the map, so
175        // inheritance stays observable in the dump below.
176        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    /// Wait (bounded) for the spawned child to flush its environment dump.
192    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}