Filter Apps page to only show standalone Goose Apps#6811
Conversation
Previously, the Apps page displayed all MCP UI resources from all extensions, including chat-dependent MCP apps that require a chat context to function. This caused confusion since those apps cannot be launched standalone. This change introduces an 'is_goose_app' field to the GooseApp struct that identifies true standalone Goose Apps by checking for JSON-LD metadata with @type: "GooseApp" in the HTML content. The list_apps API now filters to only return apps with this flag set. Changes: - Add is_goose_app field to GooseApp struct - Add has_goose_app_metadata() helper to detect Goose App JSON-LD metadata - Set is_goose_app in from_html(), fetch_mcp_apps(), and handle_create_app() - Filter by is_goose_app in list_apps route handler - Regenerate OpenAPI spec and TypeScript types
There was a problem hiding this comment.
Pull request overview
Filters the Apps page to only show standalone goose apps by adding an explicit is_goose_app flag derived from Goose App JSON-LD metadata and applying that filter in the server’s list_apps endpoint.
Changes:
- Added
is_goose_apptoGooseAppand logic to detect Goose App JSON-LD metadata. - Marked newly created apps as standalone (
is_goose_app: true) and filtered/agent/list_appsresults accordingly. - Regenerated OpenAPI + TypeScript client types to expose
isGooseApp.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/api/types.gen.ts | Adds isGooseApp to generated TS types for app filtering. |
| ui/desktop/openapi.json | Adds isGooseApp to the OpenAPI schema for GooseApp. |
| crates/goose/src/goose_apps/app.rs | Adds is_goose_app to GooseApp and implements metadata detection. |
| crates/goose/src/agents/apps_extension.rs | Marks newly created apps as standalone (is_goose_app: true). |
| crates/goose-server/src/routes/agent.rs | Filters list_apps results to only return is_goose_app apps and updates caching behavior. |
Comments suppressed due to low confidence (1)
crates/goose-server/src/routes/agent.rs:1023
- Cache cleanup now derives
active_extensionsfrom the filteredappslist, so extensions that only provide non-Goose apps will never have their cached entries deleted, causing stale/unused cache files to accumulate; compute the set fromall_apps(or fromui_resources) and then store only the filtered subset.
if let Some(cache) = cache.as_ref() {
let active_extensions: HashSet<String> = apps
.iter()
.flat_map(|app| app.mcp_servers.iter().cloned())
.collect();
for extension_name in active_extensions {
if let Err(e) = cache.delete_extension_apps(&extension_name) {
warn!(
crates/goose/src/goose_apps/app.rs
Outdated
| let metadata_re = match Regex::new(&format!( | ||
| r#"(?s)<script type="{}"[^>]*>\s*(.*?)\s*</script>"#, | ||
| regex::escape(Self::METADATA_SCRIPT_TYPE) | ||
| )) { | ||
| Ok(re) => re, | ||
| Err(_) => return false, | ||
| }; |
There was a problem hiding this comment.
This recompiles the same regex every time has_goose_app_metadata is called (likely once per UI resource on each /list_apps), which is unnecessarily expensive; consider a precompiled static (e.g., once_cell::sync::Lazy<Regex>) or reusing the existing regex from from_html.
crates/goose/src/goose_apps/app.rs
Outdated
| let Some(json_str) = metadata_re.captures(html).and_then(|cap| cap.get(1)) else { | ||
| return false; | ||
| }; | ||
|
|
||
| let Ok(metadata) = serde_json::from_str::<serde_json::Value>(json_str.as_str()) else { | ||
| return false; | ||
| }; | ||
|
|
||
| metadata | ||
| .get("@type") | ||
| .and_then(|v| v.as_str()) | ||
| .is_some_and(|t| t == Self::GOOSE_APP_TYPE) |
There was a problem hiding this comment.
has_goose_app_metadata only inspects the first application/ld+json <script> match, so it can return false even if a later JSON-LD block contains @type: "GooseApp"; iterate over all matches and return true if any block qualifies.
| let Some(json_str) = metadata_re.captures(html).and_then(|cap| cap.get(1)) else { | |
| return false; | |
| }; | |
| let Ok(metadata) = serde_json::from_str::<serde_json::Value>(json_str.as_str()) else { | |
| return false; | |
| }; | |
| metadata | |
| .get("@type") | |
| .and_then(|v| v.as_str()) | |
| .is_some_and(|t| t == Self::GOOSE_APP_TYPE) | |
| for caps in metadata_re.captures_iter(html) { | |
| let Some(json_match) = caps.get(1) else { | |
| continue; | |
| }; | |
| let Ok(metadata) = | |
| serde_json::from_str::<serde_json::Value>(json_match.as_str()) | |
| else { | |
| continue; | |
| }; | |
| if metadata | |
| .get("@type") | |
| .and_then(|v| v.as_str()) | |
| .is_some_and(|t| t == Self::GOOSE_APP_TYPE) | |
| { | |
| return true; | |
| } | |
| } | |
| false |
|
as discussed, I think we should do the filtering on the client |
Filter the Apps page to only display apps from the 'apps' extension
(vibe coded apps built by Goose). This is a client-side only change
that uses the existing mcpServers field to identify custom apps.
No backend changes required - uses existing mcpServers.includes('apps')
pattern that was already used for the 'Custom app' label.
|
@DOsinga, we're client-side only now. LMK if this is cool. |
| // Only show apps from the "apps" extension (vibe coded apps built by Goose) | ||
| setApps(cachedApps.filter((a) => a.mcpServers?.includes('apps'))); |
There was a problem hiding this comment.
This filtering uses a.mcpServers?.includes('apps'), but the PR description says the Apps page should filter on app.isGooseApp; either update the implementation to use the isGooseApp field (and ensure it exists in the generated GooseApp type) or update the PR description/title to match the actual filter criterion.
| // Only show apps from the "apps" extension (vibe coded apps built by Goose) | ||
| setApps(freshApps.filter((a) => a.mcpServers?.includes('apps'))); |
There was a problem hiding this comment.
The same apps.filter((a) => a.mcpServers?.includes('apps')) logic is duplicated in several places; factor this into a small helper (and/or a shared constant for the extension name) to avoid missing an update if the filter rule changes again.
|
@DOsinga, friendly ping for when you have a moment |
…ntext * 'main' of github.com:block/goose: feat: add onFallbackRequest handler to McpAppRenderer (#7208) feat: add streaming support for Claude Code CLI provider (#6833) fix: The detected filetype is PLAIN_TEXT, but the provided filetype was HTML (#6885) Add prompts (#7212) Add testing instructions for speech to text (#7185) Diagnostic files copying (#7209) fix: allow concurrent tool execution within the same MCP extension (#7202) fix: handle missing arguments in MCP tool calls to prevent GUI crash (#7143) Filter Apps page to only show standalone Goose Apps (#6811) opt: use static for Regex (#7205) nit: show dir in title, and less... jank (#7138) feat(gemini-cli): use stream-json output and re-use session (#7118) chore(deps): bump qs from 6.14.1 to 6.14.2 in /documentation (#7191) Switch jsonwebtoken to use aws-lc-rs (already used by rustls) (#7189) chore(deps): bump qs from 6.14.1 to 6.14.2 in /evals/open-model-gym/mcp-harness (#7184) Add SLSA build provenance attestations to release workflows (#7097) fix save and run recipe not working (#7186) Upgraded npm packages for latest security updates (#7183) docs: reasoning effort levels for Codex provider (#6798)
* origin/main: (42 commits) fix: use dynamic port for Tetrate auth callback server (#7228) docs: removing LLM Usage admonitions (#7227) feat(otel): respect standard OTel env vars for exporter selection (#7144) fix: fork session (#7219) Bump version numbers for 1.24.0 release (#7214) Move platform extensions into their own folder (#7210) fix: ignore deprecated skills extension (#7139) Add a goosed over HTTP integration test, and test the developer tool PATH (#7178) feat: add onFallbackRequest handler to McpAppRenderer (#7208) feat: add streaming support for Claude Code CLI provider (#6833) fix: The detected filetype is PLAIN_TEXT, but the provided filetype was HTML (#6885) Add prompts (#7212) Add testing instructions for speech to text (#7185) Diagnostic files copying (#7209) fix: allow concurrent tool execution within the same MCP extension (#7202) fix: handle missing arguments in MCP tool calls to prevent GUI crash (#7143) Filter Apps page to only show standalone Goose Apps (#6811) opt: use static for Regex (#7205) nit: show dir in title, and less... jank (#7138) feat(gemini-cli): use stream-json output and re-use session (#7118) ...
Summary
Filter the Apps page to only display apps from the "apps" extension (vibe coded apps built by Goose).
Changes
AppsView.tsxmcpServers?.includes('apps')- the same pattern already used for the "Custom app" labelWhy
Previously, the Apps page displayed all MCP UI resources from all extensions, including chat-dependent MCP apps that require a chat context to function. This caused confusion since those apps cannot be launched standalone.
Now only apps created by Goose (vibe coded apps) are shown on the Apps page.