Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Who an MCP request is from

mcpx’s own MCP server passes every call on to an upstream server, and which instance of that server answers depends on its scope: one per session, per cwd, per pid, per repository, or per call. So every request has to answer the question “who is asking?”, and the answer has to come from the transport, not from the process.

What went wrong

The backend used to ask the process. mcpSession() returned $MCPX_SESSION_ID or mcp-<pid>, and callContext() filled in the process’s cwd and pid. That is right for mcpx serve, where one process serves one client over stdio. It is wrong inside the daemon, which serves /mcp for every HTTP client from one process: every client, every Mcp-Session-Id, resolved to session:mcp-<daemonpid>, cwd:<daemon's cwd> and pid:<daemonpid>. Two agents talking to one daemon shared one browser, one REPL, one set of cookies, and saw each other’s mcpx_exec artifacts in resources/list (conflict #2 in the WP7 record). Over stdio the same test gave separate instances, so the two transports disagreed about what scope: session means.

What mcpx does now

mcpserver resolves an Identity for each request from the connection it arrived on, and puts it in the request’s context (internal/mcpserver/identity.go). The backend builds the daemon call context from that (mcpCaller in internal/cli/serve.go):

request arrives onidentitysessioncwd / pid
stdio (mcpx serve)the process$MCPX_SESSION_ID, else mcp-<pid>the process’s
a legacy Streamable HTTP sessionits Mcp-Session-Idmcp-<session id>unknown
any request carrying _meta["dev.mcpx/session"]that namethe name, verbatimunknown
any request carrying _meta["ai.opencode/sessionID"] (and no dev.mcpx/session)that idthe id, verbatimunknown
a 2026-07-28 request without it, or a legacy POST outside a sessionnonenone: a fresh call idunknown

The rows are in precedence order except that an explicit session key wins over the connection’s own identity. When a request carries more than one listed key, the first key in transport.sessionMetaKeys order whose value is a non-empty string wins.

Why cwd and pid are unknown over HTTP

The daemon’s cwd and pid are not the client’s, and the transport carries neither. A scope that needs one (cwd, repo, worktree, pid) degrades to per-call and logs the degradation once, which is the direction that can’t leak one client’s state into another. A client’s roots would be a better source for cwd. That needs a roots/list round trip that mcpx doesn’t make yet.

Why a modern request gets no identity

2026-07-28 has no sessions. The streamable-http page tells a server to ignore Mcp-Session-Id and not to mint one, so no connection-level identity exists. mcpx doesn’t invent one: an unnamed request is its own scope, which for a session-scoped upstream means a new instance per request.

The session keys: transport.sessionMetaKeys

For a client that wants continuity, such as an agent host that keeps one browser across its calls, mcpx reads a session name (a string) from any request of any revision, under the keys listed in the transport.sessionMetaKeys setting, in order. The default list is dev.mcpx/session — mcpx’s own key, which uses mcpx’s prefix because the specification reserves io.modelcontextprotocol/ and defines no session key of its own — followed by ai.opencode/sessionID, the key OpenCode v2 sends on every tools/call, so each OpenCode session is scoped as its own session instead of sharing the daemon’s. A deployment can replace the list (dropping either key, or adding a host’s own) in configuration; an empty or non-string value is ignored, as if the key were absent. The value is used verbatim as the session id, so a client can join the same session a CLI user names with --session or $MCPX_SESSION_ID. That is deliberate. Anyone who can reach the daemon can already run any tool as its user (see the warning the daemon prints for a non-loopback address), so a session name is a label and grants no authority.

ai.opencode/sessionID and transport.sessionMetaKeys

opencode v2 puts _meta["ai.opencode/sessionID"] on every tools/call, including calls made from inside its code mode, so a client that never injects MCPX_SESSION_ID can still get one stateful scope per opencode session. mcpx reads a precedence list of _meta keys, transport.sessionMetaKeys (default dev.mcpx/session,ai.opencode/sessionID): the first key present on a request wins, and an empty value counts as absent. dev.mcpx/session is first so a name chosen for mcpx beats the id a host attaches automatically; a client that sends only the opencode key is keyed by it verbatim, exactly as it would be by dev.mcpx/session. The setting is hot: a change applies to the next request, not the next daemon start. Send a key outside the list and it is ignored, which is the documented fallback rather than an error.

What it touches

Identity reaches every backend path that picks an instance: mcpx_call, mcpx_exec (its artifact session), resources/read, prompts/get, completion/complete, the artifact listing in resources/list, and the ask path (/v1/ask with the same context). A caller with no identity lists no artifacts, because the daemon reads an empty session as “everyone’s”.

The tests are 2025-11-25/transport/http-mcp-clients-do-not-share-scoped-instances, 2026-07-28/transport/client-named-session-is-kept-across-requests, 2026-07-28/transport/unnamed-requests-are-their-own-scope (e2e, against a session-scoped fakemcp), and the TestEachConnectionHasItsOwnIdentity cases in internal/mcpserver/wp11_test.go.