use std::path::Path;
use serde_json::{json, Value};
use super::super::{RecoverableError, ToolContext};
pub(crate) async fn run_command_interactive(
command: &str,
cwd_param: Option<&str>,
_timeout_secs: u64,
root: &Path,
security: &crate::util::path_security::PathSecurityConfig,
ctx: &ToolContext,
) -> anyhow::Result<Value> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
if ctx.peer.is_none() {
return Err(RecoverableError::with_hint(
"interactive mode requires elicitation support",
"The MCP client does not support elicitation. Use run_command without interactive: true.",
)
.into());
}
if let Some(reason) = crate::util::path_security::is_dangerous_command(command, security) {
return Err(RecoverableError::with_hint(
format!("interactive mode blocked dangerous command: {reason}"),
"Remove the dangerous pattern or use the non-interactive path with acknowledge_risk: true.",
)
.into());
}
let work_dir = if let Some(rel) = cwd_param {
let candidate = root.join(rel);
candidate.canonicalize().map_err(|e| {
RecoverableError::with_hint(
format!("cwd '{rel}' is not a valid directory: {e}"),
"Provide a relative path to an existing subdirectory of the project.",
)
})?
} else {
root.to_path_buf()
};
let mut cmd = crate::platform::shell_command_configured(command);
let mut child = cmd
.current_dir(&work_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let mut stdin = child.stdin.take().expect("stdin piped");
let mut stdout_reader = tokio::io::BufReader::new(child.stdout.take().expect("stdout piped"));
let mut stderr_reader = tokio::io::BufReader::new(child.stderr.take().expect("stderr piped"));
let mut accumulated_output = String::new();
async fn drain_with_settle(
stdout_reader: &mut tokio::io::BufReader<tokio::process::ChildStdout>,
stderr_reader: &mut tokio::io::BufReader<tokio::process::ChildStderr>,
settle_ms: u64,
) -> String {
let settle = std::time::Duration::from_millis(settle_ms);
let mut output = String::new();
let mut out_buf = [0u8; 4096];
let mut err_buf = [0u8; 4096];
loop {
tokio::select! {
result = tokio::time::timeout(settle, stdout_reader.read(&mut out_buf)) => {
match result {
Ok(Ok(n)) if n > 0 => {
output.push_str(&String::from_utf8_lossy(&out_buf[..n]));
}
_ => break, }
}
result = tokio::time::timeout(settle, stderr_reader.read(&mut err_buf)) => {
match result {
Ok(Ok(n)) if n > 0 => {
output.push_str(&String::from_utf8_lossy(&err_buf[..n]));
}
_ => break, }
}
}
}
output
}
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
struct InteractiveInput {
input: String,
}
rmcp::elicit_safe!(InteractiveInput);
let mut round = 0u32;
const MAX_ROUNDS: u32 = 50; loop {
if round >= MAX_ROUNDS {
let _ = child.kill().await;
accumulated_output.push_str("\n[interactive: max rounds reached, process killed]");
break;
}
round += 1;
let chunk = drain_with_settle(&mut stdout_reader, &mut stderr_reader, 150).await;
if !chunk.is_empty() {
accumulated_output.push_str(&chunk);
}
match child.try_wait() {
Ok(Some(status)) => {
let code = status.code().unwrap_or(-1);
let tail = drain_with_settle(&mut stdout_reader, &mut stderr_reader, 50).await;
if !tail.is_empty() {
accumulated_output.push_str(&tail);
}
return Ok(json!({
"exit_code": code,
"stdout": accumulated_output,
"interactive_rounds": round,
}));
}
Ok(None) => {} Err(e) => {
accumulated_output.push_str(&format!("\n[interactive: wait error: {e}]"));
break;
}
}
let display_output = if accumulated_output.len() > 4000 {
&accumulated_output[crate::tools::floor_char_boundary(
&accumulated_output,
accumulated_output.len() - 4000,
)..]
} else {
&accumulated_output
};
let prompt = format!(
"Process output (round {round}):\n```\n{display_output}\n```\n\nEnter input to send to stdin, or leave empty to cancel:"
);
let elicited = ctx.elicit::<InteractiveInput>(prompt).await?;
match elicited {
None => {
let _ = child.kill().await;
accumulated_output
.push_str("\n[interactive: elicitation unavailable, process killed]");
break;
}
Some(InteractiveInput { input }) if input.is_empty() => {
let _ = child.kill().await;
accumulated_output.push_str("\n[interactive: cancelled by user]");
break;
}
Some(InteractiveInput { mut input }) => {
if !input.ends_with('\n') {
input.push('\n');
}
if let Err(e) = stdin.write_all(input.as_bytes()).await {
accumulated_output
.push_str(&format!("\n[interactive: stdin write error: {e}]"));
let _ = child.kill().await;
break;
}
}
}
}
let tail = drain_with_settle(&mut stdout_reader, &mut stderr_reader, 50).await;
if !tail.is_empty() {
accumulated_output.push_str(&tail);
}
Ok(json!({
"exit_code": -1,
"stdout": accumulated_output,
"interactive_rounds": round,
"note": "process killed or loop exited before natural termination",
}))
}