1use sim_kernel::{CapabilityName, Error, Result};
2use sim_lib_mcp::McpProfile;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct CliOptions {
7 pub transport: Transport,
9 pub profile: McpProfile,
11 pub capabilities: Vec<CapabilityName>,
13 pub log_stderr: bool,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum Transport {
20 Stdio,
22 Http {
24 address: String,
26 route: String,
28 },
29}
30
31impl CliOptions {
32 pub fn parse() -> Result<Self> {
34 Self::parse_from(std::env::args().skip(1))
35 }
36
37 pub fn parse_from(args: impl IntoIterator<Item = String>) -> Result<Self> {
39 let mut transport = None;
40 let mut profile = McpProfile::all();
41 let mut capabilities = Vec::new();
42 let mut route = "/mcp".to_owned();
43 let mut log_stderr = false;
44
45 let mut iter = args.into_iter();
46 while let Some(arg) = iter.next() {
47 match arg.as_str() {
48 "--stdio" => set_transport(&mut transport, Transport::Stdio)?,
49 "--http" => {
50 let address = next_arg(&mut iter, "--http expects host:port")?;
51 set_transport(
52 &mut transport,
53 Transport::Http {
54 address,
55 route: route.clone(),
56 },
57 )?;
58 }
59 "--route" => {
60 route = next_arg(&mut iter, "--route expects a path")?;
61 if let Some(Transport::Http {
62 route: http_route, ..
63 }) = &mut transport
64 {
65 *http_route = route.clone();
66 }
67 }
68 "--profile" => {
69 let name = next_arg(&mut iter, "--profile expects a name")?;
70 profile = parse_profile(&name)?;
71 }
72 "--allow-tool" => {
73 profile = profile.with_allowed_name(next_arg(
74 &mut iter,
75 "--allow-tool expects a name or glob",
76 )?);
77 }
78 "--deny-tool" => {
79 profile = profile.with_denied_name(next_arg(
80 &mut iter,
81 "--deny-tool expects a name or glob",
82 )?);
83 }
84 "--cap" => {
85 capabilities.push(CapabilityName::new(next_arg(
86 &mut iter,
87 "--cap expects a capability name",
88 )?));
89 }
90 "--no-default-tools" => {
91 profile = profile.with_denied_name("*");
92 }
93 "--log-stderr" => log_stderr = true,
94 other => {
95 return Err(Error::Eval(format!(
96 "unknown sim-mcp-server option {other}"
97 )));
98 }
99 }
100 }
101
102 Ok(Self {
103 transport: transport.unwrap_or(Transport::Stdio),
104 profile,
105 capabilities,
106 log_stderr,
107 })
108 }
109}
110
111fn set_transport(slot: &mut Option<Transport>, transport: Transport) -> Result<()> {
112 if slot.is_some() {
113 return Err(Error::Eval(
114 "sim-mcp-server accepts one transport option".to_owned(),
115 ));
116 }
117 *slot = Some(transport);
118 Ok(())
119}
120
121fn parse_profile(name: &str) -> Result<McpProfile> {
122 match name {
123 "default" => Ok(McpProfile::all()),
124 other => Err(Error::Eval(format!("unknown MCP profile {other}"))),
125 }
126}
127
128fn next_arg(iter: &mut impl Iterator<Item = String>, message: &'static str) -> Result<String> {
129 iter.next().ok_or_else(|| Error::Eval(message.to_owned()))
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn parses_stdio_profile_caps_and_filters() {
138 let opts = CliOptions::parse_from([
139 "--stdio".to_owned(),
140 "--profile".to_owned(),
141 "default".to_owned(),
142 "--allow-tool".to_owned(),
143 "core.*".to_owned(),
144 "--deny-tool".to_owned(),
145 "*.danger*".to_owned(),
146 "--cap".to_owned(),
147 "mcp.tools.call".to_owned(),
148 "--log-stderr".to_owned(),
149 ])
150 .unwrap();
151
152 assert_eq!(opts.transport, Transport::Stdio);
153 assert_eq!(
154 opts.capabilities,
155 vec![CapabilityName::new("mcp.tools.call")]
156 );
157 assert!(opts.log_stderr);
158 assert!(opts.profile.allows_name("core.echo"));
159 assert!(!opts.profile.allows_name("core.dangerous"));
160 }
161
162 #[test]
163 fn duplicate_transport_is_rejected() {
164 let err =
165 CliOptions::parse_from(["--stdio".to_owned(), "--http".to_owned(), "x:1".to_owned()])
166 .unwrap_err();
167
168 assert!(format!("{err}").contains("one transport"));
169 }
170}