daedalus
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@daedalusTake a screenshot of the active tab and show it to me."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Daedalus
Remote browser control via a Chrome extension. Eval bridge + persistent hotfixes + per-tab control + screenshots, CDP, cookies, network capture on pages where Chrome runs the extension.
Install
Know this before you install: ordinary eval uses banner-free MAIN-world injection. When a source-free CSP probe cannot establish that dynamic compilation is available, Daedalus tries the CDP fallback; a successful attach shows Chrome's "Daedalus started debugging this browser" banner while that fallback runs. A kept CDP session or network capture can hold the attachment longer. The banner identifies a debugger attachment, not a value-integrity guarantee.
Load the unpacked extension (extension/) in Chrome:
Visit
chrome://extensionsEnable Developer mode
Click Load unpacked, select
extension/
A unique token is auto-generated on first install and stored in chrome.storage.local. View/change it from the extension's options page (puzzle icon → Daedalus → Options).
For multi-tab parallel scraping, disable Chrome's background tab throttling:
chrome --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgroundingHow It Works
Token: Generated once on install via
crypto.randomUUID(), stored inchrome.storage.localTab IDs: Chrome's native
tabsAPI — each tab is identified by its ChrometabId, registered on create/update with a 30schrome.alarmsheartbeatSingle SSE stream:
background.jsopens one persistentfetchSSE connection (tab=extension) and dispatches incoming commands to the right tabPage bridge:
content.js(ISOLATED world) relayswindow.GMmessages between background andpage.js(MAIN world). Eval useschrome.scriptingMAIN-world injection first. A source-free dynamic-compilation probe routes CSP-blocked pages to CDP; failure to attach there reaches the page relay. Every channel executes with pageMAIN-world semantics.Hotfixes: Stored in the extension-wide
chrome.storage.localkeydaedalus-hotfixes(not per-token), replayed in each eligible top-level page on load, and version-gated for non-permanent fixes. Rotating the token neither isolates nor clears this store.
Sending Commands
Daedalus exposes its extension command surface as an MCP server at <your-bridge>/mcp (streamable-HTTP transport). Before dispatching a request, it requires the Bearer value to exactly match the bridge token resolved through the CLI's existing configuration path: TOKEN overrides DAEDALUS_TOKEN, including an optional _settings provider. With no configured token, the MCP surface fails closed with 401. Add it to Claude Code (or any MCP client) with:
{
"mcpServers": {
"daedalus": {
"url": "https://daedalus.example.com/mcp",
"headers": { "Authorization": "Bearer <your-bridge-token>" }
}
}
}The bridge token is the one the extension generates on install — visible in the extension options page (puzzle icon → Daedalus → Options).
That example fronts the MCP server with a public hostname, but the transport's default allowed hosts are loopback-only (127.0.0.1:*,localhost:*): name the public hostname in DAEDALUS_MCP_ALLOWED_HOSTS or the proxied requests are rejected. See "Server" below for all three MCP settings.
40 tools in 7 groups — tabs, eval/debug, media, cookies, CSS/blocking, hotfixes, network/CDP. See CLAUDE.md <mcp> section for the full list, or call tools/list on the MCP endpoint.
Manual verification helper (no MCP client needed):
TOKEN=<tok> python3 scripts/mcp_probe.py list
TOKEN=<tok> python3 scripts/mcp_probe.py call title '{"tab_id":"<tabId>"}'
TOKEN=<tok> python3 scripts/mcp_probe.py call screenshot '{"include_image":true}'CLI
A shell CLI ships as the daedalus-cli wheel in this repository
(daedalus_cli/), installed as the daedalus command. It reads DAEDALUS_URL
and DAEDALUS_TOKEN from the environment, with TOKEN as a one-off override
and ID=<tabId> for per-tab targeting (omit it to broadcast):
DAEDALUS_TOKEN=<tok> daedalus tabs
DAEDALUS_TOKEN=<tok> ID=<tabId> daedalus title
DAEDALUS_TOKEN=<tok> daedalus exec myid 'document.title'daedalus --help lists every subcommand, and each takes its own --help. The
exec code you send is an expression or a function body whose return value
comes back as the result — see "Sending Commands" above for the contract.
An optional import seam is also available: if a module named _settings is
importable on sys.path, the CLI uses its setting(name, default) and
required(name) functions for DAEDALUS_URL and DAEDALUS_TOKEN instead of
the built-in environment fallback. The TOKEN environment variable remains
the one-off token override and takes precedence; without _settings,
DAEDALUS_URL uses its default and DAEDALUS_TOKEN is required. ID remains
the environment variable for per-tab targeting.
Or atomically publish a raw command file (no MCP, no CLI). Direct redirection
to the final .json name is unsupported because the stream can observe a file
before the writer finishes. Write a sibling name ending in .tmp, then rename
it within the same directory:
# Broadcast to all tabs
commands_dir="$DAEDALUS_DIR/commands"
final="$commands_dir/<token>.json"
tmp="$(mktemp "$commands_dir/.<token>.XXXXXX.tmp")"
printf '%s\n' '{"id":"test1","code":"document.title"}' > "$tmp" &&
mv "$tmp" "$final"
# Target a specific tab
final="$commands_dir/<token>_<tabId>.json"
tmp="$(mktemp "$commands_dir/.<token>_<tabId>.XXXXXX.tmp")"
printf '%s\n' '{"id":"test1","code":"document.title"}' > "$tmp" &&
mv "$tmp" "$final"The reader ignores sibling .tmp names. If an older writer exposes malformed
JSON at the final name, the reader leaves it untouched and retries instead of
deleting a possibly in-progress write. After the atomic rename, the SSE stream
delivers and consumes the command. Result lands in $DAEDALUS_DIR/results/<token>_<tabId>.json (per-tab) and $DAEDALUS_DIR/results/<token>.json (last-writer-wins). page-main injection and page-relay eval completions can carry an exec_ms field — the page-context execution time in milliseconds. A page can falsify or omit this page-timed field on either channel. CDP completions carry no exec_ms. Results associated with a queued delivery also carry roundtrip_ms — the full server-observed roundtrip in milliseconds, from command enqueue (PUT /command) to result arrival (POST /result); it spans queue wait + SSE delivery + client relay + execution + return trip, so roundtrip_ms − exec_ms approximates transport/queue overhead when both fields are present. These measurements use different clocks: exec_ms uses the page's performance.now(), while roundtrip_ms uses server wall-clock milliseconds, so their difference is an approximation rather than an exact subtraction. A legacy frame without _did has no roundtrip_ms.
Async Support
The default page-main channel runs expressions through page-owned eval and
function bodies through page-owned Function; an async wrapper supplies
top-level await. Before submitting source, the background injects only a
constant Function probe. If that probe returns true, the source injection is
attempted once and every outcome is terminal: a value, an exception, or an
injection transport error is reported without retrying the source on CDP. The
page owns Function and can influence this routing hint, but the probe contains
no submitted source, so choosing another channel cannot duplicate its side
effects.
If the source-free probe does not return true, most commonly because page CSP
blocks dynamic compilation, Daedalus tries CDP. A successful attach displays
Chrome's debugger banner. Runtime.evaluate uses REPL mode for top-level
await; source containing return is treated as a function body unless a
wrapper probe parses it as an expression. That wrapper probe is a parser
heuristic, not a non-executing security boundary: operator-crafted source can
escape it and run. After the final CDP evaluation is dispatched, every outcome
is terminal. Only attach or shape failure before submitted source runs reaches
the page relay. CDP promise settlement is bounded at 10 seconds, and result and
exception object handles are released even when a kept session or capture
retains the debugger attachment.
JavaScript evaluated in a page you do not control returns a value that page can
choose, regardless of which channel executed it. The world field records only
which execution channel ran the submitted source; it is diagnostic metadata for
CSP and debugger behaviour, not a trust signal. Its eval values are page-main
for ordinary injection, cdp for the inspector fallback, and
page:<hostname> for the final relay, where <hostname> is the content
script's location.hostname. The background adds the page: prefix, so a
relay hostname cannot collide with cdp or page-main; this namespace fact
does not say anything about the returned value. The CLI renders the field as
channel=..., the dashboard does the same, and MCP exec, put, result, and
ping preserve the exact world value. None assigns a trust class.
The default injection and CDP fallback preserve classic-script behaviour for
the documented sloppy-mode cases: with compiles, legacy octal literals are
accepted, and an undeclared assignment creates a global. CDP REPL mode also
allows repeated let or const declarations. CLI and MCP result waits default
to 15 seconds; a caller timeout does not cancel code already running in the
page. The Blob relay separately waits up to 10 seconds for code containing
await, and 3 seconds otherwise, before reporting a fallback timeout.
Relay correlation remains descriptive and bounded: the sender tab must match, one accepted message consumes the random relay id, and no relay id is registered until the source-free injection probe and the pre-dispatch CDP fallback have finished. These controls prevent cross-tab or duplicate relay completion; they do not establish value integrity.
At most 1,000 page-relay entries may be live. A new fallback at capacity receives one terminal capacity error without evicting live work. Each registered entry expires after 300,000 ms and receives one terminal timeout error if no same-tab completion or earlier send failure removes it.
Dashboard
A browser-based control surface at <your-bridge>/dashboard that drives the whole extension command set without the CLI — live tab list, eval REPL, screenshots, cookies, hotfixes, block rules, net capture, CDP, CSS injection, fetch timings, upload browser.
Open the URL in a tab the extension controls.
Scroll to §12 Settings and paste the token from the extension options page (puzzle icon → Daedalus → Options). Save.
The SSE status dot in the top bar turns cyan when the dashboard is subscribed to live events.
Served directly by server.py from the repo's dashboard/ directory (vanilla JS + ES modules, no build). Live updates come through the existing /stream endpoint: server.py enqueues events into commands/<token>_dashboard/<ts>_<uuid>.json when /register updates an existing tab, and on the successful paths of /sync-tabs, /unregister, and /result; /unregister emits even when no tab was present. The dashboard drains them as kind:'event' frames.
Caveat: the extension's content + page scripts inject into matching pages, including the dashboard's tab. Broadcast eval commands (exec -b) run inside the dashboard too — prefer per-tab targeting or close the dashboard before broadcasting disruptive code.
Self-Patch
Hotfixes
Persist small patches that replay on each eligible top-level page load. Via MCP tools: store_hotfix, list_hotfixes, clear_hotfix, clear_hotfixes, set_permanent. Example (via the probe script):
TOKEN=<tok> python3 scripts/mcp_probe.py call store_hotfix '{"fix_id":"my-fix","code":"console.log(\"patched\")"}'
TOKEN=<tok> python3 scripts/mcp_probe.py call store_hotfix '{"fix_id":"always-on","code":"console.log(\"baseline\")","permanent":true}'
TOKEN=<tok> python3 scripts/mcp_probe.py call set_permanent '{"fix_id":"my-fix","permanent":true}'
TOKEN=<tok> python3 scripts/mcp_probe.py call list_hotfixes
TOKEN=<tok> python3 scripts/mcp_probe.py call clear_hotfix '{"fix_id":"my-fix"}'
TOKEN=<tok> python3 scripts/mcp_probe.py call clear_hotfixes
TOKEN=<tok> python3 scripts/mcp_probe.py call clear_hotfixes '{"include_permanent":true}'Hotfixes are version-gated by default. After an extension version change, retained non-permanent fixes are skipped rather than deleted; storing a fix updates the record to the current version and makes its retained non-permanent fixes eligible again. Mark a fix as permanent (via permanent: true on store_hotfix, or set_permanent) to replay it across version changes. clear_hotfixes removes non-permanent fixes and keeps permanents by default; pass include_permanent: true to remove the entire store.
Extension Commands
The background service worker accepts typed commands (issued via MCP tools or by writing JSON with "type": "..." to $DAEDALUS_DIR/commands/):
Command | Purpose |
| Capture visible tab as PNG |
| Issue raw Chrome DevTools Protocol calls |
| Full request/response interception via CDP |
| Cookie jar access |
| Tab control |
| Per-tab CSS injection |
| declarativeNetRequest blocking |
| Hotfix management (permanent fixes survive version bumps) |
| Reload the extension itself from disk |
| Diagnostic ring buffer for fetch relays |
GM Bridge
window.GM (in page.js, MAIN world) provides this Tampermonkey-style subset:
Method | Description |
| Read a non-reserved string key from extension-wide |
| Write a non-reserved string key to extension-wide storage |
| Delete a non-reserved string key from extension-wide storage |
| List non-reserved storage keys |
| Background-relayed HTTP request (CSP-immune) |
| Inject CSS |
| Write to clipboard |
| Desktop notification |
| Open new tab |
| Trigger download |
| Script metadata |
Cookie access is an operator capability, not a page one: it goes through the
token-authenticated cookies / set-cookie / remove-cookie / clear-cookies
commands above and is deliberately not exposed to page context — page.js runs
in each matching top-level page, which could otherwise read cookies its own
document.cookie cannot see.
Architecture
Browser (matching tab) Server (your bridge host)
┌────────────────────────────────────────────┐ ┌──────────────────────┐
│ MAIN world │ │ bridge (server.py) │
│ ├─ page-main: default injection channel │ │ /stream?token │
│ └─ page.js: GM + relay channel │ │ watches commands/ │
│ ▲ │ │ │
│ │ window.postMessage │ │ /result writes │
│ content.js (ISOLATED) │ │ results/ │
│ ▲ │ │ │
│ │ chrome.runtime │ │ │
│ background.js (service worker) │ │ │
│ ├─ CDP: CSP fallback channel │ │ │
│ ├─ single SSE stream ◄────────────────────┼───┤ │
│ └─ POST result, fetch ────────────────────┼──►│ │
└────────────────────────────────────────────┘ └──────────────────────┘One SSE stream: Background opens a single
fetchSSE connection (tab=extension) and routes commands to the targeted tab viachrome.tabs.sendMessage. 30s watchdog forces reconnect on stale streams.Per-tab routing: Commands address a specific Chrome
tabIdor broadcast to all tabs.CSP behaviour:
GM.xmlhttpRequestsends HTTP work to the background service worker'sfetch. Eval normally uses banner-free MAIN-world injection. When the source-free probe reports dynamic compilation unavailable, CDP supplies the CSP fallback and shows Chrome's debugger banner if it attaches; an attach failure reaches the page/Blob relay. The resultingworldvalue describes that channel and makes no integrity claim.
Server
Start the bridge with its three mandatory settings — it exits at startup without DAEDALUS_DIR or DAEDALUS_PORT, and every bridge-control route fails closed without a configured token:
DAEDALUS_DIR=<data-dir> DAEDALUS_PORT=<port> DAEDALUS_TOKEN=<bridge-token> python3 server.pyserver.py runs under whatever supervisor you use (a systemd unit here). Put a TLS-terminating reverse proxy in front of it if you expose it beyond localhost; the bridge itself speaks plain HTTP. Bridge-control and storage routes compare their token with one configured secret, resolved through the CLI configuration path: TOKEN is the one-off override, otherwise DAEDALUS_TOKEN is required (and an embedding _settings module may supply it). Missing configuration and mismatches fail closed. Only the page-facing POST /segment and GET /segment-status routes use a job-scoped capability instead.
The in-process MCP front end takes three optional settings, documented together because they describe one listener: DAEDALUS_MCP_PORT (default 8086) is the loopback port it binds; its tool handlers use the bridge's actual bound loopback URL, including when DAEDALUS_PORT=0; DAEDALUS_LOCAL_URL explicitly overrides that URL for a standalone MCP deployment fronting a bridge that runs elsewhere; and DAEDALUS_MCP_ALLOWED_HOSTS (default 127.0.0.1:*,localhost:*) is the comma-separated host allowlist its DNS-rebinding protection accepts, so fronting /mcp with a public hostname means naming that hostname there.
Endpoints: GET /stream, GET /tabs, GET /health, GET /dashboard[/<asset>], POST /register, POST /sync-tabs, POST /unregister, POST /poll, POST /result, PUT /command, GET /result, POST/GET/DELETE /upload, GET /screenshot, POST /segment-job, POST /segment + GET /segment-status. POST /segment-job requires the configured bridge token; only POST /segment and GET /segment-status take the job-scoped sig. See CLAUDE.md for payload and endpoint notes.
POST /poll consumes and deletes the legacy broadcast command file when one is present.
PUT /command enqueues into a per-target FIFO directory queue (back-to-back commands to one tab no longer overwrite), stamps each with a delivery id (_did) so the extension can dedup a redelivered frame, and TTL-drops commands unclaimed after DAEDALUS_CMD_TTL seconds (default 90). A background collector applies that TTL and removes empty queue directories even when no SSE consumer ever connects. GET /health reports stream/registry/last-delivery liveness for detecting a silently-dead bridge.
The bridge rejects repeated authority carriers instead of selecting one value:
this includes token in query strings or JSON bodies and job / sig on the
segment-capability routes, even when the repeated values are equal or blank.
The MCP transport likewise rejects repeated Authorization, Mcp-Session-Id,
Host, and Origin headers, plus repeated job arguments for the segment
tools.
When the extension posts a result, the server exposes the command's _did as
deliveryId and assigns a fresh resultGeneration. CLI and MCP waiters first
peek at the shared result slot, match both command id and deliveryId, then
conditionally consume that generation with
GET /result?...&consume=1&expected=<resultGeneration>. If another result
replaces the slot between those requests, the conditional consume leaves the
new result in place and reports that it was not consumed. A bare consume=1
without expected remains a destructive compatibility read of the current
slot.
GET /stream holds the SSE connection open indefinitely, proving liveness with a keepalive comment every DAEDALUS_STREAM_KEEPALIVE seconds (default 15) rather than cycling the connection on a timer; DAEDALUS_STREAM_MAX_AGE (default 3600) is a last-resort ceiling only. The close path is pinned by tests/test_stream_lifecycle.py.
DAEDALUS_MAX_BODY_SIZE (default 64 * 1024 * 1024 bytes, 64 MiB) bounds request bodies read by the POST, PUT, and DELETE handlers; a declared body larger than the limit receives 413. Increase it when relaying larger segments or other payloads.
Filesystem-backed caller values use one path-component policy: it rejects
.., C0/C1 control and surrogate characters, Windows-invalid path characters
and device names, trailing dots or spaces, and UTF-8 encodings longer than 240
bytes. Bridge tokens are stricter and also reject dots and underscores. Other
UTF-8 job names are accepted, so clients must URL-encode them in query strings.
POST /segment-job copies three fixed quotas into each new job record:
DAEDALUS_MAX_SEGMENT_INDEX (default 99999),
DAEDALUS_MAX_SEGMENTS_PER_JOB (default 10000), and
DAEDALUS_MAX_SEGMENT_JOB_SIZE (default 4 * 1024 * 1024 * 1024 bytes,
4 GiB). Changing those settings affects subsequently created jobs; a job with
stored quotas continues to use its recorded values. DAEDALUS_MAX_BODY_SIZE
also bounds each individual segment request.
Security
Read this before installing the extension.
It injects a window.GM shim into every matching top-level page
(<all_urls>, MAIN world),
which is what lets a script sent with put make cross-origin requests. The
consequence is the part to be deliberate about: any matching site you visit
can call that shim, so that site can issue cross-origin requests through the
extension's privileges, exactly as a userscript manager with a wildcard
@match would. If that is not acceptable for your browsing, narrow the
matches in extension/manifest.json to the hosts you actually drive, and
accept that the bridge then does nothing on other pages.
The bridge token and server URL are not reachable through the page's GM
storage methods. They live in chrome.storage.local under the daedalus-
prefix, and the relay refuses to read, write or enumerate that namespace for
page scripts. Without that rule any visited site could read your bridge token
with GM.getValue('daedalus-token') or repoint the bridge with
GM.setValue('daedalus-server', ...), silently. tests/test_repo_contract.py
pins the rule.
Hotfix source is deliberately page-delivered state, not confidential extension
state. content.js reads the daedalus-hotfixes record, selects permanent
fixes plus non-permanent fixes matching the extension version, and posts the
selected full objects into each eligible page. MAIN-world page.js then
evaluates each object's code, so the page can observe that page-delivered
source.
An MCP bearer cannot name a server-host file for the MCP process to read.
The MCP put, CSS injection/removal, and hotfix-storage tools accept inline
source only. The local CLI keeps its file-path convenience, but the CLI process
reads that operator-selected file and submits its contents inline. Holding the
bridge token therefore grants the documented browser and extension-control
authority, including reading stored hotfix source, but it does not add an
arbitrary host-filesystem read through these tools.
Bridge-control and storage routes require the configured bridge token;
/segment and /segment-status are the only capability exception. The server
compares request tokens with the secret resolved from TOKEN or
DAEDALUS_TOKEN and refuses requests when no secret is configured.
POST /segment-job requires that bridge token because it mints the job-scoped
capability; untrusted page JavaScript uses the capability only to post and query
that job. Anyone who holds the bridge token can drive your browser. Do not expose
the bridge port beyond loopback without a reverse proxy that terminates TLS, and
treat the token as a credential.
Eval results carry no value-integrity guarantee. JavaScript evaluated in a
page you do not control returns a value that page can choose, regardless of
which channel executed it. The world field says how the submitted source ran,
not whether to trust its value: page-main is ordinary MAIN-world injection,
cdp is the inspector CSP fallback, and page:<hostname> is the relay. The
mandatory page: prefix is added outside page context, so no hostname can
produce cdp or page-main; that collision resistance is descriptive only.
On page-main, page-owned eval and Function bindings can read the submitted
source as well as influence its returned value.
CDP compilation avoids resolving the page's eval and Function bindings, and
the implementation retrieves direct handles by reference before serializing
them. Those transport mechanics do not change the trust boundary: submitted
source still reads page-controlled state and can route any value, including a
primitive, through page promise machinery before CDP receives it. Whenever the
submitted source uses those page-controlled paths, the page can choose the value
returned on cdp too.
The relay accepts only the stored invocation associated with that random id, requires the sender's tab to match, and consumes the entry once. The downgrade does not disclose the bridge token, server URL, delivery id or result route, does not grant additional extension or browser authority, and cannot affect an invocation in another tab. Those properties protect routing and browser authority; they do not turn the relay marker or its returned JavaScript value into a trust signal.
Deployment
The bridge speaks plain HTTP on loopback. Bridge-control and storage routes
require the configured token and fail closed when it is absent; only /segment
and /segment-status use the job-scoped capability. Two things it deliberately
does NOT do, because they belong to whatever sits in front of it:
TLS and CORS.
server.pysends no CORS headers. When the bridge is cross-origin, the example HLS relay's page-sidefetchcalls need access to bothGET /segment-statusandPOST /segment; the proxy must allow the page origin on both routes and handle the methods/headers required by the POST's preflight. If the status GET is blocked, unavailable, non-2xx, or invalid, the example treats the job as fresh and re-POSTs every segment. Those writes still replace the same per-index files, but the run loses its resume/skip savings. Route both requests through the extension's GM bridge instead if the deployment cannot provide that CORS policy.Serving stored uploads. The dashboard links downloads at
/uploads/<path>, but the bridge has no such route. Do not serve caller-supplied upload names and bytes directly on the dashboard origin, where executable content would share an origin with the dashboard's token-bearing storage. Either leave those links unavailable, or make/uploads/redirect to a separate download-only origin that forcesContent-Disposition: attachment, usesapplication/octet-stream, and sendsX-Content-Type-Options: nosniff.GET /upload(listing) andGET /screenshotare served by the bridge itself and need no proxy help.
Run it directly with a configured token and both of those are simply absent — everything else works.
Files
File | Description |
| MV3 manifest |
| Service worker — SSE, command dispatch, fetch relay, screenshots, CDP, cookies, downloads |
| Message relay between page and background |
| MAIN-world bridge — |
| Token/server settings UI |
| Debug server (also hosts the MCP daemon thread on 127.0.0.1:8086) |
| MCP server — bridges the extension command surface to |
| Minimal MCP client helper for manual verification |
| Version-consistency check / bump across every version site |
| pre-commit + pre-push version-consistency gates |
| The shell CLI, published as the |
| The browser control surface |
| Scripts to run in a page via |
| The suites; |
Examples
examples/ holds scripts meant to be run inside a page with put. Five of
the six demonstrate a part of the bridge rather than a particular site; the
Discord one is deliberately site-specific, because scrolling back through a
virtualised list is a technique you cannot show without a real virtualised
list:
Example | Shows |
| A permanent MAIN-world hotfix at |
| The |
| Finding and destroying a player instance that will not stop retrying |
| Filling a React-controlled input so React's own state actually changes |
| Scrolling back through a virtualised message list and extracting it |
| The smallest possible |
They take their configuration through __PLACEHOLDER__ substitution before
being sent, because put ships a script rather than calling a function and has
no way to pass arguments.
Each one is the BODY of an async function, not a standalone script: the bridge
wraps what you send. That is why most end in a top-level return, and one of
them — scrape-discord-messages.js — also uses a top-level await; both are
valid where these actually run. node --check parses each file in the
CommonJS wrapper, which permits the top-level return, but it rejects the
top-level await — so exactly that one file fails the syntax check.
Development
The version string lives in several places across the extension, the dashboard
and the CLI package. python3 scripts/check_versions.py is the list — it names
every site and reports how many it found, which is why this paragraph does not
restate a count that would go stale the next time one is added.
Bump them together, never by hand:
python3 scripts/check_versions.py --set 0.18.0 # rewrite every site
python3 scripts/check_versions.py # verify the working tree.githooks/pre-commit checks the index and .githooks/pre-push checks each pushed
commit, so a half-bump can't land. Every clone must opt in once — git won't run
hooks out of a tracked directory otherwise:
git config core.hooksPath .githooksIf the browser loads the extension from a different checkout than the one you edit, install the hooks there too — and remember that reloading the extension re-reads the files on the browser's side, not yours.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Hosted real Google Chrome MCP with per-user persistent state. Navigate, click, type, screenshot.
Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.
Access Kernel's cloud-based browsers and app actions via MCP (remote HTTP + OAuth).
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Nitjsefnie-Harness-Commons/daedalus'
If you have feedback or need assistance with the MCP directory API, please join our Discord server