Vibe mcp apps (#6569)
Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -366,6 +366,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::read_resource,
|
||||
super::routes::agent::call_tool,
|
||||
super::routes::agent::list_apps,
|
||||
super::routes::agent::export_app,
|
||||
super::routes::agent::import_app,
|
||||
super::routes::agent::update_from_session,
|
||||
super::routes::agent::agent_add_extension,
|
||||
super::routes::agent::agent_remove_extension,
|
||||
@@ -544,6 +546,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::CallToolResponse,
|
||||
super::routes::agent::ListAppsRequest,
|
||||
super::routes::agent::ListAppsResponse,
|
||||
super::routes::agent::ImportAppRequest,
|
||||
super::routes::agent::ImportAppResponse,
|
||||
super::routes::agent::StartAgentRequest,
|
||||
super::routes::agent::ResumeAgentRequest,
|
||||
super::routes::agent::StopAgentRequest,
|
||||
|
||||
@@ -32,7 +32,7 @@ use goose::{
|
||||
use rmcp::model::{CallToolRequestParam, Content};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -1000,9 +1000,9 @@ async fn list_apps(
|
||||
})?;
|
||||
|
||||
if let Some(cache) = cache.as_ref() {
|
||||
let active_extensions: std::collections::HashSet<String> = apps
|
||||
let active_extensions: HashSet<String> = apps
|
||||
.iter()
|
||||
.filter_map(|app| app.mcp_server.clone())
|
||||
.flat_map(|app| app.mcp_servers.iter().cloned())
|
||||
.collect();
|
||||
|
||||
for extension_name in active_extensions {
|
||||
@@ -1024,6 +1024,122 @@ async fn list_apps(
|
||||
Ok(Json(ListAppsResponse { apps }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/agent/export_app/{name}",
|
||||
params(
|
||||
("name" = String, Path, description = "Name of the app to export")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "App HTML exported successfully", body = String),
|
||||
(status = 404, description = "App not found", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
tag = "Agent"
|
||||
)]
|
||||
async fn export_app(
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
) -> Result<impl IntoResponse, ErrorResponse> {
|
||||
let cache = McpAppCache::new().map_err(|e| ErrorResponse {
|
||||
message: format!("Failed to access app cache: {}", e),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
let apps = cache.list_apps().map_err(|e| ErrorResponse {
|
||||
message: format!("Failed to list apps: {}", e),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
let app = apps
|
||||
.into_iter()
|
||||
.find(|a| a.resource.name == name)
|
||||
.ok_or_else(|| ErrorResponse {
|
||||
message: format!("App '{}' not found", name),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
})?;
|
||||
|
||||
let html = app.to_html().map_err(|e| ErrorResponse {
|
||||
message: format!("Failed to generate HTML: {}", e),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportAppRequest {
|
||||
pub html: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportAppResponse {
|
||||
pub name: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/import_app",
|
||||
request_body = ImportAppRequest,
|
||||
responses(
|
||||
(status = 201, description = "App imported successfully", body = ImportAppResponse),
|
||||
(status = 400, description = "Bad request - Invalid HTML", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
tag = "Agent"
|
||||
)]
|
||||
async fn import_app(
|
||||
Json(body): Json<ImportAppRequest>,
|
||||
) -> Result<(StatusCode, Json<ImportAppResponse>), ErrorResponse> {
|
||||
let cache = McpAppCache::new().map_err(|e| ErrorResponse {
|
||||
message: format!("Failed to access app cache: {}", e),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
let mut app = GooseApp::from_html(&body.html).map_err(|e| ErrorResponse {
|
||||
message: format!("Invalid Goose App HTML: {}", e),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
})?;
|
||||
|
||||
let original_name = app.resource.name.clone();
|
||||
let mut counter = 1;
|
||||
|
||||
let existing_apps = cache.list_apps().unwrap_or_default();
|
||||
let existing_names: HashSet<String> = existing_apps
|
||||
.iter()
|
||||
.map(|a| a.resource.name.clone())
|
||||
.collect();
|
||||
|
||||
while existing_names.contains(&app.resource.name) {
|
||||
app.resource.name = format!("{}_{}", original_name, counter);
|
||||
app.resource.uri = format!("ui://apps/{}", app.resource.name);
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
app.mcp_servers = vec!["apps".to_string()];
|
||||
|
||||
cache.store_app(&app).map_err(|e| ErrorResponse {
|
||||
message: format!("Failed to store app: {}", e),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(ImportAppResponse {
|
||||
name: app.resource.name.clone(),
|
||||
message: format!("App '{}' imported successfully", app.resource.name),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/agent/start", post(start_agent))
|
||||
@@ -1034,6 +1150,8 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/agent/read_resource", post(read_resource))
|
||||
.route("/agent/call_tool", post(call_tool))
|
||||
.route("/agent/list_apps", get(list_apps))
|
||||
.route("/agent/export_app/{name}", get(export_app))
|
||||
.route("/agent/import_app", post(import_app))
|
||||
.route("/agent/update_provider", post(update_agent_provider))
|
||||
.route("/agent/update_from_session", post(update_from_session))
|
||||
.route("/agent/add_extension", post(agent_add_extension))
|
||||
|
||||
Reference in New Issue
Block a user