From ef496329b9ed75a977c2170f5a569f09cf8cfdb6 Mon Sep 17 00:00:00 2001 From: Yingjie He Date: Sat, 19 Jul 2025 13:58:06 -0700 Subject: [PATCH] feat: deprecate jetbrains extension in favor of public one (#2589) --- crates/goose-cli/src/commands/mcp.rs | 4 +- crates/goose-mcp/src/jetbrains/mod.rs | 236 ------------ crates/goose-mcp/src/jetbrains/proxy.rs | 343 ------------------ crates/goose-mcp/src/lib.rs | 2 - crates/goose-server/src/commands/mcp.rs | 4 +- ui/desktop/openapi.json | 5 + ui/desktop/src/built-in-extensions.json | 9 - .../extensions/bundled-extensions.json | 11 - 8 files changed, 7 insertions(+), 607 deletions(-) delete mode 100644 crates/goose-mcp/src/jetbrains/mod.rs delete mode 100644 crates/goose-mcp/src/jetbrains/proxy.rs diff --git a/crates/goose-cli/src/commands/mcp.rs b/crates/goose-cli/src/commands/mcp.rs index 8fbd399f..f70bd8c6 100644 --- a/crates/goose-cli/src/commands/mcp.rs +++ b/crates/goose-cli/src/commands/mcp.rs @@ -1,7 +1,6 @@ use anyhow::Result; use goose_mcp::{ - ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter, - TutorialRouter, + ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, MemoryRouter, TutorialRouter, }; use mcp_server::router::RouterService; use mcp_server::{BoundedService, ByteTransport, Server}; @@ -26,7 +25,6 @@ pub async fn run_server(name: &str) -> Result<()> { let router: Option> = match name { "developer" => Some(Box::new(RouterService(DeveloperRouter::new()))), "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - "jetbrains" => Some(Box::new(RouterService(JetBrainsRouter::new()))), "google_drive" | "googledrive" => { let router = GoogleDriveRouter::new().await; Some(Box::new(RouterService(router))) diff --git a/crates/goose-mcp/src/jetbrains/mod.rs b/crates/goose-mcp/src/jetbrains/mod.rs deleted file mode 100644 index d6d71822..00000000 --- a/crates/goose-mcp/src/jetbrains/mod.rs +++ /dev/null @@ -1,236 +0,0 @@ -mod proxy; - -use anyhow::Result; -use mcp_core::{ - handler::{PromptError, ResourceError, ToolError}, - prompt::Prompt, - protocol::{JsonRpcMessage, ServerCapabilities}, - resource::Resource, - role::Role, - tool::Tool, -}; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use rmcp::model::Content; -use serde_json::Value; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::{sleep, Duration}; -use tracing::error; - -use self::proxy::JetBrainsProxy; - -pub struct JetBrainsRouter { - tools: Arc>>, - proxy: Arc, - instructions: String, -} - -impl Default for JetBrainsRouter { - fn default() -> Self { - Self::new() - } -} - -impl JetBrainsRouter { - pub fn new() -> Self { - let tools = Arc::new(Mutex::new(Vec::new())); - let proxy = Arc::new(JetBrainsProxy::new()); - let instructions = "JetBrains IDE integration".to_string(); - - // Initialize the proxy - let proxy_clone = Arc::clone(&proxy); - tokio::spawn(async move { - if let Err(e) = proxy_clone.start().await { - error!("Failed to start JetBrains proxy: {}", e); - } - }); - - // Start the background task to update tools - let tools_clone = Arc::clone(&tools); - let proxy_clone = Arc::clone(&proxy); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(5)); - loop { - interval.tick().await; - match proxy_clone.list_tools().await { - Ok(new_tools) => { - let mut tools = tools_clone.lock().await; - *tools = new_tools; - } - Err(e) => { - error!("Failed to update tools: {}", e); - } - } - } - }); - - Self { - tools, - proxy, - instructions, - } - } - - async fn call_proxy_tool( - &self, - tool_name: String, - arguments: Value, - ) -> Result, ToolError> { - let result = self - .proxy - .call_tool(&tool_name, arguments) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Create a success message for the assistant - let mut contents = vec![ - Content::text(format!("Tool {} executed successfully", tool_name)) - .with_audience(vec![Role::Assistant]), - ]; - - // Add the tool's result contents - contents.extend(result.content); - - Ok(contents) - } - - async fn ensure_tools(&self) -> Result<(), ToolError> { - let mut retry_count = 0; - let max_retries = 50; // 5 second total wait time - let retry_delay = Duration::from_millis(100); - - while retry_count < max_retries { - let tools = self.tools.lock().await; - if !tools.is_empty() { - return Ok(()); - } - drop(tools); // Release the lock before sleeping - - sleep(retry_delay).await; - retry_count += 1; - } - - Err(ToolError::ExecutionError("Failed to get tools list from IDE. Make sure the IDE is running and the plugin is installed.".to_string())) - } -} - -impl Router for JetBrainsRouter { - fn name(&self) -> String { - "jetbrains".to_string() - } - - fn instructions(&self) -> String { - self.instructions.clone() - } - - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new().with_tools(true).build() - } - - fn list_tools(&self) -> Vec { - // Use block_in_place to avoid blocking the runtime - tokio::task::block_in_place(|| { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let tools = self.tools.lock().await; - if tools.is_empty() { - drop(tools); - if let Err(e) = self.ensure_tools().await { - error!("Failed to ensure tools: {}", e); - vec![] - } else { - self.tools.lock().await.clone() - } - } else { - tools.clone() - } - }) - }) - } - - fn call_tool( - &self, - tool_name: &str, - arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ToolError>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - Box::pin(async move { - this.ensure_tools().await?; - this.call_proxy_tool(tool_name, arguments).await - }) - } - - fn list_resources(&self) -> Vec { - vec![] - } - - fn read_resource( - &self, - _uri: &str, - ) -> Pin> + Send + 'static>> { - Box::pin(async { Err(ResourceError::NotFound("Resource not found".into())) }) - } - - fn list_prompts(&self) -> Vec { - vec![] - } - - fn get_prompt( - &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))) - }) - } -} - -impl Clone for JetBrainsRouter { - fn clone(&self) -> Self { - Self { - tools: Arc::clone(&self.tools), - proxy: Arc::clone(&self.proxy), - instructions: self.instructions.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tokio::sync::OnceCell; - - static JETBRAINS_ROUTER: OnceCell = OnceCell::const_new(); - - async fn get_router() -> &'static JetBrainsRouter { - JETBRAINS_ROUTER - .get_or_init(|| async { JetBrainsRouter::new() }) - .await - } - - #[tokio::test] - async fn test_router_creation() { - let router = get_router().await; - assert_eq!(router.name(), "jetbrains"); - assert!(!router.instructions().is_empty()); - } - - #[tokio::test] - async fn test_capabilities() { - let router = get_router().await; - let capabilities = router.capabilities(); - assert!(capabilities.tools.is_some()); - } -} diff --git a/crates/goose-mcp/src/jetbrains/proxy.rs b/crates/goose-mcp/src/jetbrains/proxy.rs deleted file mode 100644 index b75b22c7..00000000 --- a/crates/goose-mcp/src/jetbrains/proxy.rs +++ /dev/null @@ -1,343 +0,0 @@ -use anyhow::{anyhow, Result}; -use mcp_core::Tool; -use reqwest::Client; -use rmcp::model::Content; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::env; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; -use tracing::{debug, error, info}; - -const PORT_RANGE_START: u16 = 63342; -const PORT_RANGE_END: u16 = 63352; -const ENDPOINT_CHECK_INTERVAL: Duration = Duration::from_secs(10); - -#[derive(Debug, Serialize, Deserialize)] -struct IDEResponseOk { - status: String, - error: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct IDEResponseErr { - status: Option, - error: String, -} - -#[derive(Debug, Serialize)] -pub struct CallToolResult { - pub content: Vec, - pub is_error: bool, -} - -#[derive(Debug)] -pub struct JetBrainsProxy { - cached_endpoint: Arc>>, - previous_response: Arc>>, - client: Client, -} - -impl JetBrainsProxy { - pub fn new() -> Self { - Self { - cached_endpoint: Arc::new(RwLock::new(None)), - previous_response: Arc::new(RwLock::new(None)), - client: Client::new(), - } - } - - async fn test_list_tools(&self, endpoint: &str) -> Result { - debug!("Sending test request to {}/mcp/list_tools", endpoint); - - let response = match self - .client - .get(format!("{}/mcp/list_tools", endpoint)) - .send() - .await - { - Ok(resp) => { - debug!("Got response with status: {}", resp.status()); - resp - } - Err(e) => { - debug!("Error testing endpoint {}: {}", endpoint, e); - return Ok(false); - } - }; - - if !response.status().is_success() { - debug!("Test request failed with status {}", response.status()); - return Ok(false); - } - - let current_response = response.text().await?; - 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() { - if prev != ¤t_response { - debug!("Response changed since last check"); - self.send_tools_changed().await; - } - } - *prev_response = Some(current_response); - - Ok(true) - } - - async fn find_working_ide_endpoint(&self) -> Result { - debug!("Attempting to find working IDE endpoint..."); - - // 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); - debug!("Testing port {}...", port); - - if self.test_list_tools(&candidate_endpoint).await? { - debug!("Found working IDE endpoint at {}", candidate_endpoint); - return Ok(candidate_endpoint); - } - } - - 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.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> { - 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![]); - } - } - }; - - debug!("Sending list_tools request to {}/mcp/list_tools", endpoint); - let response = match self - .client - .get(format!("{}/mcp/list_tools", endpoint)) - .send() - .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 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(|| { - 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)) = - (t["name"].as_str(), t["description"].as_str()) - { - // Get just the first sentence of the description - let first_sentence = description - .split('.') - .next() - .unwrap_or(description) - .trim() - .to_string() - + "."; - - // 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: first_sentence, - input_schema, - annotations: None, - }) - } else { - debug!("Skipping invalid tool entry: {:?}", t); - None - } - }) - .collect(); - - debug!("Collected {} tools", tools.len()); - Ok(tools) - } - - pub async fn call_tool(&self, name: &str, args: Value) -> Result { - let endpoint = self - .cached_endpoint - .read() - .await - .clone() - .ok_or_else(|| anyhow!("No working IDE endpoint available"))?; - - debug!( - "ENDPOINT: {} | Tool name: {} | args: {}", - endpoint, name, args - ); - - let response = self - .client - .post(format!("{}/mcp/{}", endpoint, name)) - .json(&args) - .send() - .await?; - - if !response.status().is_success() { - debug!("Response failed with status: {}", response.status()); - return Err(anyhow!("Response failed: {}", response.status())); - } - - let ide_response: Value = response.json().await?; - let (is_error, text) = match ide_response { - Value::Object(map) => { - let status = map.get("status").and_then(|v| v.as_str()); - let error = map.get("error").and_then(|v| v.as_str()); - - match (status, error) { - (Some(s), None) => (false, s.to_string()), - (None, Some(e)) => (true, e.to_string()), - _ => { - debug!("Invalid response format from IDE"); - return Err(anyhow!("Invalid response format from IDE")); - } - } - } - _ => { - debug!("Unexpected response type from IDE"); - return Err(anyhow!("Unexpected response type from IDE")); - } - }; - - Ok(CallToolResult { - content: vec![Content::text(text)], - is_error, - }) - } - - async fn send_tools_changed(&self) { - debug!("Sending tools changed notification"); - // TODO: Implement notification mechanism when needed - } - - 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 - let proxy = self.clone(); - 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(()) - } -} - -impl Clone for JetBrainsProxy { - fn clone(&self) -> Self { - Self { - cached_endpoint: Arc::clone(&self.cached_endpoint), - previous_response: Arc::clone(&self.previous_response), - client: Client::new(), - } - } -} diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index 472349f5..c112c8fe 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -10,13 +10,11 @@ pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { pub mod computercontroller; mod developer; pub mod google_drive; -mod jetbrains; mod memory; mod tutorial; pub use computercontroller::ComputerControllerRouter; pub use developer::DeveloperRouter; pub use google_drive::GoogleDriveRouter; -pub use jetbrains::JetBrainsRouter; pub use memory::MemoryRouter; pub use tutorial::TutorialRouter; diff --git a/crates/goose-server/src/commands/mcp.rs b/crates/goose-server/src/commands/mcp.rs index 5a23339a..85395352 100644 --- a/crates/goose-server/src/commands/mcp.rs +++ b/crates/goose-server/src/commands/mcp.rs @@ -1,7 +1,6 @@ use anyhow::Result; use goose_mcp::{ - ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter, - TutorialRouter, + ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, MemoryRouter, TutorialRouter, }; use mcp_server::router::RouterService; use mcp_server::{BoundedService, ByteTransport, Server}; @@ -15,7 +14,6 @@ pub async fn run(name: &str) -> Result<()> { let router: Option> = match name { "developer" => Some(Box::new(RouterService(DeveloperRouter::new()))), "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - "jetbrains" => Some(Box::new(RouterService(JetBrainsRouter::new()))), "google_drive" | "googledrive" => { let router = GoogleDriveRouter::new().await; Some(Box::new(RouterService(router))) diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index d891d653..30ac193e 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1827,6 +1827,11 @@ "format": "double", "description": "Cost per token for output (optional)", "nullable": true + }, + "supports_cache_control": { + "type": "boolean", + "description": "Whether this model supports cache control", + "nullable": true } } }, diff --git a/ui/desktop/src/built-in-extensions.json b/ui/desktop/src/built-in-extensions.json index ee620bf2..ea575573 100644 --- a/ui/desktop/src/built-in-extensions.json +++ b/ui/desktop/src/built-in-extensions.json @@ -26,15 +26,6 @@ "env_keys": [], "timeout": 300 }, - { - "id": "jetbrains", - "name": "Jetbrains", - "description": "Integration with any Jetbrains IDE", - "enabled": false, - "type": "builtin", - "env_keys": [], - "timeout": 300 - }, { "id": "tutorial", "name": "Tutorial", diff --git a/ui/desktop/src/components/settings/extensions/bundled-extensions.json b/ui/desktop/src/components/settings/extensions/bundled-extensions.json index 42a05459..42392d26 100644 --- a/ui/desktop/src/components/settings/extensions/bundled-extensions.json +++ b/ui/desktop/src/components/settings/extensions/bundled-extensions.json @@ -32,17 +32,6 @@ "timeout": 300, "bundled": true }, - { - "id": "jetbrains", - "display_name": "Jetbrains", - "name": "jetbrains", - "description": "Integration with any Jetbrains IDE", - "enabled": false, - "type": "builtin", - "env_keys": [], - "timeout": 300, - "bundled": true - }, { "id": "tutorial", "name": "tutorial",