-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Standalone mcp apps #6458
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
Standalone mcp apps #6458
Changes from all commits
2311e8d
d1d881c
4378820
aa8f986
6366ee6
0a50cfa
6c67d07
c131bd4
5d7079e
56c30ce
226c768
6e09576
bad48ff
73915e3
9ac6117
3b8ba68
f0ecb1e
ded3330
476e836
756bcd5
5d8e867
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 | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,203 @@ | ||||||
| //! goose Apps module | ||||||
| //! | ||||||
| //! This module contains types and utilities for working with goose Apps, | ||||||
| //! which are UI resources that can be rendered in an MCP server or native | ||||||
| //! goose apps, or something in between. | ||||||
| pub mod resource; | ||||||
|
|
||||||
| use crate::agents::ExtensionManager; | ||||||
| use crate::config::paths::Paths; | ||||||
| use rmcp::model::ErrorData; | ||||||
| use serde::{Deserialize, Serialize}; | ||||||
| use sha2::{Digest, Sha256}; | ||||||
| use std::fs; | ||||||
| use std::path::PathBuf; | ||||||
| use tokio_util::sync::CancellationToken; | ||||||
| use tracing::warn; | ||||||
| use utoipa::ToSchema; | ||||||
|
|
||||||
| pub use resource::{CspMetadata, McpAppResource, ResourceMetadata, UiMetadata}; | ||||||
|
|
||||||
| #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] | ||||||
| #[serde(rename_all = "camelCase")] | ||||||
| pub struct WindowProps { | ||||||
| pub width: u32, | ||||||
| pub height: u32, | ||||||
| pub resizable: bool, | ||||||
| } | ||||||
|
|
||||||
| /// A Goose App combining MCP resource data with Goose-specific metadata | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] | ||||||
| #[serde(rename_all = "camelCase")] | ||||||
| pub struct GooseApp { | ||||||
| #[serde(flatten)] | ||||||
| pub resource: McpAppResource, | ||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||
| pub mcp_server: Option<String>, | ||||||
| #[serde(flatten, skip_serializing_if = "Option::is_none")] | ||||||
| pub window_props: Option<WindowProps>, | ||||||
| } | ||||||
|
|
||||||
| pub struct McpAppCache { | ||||||
| cache_dir: PathBuf, | ||||||
| } | ||||||
|
|
||||||
| impl McpAppCache { | ||||||
| pub fn new() -> Result<Self, std::io::Error> { | ||||||
| let config_dir = Paths::config_dir(); | ||||||
| let cache_dir = config_dir.join("mcp-apps-cache"); | ||||||
| Ok(Self { cache_dir }) | ||||||
| } | ||||||
|
|
||||||
| fn cache_key(extension_name: &str, resource_uri: &str) -> String { | ||||||
| let input = format!("{}::{}", extension_name, resource_uri); | ||||||
| let hash = Sha256::digest(input.as_bytes()); | ||||||
| format!("{}_{:x}", extension_name, hash) | ||||||
| } | ||||||
|
|
||||||
| pub fn list_apps(&self) -> Result<Vec<GooseApp>, std::io::Error> { | ||||||
| let mut apps = Vec::new(); | ||||||
|
|
||||||
| if !self.cache_dir.exists() { | ||||||
| return Ok(apps); | ||||||
| } | ||||||
|
|
||||||
| for entry in fs::read_dir(&self.cache_dir)? { | ||||||
| let entry = entry?; | ||||||
| let path = entry.path(); | ||||||
|
|
||||||
| if path.extension().and_then(|s| s.to_str()) == Some("json") { | ||||||
| match fs::read_to_string(&path) { | ||||||
| Ok(content) => match serde_json::from_str::<GooseApp>(&content) { | ||||||
| Ok(app) => apps.push(app), | ||||||
| Err(e) => warn!("Failed to parse cached app from {:?}: {}", path, e), | ||||||
| }, | ||||||
| Err(e) => warn!("Failed to read cached app from {:?}: {}", path, e), | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Ok(apps) | ||||||
| } | ||||||
|
|
||||||
| pub fn store_app(&self, app: &GooseApp) -> Result<(), std::io::Error> { | ||||||
| fs::create_dir_all(&self.cache_dir)?; | ||||||
|
|
||||||
| if let Some(ref extension_name) = app.mcp_server { | ||||||
| let cache_key = Self::cache_key(extension_name, &app.resource.uri); | ||||||
| let app_path = self.cache_dir.join(format!("{}.json", cache_key)); | ||||||
| let json = serde_json::to_string_pretty(app).map_err(std::io::Error::other)?; | ||||||
|
||||||
| let json = serde_json::to_string_pretty(app).map_err(std::io::Error::other)?; | |
| let json = serde_json::to_string(app).map_err(std::io::Error::other)?; |
Copilot
AI
Jan 15, 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.
This function implements title case conversion but doesn't handle edge cases like acronyms or already-capitalized words well. Consider using a library like heck for more robust case conversion, or document the expected input format.
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 logic deletes all cached apps for extensions that have active apps, then immediately re-caches them. This clears the cache even when nothing changed. Only delete cached apps for extensions that are no longer present, or compare cached vs fresh apps to update selectively.