mcp_runner/server/
process.rs1use crate::config::ServerConfig;
3use crate::error::{Error, Result};
4use async_process::{Child, Command, Stdio};
5use std::fmt;
6use tracing;
7use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ServerId(Uuid);
12
13impl ServerId {
14 pub(crate) fn new() -> Self {
16 Self(Uuid::new_v4())
17 }
18}
19
20impl fmt::Display for ServerId {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 write!(f, "{}", self.0)
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServerStatus {
30 Starting,
32 Running,
34 Stopping,
36 Stopped,
38 Failed,
40}
41
42pub struct ServerProcess {
48 config: ServerConfig,
50 name: String,
52 id: ServerId,
54 child: Option<Child>,
56 status: ServerStatus,
58}
59
60impl ServerProcess {
61 #[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 pub fn id(&self) -> ServerId {
77 self.id
78 }
79
80 pub fn name(&self) -> &str {
82 &self.name
83 }
84
85 #[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 #[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 for (key, value) in &self.config.env {
111 command.env(key, value);
112 }
113
114 command
116 .stdin(Stdio::piped())
117 .stdout(Stdio::piped())
118 .stderr(Stdio::piped());
119
120 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 #[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 if let Err(e) = child.kill() {
145 tracing::error!("Failed to kill process: {}", e);
146 }
149
150 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 #[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 #[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 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}