feat: Enable frontend tools (#1778)

This commit is contained in:
Bradley Axen
2025-04-03 14:12:42 -07:00
committed by GitHub
parent 51edc0c7af
commit c8f8963545
16 changed files with 684 additions and 132 deletions
+39 -1
View File
@@ -8,6 +8,7 @@ use goose::{
};
use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use tracing;
/// Enum representing the different types of extension configuration requests.
#[derive(Deserialize)]
@@ -48,6 +49,16 @@ enum ExtensionConfigRequest {
display_name: Option<String>,
timeout: Option<u64>,
},
/// Frontend extension that provides tools to be executed by the frontend.
#[serde(rename = "frontend")]
Frontend {
/// The name to identify this extension
name: String,
/// The tools provided by this extension
tools: Vec<mcp_core::tool::Tool>,
/// Optional instructions for using the tools
instructions: Option<String>,
},
}
/// Response structure for adding an extension.
@@ -64,8 +75,26 @@ struct ExtensionResponse {
async fn add_extension(
State(state): State<AppState>,
headers: HeaderMap,
Json(request): Json<ExtensionConfigRequest>,
raw: axum::extract::Json<serde_json::Value>,
) -> Result<Json<ExtensionResponse>, StatusCode> {
// Log the raw request for debugging
tracing::info!(
"Received extension request: {}",
serde_json::to_string_pretty(&raw.0).unwrap()
);
// Try to parse into our enum
let request: ExtensionConfigRequest = match serde_json::from_value(raw.0.clone()) {
Ok(req) => req,
Err(e) => {
tracing::error!("Failed to parse extension request: {}", e);
tracing::error!(
"Raw request was: {}",
serde_json::to_string_pretty(&raw.0).unwrap()
);
return Err(StatusCode::UNPROCESSABLE_ENTITY);
}
};
// Verify the presence and validity of the secret key.
let secret_key = headers
.get("X-Secret-Key")
@@ -167,6 +196,15 @@ async fn add_extension(
display_name,
timeout,
},
ExtensionConfigRequest::Frontend {
name,
tools,
instructions,
} => ExtensionConfig::Frontend {
name,
tools,
instructions,
},
};
// Acquire a lock on the agent and attempt to add the extension.
+49 -2
View File
@@ -13,9 +13,9 @@ use goose::{
agents::SessionConfig,
message::{Message, MessageContent},
};
use mcp_core::role::Role;
use mcp_core::{role::Role, Content, ToolResult};
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::Value;
use std::{
convert::Infallible,
@@ -391,12 +391,59 @@ async fn confirm_handler(
Ok(Json(Value::Object(serde_json::Map::new())))
}
#[derive(Debug, Deserialize)]
struct ToolResultRequest {
id: String,
result: ToolResult<Vec<Content>>,
}
async fn submit_tool_result(
State(state): State<AppState>,
headers: HeaderMap,
raw: axum::extract::Json<serde_json::Value>,
) -> Result<Json<Value>, StatusCode> {
// Log the raw request for debugging
tracing::info!(
"Received tool result request: {}",
serde_json::to_string_pretty(&raw.0).unwrap()
);
// Try to parse into our struct
let payload: ToolResultRequest = match serde_json::from_value(raw.0.clone()) {
Ok(req) => req,
Err(e) => {
tracing::error!("Failed to parse tool result request: {}", e);
tracing::error!(
"Raw request was: {}",
serde_json::to_string_pretty(&raw.0).unwrap()
);
return Err(StatusCode::UNPROCESSABLE_ENTITY);
}
};
// Verify secret key
let secret_key = headers
.get("X-Secret-Key")
.and_then(|value| value.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
if secret_key != state.secret_key {
return Err(StatusCode::UNAUTHORIZED);
}
let agent = state.agent.read().await;
let agent = agent.as_ref().ok_or(StatusCode::NOT_FOUND)?;
agent.handle_tool_result(payload.id, payload.result).await;
Ok(Json(json!({"status": "ok"})))
}
// Configure routes for this module
pub fn routes(state: AppState) -> Router {
Router::new()
.route("/reply", post(handler))
.route("/ask", post(ask_handler))
.route("/confirm", post(confirm_handler))
.route("/tool_result", post(submit_tool_result))
.with_state(state)
}