diff --git a/crates/goose-api/Cargo.toml b/crates/goose-api/Cargo.toml index 8cf2fec5..1caa2ffe 100644 --- a/crates/goose-api/Cargo.toml +++ b/crates/goose-api/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" goose = { path = "../goose" } goose-mcp = { path = "../goose-mcp" } mcp-client = { path = "../mcp-client" } +mcp-core = { path = "../mcp-core" } tokio = { version = "1", features = ["full"] } warp = "0.3" serde = { version = "1", features = ["derive"] } diff --git a/crates/goose-api/README.md b/crates/goose-api/README.md index 47115506..a64f2d03 100644 --- a/crates/goose-api/README.md +++ b/crates/goose-api/README.md @@ -165,7 +165,56 @@ By default, the server runs on `127.0.0.1:8080`. You can modify this using confi } ``` -### 4. Get Provider Configuration +### 4. Add Extension + +**Endpoint**: `POST /extensions/add` + +**Description**: Installs or enables an extension. + +**Request**: +- Headers: + - Content-Type: application/json + - x-api-key: [your-api-key] +- Body (example): +```json +{ + "type": "builtin", + "name": "mcp_say" +} +``` + +**Response**: +```json +{ + "error": false, + "message": null +} +``` + +### 5. Remove Extension + +**Endpoint**: `POST /extensions/remove` + +**Description**: Removes or disables an extension by name. + +**Request**: +- Headers: + - Content-Type: application/json + - x-api-key: [your-api-key] +- Body: +```json +"mcp_say" +``` + +**Response**: +```json +{ + "error": false, + "message": null +} +``` + +### 6. Get Provider Configuration **Endpoint**: `GET /provider/config` @@ -204,6 +253,18 @@ curl -X POST http://localhost:8080/session/reply \ curl -X GET http://localhost:8080/extensions/list \ -H "x-api-key: your_secure_api_key" +# Add an extension +curl -X POST http://localhost:8080/extensions/add \ + -H "Content-Type: application/json" \ + -H "x-api-key: your_secure_api_key" \ + -d '{"type": "builtin", "name": "mcp_say"}' + +# Remove an extension +curl -X POST http://localhost:8080/extensions/remove \ + -H "Content-Type: application/json" \ + -H "x-api-key: your_secure_api_key" \ + -d '"mcp_say"' + # Get provider configuration curl -X GET http://localhost:8080/provider/config \ -H "x-api-key: your_secure_api_key" @@ -241,6 +302,22 @@ print(response.json()) response = requests.get(f"{API_URL}/extensions/list", headers=HEADERS) print(response.json()) +# Add an extension +response = requests.post( + f"{API_URL}/extensions/add", + headers=HEADERS, + json={"type": "builtin", "name": "mcp_say"} +) +print(response.json()) + +# Remove an extension +response = requests.post( + f"{API_URL}/extensions/remove", + headers=HEADERS, + json="mcp_say" +) +print(response.json()) + # Get provider configuration response = requests.get(f"{API_URL}/provider/config", headers=HEADERS) print(response.json()) diff --git a/crates/goose-api/src/main.rs b/crates/goose-api/src/main.rs index ac2e7f82..0e6e01dd 100644 --- a/crates/goose-api/src/main.rs +++ b/crates/goose-api/src/main.rs @@ -2,9 +2,15 @@ use warp::{Filter, Rejection}; use warp::http::HeaderValue; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; +use goose::agents::{ + extension::Envs, + Agent, + extension_manager::ExtensionManager, + ExtensionConfig, +}; +use mcp_core::tool::Tool; use std::collections::HashMap; use uuid::Uuid; -use goose::agents::{Agent, extension_manager::ExtensionManager}; use goose::config::Config; use goose::providers::{create, providers}; use goose::model::ModelConfig; @@ -67,6 +73,51 @@ struct ProviderConfig { model: String, } +#[derive(Debug, Serialize, Deserialize)] +struct ExtensionResponse { + error: bool, + message: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +enum ExtensionConfigRequest { + #[serde(rename = "sse")] + Sse { + name: String, + uri: String, + #[serde(default)] + envs: Envs, + #[serde(default)] + env_keys: Vec, + timeout: Option, + }, + #[serde(rename = "stdio")] + Stdio { + name: String, + cmd: String, + #[serde(default)] + args: Vec, + #[serde(default)] + envs: Envs, + #[serde(default)] + env_keys: Vec, + timeout: Option, + }, + #[serde(rename = "builtin")] + Builtin { + name: String, + display_name: Option, + timeout: Option, + }, + #[serde(rename = "frontend")] + Frontend { + name: String, + tools: Vec, + instructions: Option, + }, +} + async fn start_session_handler( req: SessionRequest, _api_key: String, @@ -258,6 +309,128 @@ async fn get_provider_config_handler() -> Result { Ok::(warp::reply::json(&response)) } +async fn add_extension_handler( + req: ExtensionConfigRequest, + _api_key: String, +) -> Result { + info!("Adding extension: {:?}", req); + + #[cfg(target_os = "windows")] + if let ExtensionConfigRequest::Stdio { cmd, .. } = &req { + if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") { + let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe").exists() + || std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists(); + + if !node_exists { + let cmd_path = std::path::Path::new(cmd); + let script_dir = cmd_path.parent().ok_or_else(|| warp::reject())?; + + let install_script = script_dir.join("install-node.cmd"); + + if install_script.exists() { + eprintln!("Installing Node.js..."); + let output = std::process::Command::new(&install_script) + .arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi") + .output() + .map_err(|_| warp::reject())?; + + if !output.status.success() { + eprintln!( + "Failed to install Node.js: {}", + String::from_utf8_lossy(&output.stderr) + ); + let resp = ExtensionResponse { + error: true, + message: Some(format!( + "Failed to install Node.js: {}", + String::from_utf8_lossy(&output.stderr) + )), + }; + return Ok(warp::reply::json(&resp)); + } + eprintln!("Node.js installation completed"); + } else { + eprintln!( + "Node.js installer script not found at: {}", + install_script.display() + ); + let resp = ExtensionResponse { + error: true, + message: Some("Node.js installer script not found".to_string()), + }; + return Ok(warp::reply::json(&resp)); + } + } + } + } + + let extension = match req { + ExtensionConfigRequest::Sse { name, uri, envs, env_keys, timeout } => { + ExtensionConfig::Sse { + name, + uri, + envs, + env_keys, + description: None, + timeout, + bundled: None, + } + } + ExtensionConfigRequest::Stdio { name, cmd, args, envs, env_keys, timeout } => { + ExtensionConfig::Stdio { + name, + cmd, + args, + envs, + env_keys, + timeout, + description: None, + bundled: None, + } + } + ExtensionConfigRequest::Builtin { name, display_name, timeout } => { + ExtensionConfig::Builtin { + name, + display_name, + timeout, + bundled: None, + } + } + ExtensionConfigRequest::Frontend { name, tools, instructions } => { + ExtensionConfig::Frontend { + name, + tools, + instructions, + bundled: None, + } + } + }; + + let agent = AGENT.lock().await; + let result = agent.add_extension(extension).await; + + let resp = match result { + Ok(_) => ExtensionResponse { error: false, message: None }, + Err(e) => ExtensionResponse { + error: true, + message: Some(format!("Failed to add extension configuration, error: {:?}", e)), + }, + }; + Ok(warp::reply::json(&resp)) +} + +async fn remove_extension_handler( + name: String, + _api_key: String, +) -> Result { + info!("Removing extension: {}", name); + let agent = AGENT.lock().await; + agent.remove_extension(&name).await; + + let resp = ExtensionResponse { error: false, message: None }; + Ok(warp::reply::json(&resp)) +} + fn with_api_key(api_key: String) -> impl Filter + Clone { warp::header::value("x-api-key") .and_then(move |header_api_key: HeaderValue| { @@ -433,6 +606,22 @@ async fn main() -> Result<(), anyhow::Error> { .and(warp::path("list")) .and(warp::get()) .and_then(list_extensions_handler); + + // Add extension endpoint + let add_extension = warp::path("extensions") + .and(warp::path("add")) + .and(warp::post()) + .and(warp::body::json()) + .and(with_api_key(api_key.clone())) + .and_then(add_extension_handler); + + // Remove extension endpoint + let remove_extension = warp::path("extensions") + .and(warp::path("remove")) + .and(warp::post()) + .and(warp::body::json()) + .and(with_api_key(api_key.clone())) + .and_then(remove_extension_handler); // Get provider configuration endpoint let get_provider_config = warp::path("provider") @@ -445,6 +634,8 @@ async fn main() -> Result<(), anyhow::Error> { .or(reply_session) .or(end_session) .or(list_extensions) + .or(add_extension) + .or(remove_extension) .or(get_provider_config); // Get bind address from configuration or use default