Conversation
Generate an ephemeral self-signed TLS certificate at startup so goosed serves over HTTPS. This gives MCP app iframes a secure context, enabling browser APIs (crypto.subtle, clipboard, etc.) and making 3rd-party iframe injection viable. Changes: - Add tls module that generates a self-signed cert for 127.0.0.1/localhost - Switch axum::serve to axum_server::bind_rustls for TLS termination - Update Electron baseUrl from http to https - Add certificate-error handler in Electron for localhost self-signed cert - Set NODE_TLS_REJECT_UNAUTHORIZED=0 for main-process health checks - Update CSP connect-src to allow https://127.0.0.1:*
MCP app guest iframes previously used srcdoc to inject HTML, which gave
them a URL of about:srcdoc. This caused third-party SDKs (like Square
Web Payments SDK) to fail because they check window.location.protocol
and reject anything that isn't https: or localhost.
This commit adds a /mcp-app-guest endpoint that stores guest HTML
server-side and serves it from a real https://localhost:{port} URL:
Server changes (mcp_app_proxy.rs):
- Added POST /mcp-app-guest to store HTML with a unique nonce
- Added GET /mcp-app-guest to retrieve and serve stored HTML with
proper CSP headers (script-src, connect-src, frame-src, style-src)
- In-memory HashMap<String, StoredHtml> with 5-minute TTL and
automatic cleanup on each request
- HTML is consumed on first retrieval (one-time use)
Proxy template changes (mcp_app_proxy.html):
- On receiving guest HTML via postMessage, the proxy now POSTs it
to /mcp-app-guest to get a nonce, then sets the guest iframe src
to GET /mcp-app-guest?secret=...&nonce=...
- Falls back to srcdoc if the endpoint is unavailable
- createGuestIframe is now async to support the fetch workflow
Auth changes (auth.rs):
- Added /mcp-app-guest to the auth exemption list (it uses its own
secret query parameter for authentication, same as /mcp-app-proxy)
Desktop changes (goosed.ts, main.ts):
- Switched baseUrl from https://127.0.0.1:{port} to
https://localhost:{port} so that SDKs with built-in localhost
exceptions (like Square) recognize the origin
- Added http://localhost:* and https://localhost:* to the Electron
CSP connect-src allowlist
- Updated default GOOSE_API_HOST to http://localhost
Replace process-wide NODE_TLS_REJECT_UNAUTHORIZED=0 with Electron's net.fetch for the goosed API client. net.fetch uses Chromium's network stack which respects the certificate-error handler already scoped to localhost/127.0.0.1, so external fetch calls (fetch-metadata, getAllowList) retain strict TLS verification.
The handler was inside appMain() but net.fetch needs it registered before any createChat() call. Code paths like open-url and second-instance can call createChat() before appMain() runs.
The certificate-error handler only applies to webContents (renderer) requests. net.fetch in the main process needs setCertificateVerifyProc on the default session to accept self-signed certs from localhost. Registered via app.whenReady() before appMain() to ensure it's active before any health check.
Wire up the onFallbackRequest callback on AppRenderer to handle unrecognized MCP JSON-RPC requests. This is a stub that logs the request method and returns success, with a todo to implement sampling/createMessage per #7039. - Import RequestHandlerExtra from @mcp-ui/client - Import JSONRPCRequest from @modelcontextprotocol/sdk/types.js - Add handleFallbackRequest callback with correct types - Pass handler to AppRenderer's onFallbackRequest prop
There was a problem hiding this comment.
Pull request overview
This PR adds infrastructure for handling MCP app secure contexts and wires up a fallback request handler to McpAppRenderer. The changes enable MCP apps to run in HTTPS contexts (required by SDKs like Square Web Payments) by implementing self-signed TLS certificates for the local goosed server.
Changes:
- Added
onFallbackRequesthandler to McpAppRenderer (stub implementation logging requests) - Implemented self-signed TLS certificate generation for localhost in goosed server
- Modified goosed to serve over HTTPS instead of HTTP
- Added guest HTML storage system to serve iframe content from real HTTPS URLs
- Updated Electron to trust self-signed certificates from localhost/127.0.0.1
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/components/McpApps/McpAppRenderer.tsx | Added handleFallbackRequest callback with correct types and imports |
| crates/goose-server/src/tls.rs | New module for generating self-signed TLS certificates using rcgen |
| ui/desktop/src/main.ts | Added certificate error handlers and CSP updates for HTTPS support |
| ui/desktop/src/goosed.ts | Changed base URL from http to https |
| crates/goose-server/src/routes/templates/mcp_app_proxy.html | Added JavaScript to store guest HTML server-side and load via real HTTPS URLs |
| crates/goose-server/src/routes/mcp_app_proxy.rs | Added endpoints for storing/serving guest HTML with nonce-based retrieval |
| crates/goose-server/src/commands/agent.rs | Modified to use axum-server with TLS config and graceful shutdown |
| crates/goose-server/src/auth.rs | Added /mcp-app-guest to public endpoint list |
| crates/goose-server/Cargo.toml | Added rcgen and axum-server dependencies |
| /// In-memory store for guest HTML content. | ||
| /// Maps nonce -> (html_content, csp_string) | ||
| /// Entries are consumed on first read (one-time use). | ||
| type GuestHtmlStore = Arc<RwLock<HashMap<String, (String, String)>>>; |
There was a problem hiding this comment.
The guest HTML store could accumulate unconsumed entries if a nonce is stored but never retrieved. Consider adding a cleanup mechanism such as:
- Entries with timestamps that expire after a timeout (e.g., 5 minutes)
- A periodic cleanup task to remove stale entries
- A size limit with eviction policy
Without cleanup, failed iframe loads or app crashes could leave entries in memory indefinitely.
| headers.insert( | ||
| header::HeaderName::from_static("referrer-policy"), | ||
| "strict-origin".parse().unwrap(), | ||
| ); | ||
| if !csp.is_empty() { | ||
| headers.insert( | ||
| header::CONTENT_SECURITY_POLICY, | ||
| csp.parse().unwrap(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Using unwrap on CSP header parsing could panic if the CSP string contains invalid header value characters. Consider handling this error gracefully by either:
- Validating the CSP string before inserting it into storage
- Using a Result type and returning an error response if parsing fails
- Providing a fallback CSP or omitting the header if invalid
The codebase convention (per HOWTOAI.md) is to use anyhow::Result for error handling rather than panic-prone patterns.
| headers.insert( | |
| header::HeaderName::from_static("referrer-policy"), | |
| "strict-origin".parse().unwrap(), | |
| ); | |
| if !csp.is_empty() { | |
| headers.insert( | |
| header::CONTENT_SECURITY_POLICY, | |
| csp.parse().unwrap(), | |
| ); | |
| } | |
| if let Ok(referrer_policy) = "strict-origin".parse() { | |
| headers.insert( | |
| header::HeaderName::from_static("referrer-policy"), | |
| referrer_policy, | |
| ); | |
| } | |
| if !csp.is_empty() { | |
| if let Ok(csp_header) = csp.parse() { | |
| headers.insert( | |
| header::CONTENT_SECURITY_POLICY, | |
| csp_header, | |
| ); | |
| } | |
| } |
| onLoggingMessage={handleLoggingMessage} | ||
| onSizeChanged={handleSizeChanged} | ||
| onError={handleError} | ||
| onFallbackRequest={handleFallbackRequest} |
There was a problem hiding this comment.
The PR title and description focus on adding the onFallbackRequest handler to McpAppRenderer, but this PR includes substantial infrastructure changes:
- Self-signed TLS certificate generation
- Certificate verification bypass for localhost
- Guest HTML storage system with nonce-based retrieval
- CSP modifications
- URL scheme changes from http to https
Consider splitting these into separate PRs or updating the description to accurately reflect all changes. This makes the PR easier to review and understand the full scope of modifications.
accidental PR