1use std::net::Ipv6Addr;
10
11use rmcp::{
12 transport::streamable_http_server::{
13 session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
14 },
15 ServerHandler,
16};
17
18#[derive(Clone, Copy, Debug)]
23pub struct AllowedHostsInput<'a> {
24 pub bind_host: &'a str,
25 pub port: u16,
26 pub extra_hosts: &'a [String],
27 pub public_url: Option<&'a str>,
28 pub public_url_label: &'a str,
34}
35
36pub fn allowed_hosts(input: AllowedHostsInput<'_>) -> Vec<String> {
37 let mut hosts = vec!["localhost".to_string(), "127.0.0.1".to_string()];
38 push_host_variants(&mut hosts, input.bind_host, input.port);
39 push_host_variants(&mut hosts, "localhost", input.port);
40 push_host_variants(&mut hosts, "127.0.0.1", input.port);
41 push_host_variants(&mut hosts, "::1", input.port);
42 for host in input.extra_hosts {
43 push_host_variants(&mut hosts, host, input.port);
44 }
45 if let Some(public_url) = input.public_url {
46 push_public_url_hosts(&mut hosts, public_url, input.port, input.public_url_label);
47 }
48 hosts.sort();
49 hosts.dedup();
50 hosts
51}
52
53#[derive(Clone, Copy, Debug)]
58pub struct AllowedOriginsInput<'a> {
59 pub port: u16,
60 pub extra_origins: &'a [String],
61 pub public_url: Option<&'a str>,
62 pub extra_origins_label: &'a str,
67 pub public_url_label: &'a str,
72}
73
74pub fn allowed_origins(input: AllowedOriginsInput<'_>) -> Vec<String> {
75 let mut origins = vec![
76 format!("http://localhost:{}", input.port),
77 format!("http://127.0.0.1:{}", input.port),
78 ];
79 for origin in input.extra_origins {
80 push_configured_origin(&mut origins, origin, input.extra_origins_label);
81 }
82 if let Some(public_url) = input.public_url {
83 if let Some(origin) = extract_origin_with_label(public_url, input.public_url_label) {
84 origins.push(origin);
85 }
86 }
87 origins.sort();
88 origins.dedup();
89 origins
90}
91
92pub fn streamable_http_config(
95 hosts: Vec<String>,
96 origins: Vec<String>,
97) -> StreamableHttpServerConfig {
98 StreamableHttpServerConfig::default()
99 .with_stateful_mode(false)
100 .with_json_response(true)
101 .with_allowed_hosts(hosts)
102 .with_allowed_origins(origins)
103}
104
105pub fn streamable_http_service<S, F>(
109 factory: F,
110 config: StreamableHttpServerConfig,
111) -> StreamableHttpService<S, LocalSessionManager>
112where
113 S: ServerHandler,
114 F: Fn() -> Result<S, std::io::Error> + Send + Sync + 'static,
115{
116 StreamableHttpService::new(factory, Default::default(), config)
117}
118
119fn push_configured_origin(origins: &mut Vec<String>, origin: &str, label: &str) {
122 let Some(origin) = extract_configured_origin_with_label(origin, label) else {
123 return;
124 };
125 origins.push(origin);
126}
127
128fn push_host_variants(hosts: &mut Vec<String>, host: &str, port: u16) {
129 let host = host.trim();
130 if host.is_empty() {
131 return;
132 }
133 hosts.push(host.to_string());
134 if host.starts_with('[') && host.contains("]:") {
135 return;
136 }
137 if let Some(inner) = host.strip_prefix('[').and_then(|v| v.strip_suffix(']')) {
138 if !inner.is_empty() {
139 hosts.push(format!("[{inner}]:{port}"));
140 }
141 } else if host.parse::<Ipv6Addr>().is_ok() {
142 hosts.push(format!("[{host}]"));
143 hosts.push(format!("[{host}]:{port}"));
144 } else if !has_port(host) {
145 hosts.push(format!("{host}:{port}"));
146 }
147}
148
149fn push_public_url_hosts(hosts: &mut Vec<String>, url: &str, listen_port: u16, label: &str) {
150 let Ok(parsed) = url::Url::parse(url) else {
151 tracing::warn!(
152 setting = label,
153 public_url = url,
154 "MCP public URL is not a valid URL"
155 );
156 return;
157 };
158 let Some(host) = parsed.host_str() else {
159 return;
160 };
161 if host.contains('*') {
162 tracing::warn!(
163 setting = label,
164 host,
165 "MCP public URL host contains wildcard; skipping"
166 );
167 return;
168 }
169 let explicit_port = parsed.port();
170 let scheme_default = match parsed.scheme() {
171 "https" => Some(443u16),
172 "http" => Some(80u16),
173 _ => None,
174 };
175 if let Some(p) = explicit_port {
176 push_host_variants(hosts, host, p);
177 let with_port = format!("{host}:{p}");
178 if !hosts.contains(&with_port) {
179 hosts.push(with_port);
180 }
181 } else if let Some(default_port) = scheme_default {
182 let bare = host.to_string();
183 if !hosts.contains(&bare) {
184 hosts.push(bare);
185 }
186 let with_default = format!("{host}:{default_port}");
187 if !hosts.contains(&with_default) {
188 hosts.push(with_default);
189 }
190 } else {
191 push_host_variants(hosts, host, listen_port);
192 }
193}
194
195fn has_port(host: &str) -> bool {
196 host.rsplit_once(':')
197 .and_then(|(_, p)| p.parse::<u16>().ok())
198 .is_some()
199}
200
201fn extract_origin_with_label(url: &str, label: &str) -> Option<String> {
202 let parsed = url::Url::parse(url)
203 .map_err(|e| tracing::warn!(setting = label, url, error = %e, "invalid MCP origin URL"))
204 .ok()?;
205 let scheme = parsed.scheme();
206 let host = parsed.host()?;
207 let host_text = format_origin_host(host);
208 if host_text.contains('*') {
209 tracing::warn!(
210 setting = label,
211 host = %host_text,
212 "MCP origin host contains wildcard; skipping"
213 );
214 return None;
215 }
216 let default_port = match scheme {
217 "http" => Some(80u16),
218 "https" => Some(443u16),
219 _ => {
220 tracing::warn!(
221 setting = label,
222 scheme,
223 "MCP origin URL must use http or https"
224 );
225 return None;
226 }
227 };
228 let origin = match parsed.port() {
229 Some(port) if default_port != Some(port) => format!("{scheme}://{host_text}:{port}"),
230 _ => format!("{scheme}://{host_text}"),
231 };
232 Some(origin)
233}
234
235fn extract_configured_origin_with_label(url: &str, label: &str) -> Option<String> {
236 match extract_origin_with_label(url, label) {
237 Some(origin) => Some(origin),
238 None => {
239 let parsed = url::Url::parse(url).ok()?;
240 if matches!(parsed.scheme(), "http" | "https") {
241 return None;
242 }
243 Some(url.trim().to_string())
244 }
245 }
246}
247
248fn format_origin_host(host: url::Host<&str>) -> String {
249 match host {
250 url::Host::Domain(domain) => domain.to_string(),
251 url::Host::Ipv4(addr) => addr.to_string(),
252 url::Host::Ipv6(addr) => format!("[{addr}]"),
253 }
254}
255
256#[cfg(test)]
257#[path = "http_tests.rs"]
258mod tests;