use crate::internal::domain::{ErrorCode, GatewayError};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use url::Url;
pub const MAX_CLOCK_SKEW_SECONDS: u64 = 300;
pub const DEFAULT_RATE_LIMIT_MAX_REQUESTS: u32 = 120;
pub const DEFAULT_RATE_LIMIT_WINDOW_SECONDS: u64 = 60;
pub const DEFAULT_MAX_CONNECTIONS: usize = 64;
pub const MAX_RATE_LIMIT_WINDOW_SECONDS: u64 = 3_600;
pub const MAX_RATE_LIMIT_MAX_REQUESTS: u32 = 100_000;
pub const MAX_MAX_CONNECTIONS: usize = 4_096;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RemoteMcpConfig {
pub enabled: bool,
pub bind_address: String,
#[schemars(with = "Option<String>")]
pub resource: Option<Url>,
#[schemars(with = "Option<String>")]
pub issuer: Option<Url>,
#[schemars(with = "Option<String>")]
pub jwks_url: Option<Url>,
#[schemars(with = "Option<String>")]
pub metadata_url: Option<Url>,
pub audiences: Vec<String>,
pub allowed_scopes: Vec<String>,
pub clock_skew_seconds: u64,
#[serde(default = "default_rate_limit_max_requests")]
pub rate_limit_max_requests: u32,
#[serde(default = "default_rate_limit_window_seconds")]
pub rate_limit_window_seconds: u64,
#[serde(default = "default_max_connections")]
pub max_connections: usize,
#[schemars(skip)]
#[serde(default, skip_serializing)]
pub token_id_hmac_secret: Option<String>,
}
impl Default for RemoteMcpConfig {
fn default() -> Self {
Self {
enabled: false,
bind_address: "127.0.0.1:8080".to_string(),
resource: None,
issuer: None,
jwks_url: None,
metadata_url: None,
audiences: Vec::new(),
allowed_scopes: Vec::new(),
clock_skew_seconds: 60,
rate_limit_max_requests: DEFAULT_RATE_LIMIT_MAX_REQUESTS,
rate_limit_window_seconds: DEFAULT_RATE_LIMIT_WINDOW_SECONDS,
max_connections: DEFAULT_MAX_CONNECTIONS,
token_id_hmac_secret: None,
}
}
}
const fn default_rate_limit_max_requests() -> u32 {
DEFAULT_RATE_LIMIT_MAX_REQUESTS
}
const fn default_rate_limit_window_seconds() -> u64 {
DEFAULT_RATE_LIMIT_WINDOW_SECONDS
}
const fn default_max_connections() -> usize {
DEFAULT_MAX_CONNECTIONS
}
pub fn validate_remote_mcp_config(
config: &RemoteMcpConfig,
safety_enabled: bool,
) -> Result<(), GatewayError> {
if config.clock_skew_seconds > MAX_CLOCK_SKEW_SECONDS {
return Err(GatewayError::new(
ErrorCode::ConfigInvalid,
format!("remote_mcp.clock_skew_seconds must be at most {MAX_CLOCK_SKEW_SECONDS}"),
false,
Some(format!(
"Lower clock_skew_seconds to {MAX_CLOCK_SKEW_SECONDS} or below"
)),
));
}
if config.rate_limit_max_requests == 0
|| config.rate_limit_max_requests > MAX_RATE_LIMIT_MAX_REQUESTS
{
return Err(GatewayError::new(
ErrorCode::ConfigInvalid,
format!(
"remote_mcp.rate_limit_max_requests must be between 1 and {MAX_RATE_LIMIT_MAX_REQUESTS}"
),
false,
Some("Configure a bounded positive remote MCP rate limit".to_string()),
));
}
if config.rate_limit_window_seconds == 0
|| config.rate_limit_window_seconds > MAX_RATE_LIMIT_WINDOW_SECONDS
{
return Err(GatewayError::new(
ErrorCode::ConfigInvalid,
format!(
"remote_mcp.rate_limit_window_seconds must be between 1 and {MAX_RATE_LIMIT_WINDOW_SECONDS}"
),
false,
Some("Configure a bounded positive remote MCP rate-limit window".to_string()),
));
}
if config.max_connections == 0 || config.max_connections > MAX_MAX_CONNECTIONS {
return Err(GatewayError::new(
ErrorCode::ConfigInvalid,
format!("remote_mcp.max_connections must be between 1 and {MAX_MAX_CONNECTIONS}"),
false,
Some("Configure a bounded positive remote MCP connection limit".to_string()),
));
}
if !config.enabled {
if safety_enabled {
return Err(GatewayError::new(
ErrorCode::ConfigRemoteMcpForbidden,
"remote_public_mcp_enabled requires remote_mcp.enabled",
false,
Some("Enable remote_mcp.enabled or disable the safety flag".to_string()),
));
}
return Ok(());
}
if !safety_enabled {
return Err(GatewayError::new(
ErrorCode::ConfigRemoteMcpForbidden,
"remote_mcp.enabled requires remote_public_mcp_enabled",
false,
Some("Set the explicit remote_public_mcp_enabled safety flag".to_string()),
));
}
if config.resource.is_none() {
return Err(missing("remote_mcp.resource"));
}
if config.issuer.is_none() {
return Err(missing("remote_mcp.issuer"));
}
if config.jwks_url.is_none() {
return Err(missing("remote_mcp.jwks_url"));
}
if config.audiences.is_empty() {
return Err(missing("remote_mcp.audiences"));
}
if config.allowed_scopes.is_empty() {
return Err(missing("remote_mcp.allowed_scopes"));
}
if config
.token_id_hmac_secret
.as_deref()
.is_none_or(|secret| secret.trim().is_empty())
{
return Err(missing("remote_mcp.token_id_hmac_secret"));
}
Ok(())
}
fn missing(field: &str) -> GatewayError {
GatewayError::new(
ErrorCode::ConfigInvalid,
format!("Remote MCP configuration is missing {field}"),
false,
Some(format!("Configure {field} before enabling remote MCP")),
)
}
#[cfg(test)]
mod tests {
use super::{
DEFAULT_MAX_CONNECTIONS, DEFAULT_RATE_LIMIT_MAX_REQUESTS,
DEFAULT_RATE_LIMIT_WINDOW_SECONDS, MAX_CLOCK_SKEW_SECONDS, MAX_MAX_CONNECTIONS,
MAX_RATE_LIMIT_MAX_REQUESTS, MAX_RATE_LIMIT_WINDOW_SECONDS, RemoteMcpConfig,
validate_remote_mcp_config,
};
use crate::internal::domain::ErrorCode;
use serde_json::json;
#[test]
fn default_clock_skew_is_within_bound() {
let config = RemoteMcpConfig::default();
assert!(config.clock_skew_seconds <= MAX_CLOCK_SKEW_SECONDS);
}
#[test]
fn default_remote_mcp_limits_are_bounded() {
let config = RemoteMcpConfig::default();
assert_eq!(
config.rate_limit_max_requests,
DEFAULT_RATE_LIMIT_MAX_REQUESTS
);
assert_eq!(
config.rate_limit_window_seconds,
DEFAULT_RATE_LIMIT_WINDOW_SECONDS
);
assert_eq!(config.max_connections, DEFAULT_MAX_CONNECTIONS);
}
#[test]
fn deserializes_missing_new_limit_fields_with_defaults()
-> Result<(), Box<dyn std::error::Error>> {
let config: RemoteMcpConfig = serde_json::from_value(json!({
"enabled": false,
"bind_address": "127.0.0.1:8080",
"resource": null,
"issuer": null,
"jwks_url": null,
"metadata_url": null,
"audiences": [],
"allowed_scopes": [],
"clock_skew_seconds": 60
}))?;
assert_eq!(
config.rate_limit_max_requests,
DEFAULT_RATE_LIMIT_MAX_REQUESTS
);
assert_eq!(
config.rate_limit_window_seconds,
DEFAULT_RATE_LIMIT_WINDOW_SECONDS
);
assert_eq!(config.max_connections, DEFAULT_MAX_CONNECTIONS);
Ok(())
}
#[test]
fn rejects_clock_skew_exceeding_cap() {
let config = RemoteMcpConfig {
clock_skew_seconds: MAX_CLOCK_SKEW_SECONDS + 1,
..RemoteMcpConfig::default()
};
let Err(error) = validate_remote_mcp_config(&config, false) else {
unreachable!("clock skew above cap must be rejected");
};
assert_eq!(error.code, ErrorCode::ConfigInvalid);
}
#[test]
fn rejects_u64_max_clock_skew() {
let config = RemoteMcpConfig {
clock_skew_seconds: u64::MAX,
..RemoteMcpConfig::default()
};
let Err(error) = validate_remote_mcp_config(&config, false) else {
unreachable!("u64::MAX clock skew must be rejected");
};
assert_eq!(error.code, ErrorCode::ConfigInvalid);
}
#[test]
fn accepts_clock_skew_at_cap() {
let config = RemoteMcpConfig {
clock_skew_seconds: MAX_CLOCK_SKEW_SECONDS,
..RemoteMcpConfig::default()
};
let outcome = validate_remote_mcp_config(&config, false);
assert!(outcome.is_ok());
}
#[test]
fn rejects_invalid_rate_limit_max_requests() {
for rate_limit_max_requests in [0, MAX_RATE_LIMIT_MAX_REQUESTS + 1] {
let config = RemoteMcpConfig {
rate_limit_max_requests,
..RemoteMcpConfig::default()
};
let Err(error) = validate_remote_mcp_config(&config, false) else {
unreachable!("invalid rate limit max requests must be rejected");
};
assert_eq!(error.code, ErrorCode::ConfigInvalid);
}
}
#[test]
fn rejects_invalid_rate_limit_window_seconds() {
for rate_limit_window_seconds in [0, MAX_RATE_LIMIT_WINDOW_SECONDS + 1] {
let config = RemoteMcpConfig {
rate_limit_window_seconds,
..RemoteMcpConfig::default()
};
let Err(error) = validate_remote_mcp_config(&config, false) else {
unreachable!("invalid rate limit window must be rejected");
};
assert_eq!(error.code, ErrorCode::ConfigInvalid);
}
}
#[test]
fn rejects_invalid_max_connections() {
for max_connections in [0, MAX_MAX_CONNECTIONS + 1] {
let config = RemoteMcpConfig {
max_connections,
..RemoteMcpConfig::default()
};
let Err(error) = validate_remote_mcp_config(&config, false) else {
unreachable!("invalid max connections must be rejected");
};
assert_eq!(error.code, ErrorCode::ConfigInvalid);
}
}
}