coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! MCP Bridge - Unified interface between minimal and full MCP implementations
//! 
//! This module provides a bridge that allows CoderLib to use either:
//! 1. Minimal MCP implementation (working now)
//! 2. Full rust-sdk protocol (when fully integrated)
//! 
//! The bridge automatically selects the best available implementation.

use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use thiserror::Error;

/// Unified MCP Error that bridges between implementations
#[derive(Error, Debug)]
pub enum BridgeError {
    #[error("Minimal MCP error: {0}")]
    Minimal(#[from] crate::mcp::minimal::MinimalMcpError),
    #[error("Protocol error: {0}")]
    Protocol(String),
    #[error("Bridge error: {0}")]
    Bridge(String),
}

pub type BridgeResult<T> = Result<T, BridgeError>;

/// Unified MCP Tool definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeTool {
    pub name: String,
    pub description: Option<String>,
    pub input_schema: Value,
}

/// Unified MCP Tool call
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeToolCall {
    pub name: String,
    pub arguments: Option<HashMap<String, Value>>,
}

/// Unified MCP Tool result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeToolResult {
    pub content: Vec<BridgeContent>,
    pub is_error: bool,
}

/// Unified MCP Content
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BridgeContent {
    #[serde(rename = "text")]
    Text { text: String },
    #[serde(rename = "image")]
    Image { data: String, mime_type: String },
    #[serde(rename = "resource")]
    Resource { resource: BridgeResource },
}

/// Unified MCP Resource
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeResource {
    pub uri: String,
    pub name: Option<String>,
    pub description: Option<String>,
    pub mime_type: Option<String>,
}

/// Unified MCP Server info
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeServerInfo {
    pub name: String,
    pub version: String,
    pub protocol_version: String,
    pub capabilities: BridgeServerCapabilities,
}

/// Unified MCP Server capabilities
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeServerCapabilities {
    pub tools: Option<BridgeToolsCapability>,
    pub resources: Option<BridgeResourcesCapability>,
    pub prompts: Option<BridgePromptsCapability>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeToolsCapability {
    pub list_changed: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeResourcesCapability {
    pub subscribe: Option<bool>,
    pub list_changed: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgePromptsCapability {
    pub list_changed: Option<bool>,
}

/// MCP Implementation Backend
#[derive(Debug, Clone)]
pub enum McpBackend {
    Minimal,
    #[cfg(feature = "mcp-full-protocol")]
    Full,
}

/// Unified MCP Client that bridges implementations
pub struct BridgeClient {
    backend: McpBackend,
    minimal_client: Option<crate::mcp::minimal::MinimalMcpClient>,
    #[cfg(feature = "mcp-full-protocol")]
    full_client: Option<rmcp::service::RunningService<rmcp::service::RoleClient, rmcp::model::ClientInfo>>,
}

impl BridgeClient {
    /// Create a new bridge client with automatic backend selection
    pub fn new() -> Self {
        let backend = Self::select_backend();
        Self {
            backend,
            minimal_client: Some(crate::mcp::minimal::MinimalMcpClient::new()),
            #[cfg(feature = "mcp-full-protocol")]
            full_client: None,
        }
    }

    /// Create a new bridge client with explicit backend
    pub fn with_backend(backend: McpBackend) -> Self {
        Self {
            backend,
            minimal_client: Some(crate::mcp::minimal::MinimalMcpClient::new()),
            #[cfg(feature = "mcp-full-protocol")]
            full_client: None,
        }
    }

    /// Select the best available backend
    fn select_backend() -> McpBackend {
        #[cfg(feature = "mcp-full-protocol")]
        {
            // Use full protocol when available
            McpBackend::Full
        }
        #[cfg(not(feature = "mcp-full-protocol"))]
        {
            McpBackend::Minimal
        }
    }

    /// Connect to an MCP server
    pub async fn connect(&mut self, command: &str, args: &[String], env: HashMap<String, String>) -> BridgeResult<BridgeServerInfo> {
        match self.backend {
            McpBackend::Minimal => {
                if let Some(client) = &mut self.minimal_client {
                    client.connect(command, args, env).await?;
                    // Convert minimal response to bridge format
                    Ok(BridgeServerInfo {
                        name: "MCP Server".to_string(),
                        version: "1.0.0".to_string(),
                        protocol_version: "2024-11-05".to_string(),
                        capabilities: BridgeServerCapabilities {
                            tools: Some(BridgeToolsCapability { list_changed: Some(true) }),
                            resources: Some(BridgeResourcesCapability { subscribe: Some(false), list_changed: Some(true) }),
                            prompts: Some(BridgePromptsCapability { list_changed: Some(true) }),
                        },
                    })
                } else {
                    Err(BridgeError::Bridge("Minimal client not initialized".to_string()))
                }
            }
            #[cfg(feature = "mcp-full-protocol")]
            McpBackend::Full => {
                // Use rust-sdk for full protocol connection
                use rmcp::{ServiceExt, transport::TokioChildProcess};

                // Create a tokio Command with args and env
                let mut cmd = tokio::process::Command::new(command);
                cmd.args(args);
                for (key, value) in env {
                    cmd.env(key, value);
                }

                let transport = TokioChildProcess::new(cmd)
                    .map_err(|e| BridgeError::Protocol(format!("Failed to create transport: {}", e)))?;

                let client_info = rmcp::model::ClientInfo::default();
                let service = client_info.serve(transport).await
                    .map_err(|e| BridgeError::Protocol(format!("Failed to start service: {}", e)))?;

                self.full_client = Some(service);

                // Return server info from full protocol
                Ok(BridgeServerInfo {
                    name: "MCP Server (Full Protocol)".to_string(),
                    version: "1.0.0".to_string(),
                    protocol_version: "2024-11-05".to_string(),
                    capabilities: BridgeServerCapabilities {
                        tools: Some(BridgeToolsCapability { list_changed: Some(true) }),
                        resources: Some(BridgeResourcesCapability { subscribe: Some(true), list_changed: Some(true) }),
                        prompts: Some(BridgePromptsCapability { list_changed: Some(true) }),
                    },
                })
            }
        }
    }

    /// List available tools
    pub async fn list_tools(&mut self) -> BridgeResult<Vec<BridgeTool>> {
        match self.backend {
            McpBackend::Minimal => {
                if let Some(client) = &mut self.minimal_client {
                    let tools = client.list_tools().await?;
                    // Convert minimal tools to bridge format
                    let bridge_tools = tools.into_iter().map(|tool| {
                        BridgeTool {
                            name: tool.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string(),
                            description: tool.get("description").and_then(|v| v.as_str()).map(|s| s.to_string()),
                            input_schema: tool.get("inputSchema").cloned().unwrap_or(Value::Object(Default::default())),
                        }
                    }).collect();
                    Ok(bridge_tools)
                } else {
                    Err(BridgeError::Bridge("Minimal client not initialized".to_string()))
                }
            }
            #[cfg(feature = "mcp-full-protocol")]
            McpBackend::Full => {
                if let Some(service) = &mut self.full_client {
                    let rmcp_tools = service.list_all_tools().await
                        .map_err(|e| BridgeError::Protocol(format!("Failed to list tools: {}", e)))?;

                    // Convert rmcp tools to bridge format
                    let bridge_tools = rmcp_tools.into_iter().map(|tool| {
                        BridgeTool {
                            name: tool.name.to_string(),
                            description: tool.description.map(|s| s.to_string()),
                            input_schema: serde_json::Value::Object((*tool.input_schema).clone()),
                        }
                    }).collect();

                    Ok(bridge_tools)
                } else {
                    Err(BridgeError::Bridge("Full client not connected".to_string()))
                }
            }
        }
    }

    /// Call a tool
    pub async fn call_tool(&mut self, request: BridgeToolCall) -> BridgeResult<BridgeToolResult> {
        match self.backend {
            McpBackend::Minimal => {
                if let Some(client) = &mut self.minimal_client {
                    let result = client.call_tool(&request.name, request.arguments.map(|args| serde_json::to_value(args).unwrap_or(Value::Null))).await?;
                    
                    // Convert minimal result to bridge format
                    let content = if let Some(content_array) = result.get("content").and_then(|v| v.as_array()) {
                        content_array.iter().filter_map(|item| {
                            if let Some(text) = item.get("text").and_then(|v| v.as_str()) {
                                Some(BridgeContent::Text { text: text.to_string() })
                            } else {
                                None
                            }
                        }).collect()
                    } else {
                        vec![BridgeContent::Text { text: result.to_string() }]
                    };

                    Ok(BridgeToolResult {
                        content,
                        is_error: result.get("isError").and_then(|v| v.as_bool()).unwrap_or(false),
                    })
                } else {
                    Err(BridgeError::Bridge("Minimal client not initialized".to_string()))
                }
            }
            #[cfg(feature = "mcp-full-protocol")]
            McpBackend::Full => {
                if let Some(service) = &mut self.full_client {
                    // Convert bridge request to rmcp format
                    let rmcp_request = rmcp::model::CallToolRequestParam {
                        name: request.name.clone().into(),
                        arguments: request.arguments.map(|args| {
                            args.into_iter().collect::<serde_json::Map<String, serde_json::Value>>()
                        }),
                    };

                    let rmcp_result = service.call_tool(rmcp_request).await
                        .map_err(|e| BridgeError::Protocol(format!("Failed to call tool: {}", e)))?;

                    // Convert rmcp result to bridge format
                    let bridge_content = rmcp_result.content.into_iter().map(|content| {
                        match content.raw {
                            rmcp::model::RawContent::Text(text_content) => {
                                BridgeContent::Text { text: text_content.text }
                            }
                            rmcp::model::RawContent::Image(image_content) => {
                                BridgeContent::Image {
                                    data: image_content.data,
                                    mime_type: image_content.mime_type
                                }
                            }
                            rmcp::model::RawContent::Resource(resource_content) => {
                                BridgeContent::Resource {
                                    resource: BridgeResource {
                                        uri: match &resource_content.resource {
                                            rmcp::model::ResourceContents::TextResourceContents { uri, .. } => uri.clone(),
                                            rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => uri.clone(),
                                        },
                                        name: None,
                                        description: None,
                                        mime_type: match &resource_content.resource {
                                            rmcp::model::ResourceContents::TextResourceContents { mime_type, .. } => mime_type.clone(),
                                            rmcp::model::ResourceContents::BlobResourceContents { mime_type, .. } => mime_type.clone(),
                                        },
                                    }
                                }
                            }
                            rmcp::model::RawContent::Audio(audio_content) => {
                                // Convert audio to image format for now (bridge doesn't have audio)
                                BridgeContent::Image {
                                    data: audio_content.data.clone(),
                                    mime_type: audio_content.mime_type.clone()
                                }
                            }
                        }
                    }).collect();

                    Ok(BridgeToolResult {
                        content: bridge_content,
                        is_error: rmcp_result.is_error.unwrap_or(false),
                    })
                } else {
                    Err(BridgeError::Bridge("Full client not connected".to_string()))
                }
            }
        }
    }

    /// Disconnect from the server
    pub async fn disconnect(&mut self) -> BridgeResult<()> {
        match self.backend {
            McpBackend::Minimal => {
                if let Some(client) = &mut self.minimal_client {
                    client.disconnect().await?;
                    Ok(())
                } else {
                    Err(BridgeError::Bridge("Minimal client not initialized".to_string()))
                }
            }
            #[cfg(feature = "mcp-full-protocol")]
            McpBackend::Full => {
                if let Some(service) = self.full_client.take() {
                    // Gracefully cancel the service
                    service.cancel().await
                        .map_err(|e| BridgeError::Protocol(format!("Failed to cancel service: {}", e)))?;
                    Ok(())
                } else {
                    // Already disconnected
                    Ok(())
                }
            }
        }
    }

    /// Get the current backend being used
    pub fn backend(&self) -> &McpBackend {
        &self.backend
    }
}

impl Default for BridgeClient {
    fn default() -> Self {
        Self::new()
    }
}