Skip to content

feat(mcp-apps): add Permission Policy support for sandbox iframes#6947

Merged
aharvard merged 2 commits intomainfrom
mcp-apps-permissions
Feb 4, 2026
Merged

feat(mcp-apps): add Permission Policy support for sandbox iframes#6947
aharvard merged 2 commits intomainfrom
mcp-apps-permissions

Conversation

@aharvard
Copy link
Collaborator

@aharvard aharvard commented Feb 4, 2026

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 appropriate allow attribute 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

Permission Iframe allow attribute
camera camera
microphone microphone
geolocation geolocation
clipboardWrite clipboard-write

Changes

Backend (Rust)

  • Added PermissionsMetadata struct with optional boolean fields for each permission
  • Updated UiMetadata to include the new permissions field

Frontend (TypeScript)

  • Pass permissions from resource metadata through the sandbox bridge
  • Updated mcp_app_proxy.html to build the iframe allow attribute from requested permissions

Example

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:

<iframe allow="camera; microphone" ...>

References

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.
@aharvard
Copy link
Collaborator Author

aharvard commented Feb 4, 2026

There are a lot of formatting changes across the generated files. Not sure what's going on there.

@aharvard aharvard marked this pull request as ready for review February 4, 2026 14:53
Copilot AI review requested due to automatic review settings February 4, 2026 14:53
pub csp: Option<CspMetadata>,
/// Sandbox permissions requested by the UI
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<PermissionsMetadata>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PermissionsMetadata in the Rust API schema and updates UiMetadata to include permissions.
  • Threads permissions through 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.

Comment on lines +52 to +55
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');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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');

Copilot uses AI. Check for mistakes.
Comment on lines 245 to 249
const { iframeRef, proxyUrl } = useSandboxBridge({
resourceHtml: resource.html || '',
resourceCsp: resource.csp,
resourcePermissions: resource.permissions,
resourceUri,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 91 to 98
if (content.text !== cachedHtml) {
setResource({
html: content.text,
csp: meta?.ui?.csp || null,
permissions: meta?.ui?.permissions || null,
prefersBorder: meta?.ui?.prefersBorder ?? true,
});
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,
});

Copilot uses AI. Check for mistakes.
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.
@aharvard aharvard added this pull request to the merge queue Feb 4, 2026
Merged via the queue into main with commit 2b90c2c Feb 4, 2026
15 of 16 checks passed
@aharvard aharvard deleted the mcp-apps-permissions branch February 4, 2026 16:57
stebbins pushed a commit to stebbins/goose that referenced this pull request Feb 4, 2026
kuccello pushed a commit to kuccello/goose that referenced this pull request Feb 7, 2026
Tyler-Hardin pushed a commit to Tyler-Hardin/goose that referenced this pull request Feb 11, 2026
Tyler-Hardin pushed a commit to Tyler-Hardin/goose that referenced this pull request Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants