feat(mcp-apps): add Permission Policy support for sandbox iframes#6947
feat(mcp-apps): add Permission Policy support for sandbox iframes#6947
Conversation
Implements _meta.ui.permissions support per MCP Apps spec Section 2. This allows MCP Apps to request browser capabilities like camera, microphone, geolocation, and clipboard-write access. Changes: - Add PermissionsMetadata struct in goose_apps/resource.rs - Pass permissions through sandbox bridge to proxy iframe - Build iframe 'allow' attribute from requested permissions - Update OpenAPI schema and generated TypeScript types This completes Section 2 (UI Resource Format) of the MCP Apps compliance checklist.
|
There are a lot of formatting changes across the generated files. Not sure what's going on there. |
| pub csp: Option<CspMetadata>, | ||
| /// Sandbox permissions requested by the UI | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub permissions: Option<PermissionsMetadata>, |
There was a problem hiding this comment.
can we make this not Option<> and just provide a default implementation? same as for the booleans above. if we just make them non option booelan and initialize them as false, the generated code becomes cleaner and the client easier to read
There was a problem hiding this comment.
Pull request overview
Adds MCP Apps UIResourceMeta.permissions support so apps can request specific browser capabilities and have Goose propagate them to the sandboxed iframe Permission Policy (allow attribute).
Changes:
- Introduces
PermissionsMetadatain the Rust API schema and updatesUiMetadatato includepermissions. - Threads
permissionsthrough the desktop renderer/bridge and into the sandbox proxy page. - Regenerates OpenAPI client/types (plus a small behavioral tweak in JSON parsing in the generated client).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/components/McpApps/useSandboxBridge.ts | Passes permissions in the sandbox “resource-ready” notification payload. |
| ui/desktop/src/components/McpApps/types.ts | Re-exports PermissionsMetadata for MCP Apps components. |
| ui/desktop/src/components/McpApps/McpAppRenderer.tsx | Extracts _meta.ui.permissions from resource metadata and passes it into the sandbox bridge. |
| ui/desktop/src/api/types.gen.ts | Adds PermissionsMetadata type and UiMetadata.permissions field. |
| ui/desktop/src/api/index.ts | Re-exports updated generated types (includes PermissionsMetadata). |
| ui/desktop/src/api/core/utils.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/types.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/serverSentEvents.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/queryKeySerializer.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/pathSerializer.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/params.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/bodySerializer.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/core/auth.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/client/utils.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/client/types.gen.ts | Generated formatting changes. |
| ui/desktop/src/api/client/client.gen.ts | Generated changes; includes empty-JSON-body handling in parseAs: 'json'. |
| ui/desktop/openapi.json | Adds PermissionsMetadata schema and wires it into UiMetadata. |
| crates/goose/src/goose_apps/resource.rs | Defines PermissionsMetadata and adds UiMetadata.permissions. |
| crates/goose/src/goose_apps/mod.rs | Re-exports PermissionsMetadata. |
| crates/goose-server/src/routes/templates/mcp_app_proxy.html | Builds inner guest iframe allow attribute from requested permissions. |
| crates/goose-server/src/openapi.rs | Registers PermissionsMetadata in OpenAPI schema derivations. |
| if (permissions && permissions.camera) allowList.push('camera'); | ||
| if (permissions && permissions.microphone) allowList.push('microphone'); | ||
| if (permissions && permissions.geolocation) allowList.push('geolocation'); | ||
| if (permissions && permissions.clipboardWrite) allowList.push('clipboard-write'); |
There was a problem hiding this comment.
The permissions checks rely on truthiness (e.g. permissions.camera), so a malformed metadata value like 'true' would unintentionally enable the feature; please validate each permission is exactly true before adding it to the allow list.
| if (permissions && permissions.camera) allowList.push('camera'); | |
| if (permissions && permissions.microphone) allowList.push('microphone'); | |
| if (permissions && permissions.geolocation) allowList.push('geolocation'); | |
| if (permissions && permissions.clipboardWrite) allowList.push('clipboard-write'); | |
| if (permissions && permissions.camera === true) allowList.push('camera'); | |
| if (permissions && permissions.microphone === true) allowList.push('microphone'); | |
| if (permissions && permissions.geolocation === true) allowList.push('geolocation'); | |
| if (permissions && permissions.clipboardWrite === true) allowList.push('clipboard-write'); |
| const { iframeRef, proxyUrl } = useSandboxBridge({ | ||
| resourceHtml: resource.html || '', | ||
| resourceCsp: resource.csp, | ||
| resourcePermissions: resource.permissions, | ||
| resourceUri, |
There was a problem hiding this comment.
permissions is passed into the sandbox bridge, but the outer proxy iframe rendered by this component never sets an allow attribute; Permission Policy is intersected across ancestor iframes, so camera/mic/geolocation/clipboard-write will likely stay blocked when the proxy URL is cross-origin (e.g., goosed baseUrl). Consider also setting allow on the React iframe based on resource.permissions.
| if (content.text !== cachedHtml) { | ||
| setResource({ | ||
| html: content.text, | ||
| csp: meta?.ui?.csp || null, | ||
| permissions: meta?.ui?.permissions || null, | ||
| prefersBorder: meta?.ui?.prefersBorder ?? true, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Guarding the state update on content.text !== cachedHtml prevents applying updated UI metadata (CSP/permissions/prefersBorder) when cached HTML matches the fetched HTML (e.g. StandaloneAppView keeps cachedHtml set even after session init); update the metadata even when the HTML is unchanged.
| if (content.text !== cachedHtml) { | |
| setResource({ | |
| html: content.text, | |
| csp: meta?.ui?.csp || null, | |
| permissions: meta?.ui?.permissions || null, | |
| prefersBorder: meta?.ui?.prefersBorder ?? true, | |
| }); | |
| } | |
| setResource({ | |
| html: content.text, | |
| csp: meta?.ui?.csp || null, | |
| permissions: meta?.ui?.permissions || null, | |
| prefersBorder: meta?.ui?.prefersBorder ?? true, | |
| }); |
Address PR feedback to simplify generated types by: - Changing PermissionsMetadata fields from Option<bool> to bool with defaults - Changing UiMetadata.permissions from Option<PermissionsMetadata> to PermissionsMetadata - Regenerating OpenAPI types This makes the client code cleaner and easier to read.
…ock#6947) Signed-off-by: Harrison <hcstebbins@gmail.com>
Summary
MCP Apps run in sandboxed iframes for security. Some apps need access to browser capabilities like the camera, microphone, or geolocation—for example, a voice recording app or a location-based search.
This PR implements support for
UIResourceMeta.permissions, allowing MCP App servers to declare which browser capabilities their UI needs. The host (Goose) reads these permissions and sets the appropriateallowattribute on the sandbox iframe, enabling the Permissions Policy for those features.Per the spec, hosts MAY honor these permissions, and apps SHOULD NOT assume they're granted—they should use JS feature detection as a fallback.
Supported Permissions
allowattributecameracameramicrophonemicrophonegeolocationgeolocationclipboardWriteclipboard-writeChanges
Backend (Rust)
PermissionsMetadatastruct with optional boolean fields for each permissionUiMetadatato include the newpermissionsfieldFrontend (TypeScript)
mcp_app_proxy.htmlto build the iframeallowattribute from requested permissionsExample
An MCP App server can declare permissions in its resource metadata:
{ "_meta": { "ui": { "permissions": { "camera": true, "microphone": true } } } }Goose will then render the iframe with:
References