use std::{future::Future, sync::Arc};
use rmcp::{
ErrorData as McpError, ServerHandler,
model::{
CallToolRequestParams, CallToolResult, CancelledNotificationParam, Implementation,
ListToolsResult, Meta, PaginatedRequestParams, ServerCapabilities, ServerInfo,
},
service::{NotificationContext, RequestContext, RoleServer},
};
use crate::{
config::HubConfig,
runtime::{SessionRuntime, SessionRuntimeBuildError},
};
#[derive(Clone)]
pub(crate) struct HubServer {
runtime: Arc<SessionRuntime>,
}
impl HubServer {
pub(crate) async fn from_config(config: HubConfig) -> Result<Self, SessionRuntimeBuildError> {
Ok(Self {
runtime: Arc::new(SessionRuntime::build(&config).await?),
})
}
pub(crate) async fn shutdown(&self) {
self.runtime.shutdown().await;
}
}
impl ServerHandler for HubServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new("mcp-hub", env!("CARGO_PKG_VERSION")))
.with_instructions("Tool-only MCP aggregation hub. This server exposes tools only.")
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, McpError> {
Ok(ListToolsResult {
tools: self.runtime.list_tools().await,
next_cursor: None,
meta: None,
})
}
async fn call_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
self.runtime
.call_tool(&request, &context)
.await
.map_err(|error| error.into_mcp_error())
}
fn on_cancelled(
&self,
mut notification: CancelledNotificationParam,
context: rmcp::service::NotificationContext<RoleServer>,
) -> impl Future<Output = ()> + rmcp::service::MaybeSendFuture + '_ {
let runtime = self.runtime.clone();
async move {
notification.meta = notification_context_meta(&context);
runtime.cancel_tool_call(notification).await;
}
}
}
fn notification_context_meta(context: &NotificationContext<RoleServer>) -> Option<Meta> {
(!context.meta.0.is_empty())
.then(|| context.meta.clone())
.or_else(|| context.extensions.get::<Meta>().cloned())
}