Build one extension per browser, in dist/chrome and dist/firefox, from the same source. A script makes each manifest. The code keeps the chrome namespace, which Firefox implements with promises in Manifest V3. The Firefox manifest runs the background as an event page, leaves out the debugger permission, and sets the add-on ID and Firefox 140 as the minimum version. Its extension page policy drops the default upgrade-insecure-requests, which would turn ws:// servers into wss://. Firefox has no debugger API, and it does not freeze background pages: it only slows their timers, which the plugin's heartbeat survives. The background therefore unfreezes in place only where the debugger API exists, and otherwise brings the tab forward and goes back, as a fallback that should rarely run. Firefox stops an event page that is idle for 30 s, even while its WebSocket is open, so the wake agent connection would drop and come back with each alarm. While it holds a connection, the background calls an extension API every 20 s, which Firefox counts as activity.
Penpot MCP Wake
A Chrome and Firefox extension that wakes a background Penpot tab when an MCP task needs it.
Chrome freezes background tabs, and a frozen tab cannot run the MCP plugin. When a task targets a frozen session, the MCP server asks this extension to activate the tab, waits for the plugin to answer again, and then runs the task.
How it works
- The content script asks the Penpot page for its MCP session. The page answers with the session ID, MCP server URL, token and connection status.
- The background (a service worker in Chrome, an event page in Firefox) maps each session to its tab and opens one WebSocket per MCP server and token, on the endpoint the plugin uses. It registers there as a wake agent.
- When the server sends a wake request, the background wakes the tab as described below.
The popup turns waking on or off and lists the paired sessions and server connections.
How a tab is woken
- Chrome unfreezes the page in place: the extension attaches the debugger to the tab,
sets the page lifecycle state to active, and detaches, without switching tabs. Chrome
shows a debugging notice while the extension is attached. Chrome does not allow
debuggeras an optional permission, so the extension requires it, and Chrome warns about it at install. - Firefox has no debugger API, but it does not freeze background pages either: it only slows their timers, which the plugin's heartbeat survives. The extension keeps tabs that hold a session from being unloaded.
- Fallback, when the debugger fails in Chrome (for instance with DevTools open on the tab) and always in Firefox: the extension brings the tab forward, waits until the page reports that it runs again, then returns to the tab and window that had the focus. Whether the window comes forward depends on the window manager.
Build and load
From mcp/:
pnpm install
pnpm --filter mcp-common run build
pnpm --filter mcp-browser-extension run build
The build writes one extension per browser, in dist/chrome and dist/firefox.
- Chrome: open
chrome://extensions, turn on developer mode, choose "Load unpacked" and pickmcp/packages/browser-extension/dist/chrome. - Firefox (140 or later): open
about:debugging#/runtime/this-firefox, choose "Load Temporary Add-on…" and pickmcp/packages/browser-extension/dist/firefox/manifest.json. Firefox removes temporary add-ons when it closes.
Reload Penpot tabs opened before the extension was loaded, so the content script runs in them.
The content script runs on https://design.penpot.app and on localhost at any port.
In Chrome, the service worker does not use the exception you grant a page for an untrusted
certificate. With the development environment's self-signed certificate it fails with
net::ERR_CERT_AUTHORITY_INVALID until Chrome trusts the certificate. On Linux, copy it out
of the devenv's main container and add it to Chrome's certificate store:
docker cp <main-container>:/home/selfsigned.crt /tmp/penpot-devenv.crt
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "Penpot devenv localhost" -i /tmp/penpot-devenv.crt
Restart Chrome afterwards.