#[cfg(feature = "http")]
use crate::http;
use crate::{
ClapMcpConfig, ClapMcpConfigProvider, ClapMcpError, ClapMcpSchemaMetadata,
ClapMcpSchemaMetadataProvider, ClapMcpServeOptions, ClapMcpToolExecutor,
ClapMcpToolExecutorWithState, InProcessToolHandler, McpListen, build_mcp_blocking_runtime,
prepare_derive_mcp_serve, prepare_derive_mcp_serve_with_state, server,
};
use clap::CommandFactory;
use std::{path::PathBuf, pin::Pin, sync::Arc};
use tokio::io::{AsyncRead, AsyncWrite};
#[derive(Default)]
pub(crate) enum McpStdioIo {
#[default]
Process,
Custom {
read: Pin<Box<dyn AsyncRead + Send + Unpin>>,
write: Pin<Box<dyn AsyncWrite + Send + Unpin>>,
},
}
impl McpStdioIo {
pub(crate) fn is_custom(&self) -> bool {
matches!(self, Self::Custom { .. })
}
}
pub struct ServeMcp {
listen: McpListen,
schema_json: String,
executable_path: Option<PathBuf>,
config: ClapMcpConfig,
in_process_handler: Option<InProcessToolHandler>,
serve_options: ClapMcpServeOptions,
metadata: ClapMcpSchemaMetadata,
stdio_io: McpStdioIo,
}
impl ServeMcp {
pub async fn serve(self) -> Result<(), ClapMcpError> {
validate_embedder_runtime(&self.config)?;
match self.listen {
McpListen::Stdio => {
server::serve_schema_json_over_stdio(
self.schema_json,
self.executable_path,
self.config,
self.in_process_handler,
self.serve_options,
&self.metadata,
self.stdio_io,
)
.await
}
#[cfg(feature = "http")]
McpListen::Http(addr) => {
http::serve_schema_json_over_http(
addr,
self.schema_json,
self.executable_path,
self.config,
self.in_process_handler,
self.serve_options,
&self.metadata,
)
.await
}
}
}
pub fn serve_blocking(self) -> Result<(), ClapMcpError> {
build_mcp_blocking_runtime(&self.config)?.block_on(self.serve())
}
}
#[derive(Default)]
pub struct ServeMcpBuilder {
listen: Option<McpListen>,
schema_json: Option<String>,
config: Option<ClapMcpConfig>,
metadata: Option<ClapMcpSchemaMetadata>,
serve_options: ClapMcpServeOptions,
executable_path: Option<PathBuf>,
in_process_handler: Option<InProcessToolHandler>,
stdio_io: McpStdioIo,
}
impl ServeMcpBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn for_cli<T>(listen: McpListen) -> Self
where
T: ClapMcpToolExecutor
+ ClapMcpSchemaMetadataProvider
+ ClapMcpConfigProvider
+ CommandFactory
+ clap::FromArgMatches
+ 'static,
{
let config = T::clap_mcp_config();
let prepared = prepare_derive_mcp_serve::<T>(&config, &ClapMcpServeOptions::default());
Self {
listen: Some(listen),
schema_json: Some(prepared.schema_json),
config: Some(config),
metadata: Some(prepared.metadata),
serve_options: ClapMcpServeOptions::default(),
executable_path: prepared.executable_path,
in_process_handler: prepared.in_process_handler,
stdio_io: McpStdioIo::default(),
}
}
pub fn for_cli_with_state<T>(listen: McpListen, state: Arc<T::State>) -> Self
where
T: ClapMcpToolExecutorWithState
+ ClapMcpSchemaMetadataProvider
+ ClapMcpConfigProvider
+ CommandFactory
+ clap::FromArgMatches
+ 'static,
{
let config = T::clap_mcp_config();
let prepared = prepare_derive_mcp_serve_with_state::<T>(
&config,
&ClapMcpServeOptions::default(),
state,
);
Self {
listen: Some(listen),
schema_json: Some(prepared.schema_json),
config: Some(config),
metadata: Some(prepared.metadata),
serve_options: ClapMcpServeOptions::default(),
executable_path: prepared.executable_path,
in_process_handler: prepared.in_process_handler,
stdio_io: McpStdioIo::default(),
}
}
pub fn listen(mut self, listen: McpListen) -> Self {
self.listen = Some(listen);
self
}
pub fn schema_json(mut self, schema_json: impl Into<String>) -> Self {
self.schema_json = Some(schema_json.into());
self
}
pub fn config(mut self, config: ClapMcpConfig) -> Self {
self.config = Some(config);
self
}
pub fn metadata(mut self, metadata: ClapMcpSchemaMetadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn serve_options(mut self, serve_options: ClapMcpServeOptions) -> Self {
self.serve_options = serve_options;
self
}
pub fn executable_path(mut self, executable_path: Option<PathBuf>) -> Self {
self.executable_path = executable_path;
self
}
pub fn in_process_handler(mut self, in_process_handler: Option<InProcessToolHandler>) -> Self {
self.in_process_handler = in_process_handler;
self
}
pub fn stdio_io<R, W>(mut self, read: R, write: W) -> Self
where
R: AsyncRead + Send + Unpin + 'static,
W: AsyncWrite + Send + Unpin + 'static,
{
self.stdio_io = McpStdioIo::Custom {
read: Box::pin(read),
write: Box::pin(write),
};
self
}
pub fn build(self) -> Result<ServeMcp, ClapMcpError> {
let listen = self.listen.ok_or_else(|| missing_field("listen"))?;
#[cfg(feature = "http")]
if matches!(listen, McpListen::Http(_)) && self.stdio_io.is_custom() {
return Err(ClapMcpError::InvalidConfig(
"ServeMcpBuilder::stdio_io is only valid with McpListen::Stdio".into(),
));
}
let schema_json = self
.schema_json
.ok_or_else(|| missing_field("schema_json"))?;
let config = self.config.ok_or_else(|| missing_field("config"))?;
let metadata = self.metadata.ok_or_else(|| missing_field("metadata"))?;
Ok(ServeMcp {
listen,
schema_json,
executable_path: self.executable_path,
config,
in_process_handler: self.in_process_handler,
serve_options: self.serve_options,
metadata,
stdio_io: self.stdio_io,
})
}
pub async fn serve(self) -> Result<(), ClapMcpError> {
self.build()?.serve().await
}
pub fn serve_blocking(self) -> Result<(), ClapMcpError> {
self.build()?.serve_blocking()
}
}
fn missing_field(field: &str) -> ClapMcpError {
ClapMcpError::InvalidConfig(format!("ServeMcpBuilder missing required field `{field}`"))
}
pub(crate) fn validate_embedder_runtime(config: &ClapMcpConfig) -> Result<(), ClapMcpError> {
if !config.needs_multi_thread_runtime() {
return Ok(());
}
if let Ok(handle) = tokio::runtime::Handle::try_current()
&& handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread
{
return Err(ClapMcpError::RequiresMultiThreadRuntime {
reason: "serve_mcp requires a multi-thread tokio runtime when reinvocation_safe is \
true and share_runtime or parallel_safe is enabled; use \
#[tokio::main(flavor = \"multi_thread\")] or serve_mcp_blocking"
.into(),
});
}
Ok(())
}