use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
use crate::circuit_breaker::CircuitBreakerStats;
use crate::hub_common::HubConnections;
use crate::protocol::McpToolDefinition;
use crate::transport::{McpServerConnectionConfig, McpTransport, McpTransportError};
pub struct McpHub {
connections: HubConnections,
discovery_timeout: Duration,
}
impl Default for McpHub {
fn default() -> Self {
Self::new()
}
}
impl McpHub {
pub fn new() -> Self {
Self {
connections: HubConnections::new(),
discovery_timeout: Duration::from_secs(30),
}
}
pub fn with_discovery_timeout(timeout: Duration) -> Self {
Self {
connections: HubConnections::new(),
discovery_timeout: timeout,
}
}
pub async fn connect(
&self,
config: McpServerConnectionConfig,
) -> Result<Arc<dyn McpTransport>, McpTransportError> {
let conn = self.connections.connect(config).await?;
conn.get_transport()
.await
.ok_or(McpTransportError::ConnectionClosed)
}
pub async fn call_tool(&self, name: &str, args: Value) -> Result<Value, McpTransportError> {
self.connections.call_tool(name, args).await
}
pub async fn list_tools(&self) -> Result<Vec<(String, McpToolDefinition)>, McpTransportError> {
Ok(self.connections.list_tools())
}
pub async fn list_all_tools(&self) -> Result<Vec<McpToolDefinition>, McpTransportError> {
Ok(self.connections.list_tool_definitions())
}
pub async fn discover_tools_parallel(
&self,
) -> Result<Vec<(String, McpToolDefinition)>, McpTransportError> {
self.connections
.discover_tools_parallel(self.discovery_timeout)
.await
}
pub async fn refresh_tool_cache(&self) -> Result<(), McpTransportError> {
self.connections
.refresh_tools_parallel(self.discovery_timeout)
.await
}
pub async fn shutdown_all(&self) -> Result<(), McpTransportError> {
let mut errors = Vec::new();
for (server_name, conn) in self.connections.iter() {
if let Some(transport) = conn.get_transport().await {
if let Err(e) = transport.shutdown().await {
errors.push(format!("{}: {}", server_name, e));
}
}
}
self.connections.clear();
if errors.is_empty() {
Ok(())
} else {
Err(McpTransportError::TransportError(errors.join("; ")))
}
}
pub async fn disconnect(&self, server_name: &str) -> Result<(), McpTransportError> {
let conn = self
.connections
.remove(server_name)
.ok_or_else(|| McpTransportError::ServerNotFound(server_name.to_string()))?;
self.connections.clear_tools_for_server(server_name);
if let Some(transport) = conn.get_transport().await {
transport.shutdown().await?;
}
Ok(())
}
pub fn list_servers(&self) -> Vec<String> {
self.connections.list_servers()
}
pub fn is_connected(&self, server_name: &str) -> bool {
self.connections.is_connected(server_name)
}
pub async fn health_check(&self) -> Vec<(String, bool)> {
self.connections.health_check().await
}
pub fn server_for_tool(&self, tool_name: &str) -> Option<String> {
self.connections.server_for_tool(tool_name)
}
pub fn circuit_breaker_stats(&self, server_name: &str) -> Option<CircuitBreakerStats> {
self.connections.circuit_breaker_stats(server_name)
}
pub fn reset_circuit_breaker(&self, server_name: &str) {
self.connections.reset_circuit_breaker(server_name);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_hub_creation() {
let hub = McpHub::new();
let servers = hub.list_servers();
assert!(servers.is_empty());
}
#[tokio::test]
async fn test_hub_unknown_tool() {
let hub = McpHub::new();
let result = hub
.call_tool("nonexistent_tool", serde_json::json!({}))
.await;
assert!(matches!(result, Err(McpTransportError::UnknownTool(_))));
}
#[test]
fn test_connection_config() {
let config =
McpServerConnectionConfig::stdio("test", "node", vec!["server.js".to_string()])
.with_timeout(60);
assert_eq!(config.name, "test");
assert_eq!(config.timeout_secs, 60);
}
}