use std::collections::BTreeMap;
use std::sync::Arc;
use futures::future::BoxFuture;
use turbomcp_core::{Implementation, McpError, McpResult, ProtocolVersion, RequestContext};
use turbomcp_protocol::neutral;
use crate::builder::ServerBuilder;
use crate::context::{
CallToolContext, CompleteContext, GetPromptContext, ListPromptsContext,
ListResourceTemplatesContext, ListResourcesContext, ListToolsContext, ReadResourceContext,
};
use crate::router::MethodRouter;
use crate::traits::{McpServerCore, WithCompletions, WithPrompts, WithResources, WithTools};
const SEP: char = '.';
const CURSOR_SEP: char = ':';
const MAX_TOOL_NAME: usize = 128;
trait Mounted: Send + Sync + 'static {
fn has_tools(&self) -> bool;
fn has_resources(&self) -> bool;
fn has_prompts(&self) -> bool;
fn has_completions(&self) -> bool;
fn list_tools(
&self,
ctx: ListToolsContext,
params: neutral::ListParams,
) -> Option<BoxFuture<'static, McpResult<neutral::ListToolsResult>>>;
fn call_tool(
&self,
ctx: CallToolContext,
params: neutral::CallToolParams,
) -> Option<BoxFuture<'static, McpResult<neutral::CallToolResult>>>;
fn list_resources(
&self,
ctx: ListResourcesContext,
params: neutral::ListParams,
) -> Option<BoxFuture<'static, McpResult<neutral::ListResourcesResult>>>;
fn read_resource(
&self,
ctx: ReadResourceContext,
params: neutral::ReadResourceParams,
) -> Option<BoxFuture<'static, McpResult<neutral::ReadResourceResult>>>;
fn list_resource_templates(
&self,
ctx: ListResourceTemplatesContext,
params: neutral::ListParams,
) -> Option<BoxFuture<'static, McpResult<neutral::ListResourceTemplatesResult>>>;
fn list_prompts(
&self,
ctx: ListPromptsContext,
params: neutral::ListParams,
) -> Option<BoxFuture<'static, McpResult<neutral::ListPromptsResult>>>;
fn get_prompt(
&self,
ctx: GetPromptContext,
params: neutral::GetPromptParams,
) -> Option<BoxFuture<'static, McpResult<neutral::GetPromptResult>>>;
fn complete(
&self,
ctx: CompleteContext,
params: neutral::CompleteParams,
) -> Option<BoxFuture<'static, McpResult<neutral::CompleteResult>>>;
}
struct Erased<S> {
server: S,
router: MethodRouter<S>,
}
macro_rules! forward {
($name:ident, $dispatch:ident, $ctx:ty, $params:ty, $result:ty) => {
fn $name(
&self,
ctx: $ctx,
params: $params,
) -> Option<BoxFuture<'static, McpResult<$result>>> {
self.router.$dispatch(self.server.clone(), ctx, params)
}
};
}
impl<S: McpServerCore> Mounted for Erased<S> {
fn has_tools(&self) -> bool {
self.router.has_tools()
}
fn has_resources(&self) -> bool {
self.router.has_resources()
}
fn has_prompts(&self) -> bool {
self.router.has_prompts()
}
fn has_completions(&self) -> bool {
self.router.has_completions()
}
forward!(
list_tools,
dispatch_list_tools,
ListToolsContext,
neutral::ListParams,
neutral::ListToolsResult
);
forward!(
call_tool,
dispatch_call_tool,
CallToolContext,
neutral::CallToolParams,
neutral::CallToolResult
);
forward!(
list_resources,
dispatch_list_resources,
ListResourcesContext,
neutral::ListParams,
neutral::ListResourcesResult
);
forward!(
read_resource,
dispatch_read_resource,
ReadResourceContext,
neutral::ReadResourceParams,
neutral::ReadResourceResult
);
forward!(
list_resource_templates,
dispatch_list_resource_templates,
ListResourceTemplatesContext,
neutral::ListParams,
neutral::ListResourceTemplatesResult
);
forward!(
list_prompts,
dispatch_list_prompts,
ListPromptsContext,
neutral::ListParams,
neutral::ListPromptsResult
);
forward!(
get_prompt,
dispatch_get_prompt,
GetPromptContext,
neutral::GetPromptParams,
neutral::GetPromptResult
);
forward!(
complete,
dispatch_complete,
CompleteContext,
neutral::CompleteParams,
neutral::CompleteResult
);
}
struct Mount {
prefix: Option<String>,
cursor_id: String,
name: String,
server: Box<dyn Mounted>,
}
impl Mount {
fn label(&self) -> &str {
self.prefix.as_deref().unwrap_or(&self.name)
}
}
pub struct Composite {
info: Implementation,
instructions: Option<String>,
versions: &'static [ProtocolVersion],
mounts: Vec<Mount>,
}
impl std::fmt::Debug for Composite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Composite")
.field("info", &self.info)
.field("versions", &self.versions)
.field("mounts", &Mounts(&self.mounts))
.finish()
}
}
struct Mounts<'a>(&'a [Mount]);
impl std::fmt::Debug for Mounts<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|m| match &m.prefix {
Some(prefix) => prefix.clone(),
None => format!("{{{}}}", m.name),
}))
.finish()
}
}
impl Composite {
#[must_use]
pub fn new(info: Implementation) -> Self {
Self {
info,
instructions: None,
versions: ProtocolVersion::SUPPORTED,
mounts: Vec::new(),
}
}
#[must_use]
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
#[must_use]
pub fn protocols(mut self, versions: &'static [ProtocolVersion]) -> Self {
self.versions = versions;
self
}
pub fn mount<S>(self, prefix: impl AsRef<str>, server: ServerBuilder<S>) -> McpResult<Self>
where
S: McpServerCore,
{
let prefix = prefix.as_ref();
validate_prefix(prefix)?;
if self
.mounts
.iter()
.any(|m| m.prefix.as_deref() == Some(prefix))
{
return Err(McpError::invalid_params(format!(
"`{prefix}` is already mounted; give each mounted server a distinct prefix"
)));
}
self.push(Some(prefix.to_owned()), server)
}
pub fn mount_flat<S>(self, server: ServerBuilder<S>) -> McpResult<Self>
where
S: McpServerCore,
{
self.push(None, server)
}
fn push<S>(mut self, prefix: Option<String>, server: ServerBuilder<S>) -> McpResult<Self>
where
S: McpServerCore,
{
let at = match &prefix {
Some(prefix) => format!("mounted at `{prefix}`"),
None => "mounted flat".to_owned(),
};
if let Some(setting) = server.dispatcher_setting() {
return Err(McpError::invalid_params(format!(
"the server {at} sets `{setting}`, which configures the dispatcher — there \
is one dispatcher and it is the composite's. Move the call to the \
composite's own builder."
)));
}
let (server, router) = server.into_parts();
let narrowed: Vec<&str> = self
.versions
.iter()
.filter(|v| !server.supported_versions().contains(v))
.map(ProtocolVersion::as_str)
.collect();
if !narrowed.is_empty() {
return Err(McpError::invalid_params(format!(
"the server {at} does not accept {narrowed:?}, which this composite does. \
Handlers are version-neutral, so mounting cannot honor a sub-server's pin \
— narrow the composite with `.protocols(…)` instead."
)));
}
let cursor_id = prefix
.clone()
.unwrap_or_else(|| format!("#{}", self.mounts.len()));
let name = server.server_info().name;
self.mounts.push(Mount {
prefix,
cursor_id,
name,
server: Box::new(Erased { server, router }),
});
Ok(self)
}
#[must_use]
pub fn build(self) -> CompositeServer {
CompositeServer {
inner: Arc::new(self),
}
}
#[must_use]
pub fn into_server(self) -> ServerBuilder<CompositeServer> {
self.build().into_server()
}
}
fn validate_prefix(prefix: &str) -> McpResult<()> {
if prefix.is_empty() {
return Err(McpError::invalid_params(
"a mount prefix may not be empty — use `mount_flat` to mount a server whose \
tools and prompts keep their own names",
));
}
if let Some(bad) = prefix
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '_' | '-')))
{
let why = if bad == SEP {
"`.` separates the prefix from the component name, so a prefix containing one \
would make the split ambiguous"
} else {
"a composed name must stay a legal tool name: ASCII letters, digits, `_`, `-`"
};
return Err(McpError::invalid_params(format!(
"the mount prefix `{prefix}` contains `{bad}` — {why}"
)));
}
Ok(())
}
#[derive(Clone)]
pub struct CompositeServer {
inner: Arc<Composite>,
}
impl std::fmt::Debug for CompositeServer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompositeServer")
.field("info", &self.inner.info)
.field("mounts", &Mounts(&self.inner.mounts))
.finish()
}
}
impl CompositeServer {
#[must_use]
pub fn into_server(self) -> ServerBuilder<Self> {
let (tools, resources, prompts, completions) = (
self.any(Mounted::has_tools),
self.any(Mounted::has_resources),
self.any(Mounted::has_prompts),
self.any(Mounted::has_completions),
);
let mut builder = ServerBuilder::new(self);
if tools {
builder = builder.with_tools();
}
if resources {
builder = builder.with_resources();
}
if prompts {
builder = builder.with_prompts();
}
if completions {
builder = builder.with_completions();
}
builder
}
fn any(&self, has: impl Fn(&dyn Mounted) -> bool) -> bool {
self.inner.mounts.iter().any(|m| has(m.server.as_ref()))
}
pub async fn preflight(&self, request: RequestContext) -> McpResult<()> {
const MAX_PAGES: usize = 1000;
macro_rules! drain {
($ctx:expr, $list:ident, $label:literal) => {{
let ctx = $ctx;
let mut cursor = None;
for page in 0.. {
if page == MAX_PAGES {
return Err(McpError::internal(format!(
"a mounted server is still paginating {} after {MAX_PAGES} pages \
— it is not terminating its cursor",
$label
)));
}
let mut params = neutral::ListParams::default();
params.cursor = cursor;
let result = self.$list(&ctx, params).await?;
match result.next_cursor {
Some(next) => cursor = Some(next),
None => break,
}
}
}};
}
drain!(ListToolsContext::new(request.clone()), list_tools, "tools");
drain!(
ListResourcesContext::new(request.clone()),
list_resources,
"resources"
);
drain!(
ListResourceTemplatesContext::new(request.clone()),
list_resource_templates,
"resource templates"
);
drain!(ListPromptsContext::new(request), list_prompts, "prompts");
Ok(())
}
fn route(&self, qualified: &str) -> Option<(&Mount, String)> {
let (prefix, name) = qualified.split_once(SEP)?;
let mount = self
.inner
.mounts
.iter()
.find(|m| m.prefix.as_deref() == Some(prefix))?;
Some((mount, name.to_owned()))
}
fn has_flat(&self) -> bool {
self.inner.mounts.iter().any(|m| m.prefix.is_none())
}
async fn flat_owner<'a, T, F>(
mounts: &'a [Mount],
name: &str,
mut list: F,
) -> McpResult<Option<&'a Mount>>
where
F: FnMut(&'a Mount) -> Option<BoxFuture<'static, McpResult<T>>>,
T: Names,
{
for mount in mounts.iter().filter(|m| m.prefix.is_none()) {
let Some(fut) = list(mount) else { continue };
if fut.await?.has(name) {
return Ok(Some(mount));
}
}
Ok(None)
}
}
trait Names {
fn has(&self, name: &str) -> bool;
}
impl Names for neutral::ListToolsResult {
fn has(&self, name: &str) -> bool {
self.tools.iter().any(|t| t.name == name)
}
}
impl Names for neutral::ListPromptsResult {
fn has(&self, name: &str) -> bool {
self.prompts.iter().any(|p| p.name == name)
}
}
fn qualify(prefix: &str, name: &str) -> String {
let mut out = String::with_capacity(prefix.len() + 1 + name.len());
out.push_str(prefix);
out.push(SEP);
out.push_str(name);
out
}
fn resume_at(mounts: &[Mount], cursor: Option<&str>) -> McpResult<(usize, Option<String>)> {
let Some(cursor) = cursor else {
return Ok((0, None));
};
let bad = || McpError::invalid_params(format!("not a cursor this server issued: `{cursor}`"));
let (id, own) = cursor.split_once(CURSOR_SEP).ok_or_else(bad)?;
let at = mounts
.iter()
.position(|m| m.cursor_id == id)
.ok_or_else(bad)?;
Ok((at, (!own.is_empty()).then(|| own.to_owned())))
}
fn resume_cursor(mount: &Mount, own: &str) -> String {
let mut out = String::with_capacity(mount.cursor_id.len() + 1 + own.len());
out.push_str(&mount.cursor_id);
out.push(CURSOR_SEP);
out.push_str(own);
out
}
macro_rules! page_through {
($self:ident, $ctx:ident, $params:ident, $dispatch:ident, $field:ident, $result:ty,
|$mount:ident, $item:ident| $adapt:block) => {{
let mounts = &$self.inner.mounts;
let (start, mut own) = resume_at(mounts, $params.cursor.as_deref())?;
let mut items = Vec::new();
let mut next = None;
for $mount in &mounts[start..] {
let mut params = $params.clone();
params.cursor = own.take();
let Some(fut) = $mount.server.$dispatch($ctx.clone(), params) else {
continue;
};
let page = fut.await?;
#[allow(unused_mut)]
for mut $item in page.$field {
$adapt
items.push($item);
}
if let Some(cursor) = page.next_cursor {
next = Some(resume_cursor($mount, &cursor));
break;
}
}
let mut out = <$result>::new(items);
out.next_cursor = next;
Ok(out)
}};
}
impl McpServerCore for CompositeServer {
fn server_info(&self) -> Implementation {
self.inner.info.clone()
}
fn supported_versions(&self) -> &'static [ProtocolVersion] {
self.inner.versions
}
fn instructions(&self) -> Option<String> {
self.inner.instructions.clone()
}
}
impl WithTools for CompositeServer {
async fn list_tools(
&self,
ctx: &ListToolsContext,
params: neutral::ListParams,
) -> McpResult<neutral::ListToolsResult> {
let mut seen: BTreeMap<String, String> = BTreeMap::new();
page_through!(
self,
ctx,
params,
list_tools,
tools,
neutral::ListToolsResult,
|mount, tool| {
if let Some(prefix) = &mount.prefix {
tool.name = qualify(prefix, &tool.name);
}
if tool.name.len() > MAX_TOOL_NAME {
return Err(McpError::internal(format!(
"the tool `{}` from `{}` is {} characters, over the spec's \
{MAX_TOOL_NAME}-character limit{}",
tool.name,
mount.label(),
tool.name.len(),
if mount.prefix.is_some() {
" once mounted — use a shorter prefix"
} else {
""
},
)));
}
claim(&mut seen, &tool.name, mount, Kind::Tool)?;
}
)
}
async fn call_tool(
&self,
ctx: &CallToolContext,
mut params: neutral::CallToolParams,
) -> McpResult<neutral::CallToolResult> {
let routed = match self.route(¶ms.name) {
Some(routed) => Some(routed),
None if self.has_flat() => {
Self::flat_owner(&self.inner.mounts, ¶ms.name, |mount| {
mount
.server
.list_tools(ListToolsContext::new(ctx.base.clone()), Default::default())
})
.await?
.map(|mount| (mount, params.name.clone()))
}
None => None,
};
let Some((mount, name)) = routed else {
return Ok(neutral::CallToolResult::error(format!(
"unknown tool: {}",
params.name
)));
};
params.name = name;
let Some(fut) = mount.server.call_tool(ctx.clone(), params) else {
return Ok(neutral::CallToolResult::error(format!(
"the server mounted at `{}` serves no tools",
mount.label()
)));
};
fut.await
}
}
impl WithResources for CompositeServer {
async fn list_resources(
&self,
ctx: &ListResourcesContext,
params: neutral::ListParams,
) -> McpResult<neutral::ListResourcesResult> {
let mut seen: BTreeMap<String, String> = BTreeMap::new();
page_through!(
self,
ctx,
params,
list_resources,
resources,
neutral::ListResourcesResult,
|mount, resource| {
claim(&mut seen, &resource.uri, mount, Kind::Resource)?;
}
)
}
async fn list_resource_templates(
&self,
ctx: &ListResourceTemplatesContext,
params: neutral::ListParams,
) -> McpResult<neutral::ListResourceTemplatesResult> {
let mut seen: BTreeMap<String, String> = BTreeMap::new();
page_through!(
self,
ctx,
params,
list_resource_templates,
resource_templates,
neutral::ListResourceTemplatesResult,
|mount, template| {
claim(&mut seen, &template.uri_template, mount, Kind::Template)?;
}
)
}
async fn read_resource(
&self,
ctx: &ReadResourceContext,
params: neutral::ReadResourceParams,
) -> McpResult<neutral::ReadResourceResult> {
for mount in &self.inner.mounts {
let Some(fut) = mount.server.read_resource(ctx.clone(), params.clone()) else {
continue;
};
match fut.await {
Err(McpError::ResourceNotFound(_)) => continue,
other => return other,
}
}
Err(McpError::resource_not_found(params.uri))
}
}
impl WithPrompts for CompositeServer {
async fn list_prompts(
&self,
ctx: &ListPromptsContext,
params: neutral::ListParams,
) -> McpResult<neutral::ListPromptsResult> {
let mut seen: BTreeMap<String, String> = BTreeMap::new();
page_through!(
self,
ctx,
params,
list_prompts,
prompts,
neutral::ListPromptsResult,
|mount, prompt| {
if let Some(prefix) = &mount.prefix {
prompt.name = qualify(prefix, &prompt.name);
}
claim(&mut seen, &prompt.name, mount, Kind::Prompt)?;
}
)
}
async fn get_prompt(
&self,
ctx: &GetPromptContext,
mut params: neutral::GetPromptParams,
) -> McpResult<neutral::GetPromptResult> {
let routed = match self.route(¶ms.name) {
Some(routed) => Some(routed),
None if self.has_flat() => {
Self::flat_owner(&self.inner.mounts, ¶ms.name, |mount| {
mount.server.list_prompts(
ListPromptsContext::new(ctx.base.clone()),
Default::default(),
)
})
.await?
.map(|mount| (mount, params.name.clone()))
}
None => None,
};
let Some((mount, name)) = routed else {
return Err(McpError::invalid_params(format!(
"unknown prompt: {}",
params.name
)));
};
params.name = name;
let Some(fut) = mount.server.get_prompt(ctx.clone(), params) else {
return Err(McpError::invalid_params(format!(
"the server mounted at `{}` serves no prompts",
mount.label()
)));
};
fut.await
}
}
impl WithCompletions for CompositeServer {
async fn complete(
&self,
ctx: &CompleteContext,
mut params: neutral::CompleteParams,
) -> McpResult<neutral::CompleteResult> {
match &mut params.reference {
neutral::CompletionReference::Prompt { name } => {
let routed = match self.route(name) {
Some(routed) => Some(routed),
None if self.has_flat() => {
Self::flat_owner(&self.inner.mounts, name, |mount| {
mount.server.list_prompts(
ListPromptsContext::new(ctx.base.clone()),
Default::default(),
)
})
.await?
.map(|mount| (mount, name.clone()))
}
None => None,
};
let Some((mount, own)) = routed else {
return Ok(neutral::CompleteResult::new(vec![]));
};
*name = own;
match mount.server.complete(ctx.clone(), params) {
Some(fut) => fut.await,
None => Ok(neutral::CompleteResult::new(vec![])),
}
}
neutral::CompletionReference::ResourceTemplate { .. } => {
for mount in &self.inner.mounts {
let Some(fut) = mount.server.complete(ctx.clone(), params.clone()) else {
continue;
};
let result = fut.await?;
if !result.values.is_empty() {
return Ok(result);
}
}
Ok(neutral::CompleteResult::new(vec![]))
}
_ => Ok(neutral::CompleteResult::new(vec![])),
}
}
}
#[derive(Clone, Copy)]
enum Kind {
Tool,
Prompt,
Resource,
Template,
}
impl Kind {
fn what(self) -> &'static str {
match self {
Self::Tool => "tool",
Self::Prompt => "prompt",
Self::Resource => "resource URI",
Self::Template => "template URI",
}
}
fn remedy(self) -> &'static str {
match self {
Self::Tool | Self::Prompt => {
"flat mounts do not rename, so two of them cannot expose the same name — \
mount one under a prefix instead"
}
Self::Resource | Self::Template => {
"resource URIs are never prefixed by mounting — give each server its own \
scheme or authority"
}
}
}
}
fn claim(
seen: &mut BTreeMap<String, String>,
id: &str,
mount: &Mount,
kind: Kind,
) -> McpResult<()> {
if let Some(first) = seen.insert(id.to_owned(), mount.label().to_owned()) {
return Err(McpError::internal(format!(
"two mounted servers expose the {} `{id}`: `{first}` and `{}` — {}",
kind.what(),
mount.label(),
kind.remedy(),
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::IntoServerBuilder;
#[derive(Clone)]
struct Bare;
impl McpServerCore for Bare {
fn server_info(&self) -> Implementation {
Implementation::new("bare", "1.0.0")
}
}
#[derive(Clone)]
struct Pinned;
impl McpServerCore for Pinned {
fn server_info(&self) -> Implementation {
Implementation::new("pinned", "1.0.0")
}
fn supported_versions(&self) -> &'static [ProtocolVersion] {
&[ProtocolVersion::V2025_11_25]
}
}
fn composite() -> Composite {
Composite::new(Implementation::new("gateway", "1.0.0"))
}
#[test]
fn a_prefix_must_survive_being_part_of_a_tool_name() {
for bad in ["", "we.ather", "we ather", "wéather", "weather/x"] {
let err = composite()
.mount(bad, Bare.into_server())
.expect_err("`{bad}` should be rejected");
assert!(
err.to_string().contains("prefix"),
"unhelpful message for `{bad}`: {err}"
);
}
for good in ["weather", "weather-api", "weather_api", "v2"] {
composite()
.mount(good, Bare.into_server())
.unwrap_or_else(|e| panic!("`{good}` should be accepted: {e}"));
}
}
#[test]
fn a_prefix_may_be_used_once() {
let err = composite()
.mount("weather", Bare.into_server())
.unwrap()
.mount("weather", Bare.into_server())
.expect_err("the second mount should be rejected");
assert!(err.to_string().contains("already mounted"), "{err}");
}
#[test]
fn a_mount_may_not_be_narrower_than_the_composite() {
let err = composite()
.mount("pinned", Pinned.into_server())
.expect_err("a narrower mount should be rejected");
assert!(err.to_string().contains("2025-06-18"), "{err}");
assert!(err.to_string().contains(".protocols("), "{err}");
composite()
.protocols(&[ProtocolVersion::V2025_11_25])
.mount("pinned", Pinned.into_server())
.expect("a matching composite should accept it");
}
#[test]
fn dispatcher_settings_on_a_mount_are_refused_not_ignored() {
let err = composite()
.mount("weather", Bare.into_server().with_tasks())
.expect_err("a dispatcher-level setting should be rejected");
assert!(err.to_string().contains("with_tasks"), "{err}");
assert!(err.to_string().contains("dispatcher"), "{err}");
}
}