feat: deprecate jetbrains extension in favor of public one (#2589)

This commit is contained in:
Yingjie He
2025-07-19 13:58:06 -07:00
committed by GitHub
parent 78b30cc0ca
commit ef496329b9
8 changed files with 7 additions and 607 deletions

View File

@@ -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<Box<dyn BoundedService>> = 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)))

View File

@@ -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<Mutex<Vec<Tool>>>,
proxy: Arc<JetBrainsProxy>,
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<Vec<Content>, 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<Tool> {
// 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<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, 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<Resource> {
vec![]
}
fn read_resource(
&self,
_uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
Box::pin(async { Err(ResourceError::NotFound("Resource not found".into())) })
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![]
}
fn get_prompt(
&self,
prompt_name: &str,
) -> Pin<Box<dyn Future<Output = Result<String, PromptError>> + 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<JetBrainsRouter> = 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());
}
}

View File

@@ -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<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct IDEResponseErr {
status: Option<String>,
error: String,
}
#[derive(Debug, Serialize)]
pub struct CallToolResult {
pub content: Vec<Content>,
pub is_error: bool,
}
#[derive(Debug)]
pub struct JetBrainsProxy {
cached_endpoint: Arc<RwLock<Option<String>>>,
previous_response: Arc<RwLock<Option<String>>>,
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<bool> {
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::<Vec<Value>>(&current_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 != &current_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<String> {
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<Vec<Tool>> {
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<Tool> = 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<CallToolResult> {
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(),
}
}
}

View File

@@ -10,13 +10,11 @@ pub static APP_STRATEGY: Lazy<AppStrategyArgs> = 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;

View File

@@ -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<Box<dyn BoundedService>> = 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)))