-
Notifications
You must be signed in to change notification settings - Fork 0
Administration
The token, the admin UI, and the files a session leaves behind.
Setting MCP_AUTH_TOKEN turns on authentication for both surfaces at once.
Leave it unset and the server is open — correct for docker compose up, and the
reason a real deployment always sets one.
It is not only a credential. The same value is the signing key for file links and the event stream, so rotating it revokes every link already handed out. That is deliberate: it means there is one secret to manage rather than three, and one action that invalidates everything.
Any long random string works. What matters is that it survives being pasted into a client config, so keep it alphanumeric:
openssl rand -hex 32In Kubernetes, don't. Let External Secrets generate it so no value is ever authored, committed, or staged in a vault — see Deployment. To read the current value:
kubectl get secret selenium-flow-auth -n flow \
-o jsonpath='{.data.MCP_AUTH_TOKEN}' | base64 -dAuthorization: Bearer <token>
Both surfaces accept that. The HTTP endpoints additionally accept the bare token as the whole header value, for clients that cannot express a scheme.
GET /health and GET /openapi.yaml are always open. A kubelet has no token,
and a contract you must authenticate to read is needlessly awkward.
- Replace the secret. In Kubernetes, delete it and let the generator remake it:
kubectl delete secret selenium-flow-auth -n flow -
kubectl rollout restart deploy/selenium-flow -n flow—envFromdoes not reload, so the pod keeps the old value until it restarts. - Update every client. Anything holding the old token now gets a 401, and every signed file link stops working.
GET /admin — the browsers the Grid is running, what each has downloaded, and
the Grid's own console framed as a second tab.
There are no accounts. The sign-in box asks for the server's token, because
anyone holding it can already drive every browser through the API; a second
identity system would only be one more thing to get wrong. It is kept in
sessionStorage, so it does not outlive the tab you typed it into.
The Grid is the superset — it runs every browser on it, whoever asked. This server knows about the subset it opened, so each row says which it is:
| Row shows | Means |
|---|---|
| a name | opened through this server with ?session=<name> or X-Session-Key
|
| mcp client | opened through this server, keyed on a negotiated transport id |
| not opened through this server | somebody else's browser on the same Grid |
The name comes from a join against the session store, not from the Grid — the Grid has never heard of your names.
Each row is also marked with the browser it is running — 🟢 Chrome, 🦊 Firefox — read from the capabilities the Grid reports, so it is the browser actually running rather than the one that was asked for.
The list pushes its own updates over Server-Sent Events, so there is no refresh button. One poll loop on the server feeds every open page, rather than each tab polling on its own.
Every row carries an End button, and a session's detail view carries End session. Both quit the browser and give its Grid slot back immediately.
You do not have to. An abandoned session expires on its own: the Grid reaps it
after SE_NODE_SESSION_TIMEOUT seconds idle (300 by default), and where the
browser nodes autoscale, the node then scales back to zero. The button is for
not waiting out those minutes while a slot sits held — the Grid runs a handful
of browsers in total, so a stale one is capacity somebody else needs.
It is destructive and it asks first. Whatever the browser was holding — a login, a half-filled form, an unsaved page — is gone. A caller whose saved session you end is not stranded: its next call finds the browser missing and transparently reopens where it left off.
| Route | Auth | Returns |
|---|---|---|
GET /admin |
none — it is the sign-in form | the page |
GET /admin/sessions |
bearer | the list, plus a signed events_url
|
GET /admin/events |
signature | the list again, whenever it changes |
DELETE /admin/sessions/{id} |
bearer | ends that browser, freeing its slot |
| `GET | DELETE /admin/sessions/{id}/files` | bearer |
GET /files/{session}/{name} |
signature | the file itself |
Everything a session downloads is kept by the Grid, in a per-session store on the node beside the browser. It is created with the session and deleted with it, so there is nothing to clean up and nothing accumulating on this server's disk.
Two kinds of file land there, and they are not distinguished — to you they are the same thing, files this browsing session produced:
- whatever the site served to a download: a click on Export CSV, a PDF, an installer
- whatever you kept:
screenshot(save=true),save_pdf
This needs SE_NODE_ENABLE_MANAGED_DOWNLOADS=true on the Grid node. Without it
the browser downloads somewhere nothing can reach.
A file is often wanted somewhere that cannot present a token: an <img> tag, a
markdown image in a chat transcript, a link to a colleague. Browsers do not
attach an Authorization header to an image request, so those URLs are
authorised by signature instead:
/files/{session}/{name}?exp=1788931047&sig=f2235d5d072cea12…
The signature covers that exact path and that expiry, so a link grants one
file for an hour rather than access to the API, and a recipient cannot extend
their own link by editing the query. session_files and the admin UI both hand
these out already signed; nothing needs to construct one.
Three ways this is normally used:
- an agent that cannot display an image returns the link, and a chat that renders markdown shows the picture
- the admin UI uses them as
<img src>for thumbnails, and opens images and PDFs in a lightbox rather than a new tab - you paste one to somebody who has no token at all
Rotating MCP_AUTH_TOKEN invalidates all of them at once.
Files live and die with their session, which is right for a screenshot and wrong for anything you meant to keep. A WebDAV backend behind the same interface is the intended answer and is not built yet.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Guides
Lifecycle
Going places
Doing things
Getting things out