diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index 98f16ee83..16f98e509 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -30,6 +30,7 @@ xcap = "0.0.14" reqwest = { version = "0.11", features = ["json"] } async-trait = "0.1" parking_lot = "0.12" +chrono = "0.4" [dev-dependencies] sysinfo = "0.32.1" diff --git a/crates/goose-mcp/README.md b/crates/goose-mcp/README.md index e74452199..d605054dc 100644 --- a/crates/goose-mcp/README.md +++ b/crates/goose-mcp/README.md @@ -2,6 +2,7 @@ ```bash npx @modelcontextprotocol/inspector cargo run -p developer +npx @modelcontextprotocol/inspector cargo run -p jetbrains ``` Then visit the Inspector in the browser window and test the different endpoints. diff --git a/crates/goose-mcp/src/jetbrains/proxy.rs b/crates/goose-mcp/src/jetbrains/proxy.rs index 747d9bf59..51cd41656 100644 --- a/crates/goose-mcp/src/jetbrains/proxy.rs +++ b/crates/goose-mcp/src/jetbrains/proxy.rs @@ -51,7 +51,10 @@ impl JetBrainsProxy { debug!("Sending test request to {}/mcp/list_tools", endpoint); let response = match self.client.get(&format!("{}/mcp/list_tools", endpoint)).send().await { - Ok(resp) => resp, + Ok(resp) => { + debug!("Got response with status: {}", resp.status()); + resp + } Err(e) => { debug!("Error testing endpoint {}: {}", endpoint, e); return Ok(false); @@ -64,7 +67,13 @@ impl JetBrainsProxy { } let current_response = response.text().await?; - debug!("Received response: {:.100}...", current_response); + debug!("Received response: {}", current_response); + + // Try to parse as JSON array to validate format + if serde_json::from_str::>(¤t_response).is_err() { + debug!("Response is not a valid JSON array of tools"); + return Ok(false); + } let mut prev_response = self.previous_response.write().await; if let Some(prev) = prev_response.as_ref() { @@ -83,14 +92,19 @@ impl JetBrainsProxy { // Check IDE_PORT environment variable first if let Ok(port) = env::var("IDE_PORT") { + debug!("Found IDE_PORT environment variable: {}", port); let test_endpoint = format!("http://127.0.0.1:{}/api", port); if self.test_list_tools(&test_endpoint).await? { debug!("IDE_PORT {} is working", port); return Ok(test_endpoint); } + debug!("IDE_PORT {} is not responding correctly", port); return Err(anyhow!("Specified IDE_PORT={} is not responding correctly", port)); } + debug!("No IDE_PORT environment variable, scanning port range {}-{}", + PORT_RANGE_START, PORT_RANGE_END); + // Scan port range for port in PORT_RANGE_START..=PORT_RANGE_END { let candidate_endpoint = format!("http://127.0.0.1:{}/api", port); @@ -102,55 +116,109 @@ impl JetBrainsProxy { } } + debug!("No working IDE endpoint found in port range"); Err(anyhow!("No working IDE endpoint found in range {}-{}", PORT_RANGE_START, PORT_RANGE_END)) } async fn update_ide_endpoint(&self) { + debug!("Updating IDE endpoint..."); match self.find_working_ide_endpoint().await { Ok(endpoint) => { let mut cached = self.cached_endpoint.write().await; - *cached = Some(endpoint); - debug!("Updated cached endpoint: {:?}", *cached); + *cached = Some(endpoint.clone()); + debug!("Updated cached endpoint to: {}", endpoint); } Err(e) => { + debug!("Failed to update IDE endpoint: {}", e); error!("Failed to update IDE endpoint: {}", e); } } } pub async fn list_tools(&self) -> Result> { - let endpoint = self.cached_endpoint.read().await - .clone() - .ok_or_else(|| anyhow!("No working IDE endpoint available"))?; + debug!("Listing tools..."); + let endpoint = { + let cached = self.cached_endpoint.read().await; + match cached.as_ref() { + Some(ep) => { + debug!("Using cached endpoint: {}", ep); + ep.clone() + } + None => { + debug!("No cached endpoint available"); + return Ok(vec![]); + } + } + }; - let response = self.client + debug!("Sending list_tools request to {}/mcp/list_tools", endpoint); + let response = match self.client .get(&format!("{}/mcp/list_tools", endpoint)) .send() - .await?; + .await + { + Ok(resp) => { + debug!("Got response with status: {}", resp.status()); + resp + } + Err(e) => { + debug!("Failed to send request: {}", e); + return Err(anyhow!("Failed to send request: {}", e)); + } + }; if !response.status().is_success() { + debug!("Request failed with status: {}", response.status()); return Err(anyhow!("Failed to fetch tools with status {}", response.status())); } - let tools_response: Value = response.json().await?; - let tools = tools_response + let response_text = response.text().await?; + debug!("Got response text: {}", response_text); + + let tools_response: Value = serde_json::from_str(&response_text) + .map_err(|e| { + debug!("Failed to parse response as JSON: {}", e); + anyhow!("Failed to parse response as JSON: {}", e) + })?; + + debug!("Parsed JSON response: {:?}", tools_response); + + let tools: Vec = tools_response .as_array() - .ok_or_else(|| anyhow!("Invalid tools response format"))? + .ok_or_else(|| { + debug!("Response is not a JSON array"); + anyhow!("Invalid tools response format: not an array") + })? .iter() .filter_map(|t| { - if let (Some(name), Some(description), Some(input_schema)) = (t["name"].as_str(), t["description"].as_str(), t["input_schema"].as_str()) { + if let (Some(name), Some(description)) = ( + t["name"].as_str(), + t["description"].as_str() + ) { + // Handle input_schema as either a string or an object + let input_schema = match &t["inputSchema"] { + Value::String(s) => Value::String(s.clone()), + Value::Object(o) => Value::Object(o.clone()), + _ => { + debug!("Invalid inputSchema format for tool {}: {:?}", name, t["inputSchema"]); + return None; + } + }; + Some(Tool { name: name.to_string(), description: description.to_string(), - input_schema: serde_json::json!(input_schema), + input_schema, }) } else { + debug!("Skipping invalid tool entry: {:?}", t); None } }) .collect(); + debug!("Collected {} tools", tools.len()); Ok(tools) } @@ -168,6 +236,7 @@ impl JetBrainsProxy { .await?; if !response.status().is_success() { + debug!("Response failed with status: {}", response.status()); return Err(anyhow!("Response failed: {}", response.status())); } @@ -180,10 +249,16 @@ impl JetBrainsProxy { match (status, error) { (Some(s), None) => (false, s.to_string()), (None, Some(e)) => (true, e.to_string()), - _ => return Err(anyhow!("Invalid response format from IDE")), + _ => { + debug!("Invalid response format from IDE"); + return Err(anyhow!("Invalid response format from IDE")); + } } } - _ => return Err(anyhow!("Unexpected response type from IDE")), + _ => { + debug!("Unexpected response type from IDE"); + return Err(anyhow!("Unexpected response type from IDE")); + } }; Ok(CallToolResult { @@ -198,9 +273,11 @@ impl JetBrainsProxy { } pub async fn start(&self) -> Result<()> { + debug!("Initializing JetBrains Proxy..."); info!("Initializing JetBrains Proxy..."); // Initial endpoint check + debug!("Performing initial endpoint check..."); self.update_ide_endpoint().await; // Schedule periodic endpoint checks @@ -208,10 +285,12 @@ impl JetBrainsProxy { tokio::spawn(async move { loop { tokio::time::sleep(ENDPOINT_CHECK_INTERVAL).await; + debug!("Performing periodic endpoint check..."); proxy.update_ide_endpoint().await; } }); + debug!("JetBrains Proxy running"); info!("JetBrains Proxy running"); Ok(()) } @@ -225,4 +304,4 @@ impl Clone for JetBrainsProxy { client: Client::new(), } } -} +} \ No newline at end of file diff --git a/crates/goose-mcp/src/jetbrains/router.rs b/crates/goose-mcp/src/jetbrains/router.rs index b97f310a3..e532b4cfd 100644 --- a/crates/goose-mcp/src/jetbrains/router.rs +++ b/crates/goose-mcp/src/jetbrains/router.rs @@ -5,9 +5,12 @@ use mcp_core::handler::{ToolError, ResourceError}; use mcp_core::protocol::{ServerCapabilities, ToolsCapability, ResourcesCapability}; use mcp_server::Router; use serde_json::Value; -use tracing::info; +use tracing::{info, warn, debug}; use std::future::Future; use std::pin::Pin; +use std::time::Duration; +use tokio::time::sleep; +use std::sync::atomic::{AtomicBool, Ordering}; use crate::jetbrains::proxy::JetBrainsProxy; @@ -15,6 +18,7 @@ use crate::jetbrains::proxy::JetBrainsProxy; pub struct JetBrainsRouter { proxy: Arc, tools_cache: Arc>>, + initialized: Arc, } impl JetBrainsRouter { @@ -22,17 +26,77 @@ impl JetBrainsRouter { Self { proxy: Arc::new(JetBrainsProxy::new()), tools_cache: Arc::new(parking_lot::RwLock::new(Vec::new())), + initialized: Arc::new(AtomicBool::new(false)), } } - pub async fn start(&self) -> Result<()> { + async fn populate_tools_cache(&self) -> Result<()> { + debug!("Attempting to populate tools cache..."); + + // Try multiple times with delay + for attempt in 1..=5 { + debug!("Cache population attempt {} of 5", attempt); + + match self.proxy.list_tools().await { + Ok(tools) => { + debug!("Successfully fetched {} tools from proxy", tools.len()); + if tools.is_empty() { + debug!("Tools list is empty, will retry..."); + sleep(Duration::from_secs(1)).await; + continue; + } + let mut cache = self.tools_cache.write(); + *cache = tools; + debug!("Tools cache updated successfully"); + return Ok(()); + } + Err(e) => { + debug!("Failed to fetch tools (attempt {}): {}", attempt, e); + if attempt < 5 { + debug!("Waiting before retry..."); + sleep(Duration::from_secs(1)).await; + } + } + } + } + + debug!("Failed to populate tools cache after all attempts"); + Err(anyhow::anyhow!("Failed to populate tools cache after 5 attempts")) + } + + async fn initialize(&self) -> Result<()> { + if self.initialized.load(Ordering::SeqCst) { + debug!("Router already initialized"); + return Ok(()); + } + + debug!("Starting JetBrains Router initialization..."); info!("Starting JetBrains Router..."); - // Initialize the tools cache - if let Ok(tools) = self.proxy.list_tools().await { - let mut cache = self.tools_cache.write(); - *cache = tools; + + // First start the proxy + debug!("Starting proxy..."); + let result = self.proxy.start().await; + if result.is_ok() { + debug!("Proxy started successfully"); + } else { + debug!("Failed to start proxy: {:?}", result); + return result; + } + + // Give the proxy a moment to initialize + debug!("Waiting for proxy initialization..."); + sleep(Duration::from_secs(1)).await; + + // Then try to populate the tools cache + if let Err(e) = self.populate_tools_cache().await { + debug!("Warning: Initial tools cache population failed: {}", e); + warn!("Initial tools cache population failed: {}", e); } - self.proxy.start().await + + self.initialized.store(true, Ordering::SeqCst); + debug!("Router initialization completed"); + + Ok(()) } } @@ -46,6 +110,17 @@ impl Router for JetBrainsRouter { } fn capabilities(&self) -> ServerCapabilities { + // Trigger initialization when capabilities are requested + if !self.initialized.load(Ordering::SeqCst) { + debug!("Capabilities requested before initialization, spawning init task"); + let router = self.clone(); + tokio::spawn(async move { + if let Err(e) = router.initialize().await { + debug!("Background initialization failed: {}", e); + } + }); + } + ServerCapabilities { tools: Some(ToolsCapability { list_changed: Some(true), @@ -59,7 +134,33 @@ impl Router for JetBrainsRouter { } fn list_tools(&self) -> Vec { - self.tools_cache.read().clone() + debug!("Accessing tools cache..."); + let tools = self.tools_cache.read().clone(); + + if tools.is_empty() { + debug!("Cache is empty, attempting to populate..."); + // Ensure initialization has happened + if !self.initialized.load(Ordering::SeqCst) { + debug!("Router not initialized, triggering initialization"); + let router = self.clone(); + tokio::spawn(async move { + if let Err(e) = router.initialize().await { + debug!("Background initialization failed: {}", e); + } + }); + } else { + // If initialized but cache is empty, try to populate it + let router = self.clone(); + tokio::spawn(async move { + if let Err(e) = router.populate_tools_cache().await { + debug!("Background cache population failed: {}", e); + } + }); + } + } + + debug!("Returning {} tools from cache", tools.len()); + tools } fn call_tool( @@ -69,10 +170,18 @@ impl Router for JetBrainsRouter { ) -> Pin, ToolError>> + Send + 'static>> { let proxy = Arc::clone(&self.proxy); let name = tool_name.to_string(); + Box::pin(async move { + debug!("Calling tool: {}", name); match proxy.call_tool(&name, arguments).await { - Ok(result) => Ok(result.content), - Err(e) => Err(ToolError::ExecutionError(e.to_string())), + Ok(result) => { + debug!("Tool {} completed successfully", name); + Ok(result.content) + } + Err(e) => { + debug!("Tool {} failed: {}", name, e); + Err(ToolError::ExecutionError(e.to_string())) + } } }) }