diff --git a/crates/brightstaff/src/utils/mcp_client.rs b/crates/brightstaff/src/utils/mcp_client.rs index a818e43f..e69de29b 100644 --- a/crates/brightstaff/src/utils/mcp_client.rs +++ b/crates/brightstaff/src/utils/mcp_client.rs @@ -1,235 +0,0 @@ -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tracing::{debug, warn}; - -/// MCP Tool definition from tools/list response -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct McpTool { - pub name: String, - pub description: Option, - #[serde(rename = "inputSchema")] - pub input_schema: Option, -} - -/// Response from MCP tools/list endpoint -#[derive(Debug, Serialize, Deserialize)] -struct McpToolsListResponse { - tools: Vec, -} - -/// Errors that can occur during MCP communication -#[derive(Debug, thiserror::Error)] -pub enum McpClientError { - #[error("HTTP request failed: {0}")] - HttpError(#[from] reqwest::Error), - #[error("Failed to parse response: {0}")] - ParseError(#[from] serde_json::Error), - #[error("Invalid MCP URL: {0}")] - InvalidUrl(String), - #[error("Tool not found: {0}")] - ToolNotFound(String), -} - -/// Client for communicating with MCP (Model Context Protocol) servers -pub struct McpClient { - client: Client, -} - -impl Default for McpClient { - fn default() -> Self { - Self::new() - } -} - -impl McpClient { - pub fn new() -> Self { - Self { - client: Client::new(), - } - } - - /// Parse MCP URL to extract host, port, and optional tool name - /// Supports formats: - /// - mcp://host:port - /// - mcp://host:port/tool_name - /// - mcp://host:port?tool=tool_name - fn parse_mcp_url(&self, mcp_url: &str) -> Result<(String, Option), McpClientError> { - // Remove mcp:// prefix - let url_without_scheme = mcp_url - .strip_prefix("mcp://") - .ok_or_else(|| McpClientError::InvalidUrl(format!("URL must start with mcp://: {}", mcp_url)))?; - - // Parse host:port and optional tool - let base_url: String; - let mut tool_name: Option = None; - - if let Some(query_start) = url_without_scheme.find('?') { - // Format: mcp://host:port?tool=tool_name - base_url = url_without_scheme[..query_start].to_string(); - let query = &url_without_scheme[query_start + 1..]; - - // Parse query parameters - for param in query.split('&') { - if let Some((key, value)) = param.split_once('=') { - if key == "tool" { - tool_name = Some(value.to_string()); - } - } - } - } else if let Some(path_start) = url_without_scheme.find('/') { - // Format: mcp://host:port/tool_name - base_url = url_without_scheme[..path_start].to_string(); - tool_name = Some(url_without_scheme[path_start + 1..].to_string()); - } else { - // Format: mcp://host:port - base_url = url_without_scheme.to_string(); - } - - Ok((format!("http://{}", base_url), tool_name)) - } - - /// Fetch list of tools from MCP server via SSE - pub async fn fetch_tools(&self, mcp_url: &str) -> Result, McpClientError> { - let (http_url, _) = self.parse_mcp_url(mcp_url)?; - let tools_list_url = format!("{}/sse/tools/list", http_url); - - debug!("Fetching tools from MCP endpoint: {}", tools_list_url); - - let response = self.client - .get(&tools_list_url) - .header("Accept", "text/event-stream") - .send() - .await?; - - if !response.status().is_success() { - warn!( - "Failed to fetch tools from {}: status {}", - tools_list_url, - response.status() - ); - return Ok(Vec::new()); - } - - let body = response.text().await?; - debug!("Received tools list response: {}", body); - - // Parse SSE response - looking for data: lines - let mut tools = Vec::new(); - for line in body.lines() { - if let Some(data) = line.strip_prefix("data: ") { - if data.trim() == "[DONE]" { - break; - } - - match serde_json::from_str::(data) { - Ok(response) => { - tools.extend(response.tools); - } - Err(e) => { - debug!("Failed to parse tools list data: {}, line: {}", e, data); - } - } - } - } - - debug!("Fetched {} tools from MCP server", tools.len()); - Ok(tools) - } - - /// Fetch specific tool description from MCP server - /// If tool_name is None, uses the tool name from the URL or returns the first tool - pub async fn fetch_tool_description( - &self, - mcp_url: &str, - tool_name_override: Option<&str>, - ) -> Result { - let (_, url_tool_name) = self.parse_mcp_url(mcp_url)?; - - // Determine which tool to look for - let target_tool_name = tool_name_override - .or(url_tool_name.as_deref()) - .ok_or_else(|| { - McpClientError::InvalidUrl( - "No tool name specified in URL or parameter".to_string() - ) - })?; - - debug!("Fetching description for tool: {}", target_tool_name); - - let tools = self.fetch_tools(mcp_url).await?; - - let tool = tools - .iter() - .find(|t| t.name == target_tool_name) - .ok_or_else(|| McpClientError::ToolNotFound(target_tool_name.to_string()))?; - - Ok(tool.description.clone().unwrap_or_default()) - } - - /// Fetch all tools as a map of tool name to description - pub async fn fetch_tools_map( - &self, - mcp_url: &str, - ) -> Result, McpClientError> { - let tools = self.fetch_tools(mcp_url).await?; - - Ok(tools - .into_iter() - .map(|tool| { - (tool.name, tool.description.unwrap_or_default()) - }) - .collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_mcp_url_basic() { - let client = McpClient::new(); - - let (http_url, tool) = client.parse_mcp_url("mcp://localhost:10500").unwrap(); - assert_eq!(http_url, "http://localhost:10500"); - assert_eq!(tool, None); - } - - #[test] - fn test_parse_mcp_url_with_path() { - let client = McpClient::new(); - - let (http_url, tool) = client.parse_mcp_url("mcp://localhost:10500/rewrite_query").unwrap(); - assert_eq!(http_url, "http://localhost:10500"); - assert_eq!(tool, Some("rewrite_query".to_string())); - } - - #[test] - fn test_parse_mcp_url_with_query_param() { - let client = McpClient::new(); - - let (http_url, tool) = client.parse_mcp_url("mcp://localhost:10500?tool=rewrite_query").unwrap(); - assert_eq!(http_url, "http://localhost:10500"); - assert_eq!(tool, Some("rewrite_query".to_string())); - } - - #[test] - fn test_parse_mcp_url_with_host_docker_internal() { - let client = McpClient::new(); - - let (http_url, tool) = client - .parse_mcp_url("mcp://host.docker.internal:10500/context_builder") - .unwrap(); - assert_eq!(http_url, "http://host.docker.internal:10500"); - assert_eq!(tool, Some("context_builder".to_string())); - } - - #[test] - fn test_parse_mcp_url_invalid() { - let client = McpClient::new(); - - let result = client.parse_mcp_url("http://localhost:10500"); - assert!(result.is_err()); - } -} diff --git a/docs/MCP_QUICK_START.md b/docs/MCP_QUICK_START.md index c471cc96..e69de29b 100644 --- a/docs/MCP_QUICK_START.md +++ b/docs/MCP_QUICK_START.md @@ -1,132 +0,0 @@ -# MCP Agent Description - Quick Start - -## What This Feature Does - -When processing agent filter chains in Brightstaff, the system can now automatically fetch tool descriptions from MCP (Model Context Protocol) endpoints. These descriptions are used by the LLM router to intelligently select the appropriate agent for handling user requests. - -## Configuration - -### Basic Setup - -Add agents with MCP URLs to your `arch_config.yaml`: - -```yaml -agents: - - id: rag_agent - url: mcp://host.docker.internal:10501 - - - id: query_rewriter - url: mcp://host.docker.internal:10500 - tool: rewrite_query_with_archgw # Optional: specify tool name - -listeners: - - type: agent - port: 8001 - router: arch_agent_router - agents: - - id: rag_agent - description: "RAG agent for document retrieval" # Fallback if MCP fails - filter_chain: - - query_rewriter -``` - -### MCP URL Formats - -Three formats are supported: - -```yaml -# 1. Basic - uses agent id as tool name -url: mcp://localhost:10500 - -# 2. Tool in path -url: mcp://localhost:10500/my_tool_name - -# 3. Tool as query parameter -url: mcp://localhost:10500?tool=my_tool_name -``` - -## How It Works - -1. **Request arrives** at agent listener -2. **Agent selector** needs to choose which agent to handle request -3. **For MCP agents**, description is fetched from endpoint: - ``` - GET http://host:port/sse/tools/list - Accept: text/event-stream - ``` -4. **Tool description extracted** from SSE response -5. **LLM router uses descriptions** to select best agent -6. **Selected agent processes** the request through its filter chain - -## Example MCP Response - -Your MCP server should respond to `/sse/tools/list` with: - -``` -data: {"tools": [{"name": "rewrite_query_with_archgw", "description": "Rewrites user queries using LLM for better retrieval", "inputSchema": {...}}]} -``` - -## Fallback Behavior - -If MCP endpoint fails or returns empty description: -- System logs a warning -- Falls back to `description` field from arch_config.yaml -- Processing continues normally - -## Logging - -Enable debug logging to see MCP interactions: - -```bash -RUST_LOG=debug cargo run -``` - -Look for logs like: -``` -Agent rag_agent is an MCP agent, fetching tool description from: mcp://host.docker.internal:10501 -Fetched MCP description for agent rag_agent: Rewrites user queries... -``` - -## Testing - -Test your MCP endpoint manually: - -```bash -# Check if endpoint is accessible -curl -H "Accept: text/event-stream" http://localhost:10500/sse/tools/list - -# Expected response format -data: {"tools": [{"name": "my_tool", "description": "My tool description"}]} -``` - -## Troubleshooting - -### "Failed to fetch MCP description" -- Check if MCP server is running -- Verify URL format is correct -- Ensure `/sse/tools/list` endpoint exists -- Check network connectivity - -### "MCP tool description is empty" -- Verify MCP server returns tool in response -- Check tool name matches configuration -- Ensure `description` field is populated in MCP response - -### "Tool not found" -- Verify tool name in config matches MCP server -- Check if tool is listed in `/sse/tools/list` response -- Try without explicit tool name (uses agent id) - -## Best Practices - -1. **Always provide fallback descriptions** in arch_config.yaml -2. **Use descriptive tool names** that match your config -3. **Keep MCP servers running** before starting Brightstaff -4. **Monitor logs** for MCP fetch failures -5. **Test MCP endpoints** independently before integration - -## See Also - -- [MCP Agent Integration Documentation](./MCP_AGENT_INTEGRATION.md) -- [RAG Agent Demo](../demos/use_cases/rag_agent/README.md) -- [Agent Configuration Reference](../demos/use_cases/rag_agent/arch_config.yaml)