#[cfg(feature = "mcp-server")]
use std::net::SocketAddr;
use std::{collections::BTreeMap, fmt, path::PathBuf, time::Duration};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{ToolError, name::ToolName};
#[cfg(feature = "mcp-server")]
pub use crate::sources::mcp::server::McpServer;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "transport", rename_all = "snake_case", deny_unknown_fields)]
pub enum McpConnectionConfig {
Stdio {
program: String,
args: Vec<String>,
cwd: Option<PathBuf>,
env: BTreeMap<String, String>,
},
StreamableHttp {
endpoint: Url,
headers: BTreeMap<String, String>,
},
}
impl fmt::Debug for McpConnectionConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stdio {
program,
args,
cwd,
env,
} => formatter
.debug_struct("McpConnectionConfig::Stdio")
.field("program", program)
.field("argument_count", &args.len())
.field("has_working_directory", &cwd.is_some())
.field("environment_count", &env.len())
.finish(),
Self::StreamableHttp { endpoint, headers } => formatter
.debug_struct("McpConnectionConfig::StreamableHttp")
.field("endpoint_scheme", &endpoint.scheme())
.field("endpoint_host", &endpoint.host_str())
.field("endpoint_port", &endpoint.port_or_known_default())
.field("header_count", &headers.len())
.finish(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct McpClientConfig {
connection: McpConnectionConfig,
namespace: Option<ToolName>,
request_timeout_secs: u64,
form_interactions: bool,
validate_forms: bool,
}
impl McpClientConfig {
#[must_use]
pub fn new(connection: McpConnectionConfig) -> Self {
Self {
connection,
namespace: None,
request_timeout_secs: 30,
form_interactions: false,
validate_forms: false,
}
}
pub fn with_namespace<N>(mut self, namespace: N) -> Result<Self, ToolError>
where
N: TryInto<ToolName>,
N::Error: fmt::Display,
{
self.namespace =
Some(
namespace
.try_into()
.map_err(|error| ToolError::InvalidConfiguration {
message: error.to_string(),
})?,
);
Ok(self)
}
pub fn with_request_timeout(mut self, timeout: Duration) -> Result<Self, ToolError> {
if timeout.is_zero() {
return Err(ToolError::InvalidConfiguration {
message: "MCP request timeout must be greater than zero".to_owned(),
});
}
self.request_timeout_secs = timeout.as_secs().max(1);
Ok(self)
}
#[must_use]
pub fn with_form_validation(mut self, enabled: bool) -> Self {
self.form_interactions = true;
self.validate_forms = enabled;
self
}
pub(crate) fn connection(&self) -> &McpConnectionConfig {
&self.connection
}
pub(crate) fn namespace(&self) -> Option<&ToolName> {
self.namespace.as_ref()
}
pub(crate) fn request_timeout(&self) -> Duration {
Duration::from_secs(self.request_timeout_secs)
}
pub(crate) fn validates_forms(&self) -> bool {
self.validate_forms
}
pub(crate) fn supports_forms(&self) -> bool {
self.form_interactions
}
}
#[cfg(feature = "mcp-server")]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum McpServerInteractionPolicy {
ElicitFormIfClientSupports,
#[default]
RejectWithToolError,
}
#[cfg(feature = "mcp-server")]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct McpServerConfig {
bind: SocketAddr,
endpoint_path: String,
cursor_prefix: String,
interaction_policy: McpServerInteractionPolicy,
shutdown_timeout_secs: u64,
}
#[cfg(feature = "mcp-server")]
impl McpServerConfig {
pub fn new(
bind: SocketAddr,
endpoint_path: impl Into<String>,
cursor_prefix: impl Into<String>,
) -> Result<Self, ToolError> {
let config = Self {
bind,
endpoint_path: endpoint_path.into(),
cursor_prefix: cursor_prefix.into(),
interaction_policy: McpServerInteractionPolicy::default(),
shutdown_timeout_secs: 5,
};
config.validate()?;
Ok(config)
}
#[must_use]
pub fn with_interaction_policy(mut self, policy: McpServerInteractionPolicy) -> Self {
self.interaction_policy = policy;
self
}
pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Result<Self, ToolError> {
if timeout.is_zero() {
return Err(ToolError::InvalidConfiguration {
message: "MCP server shutdown timeout must be greater than zero".to_owned(),
});
}
self.shutdown_timeout_secs = timeout.as_secs().max(1);
Ok(self)
}
pub(crate) fn validate(&self) -> Result<(), ToolError> {
if !self.bind.ip().is_loopback() || self.bind.port() == 0 {
return Err(ToolError::InvalidConfiguration {
message: "MCP server requires a loopback address with a nonzero port".to_owned(),
});
}
if self.endpoint_path.is_empty()
|| !self.endpoint_path.starts_with('/')
|| self.endpoint_path.contains('?')
|| self.endpoint_path.contains('#')
{
return Err(ToolError::InvalidConfiguration {
message: "MCP endpoint path must be absolute and contain no query or fragment"
.to_owned(),
});
}
if self.cursor_prefix.trim().is_empty() {
return Err(ToolError::InvalidConfiguration {
message: "MCP pagination cursor prefix must not be empty".to_owned(),
});
}
Ok(())
}
pub(crate) fn bind(&self) -> SocketAddr {
self.bind
}
pub(crate) fn endpoint_path(&self) -> &str {
&self.endpoint_path
}
pub(crate) fn cursor_prefix(&self) -> &str {
&self.cursor_prefix
}
pub(crate) fn interaction_policy(&self) -> McpServerInteractionPolicy {
self.interaction_policy
}
pub(crate) fn shutdown_timeout(&self) -> Duration {
Duration::from_secs(self.shutdown_timeout_secs)
}
}