mcp_runner/server/
process.rs

1// src/server/process.rs
2use crate::config::ServerConfig;
3use crate::error::{Error, Result};
4use async_process::{Child, Command, Stdio};
5use std::fmt;
6use tracing;
7use uuid::Uuid; // Import tracing
8
9/// Unique identifier for a server process
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ServerId(Uuid);
12
13impl ServerId {
14    // Private constructor, only usable within our crate
15    pub(crate) fn new() -> Self {
16        Self(Uuid::new_v4())
17    }
18}
19
20// Implement Display trait
21impl fmt::Display for ServerId {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "{}", self.0)
24    }
25}
26
27/// Status of a server process
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServerStatus {
30    /// Server is starting
31    Starting,
32    /// Server is running
33    Running,
34    /// Server is stopping
35    Stopping,
36    /// Server has stopped
37    Stopped,
38    /// Server failed to start or crashed
39    Failed,
40}
41
42/// Represents a running MCP server process.
43///
44/// Manages the lifecycle of a single MCP server, including starting, stopping,
45/// and providing access to its standard I/O streams.
46/// All public methods are instrumented with `tracing` spans.
47pub struct ServerProcess {
48    /// Server configuration
49    config: ServerConfig,
50    /// Server name
51    name: String,
52    /// Server ID
53    id: ServerId,
54    /// Child process
55    child: Option<Child>,
56    /// Server status
57    status: ServerStatus,
58}
59
60impl ServerProcess {
61    /// Create a new server process instance.
62    ///
63    /// This method is instrumented with `tracing`.
64    #[tracing::instrument(skip(config), fields(server_name = %name))]
65    pub fn new(name: String, config: ServerConfig) -> Self {
66        Self {
67            config,
68            name,
69            id: ServerId::new(),
70            child: None,
71            status: ServerStatus::Stopped,
72        }
73    }
74
75    /// Get the server ID
76    pub fn id(&self) -> ServerId {
77        self.id
78    }
79
80    /// Get the server name
81    pub fn name(&self) -> &str {
82        &self.name
83    }
84
85    /// Get the current status of the server.
86    ///
87    /// This method is instrumented with `tracing`.
88    #[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
89    pub fn status(&self) -> ServerStatus {
90        self.status
91    }
92
93    /// Start the server process.
94    ///
95    /// This method is instrumented with `tracing`.
96    #[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
97    pub async fn start(&mut self) -> Result<()> {
98        if self.child.is_some() {
99            tracing::warn!("Attempted to start an already running server");
100            return Err(Error::AlreadyRunning);
101        }
102
103        tracing::info!("Starting server process");
104        self.status = ServerStatus::Starting;
105
106        let mut command = Command::new(&self.config.command);
107        command.args(&self.config.args);
108
109        // Set environment variables
110        for (key, value) in &self.config.env {
111            command.env(key, value);
112        }
113
114        // Configure stdio
115        command
116            .stdin(Stdio::piped())
117            .stdout(Stdio::piped())
118            .stderr(Stdio::piped());
119
120        // Start the process
121        tracing::debug!(command = ?self.config.command, args = ?self.config.args, env = ?self.config.env, "Spawning process");
122        let child = command.spawn().map_err(|e| {
123            tracing::error!("Failed to spawn process: {}", e);
124            Error::Process(format!("Failed to start process: {}", e))
125        })?;
126
127        self.child = Some(child);
128        self.status = ServerStatus::Running;
129        tracing::info!("Server process started successfully");
130
131        Ok(())
132    }
133
134    /// Stop the server process.
135    ///
136    /// This method is instrumented with `tracing`.
137    #[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
138    pub async fn stop(&mut self) -> Result<()> {
139        if let Some(mut child) = self.child.take() {
140            tracing::info!("Stopping server process");
141            self.status = ServerStatus::Stopping;
142
143            // Try to kill the process gracefully
144            if let Err(e) = child.kill() {
145                tracing::error!("Failed to kill process: {}", e);
146                // We still attempt to wait for the process below, so don't return early
147                // return Err(Error::Process(format!("Failed to kill process: {}", e)));
148            }
149
150            // Wait for the process to exit
151            match child.status().await {
152                Ok(status) => tracing::info!(exit_status = ?status, "Server process stopped"),
153                Err(e) => tracing::warn!("Failed to get exit status after stopping: {}", e),
154            }
155
156            self.status = ServerStatus::Stopped;
157            Ok(())
158        } else {
159            tracing::warn!("Attempted to stop a server that was not running");
160            Err(Error::NotRunning)
161        }
162    }
163
164    /// Take ownership of the server's stdin handle.
165    ///
166    /// This method is instrumented with `tracing`.
167    #[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
168    pub fn take_stdin(&mut self) -> Result<async_process::ChildStdin> {
169        if let Some(child) = &mut self.child {
170            child.stdin.take().ok_or_else(|| {
171                Error::Process("Failed to get stdin pipe from child process".to_string())
172            })
173        } else {
174            Err(Error::NotRunning)
175        }
176    }
177
178    /// Take ownership of the server's stdout handle.
179    ///
180    /// This method is instrumented with `tracing`.
181    #[tracing::instrument(skip(self), fields(server_name = %self.name, server_id = %self.id))]
182    pub fn take_stdout(&mut self) -> Result<async_process::ChildStdout> {
183        if let Some(child) = &mut self.child {
184            child.stdout.take().ok_or_else(|| {
185                Error::Process("Failed to get stdout pipe from child process".to_string())
186            })
187        } else {
188            Err(Error::NotRunning)
189        }
190    }
191
192    /// Take the stderr pipe from the process
193    pub fn take_stderr(&mut self) -> Result<async_process::ChildStderr> {
194        if let Some(child) = &mut self.child {
195            child.stderr.take().ok_or_else(|| {
196                Error::Process("Failed to get stderr pipe from child process".to_string())
197            })
198        } else {
199            Err(Error::NotRunning)
200        }
201    }
202}