#[cfg(not(feature = "mcp"))]
fn main() {
use std::io::Write as _;
let _ignored = writeln!(
std::io::stderr().lock(),
"runx-mcp-session-probe requires the runx-runtime mcp feature"
);
std::process::exit(1);
}
#[cfg(feature = "mcp")]
fn main() {
if let Err(error) = mcp_probe::run() {
use std::io::Write as _;
let _ignored = writeln!(std::io::stderr().lock(), "{error}");
std::process::exit(1);
}
}
#[cfg(feature = "mcp")]
mod mcp_probe {
use std::collections::BTreeMap;
use std::io::Write as _;
use std::path::PathBuf;
use std::time::Instant;
use runx_contracts::{JsonObject, JsonValue};
use runx_parser::SkillMcpServer;
use runx_runtime::adapters::mcp::{McpToolCallRequest, McpTransport, ProcessMcpTransport};
use runx_runtime::credentials::SecretEnv;
use runx_runtime::sandbox::SandboxPlan;
use serde_json::json;
pub(super) fn run() -> Result<(), String> {
let mode = std::env::args()
.nth(1)
.ok_or_else(|| "usage: runx-mcp-session-probe <start|reuse>".to_owned())?;
let transport = ProcessMcpTransport::default();
let samples = match mode.as_str() {
"start" => measure_session_start(&transport)?,
"reuse" => measure_session_reuse(&transport)?,
other => return Err(format!("unknown MCP session probe mode '{other}'")),
};
let sorted = sorted_samples(&samples.durations_ns);
let mean_ns = samples.durations_ns.iter().sum::<f64>() / samples.durations_ns.len() as f64;
let output = json!({
"source": "mcp_runtime",
"unit": "iterations_per_second",
"mean_ns": mean_ns,
"p95_ns": percentile(&sorted, 0.95),
"p99_ns": percentile(&sorted, 0.99),
"throughput": 1_000_000_000_f64 / mean_ns,
"allocation_count": 0,
"spawn_count": samples.spawn_count,
"call_count": samples.call_count,
});
writeln!(std::io::stdout().lock(), "{output}").map_err(|error| error.to_string())?;
transport
.reset_session_pool()
.map_err(|error| error.sanitized_message())?;
Ok(())
}
struct ProbeSamples {
durations_ns: Vec<f64>,
spawn_count: u64,
call_count: u64,
}
fn measure_session_start(transport: &ProcessMcpTransport) -> Result<ProbeSamples, String> {
let mut durations_ns = Vec::new();
let mut max_spawn_count = 0;
for index in 0..3 {
transport
.reset_session_pool()
.map_err(|error| error.sanitized_message())?;
transport.reset_spawn_count();
durations_ns.push(timed_echo(
transport,
"start-scope",
&format!("start-{index}"),
)?);
max_spawn_count = max_spawn_count.max(transport.spawned_process_count());
}
Ok(ProbeSamples {
durations_ns,
spawn_count: max_spawn_count,
call_count: 1,
})
}
fn measure_session_reuse(transport: &ProcessMcpTransport) -> Result<ProbeSamples, String> {
transport
.reset_session_pool()
.map_err(|error| error.sanitized_message())?;
transport.reset_spawn_count();
invoke_echo(transport, "reuse-scope", "warm")?;
let mut durations_ns = Vec::new();
for index in 0..5 {
durations_ns.push(timed_echo(
transport,
"reuse-scope",
&format!("reuse-{index}"),
)?);
}
Ok(ProbeSamples {
durations_ns,
spawn_count: transport.spawned_process_count(),
call_count: 6,
})
}
fn timed_echo(
transport: &ProcessMcpTransport,
scope: &str,
message: &str,
) -> Result<f64, String> {
let started = Instant::now();
invoke_echo(transport, scope, message)?;
Ok(started.elapsed().as_secs_f64() * 1_000_000_000_f64)
}
fn invoke_echo(
transport: &ProcessMcpTransport,
scope: &str,
message: &str,
) -> Result<(), String> {
let result = transport
.call_tool(tool_call(scope, message)?)
.map_err(|error| error.sanitized_message())?;
let Some(stdout) = tool_result_text(&result) else {
return Err(format!(
"MCP probe call returned non-text result: {result:?}"
));
};
if stdout != message {
return Err(format!(
"MCP probe call returned unexpected stdout '{}'",
stdout
));
}
Ok(())
}
fn tool_call(scope: &str, message: &str) -> Result<McpToolCallRequest, String> {
let mut inputs = JsonObject::new();
inputs.insert("message".to_owned(), JsonValue::String(message.to_owned()));
let root = repo_root()?;
let server = SkillMcpServer {
command: "node".to_owned(),
args: vec![
root.join("fixtures/runtime/adapters/mcp/stdio-server.mjs")
.to_string_lossy()
.into_owned(),
],
cwd: Some(root.to_string_lossy().into_owned()),
};
let mut env = process_env();
env.insert("RUNX_MCP_SCOPE".to_owned(), scope.to_owned());
Ok(McpToolCallRequest {
server,
tool: "echo".to_owned(),
arguments: inputs,
timeout: std::time::Duration::from_secs(5),
sandbox: SandboxPlan {
command: "node".to_owned(),
args: vec![
root.join("fixtures/runtime/adapters/mcp/stdio-server.mjs")
.to_string_lossy()
.into_owned(),
],
cwd: root,
env,
metadata: JsonObject::new(),
cleanup_paths: Vec::new(),
},
secret_env: SecretEnv::default(),
})
}
fn tool_result_text(result: &JsonValue) -> Option<&str> {
let JsonValue::Array(items) = result.as_object()?.get("content")? else {
return None;
};
let first = items.first()?.as_object()?;
first.get("text")?.as_str()
}
fn repo_root() -> Result<PathBuf, String> {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.map_err(|error| format!("repository root is unavailable: {error}"))
}
fn process_env() -> BTreeMap<String, String> {
[
"PATH",
"HOME",
"TMPDIR",
"TMP",
"TEMP",
"SystemRoot",
"WINDIR",
"COMSPEC",
"PATHEXT",
]
.into_iter()
.filter_map(|name| {
std::env::var(name)
.ok()
.map(|value| (name.to_owned(), value))
})
.collect()
}
fn sorted_samples(samples: &[f64]) -> Vec<f64> {
let mut sorted = samples.to_vec();
sorted.sort_by(|left, right| left.total_cmp(right));
sorted
}
fn percentile(sorted: &[f64], percentile_value: f64) -> f64 {
let index = sorted
.len()
.saturating_sub(1)
.min((sorted.len() as f64 * percentile_value).ceil() as usize - 1);
sorted[index]
}
}