-
Notifications
You must be signed in to change notification settings - Fork 0
Sessions
Whether you pass session_id, and what happens when you get it wrong.
Nothing opens a browser implicitly. That is deliberate: open_session is the
only place a window size and the timeouts can be chosen, and hiding the call
hid the settings with it.
| Mode | When | The rule |
|---|---|---|
| saved | the server can identify you |
never pass session_id — it is not even advertised |
| stateless | it cannot, or you are on /browser/*
|
session_id is required on every call |
Using the wrong one fails every call the same way, so do not guess: read
session://current, which reports the mode and links the reference that
applies. Reading it never opens a browser — a null session_id means nothing is
held yet.
If your client cannot read MCP resources, the current_session tool returns the
same object. Add ?resources=off to make it visible.
Over the HTTP surface it is always session in, session out. That is its whole contract, so a workflow owns its session outright and can pass the id between steps.
It takes the first of these it finds, and never invents one — a caller it cannot identify is stateless, not quietly handed somebody's browser.
| Key | How it is set | How stable |
|---|---|---|
| a name you choose |
X-Session-Key header, else ?session=<name> on the MCP URL |
survives a client restart or reconnect |
| the MCP transport session | the negotiated Mcp-Session-Id
|
lasts the connection; a reconnect is a new key |
| stdio | one process, one client | lasts the process |
The header wins over the query parameter. That ordering is a permission boundary, not a preference: the header is set inside a credential, which an administrator controls, while the query parameter is written by whoever wires up the call. Leaving the header unset delegates the choice to the implementor.
n8n opens a new MCP transport for every tool call, so the negotiated id is never the same twice and a saved session keyed on it can never be found. Name the session in the URL. See Installing.
The Grid reaps idle browsers on its own timeout, so a remembered id can name one that is already gone. The next call notices, reopens, and returns to the page it was last on with the same settings — the refresh is invisible.
"The same settings" includes which browser it was. That is why the choice is
stored as a setting rather than beside them: a Firefox session that came back as
Chrome would be exactly the silent change of shape the stored settings exist to
prevent. session://current reports it as browser.
The session outlives the browsers it holds. session_id is empty when it holds
none — after the Grid reaps one, or an operator ends one from the admin UI — and
that is an ordinary state.
What survives is the context: the browser choice, the window, the last page. So the recovery is always the same one call:
open_session() # same browser, same window, back where you were
Which is also why you are never told "your browser was taken". You are told you have no browser, which is the branch you already handle.
Sessions themselves expire on SESSION_TTL, slid forward on every use — a day
by default. Nothing else removes one.
There is one distinction that matters here, because getting it wrong strands browsers. A session that fails to answer is not necessarily gone:
| The Grid says | Means |
|---|---|
200 |
healthy |
500 unexpected alert open |
alive, and blocked by a dialog — answer it with dialog
|
404 invalid session id |
genuinely reaped |
Only the last one counts as gone. Anything else, including a Grid that cannot be reached right now, is assumed alive: a wrong "alive" surfaces as a real error on the next call, while a wrong "dead" silently abandons a working browser and leaks its slot.
| Who owns it | Typical | |
|---|---|---|
| how long a browser lives | the Grid — SE_NODE_SESSION_TIMEOUT
|
300s idle |
| how long a caller is remembered | SESSION_TTL |
3600s, slid forward on every call |
| where that is remembered | SESSION_STORE |
memory, or redis to share it |
Nothing runs a cleanup loop, and nothing should — the Grid expires browsers, the store expires its own keys.
end_browser frees the slot, and slots are the scarce
resource: the Grid runs a handful of browsers in total and an abandoned one holds
its place until it is reaped. Close on failure paths too.
If you are mid-task and the caller may come back, leaving it open is the right call — just know you are holding capacity while you do.
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
Lifecycle
Going places
Doing things
Getting things out