use core::num::{NonZeroU32, NonZeroUsize};
use core::str::FromStr;
use core::time::Duration;
use std::sync::Arc;
use thiserror::Error;
use crate::domain::MaxFrameSize;
use crate::domain::RateLimit;
use crate::domain::mcp::ResourceMetadata;
use crate::domain::registry::ScopeSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Endpoint(Arc<str>);
impl Endpoint {
pub const DEFAULT: &'static str = "/mcp";
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl core::fmt::Display for Endpoint {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for Endpoint {
type Err = EndpointError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(EndpointError::Empty);
}
if !trimmed.starts_with('/') {
return Err(EndpointError::NoLeadingSlash);
}
if trimmed.contains(['?', '#']) {
return Err(EndpointError::QueryOrFragment);
}
if !trimmed
.bytes()
.all(|byte| byte.is_ascii_graphic() || byte == b'/')
{
return Err(EndpointError::NotVisibleAscii);
}
Ok(Self(Arc::from(trimmed)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum EndpointError {
#[error("the MCP endpoint path is empty")]
Empty,
#[error("the MCP endpoint path must start with '/'")]
NoLeadingSlash,
#[error(
"the MCP endpoint path must be a path only: a query string or fragment never reaches the server"
)]
QueryOrFragment,
#[error("the MCP endpoint path contains a character that is not valid in a request line")]
NotVisibleAscii,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Streaming {
#[default]
Json,
EventStream,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecurityHeaders {
content_security_policy: Arc<str>,
strict_transport_security: Arc<str>,
x_frame_options: Arc<str>,
}
impl Default for SecurityHeaders {
fn default() -> Self {
Self {
content_security_policy: Arc::from(
"default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
),
strict_transport_security: Arc::from("max-age=31536000; includeSubDomains"),
x_frame_options: Arc::from("DENY"),
}
}
}
impl SecurityHeaders {
#[must_use]
pub fn with_content_security_policy(mut self, value: impl Into<Arc<str>>) -> Self {
self.content_security_policy = value.into();
self
}
#[must_use]
pub fn with_strict_transport_security(mut self, value: impl Into<Arc<str>>) -> Self {
self.strict_transport_security = value.into();
self
}
#[must_use]
pub fn with_x_frame_options(mut self, value: impl Into<Arc<str>>) -> Self {
self.x_frame_options = value.into();
self
}
#[must_use]
pub fn content_security_policy(&self) -> &str {
&self.content_security_policy
}
#[must_use]
pub fn strict_transport_security(&self) -> &str {
&self.strict_transport_security
}
#[must_use]
pub fn x_frame_options(&self) -> &str {
&self.x_frame_options
}
}
const DEFAULT_STREAM_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_MAX_CONCURRENCY: NonZeroU32 = match NonZeroU32::new(256) {
Some(value) => value,
None => unreachable!(),
};
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_RATE_LIMIT: RateLimit = RateLimit::new(
match NonZeroU32::new(600) {
Some(value) => value,
None => unreachable!(),
},
DEFAULT_RATE_WINDOW,
);
const DEFAULT_RATE_WINDOW: Duration = Duration::from_secs(60);
const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(15);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerConfig {
endpoint: Endpoint,
max_body: MaxFrameSize,
replay_capacity: NonZeroUsize,
streaming: Streaming,
stream_timeout: Duration,
keep_alive: Duration,
security: SecurityHeaders,
required_scopes: ScopeSet,
resource_metadata: Option<Arc<ResourceMetadata>>,
max_concurrency: NonZeroU32,
request_timeout: Duration,
rate_limit: RateLimit,
}
impl ServerConfig {
pub fn new() -> Result<Self, EndpointError> {
Ok(Self {
endpoint: Endpoint::DEFAULT.parse()?,
max_body: MaxFrameSize::DEFAULT,
replay_capacity: crate::domain::RECOMMENDED_CAPACITY.into(),
streaming: Streaming::default(),
stream_timeout: DEFAULT_STREAM_TIMEOUT,
keep_alive: DEFAULT_KEEP_ALIVE,
security: SecurityHeaders::default(),
required_scopes: ScopeSet::empty(),
resource_metadata: None,
max_concurrency: DEFAULT_MAX_CONCURRENCY,
request_timeout: DEFAULT_REQUEST_TIMEOUT,
rate_limit: DEFAULT_RATE_LIMIT,
})
}
#[must_use]
pub const fn max_concurrency(&self) -> NonZeroU32 {
self.max_concurrency
}
#[must_use]
pub const fn with_max_concurrency(mut self, limit: NonZeroU32) -> Self {
self.max_concurrency = limit;
self
}
#[must_use]
pub const fn request_timeout(&self) -> Duration {
self.request_timeout
}
#[must_use]
pub const fn with_request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = timeout;
self
}
#[must_use]
pub const fn rate_limit(&self) -> &RateLimit {
&self.rate_limit
}
#[must_use]
pub const fn with_rate_limit(mut self, limit: RateLimit) -> Self {
self.rate_limit = limit;
self
}
#[must_use]
pub const fn endpoint(&self) -> &Endpoint {
&self.endpoint
}
#[must_use]
pub const fn max_body(&self) -> MaxFrameSize {
self.max_body
}
#[must_use]
pub const fn replay_capacity(&self) -> NonZeroUsize {
self.replay_capacity
}
#[must_use]
pub const fn streaming(&self) -> Streaming {
self.streaming
}
#[must_use]
pub const fn with_streaming(mut self, streaming: Streaming) -> Self {
self.streaming = streaming;
self
}
#[must_use]
pub const fn stream_timeout(&self) -> Duration {
self.stream_timeout
}
#[must_use]
pub const fn with_stream_timeout(mut self, timeout: Duration) -> Self {
self.stream_timeout = timeout;
self
}
#[must_use]
pub const fn keep_alive(&self) -> Duration {
self.keep_alive
}
#[must_use]
pub const fn with_keep_alive(mut self, keep_alive: Duration) -> Self {
self.keep_alive = keep_alive;
self
}
#[must_use]
pub const fn security(&self) -> &SecurityHeaders {
&self.security
}
#[must_use]
pub fn with_security(mut self, security: SecurityHeaders) -> Self {
self.security = security;
self
}
#[must_use]
pub const fn required_scopes(&self) -> ScopeSet {
self.required_scopes
}
#[must_use]
pub const fn with_required_scopes(mut self, scopes: ScopeSet) -> Self {
self.required_scopes = scopes;
self
}
#[must_use]
pub fn resource_metadata(&self) -> Option<&ResourceMetadata> {
self.resource_metadata.as_deref()
}
#[must_use]
pub fn with_resource_metadata(mut self, metadata: ResourceMetadata) -> Self {
self.resource_metadata = Some(Arc::new(metadata));
self
}
pub fn challenge(&self) -> Option<String> {
self.resource_metadata.as_ref()?.challenge().ok()
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
use super::{Endpoint, EndpointError, SecurityHeaders, ServerConfig, Streaming};
type TestError = Box<dyn std::error::Error>;
#[test]
fn the_default_endpoint_is_the_specification_example() -> Result<(), TestError> {
let config = ServerConfig::new()?;
assert_eq!(config.endpoint().as_str(), "/mcp");
Ok(())
}
#[test]
fn a_malformed_endpoint_is_refused_at_parse_time() -> Result<(), TestError> {
for case in ["", " ", "mcp", "mcp/stream", "/mcp stream", "/mcp\0mcp"] {
assert!(
case.parse::<Endpoint>().is_err(),
"accepted {case:?} as an endpoint"
);
}
assert_eq!(
"/mcp?debug=1".parse::<Endpoint>(),
Err(EndpointError::QueryOrFragment)
);
assert_eq!(
"/mcp#frag".parse::<Endpoint>(),
Err(EndpointError::QueryOrFragment)
);
assert_eq!(
"mcp".parse::<Endpoint>(),
Err(EndpointError::NoLeadingSlash)
);
assert_eq!("/a/b".parse::<Endpoint>()?.as_str(), "/a/b");
assert_eq!(" /mcp ".parse::<Endpoint>()?.as_str(), "/mcp");
assert_eq!("/mcp\n".parse::<Endpoint>()?.as_str(), "/mcp");
Ok(())
}
#[test]
fn a_well_formed_endpoint_round_trips() -> Result<(), TestError> {
for case in ["/mcp", "/", "/a/b", "/v1/mcp", "/mcp_v2", "/a-b_c.d~e"] {
assert_eq!(case.parse::<Endpoint>()?.as_str(), case);
assert_eq!(case.parse::<Endpoint>()?.to_string(), case);
}
Ok(())
}
#[test]
fn the_security_headers_default_to_something_enforced() {
let headers = SecurityHeaders::default();
let csp = headers.content_security_policy();
assert!(!csp.trim().is_empty(), "an empty CSP is no policy at all");
assert!(csp.contains("default-src 'none'"), "csp was {csp:?}");
assert!(csp.contains("frame-ancestors 'none'"), "csp was {csp:?}");
let hsts = headers.strict_transport_security();
assert!(hsts.contains("max-age="), "hsts was {hsts:?}");
assert!(hsts.contains("includeSubDomains"), "hsts was {hsts:?}");
assert_eq!(headers.x_frame_options(), "DENY");
}
#[test]
fn each_security_header_builder_takes_effect() {
let csp = SecurityHeaders::default().with_content_security_policy("default-src 'self'");
assert_eq!(csp.content_security_policy(), "default-src 'self'");
assert_eq!(csp.x_frame_options(), "DENY");
assert!(csp.strict_transport_security().contains("max-age="));
let hsts = SecurityHeaders::default().with_strict_transport_security("max-age=60");
assert_eq!(hsts.strict_transport_security(), "max-age=60");
assert_eq!(hsts.x_frame_options(), "DENY");
let frame = SecurityHeaders::default().with_x_frame_options("SAMEORIGIN");
assert_eq!(frame.x_frame_options(), "SAMEORIGIN");
assert!(
frame
.content_security_policy()
.contains("default-src 'none'")
);
let base = SecurityHeaders::default();
let _widened = base.clone().with_x_frame_options("SAMEORIGIN");
assert_eq!(base.x_frame_options(), "DENY");
}
#[test]
fn the_config_carries_each_setting_it_accepts() -> Result<(), TestError> {
let config = ServerConfig::new()?
.with_streaming(Streaming::EventStream)
.with_stream_timeout(Duration::from_secs(1))
.with_keep_alive(Duration::from_millis(250))
.with_security(SecurityHeaders::default().with_x_frame_options("SAMEORIGIN"));
assert_eq!(config.streaming(), Streaming::EventStream);
assert_eq!(config.stream_timeout(), Duration::from_secs(1));
assert_eq!(config.keep_alive(), Duration::from_millis(250));
assert_eq!(config.security().x_frame_options(), "SAMEORIGIN");
assert_eq!(ServerConfig::new()?.streaming(), Streaming::Json);
Ok(())
}
#[test]
fn the_replay_capacity_cannot_be_zero() -> Result<(), TestError> {
assert!(ServerConfig::new()?.replay_capacity().get() > 0);
Ok(())
}
}