Merge pull request #2 from aljazceru/codex/add-post-/extensions-routes-and-update-api

Add extension management endpoints to goose-api
This commit is contained in:
2025-05-28 19:27:27 +02:00
committed by GitHub
3 changed files with 271 additions and 2 deletions

View File

@@ -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"] }

View File

@@ -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())

View File

@@ -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<String>,
}
#[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<String>,
timeout: Option<u64>,
},
#[serde(rename = "stdio")]
Stdio {
name: String,
cmd: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
envs: Envs,
#[serde(default)]
env_keys: Vec<String>,
timeout: Option<u64>,
},
#[serde(rename = "builtin")]
Builtin {
name: String,
display_name: Option<String>,
timeout: Option<u64>,
},
#[serde(rename = "frontend")]
Frontend {
name: String,
tools: Vec<Tool>,
instructions: Option<String>,
},
}
async fn start_session_handler(
req: SessionRequest,
_api_key: String,
@@ -258,6 +309,128 @@ async fn get_provider_config_handler() -> Result<impl warp::Reply, Rejection> {
Ok::<warp::reply::Json, warp::Rejection>(warp::reply::json(&response))
}
async fn add_extension_handler(
req: ExtensionConfigRequest,
_api_key: String,
) -> Result<impl warp::Reply, Rejection> {
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<impl warp::Reply, Rejection> {
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<Extract = (String,), Error = Rejection> + 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