1use std::ffi::OsString;
2use std::io::Write;
3use std::path::PathBuf;
4use std::process::ExitCode;
5use std::{collections::BTreeMap, env};
6
7use crate::cli_args::{flag_value, optional_flag_value_or, os_arg, split_flag};
8
9#[derive(Debug, Eq, PartialEq)]
10pub struct McpPlan {
11 pub refs: Vec<PathBuf>,
12 pub receipt_dir: Option<PathBuf>,
13 pub runner: Option<String>,
14 pub http_listen: Option<String>,
17 pub http_allow_non_loopback: bool,
18}
19
20pub fn parse_mcp_plan(args: &[OsString]) -> Result<McpPlan, String> {
23 let subcommand = os_arg(args, 1, "mcp")?;
24 if subcommand != "serve" {
25 return Err(format!("unknown mcp subcommand {subcommand}"));
26 }
27 let mut refs = Vec::new();
28 let mut receipt_dir = None;
29 let mut runner = None;
30 let mut http_listen = None;
31 let mut http_allow_non_loopback = false;
32 let mut index = 2;
33 while index < args.len() {
34 let token = os_arg(args, index, "mcp")?;
35 if !token.starts_with("--") {
36 refs.push(PathBuf::from(token));
37 index += 1;
38 continue;
39 }
40 let (flag, inline_value) = split_flag(token);
41 match flag {
42 "--receipt-dir" => {
43 let (value, next_index) = flag_value(args, index, flag, inline_value, "mcp")?;
44 receipt_dir = Some(PathBuf::from(value));
45 index = next_index;
46 }
47 "--runner" => {
48 let (value, next_index) = flag_value(args, index, flag, inline_value, "mcp")?;
49 runner = Some(value);
50 index = next_index;
51 }
52 "--http-listen" => {
53 let (value, next_index) = optional_flag_value_or(
54 args,
55 index,
56 inline_value,
57 runx_runtime::adapters::mcp::DEFAULT_MCP_HTTP_LISTEN_ADDR,
58 "mcp",
59 )?;
60 http_listen = Some(value);
61 index = next_index;
62 }
63 "--http-allow-non-loopback" => {
64 if inline_value.is_some() {
65 return Err("--http-allow-non-loopback does not take a value".to_owned());
66 }
67 http_allow_non_loopback = true;
68 index += 1;
69 }
70 _ => return Err(format!("unknown mcp serve flag {flag}")),
71 }
72 }
73 if refs.is_empty() {
74 return Err("runx mcp serve requires at least one skill reference.".to_owned());
75 }
76 Ok(McpPlan {
77 refs,
78 receipt_dir,
79 runner,
80 http_listen,
81 http_allow_non_loopback,
82 })
83}
84
85pub fn run_native_mcp(plan: McpPlan) -> ExitCode {
88 let options =
89 match runx_runtime::adapters::mcp::McpServerOptions::from_skill_paths_with_execution(
90 &plan.refs,
91 "runx-cli",
92 env!("CARGO_PKG_VERSION"),
93 runx_runtime::adapters::mcp::McpServerExecutionOptions {
94 runner: plan.runner,
95 receipt_dir: plan.receipt_dir,
96 env: mcp_execution_env(),
97 },
98 ) {
99 Ok(options) => options,
100 Err(error) => {
101 let _ignored = writeln!(std::io::stderr(), "runx: {error}");
102 return ExitCode::from(1);
103 }
104 };
105 let result = match &plan.http_listen {
106 Some(listen_addr) => {
107 let bearer_token = match runx_runtime::adapters::mcp::generate_mcp_http_bearer_token() {
108 Ok(token) => token,
109 Err(error) => {
110 let _ignored = writeln!(std::io::stderr(), "runx: {error}");
111 return ExitCode::from(1);
112 }
113 };
114 let _ignored = writeln!(
115 std::io::stderr(),
116 "runx MCP HTTP bearer token: {bearer_token}"
117 );
118 let _ignored = writeln!(
119 std::io::stderr(),
120 "runx MCP HTTP requires: Authorization: Bearer {bearer_token}"
121 );
122 if plan.http_allow_non_loopback {
123 let _ignored = writeln!(
124 std::io::stderr(),
125 "runx MCP HTTP non-loopback listen explicitly enabled."
126 );
127 }
128 runx_runtime::adapters::mcp::serve_mcp_http_server_blocking(
129 listen_addr,
130 options,
131 runx_runtime::adapters::mcp::McpHttpServerSecurity {
132 bearer_token,
133 allow_non_loopback: plan.http_allow_non_loopback,
134 },
135 )
136 }
137 None => runx_runtime::adapters::mcp::serve_mcp_json_rpc(
138 std::io::stdin(),
139 std::io::stdout(),
140 options,
141 ),
142 };
143 match result {
144 Ok(()) => ExitCode::SUCCESS,
145 Err(error) => {
146 let _ignored = writeln!(std::io::stderr(), "runx: {error}");
147 ExitCode::from(1)
148 }
149 }
150}
151
152fn mcp_execution_env() -> BTreeMap<String, String> {
153 let mut env = env::vars().collect::<BTreeMap<_, _>>();
154 if !env.contains_key(runx_runtime::RUNX_CWD_ENV)
155 && let Ok(cwd) = env::current_dir()
156 {
157 env.insert(
158 runx_runtime::RUNX_CWD_ENV.to_owned(),
159 cwd.to_string_lossy().into_owned(),
160 );
161 }
162 env
163}