| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- //! Bridge between MCP tool surface (ListMcpResources, ReadMcpResource, McpAuth, MCP)
- //! and the existing McpServerManager runtime.
- //!
- //! Provides a stateful client registry that tool handlers can use to
- //! connect to MCP servers and invoke their capabilities.
- use std::collections::HashMap;
- use std::sync::{Arc, Mutex};
- use serde::{Deserialize, Serialize};
- /// Status of a managed MCP server connection.
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
- #[serde(rename_all = "snake_case")]
- pub enum McpConnectionStatus {
- Disconnected,
- Connecting,
- Connected,
- AuthRequired,
- Error,
- }
- impl std::fmt::Display for McpConnectionStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Disconnected => write!(f, "disconnected"),
- Self::Connecting => write!(f, "connecting"),
- Self::Connected => write!(f, "connected"),
- Self::AuthRequired => write!(f, "auth_required"),
- Self::Error => write!(f, "error"),
- }
- }
- }
- /// Metadata about an MCP resource.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct McpResourceInfo {
- pub uri: String,
- pub name: String,
- pub description: Option<String>,
- pub mime_type: Option<String>,
- }
- /// Metadata about an MCP tool exposed by a server.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct McpToolInfo {
- pub name: String,
- pub description: Option<String>,
- pub input_schema: Option<serde_json::Value>,
- }
- /// Tracked state of an MCP server connection.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct McpServerState {
- pub server_name: String,
- pub status: McpConnectionStatus,
- pub tools: Vec<McpToolInfo>,
- pub resources: Vec<McpResourceInfo>,
- pub server_info: Option<String>,
- pub error_message: Option<String>,
- }
- /// Thread-safe registry of MCP server connections for tool dispatch.
- #[derive(Debug, Clone, Default)]
- pub struct McpToolRegistry {
- inner: Arc<Mutex<HashMap<String, McpServerState>>>,
- }
- impl McpToolRegistry {
- #[must_use]
- pub fn new() -> Self {
- Self::default()
- }
- /// Register or update an MCP server connection.
- pub fn register_server(
- &self,
- server_name: &str,
- status: McpConnectionStatus,
- tools: Vec<McpToolInfo>,
- resources: Vec<McpResourceInfo>,
- server_info: Option<String>,
- ) {
- let mut inner = self.inner.lock().expect("mcp registry lock poisoned");
- inner.insert(
- server_name.to_owned(),
- McpServerState {
- server_name: server_name.to_owned(),
- status,
- tools,
- resources,
- server_info,
- error_message: None,
- },
- );
- }
- /// Get current state of an MCP server.
- pub fn get_server(&self, server_name: &str) -> Option<McpServerState> {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- inner.get(server_name).cloned()
- }
- /// List all registered MCP servers.
- pub fn list_servers(&self) -> Vec<McpServerState> {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- inner.values().cloned().collect()
- }
- /// List resources from a specific server.
- pub fn list_resources(&self, server_name: &str) -> Result<Vec<McpResourceInfo>, String> {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- match inner.get(server_name) {
- Some(state) => {
- if state.status != McpConnectionStatus::Connected {
- return Err(format!(
- "server '{}' is not connected (status: {})",
- server_name, state.status
- ));
- }
- Ok(state.resources.clone())
- }
- None => Err(format!("server '{}' not found", server_name)),
- }
- }
- /// Read a specific resource from a server.
- pub fn read_resource(&self, server_name: &str, uri: &str) -> Result<McpResourceInfo, String> {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- let state = inner
- .get(server_name)
- .ok_or_else(|| format!("server '{}' not found", server_name))?;
- if state.status != McpConnectionStatus::Connected {
- return Err(format!(
- "server '{}' is not connected (status: {})",
- server_name, state.status
- ));
- }
- state
- .resources
- .iter()
- .find(|r| r.uri == uri)
- .cloned()
- .ok_or_else(|| format!("resource '{}' not found on server '{}'", uri, server_name))
- }
- /// List tools exposed by a specific server.
- pub fn list_tools(&self, server_name: &str) -> Result<Vec<McpToolInfo>, String> {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- match inner.get(server_name) {
- Some(state) => {
- if state.status != McpConnectionStatus::Connected {
- return Err(format!(
- "server '{}' is not connected (status: {})",
- server_name, state.status
- ));
- }
- Ok(state.tools.clone())
- }
- None => Err(format!("server '{}' not found", server_name)),
- }
- }
- /// Call a tool on a specific server (returns placeholder for now;
- /// actual execution is handled by `McpServerManager::call_tool`).
- pub fn call_tool(
- &self,
- server_name: &str,
- tool_name: &str,
- arguments: &serde_json::Value,
- ) -> Result<serde_json::Value, String> {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- let state = inner
- .get(server_name)
- .ok_or_else(|| format!("server '{}' not found", server_name))?;
- if state.status != McpConnectionStatus::Connected {
- return Err(format!(
- "server '{}' is not connected (status: {})",
- server_name, state.status
- ));
- }
- if !state.tools.iter().any(|t| t.name == tool_name) {
- return Err(format!(
- "tool '{}' not found on server '{}'",
- tool_name, server_name
- ));
- }
- // Return structured acknowledgment — actual execution is delegated
- // to the McpServerManager which handles the JSON-RPC call.
- Ok(serde_json::json!({
- "server": server_name,
- "tool": tool_name,
- "arguments": arguments,
- "status": "dispatched",
- "message": "Tool call dispatched to MCP server"
- }))
- }
- /// Set auth status for a server.
- pub fn set_auth_status(
- &self,
- server_name: &str,
- status: McpConnectionStatus,
- ) -> Result<(), String> {
- let mut inner = self.inner.lock().expect("mcp registry lock poisoned");
- let state = inner
- .get_mut(server_name)
- .ok_or_else(|| format!("server '{}' not found", server_name))?;
- state.status = status;
- Ok(())
- }
- /// Disconnect / remove a server.
- pub fn disconnect(&self, server_name: &str) -> Option<McpServerState> {
- let mut inner = self.inner.lock().expect("mcp registry lock poisoned");
- inner.remove(server_name)
- }
- /// Number of registered servers.
- #[must_use]
- pub fn len(&self) -> usize {
- let inner = self.inner.lock().expect("mcp registry lock poisoned");
- inner.len()
- }
- #[must_use]
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn registers_and_retrieves_server() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "test-server",
- McpConnectionStatus::Connected,
- vec![McpToolInfo {
- name: "greet".into(),
- description: Some("Greet someone".into()),
- input_schema: None,
- }],
- vec![McpResourceInfo {
- uri: "res://data".into(),
- name: "Data".into(),
- description: None,
- mime_type: Some("application/json".into()),
- }],
- Some("TestServer v1.0".into()),
- );
- let server = registry.get_server("test-server").expect("should exist");
- assert_eq!(server.status, McpConnectionStatus::Connected);
- assert_eq!(server.tools.len(), 1);
- assert_eq!(server.resources.len(), 1);
- }
- #[test]
- fn lists_resources_from_connected_server() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "srv",
- McpConnectionStatus::Connected,
- vec![],
- vec![McpResourceInfo {
- uri: "res://alpha".into(),
- name: "Alpha".into(),
- description: None,
- mime_type: None,
- }],
- None,
- );
- let resources = registry.list_resources("srv").expect("should succeed");
- assert_eq!(resources.len(), 1);
- assert_eq!(resources[0].uri, "res://alpha");
- }
- #[test]
- fn rejects_resource_listing_for_disconnected_server() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "srv",
- McpConnectionStatus::Disconnected,
- vec![],
- vec![],
- None,
- );
- assert!(registry.list_resources("srv").is_err());
- }
- #[test]
- fn reads_specific_resource() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "srv",
- McpConnectionStatus::Connected,
- vec![],
- vec![McpResourceInfo {
- uri: "res://data".into(),
- name: "Data".into(),
- description: Some("Test data".into()),
- mime_type: Some("text/plain".into()),
- }],
- None,
- );
- let resource = registry
- .read_resource("srv", "res://data")
- .expect("should find");
- assert_eq!(resource.name, "Data");
- assert!(registry.read_resource("srv", "res://missing").is_err());
- }
- #[test]
- fn calls_tool_on_connected_server() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "srv",
- McpConnectionStatus::Connected,
- vec![McpToolInfo {
- name: "greet".into(),
- description: None,
- input_schema: None,
- }],
- vec![],
- None,
- );
- let result = registry
- .call_tool("srv", "greet", &serde_json::json!({"name": "world"}))
- .expect("should dispatch");
- assert_eq!(result["status"], "dispatched");
- // Unknown tool should fail
- assert!(registry
- .call_tool("srv", "missing", &serde_json::json!({}))
- .is_err());
- }
- #[test]
- fn rejects_tool_call_on_disconnected_server() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "srv",
- McpConnectionStatus::AuthRequired,
- vec![McpToolInfo {
- name: "greet".into(),
- description: None,
- input_schema: None,
- }],
- vec![],
- None,
- );
- assert!(registry
- .call_tool("srv", "greet", &serde_json::json!({}))
- .is_err());
- }
- #[test]
- fn sets_auth_and_disconnects() {
- let registry = McpToolRegistry::new();
- registry.register_server(
- "srv",
- McpConnectionStatus::AuthRequired,
- vec![],
- vec![],
- None,
- );
- registry
- .set_auth_status("srv", McpConnectionStatus::Connected)
- .expect("should succeed");
- let state = registry.get_server("srv").unwrap();
- assert_eq!(state.status, McpConnectionStatus::Connected);
- let removed = registry.disconnect("srv");
- assert!(removed.is_some());
- assert!(registry.is_empty());
- }
- #[test]
- fn rejects_operations_on_missing_server() {
- let registry = McpToolRegistry::new();
- assert!(registry.list_resources("missing").is_err());
- assert!(registry.read_resource("missing", "uri").is_err());
- assert!(registry.list_tools("missing").is_err());
- assert!(registry
- .call_tool("missing", "tool", &serde_json::json!({}))
- .is_err());
- assert!(registry
- .set_auth_status("missing", McpConnectionStatus::Connected)
- .is_err());
- }
- }
|