-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Vibe mcp apps #6569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vibe mcp apps #6569
Changes from all commits
aa93748
620a598
4882f4f
858e597
c7eef09
38c96ea
652fb1b
7e404cd
e449f8c
b00c038
9af9e96
b515fe1
d29c7db
66d49f1
138b6fe
7b8e2d4
ddc5495
bd3ae84
7bcdd33
196a45c
2a894cd
eb39167
3a17743
0eb72aa
b5aca5f
b562495
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1107
to
+1110
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1112
to
+1124
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | |
| let existing_apps = cache.list_apps().unwrap_or_default(); | |
| let existing_names: HashSet<String> = existing_apps | |
| .iter() | |
| .map(|a| a.resource.name.clone()) | |
| .collect(); | |
| if existing_names.contains(&app.resource.name) { | |
| return Err(ErrorResponse { | |
| message: format!( | |
| "An app with the name '{}' already exists. Please choose a different name.", | |
| app.resource.name | |
| ), | |
| status: StatusCode::CONFLICT, | |
| }); |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The counter-based naming strategy could result in names like "my-app_1_1_1" if an app is imported multiple times and then renamed. Consider using a UUID suffix or timestamp instead for cleaner naming.
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import functionality has a potential issue: name conflicts are resolved by appending "_1", "_2", etc., but the mcpServer field from the imported HTML is preserved. This could be misleading - an imported app might claim to be from extension "foo" but actually be from a different source. Consider either clearing the mcpServer field on import or validating it against active extensions.
| // Clear any imported MCP server binding to avoid spoofing the app's origin. | |
| if app.resource.mcp_server.is_some() { | |
| app.resource.mcp_server = None; | |
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded string "apps" should use a constant. This same value is defined as EXTENSION_NAME in apps_extension.rs and APPS_EXTENSION_NAME in cache.rs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error handling could be improved. If
app.to_html()fails but the app object is malformed rather than missing, the user gets a "not found" error instead of a more accurate error message. Consider checking if the app exists first and returning different error messages for "app not found" vs "failed to export".