Repository navigation
feat(web): list and terminate active backend sessions - #2901
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Terminating idle attached sessions leaves event streams open and retains connections and session references, while overlapping refreshes can erase termination errors.
2 open issues
What changed in this PR
Adds a web operator view for finding and terminating backend MCP sessions without restarting Inspector, complementing the detach-grace cleanup from #2824.
Changes:
- Adds authenticated session-list and termination endpoints with session metadata.
- Passes server display names to the backend and adds an Active Sessions modal with refresh and termination controls.
- Adds unit, integration, and Storybook coverage for the new behavior.
| File | Description |
|---|---|
core/mcp/remote/types.ts |
Defines session summaries and optional server labels. |
core/mcp/remote/remoteClientTransport.ts |
Sends server labels when connecting. |
core/mcp/remote/node/server.ts |
Adds listing, termination, and label sanitization. |
core/mcp/remote/node/remote-session.ts |
Tracks session metadata and consumer timestamps. |
core/mcp/remote/index.ts |
Exports session-list types. |
core/mcp/remote/createRemoteTransport.ts |
Forwards server labels to transports. |
clients/web/src/utils/sessionAge.ts |
Formats session ages. |
clients/web/src/utils/sessionAge.test.ts |
Tests age formatting. |
clients/web/src/test/integration/mcp/remote/remote-session.test.ts |
Tests summaries and consumer bookkeeping. |
clients/web/src/test/integration/mcp/remote/active-sessions-route.test.ts |
Tests authentication, listing, and process termination. |
clients/web/src/test/core/mcp/remote/remoteClientTransport.test.ts |
Tests connect-body labels. |
clients/web/src/lib/environmentFactory.ts |
Accepts and forwards server names. |
clients/web/src/lib/environmentFactory.test.ts |
Tests server-name propagation. |
clients/web/src/lib/activeSessions.ts |
Implements authenticated session API calls. |
clients/web/src/lib/activeSessions.test.ts |
Tests requests and error handling. |
clients/web/src/hooks/useConnectionLifecycle.ts |
Supplies the selected server’s name. |
clients/web/src/hooks/useActiveSessions.ts |
Manages modal state and session requests. |
clients/web/src/hooks/useActiveSessions.test.tsx |
Tests loading, termination, and stale responses. |
clients/web/src/components/views/InspectorView/types.ts |
Adds the session-panel callback. |
clients/web/src/components/views/InspectorView/InspectorView.tsx |
Passes the callback to both header states. |
clients/web/src/components/views/InspectorView/InspectorView.test.tsx |
Updates test props. |
clients/web/src/components/views/InspectorView/InspectorView.stories.tsx |
Updates story props. |
clients/web/src/components/groups/ViewHeader/ViewHeader.tsx |
Adds the Active Sessions button. |
clients/web/src/components/groups/ViewHeader/ViewHeader.test.tsx |
Tests the new button callback. |
clients/web/src/components/groups/ViewHeader/ViewHeader.stories.tsx |
Wires the callback into header stories. |
clients/web/src/components/groups/ActiveSessionsModal/ActiveSessionsModal.tsx |
Displays session details and controls. |
clients/web/src/components/groups/ActiveSessionsModal/ActiveSessionsModal.test.tsx |
Tests modal states and interactions. |
clients/web/src/components/groups/ActiveSessionsModal/ActiveSessionsModal.stories.tsx |
Demonstrates populated, empty, and error states. |
clients/web/src/App.tsx |
Connects the hook, modal, and current-session marker. |
Review effort: Balanced
…visible Address Copilot review on #2901: terminating an attached session now flushes the termination event and closes its /api/mcp/events response instead of holding it until the browser aborts, and a failed terminate invalidates any in-flight list so it cannot clear the error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 1: 2 findings, both defects in this PR, both fixed in ef2aa51 (replies are in each thread).
|
There was a problem hiding this comment.
🟡 Changes recommended
Client aborts leave termination listeners registered, retaining completed streams and cleanup closures across reattachments; listener cleanup and regression coverage are needed.
1 open issue
2 resolved since last review
Review effort: Balanced
…visible Address Copilot review on #2901: terminating an attached session now flushes the termination event and closes its /api/mcp/events response instead of holding it until the browser aborts, and a failed terminate invalidates any in-flight list so it cannot clear the error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Address Copilot round 2 on #2901: a client abort now removes the stream's listener from the session's terminate signal, so reattaching tabs do not accumulate finished streams on a long-lived session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
ef2aa51 to
54d544d
Compare
|
Copilot round 2: 1 finding (Remove termination listener when the event stream aborts, Medium), a leak introduced by the round-1 fix. Fixed in 54d544d with a regression test; the reply is in its thread. Rebased onto current |
There was a problem hiding this comment.
🟡 Changes recommended
A critical abort-handling bug can skip stream closure and leave released unresolved when the client disconnects during termination’s pending flush, keeping the handler blocked on the write.
1 open issue
1 resolved since last review
Review effort: Balanced
Address Copilot round 3 on #2901: closing and releasing the stream is now its own exactly-once step, so a client abort that lands while a terminate is still flushing releases the handler instead of leaving it waiting on a write to a departed peer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 3: 1 finding (client abort during a terminate's pending flush left the handler unreleased), a defect in this PR's end path. Fixed in 68c7288 with a regression test; the reply is in its thread. |
Add GET /api/mcp/sessions (server name, transport, created, last consumer seen, queued events) and DELETE /api/mcp/sessions/:sessionId, both behind the existing x-mcp-remote-auth middleware. Terminating closes the session's transport (ending a stdio child) and tells an attached tab why. The web client labels each session with its server name on connect and adds an Active Sessions modal, opened from the header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
…visible Address Copilot review on #2901: terminating an attached session now flushes the termination event and closes its /api/mcp/events response instead of holding it until the browser aborts, and a failed terminate invalidates any in-flight list so it cannot clear the error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Address Copilot round 2 on #2901: a client abort now removes the stream's listener from the session's terminate signal, so reattaching tabs do not accumulate finished streams on a long-lived session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Address Copilot round 3 on #2901: closing and releasing the stream is now its own exactly-once step, so a client abort that lands while a terminate is still flushing releases the handler instead of leaving it waiting on a write to a departed peer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
Address Copilot round 4 on #2901: the pid match is anchored on the JSON string's closing quote so a pid split across chunks is never read short, and the terminate test waits for the termination message itself rather than an event line that can precede its data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 4: no inline comments, but the headline raised one point: chunk-boundary assumptions in the SSE test helper. That is a real defect in this PR's test, so it is fixed in 732f7c9. The pid match is now anchored on the JSON string's closing quote, so a pid split across chunks cannot be read short, and the terminate test waits for the termination message itself rather than the |
68c7288 to
732f7c9
Compare
|
Copilot review closed: round 5 was clean (no inline comments, no headline findings, nothing suppressed; "Approval recommended"). Every finding across rounds 1–4 was in scope and fixed, with a reply in its thread. |
- web: an Active sessions section with three screenshots from modelcontextprotocol/inspector#2901. It covers the header button, what each column and status means, Terminate, the 60-second detach grace that lets a session outlive its tab, and the GET/DELETE /api/mcp/sessions routes behind the session token. - load-testing: a Multiple generator processes section for the CLI's --load-workers (modelcontextprotocol/inspector#2914), which merged after the previous revision's source snapshot. It also adds the topic-table row, the summary's workers field, and why mcpdo has no equivalent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>


Closes #2831
What
A web-backend operator view of every live MCP session, so a session whose tab went away (and the stdio child behind it) can be found and ended without restarting the Inspector.
Backend (
core/mcp/remote/node)GET /api/mcp/sessions→{ sessions: RemoteSessionSummary[] }, oldest first:sessionId,serverName,transport,createdAt,consumerAttached,lastConsumerSeenAt(now while attached,nullif never),queuedEvents,transportDead.DELETE /api/mcp/sessions/:sessionId: 404 for an unknown session. Otherwise it forgets the session, cancels its detach grace, pushes atransport_errornaming why to a still-attached tab, rejects pending request waits, and closes the transport, which ends a stdio child. The order matches the Web: closing or reloading a connected tab leaks the backend session and its spawned stdio server process #2824 detach-grace reap.x-mcp-remote-authmiddleware (a test asserts 401 without the token).RemoteSessionrecords a descriptor (server name, transport),createdAt, and the last attach/detach time, and exposesdescribe().Client plumbing
RemoteConnectRequest.serverNameis an optional display label. The web client passesserver.namethroughcreateWebEnvironment→createRemoteTransport→ the connect body. The backend keeps only a non-blank string, trimmed and capped at 256 chars, and never acts on it.Web UI
ActiveSessionsModalwith one row per session: server, transport, status (This tab / Attached / Detached / Closed), ages, queued events, and Terminate. It has a refresh button and shows list or terminate errors.useActiveSessionsfetches on open, refresh and after a terminate, never from an effect. It drops out-of-order responses.lib/activeSessions.tsholds the two calls.utils/sessionAge.tsholds the age formatter (reusing core'sformatDuration).Screenshots (1280×900, prod build, real backend)
Sessions were seeded through the API: one from a tab that went away, one from another attached tab, one whose browser never opened its stream, and one crashed server. The page itself is connected via deep link ("This tab").
Header button:

Active Sessions modal:

After terminating "everything (other tab)":

Tests
active-sessions-route.test.ts(integration, real stdio child reporting its pid): auth required; empty list; descriptor and attach state; 404; terminating a detached session kills the child and drops it from the list; an attached tab receives the terminationtransport_error; name sanitization.remote-session.test.ts:describe()and last-consumer bookkeeping under fake timers.activeSessions,useActiveSessions,sessionAge,ActiveSessionsModal(plus stories), theViewHeaderbutton, andserverNamereaching the connect body (remoteClientTransport,environmentFactory).Gate
npm run local:gatepassed every stage throughsmoke:web:firefox. The finallocal:storybookstage then flagged an axeempty-table-headeron the actions column. The fix (aVisuallyHidden"Actions" label) is the last commit; that story file now passes, and lint, typecheck and unit tests were re-run.🤖 Generated with Claude Code