unsloth/studio/frontend
Nilay 601155114d
Studio: persistent stdio MCP sessions so server state survives across tool calls (#7080)
* Studio: persistent stdio MCP sessions so server state survives across tool calls

call_tool_sync spawned a fresh stdio subprocess per tool call
(keep_alive=False) and tore it down when the call returned, so any stateful
MCP server lost its state between calls: with @playwright/mcp,
browser_navigate opened the page in one subprocess and
browser_take_screenshot ran in a brand-new one, screenshotting about:blank.

Keep one connected client per (command, env) on a dedicated event-loop
thread and reuse it across calls:

- idle sessions are reaped after 5 minutes (in-flight calls excluded) and
  everything closes at exit, preserving the old design's no-orphans property
- a dead subprocess is detected via is_connected() and retried once on a
  fresh session; tool-level errors leave the session alone
- cancel and timeout semantics are unchanged, and a timed-out call does not
  tear the session down
- updating a server's endpoint/env/enabled state or deleting it closes its
  live session
- HTTP/SSE servers stay one-shot per call

* address review feedback

* fix stdio session cleanup

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* address review: per-thread MCP scope, close-during-connect and abort races

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* address review: unblock no-limit calls on close, drain borrowers before close, scope closes to url+env

* don't retry sessions closed by config changes, re-verify server row before caching, keep env secrets out of generation keys

* fail fast on connect errors and make the stdio key-lock wait cancellable

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* quote MCP scope parts so IDs with colons can't collide

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* serialize per-session stdio calls, span one timeout budget across connect and call, hash urls in generation keys

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Harden persistent stdio MCP sessions: crash recovery, concurrency, scoping

- Evict a stdio session on any transport-level (non-ToolError) call failure and
  do not replay it, so a mid-call subprocess crash can no longer poison the scope.
  Never gate liveness on Client.is_connected() (it only reports that a session
  object exists, not that the subprocess is alive); add a version-adaptive
  dead-transport probe that works on fastmcp 3.0.2 and newer.
- Re-check closed/defunct/config and transport liveness after acquiring the call
  lock, and retire a session before releasing the lock, so a queued same-scope
  caller never reuses a session that another caller's timeout already retired.
- Force a ProactorEventLoop on Windows so the stdio transport can always spawn
  subprocesses regardless of the active event-loop policy.
- Scope stdio sessions per conversation: require thread_id to persist, and tag
  the fields so a session_id and a thread_id with the same value cannot collide.
  A session_id alone is project-wide, so it now falls back to a safe one-shot
  session instead of sharing browser/DB/REPL state across conversations.
- Forward thread_id on the Anthropic Messages path.
- Treat timeout=None as unlimited on connect and the key lock (was capped at 60s).
- Bound the session cache (default 32, override via
  UNSLOTH_STUDIO_MAX_STDIO_MCP_SESSIONS) with LRU eviction of idle sessions.
- Run config_check on cache hits, and log a redacted exe#digest label instead of
  the raw command so credentials in argv never reach the logs.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Trim the stdio MCP session cache on release and skip close-generation for HTTP servers

Two fixes from review of the persistent stdio session lifecycle:

- Re-enforce the session cap when a session goes idle. A concurrent burst of
  distinct-scope calls can overshoot the cap while every cached session is busy
  (insert-time eviction only reclaims idle sessions), and the overshoot used to
  persist until the 5-minute idle reaper. _release_stdio_session now trims the
  idle overshoot back within the cap, without ever evicting an in-flight call.
- close_stdio_sessions() now no-ops for a specific non-stdio (HTTP/SSE) url.
  Those transports are never cached as stdio sessions, so calling it on every
  HTTP server update or delete used to accrue an unbounded close-generation entry.

Both are covered by regression tests that fail before the change and pass after.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Keep the live stdio MCP session across a display-name rename

The edit dialog resends url, headers, and use_oauth unchanged whenever a
server is saved, so gating the tool-cache invalidation and stdio session
close on field presence dropped the persistent process on a plain rename
or any no-op edit. Gate on a real value change against the stored row so
only a genuine endpoint, auth, or enable change closes the session.

Regression tests: a rename that resends unchanged url/headers/oauth keeps
the session; a real command change still closes it.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten comments in the stdio MCP session lifecycle

Collapse a few verbose comments to fewer lines with the wording preserved,
and drop one that restated the clear_oauth_tokens_async docstring. Comments
only; no code change.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-14 02:28:43 -07:00
..
public chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
src Studio: persistent stdio MCP sessions so server state survives across tool calls (#7080) 2026-07-14 02:28:43 -07:00
.gitignore perf(studio): upgrade to Vite 8 + auto-install bun for faster frontend builds (#4522) 2026-03-25 04:27:41 -07:00
.gitkeep add studio root folder 2026-02-02 09:14:35 +00:00
.npmrc Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491) (#6663) 2026-06-25 04:01:43 -07:00
biome.json feat: add seed dataset support with configuration, preview, and builder utilities 2026-02-14 18:44:38 +01:00
components.json add studio root folder 2026-02-02 09:14:35 +00:00
data-designer.openapi (1).yaml save and import, and fixes 2026-02-04 14:32:49 +01:00
eslint.config.js Final cleanup 2026-03-12 18:28:04 +00:00
index.html chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
package-lock.json fix: CVE-2026-54290 security vulnerability (#6736) 2026-06-29 15:28:06 +01:00
package.json fix: CVE-2026-54290 security vulnerability (#6736) 2026-06-29 15:28:06 +01:00
tsconfig.app.json chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
tsconfig.json chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
tsconfig.node.json chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
vite.config.ts Fix Install commands for Windows + 1 line installs (#4447) 2026-03-19 02:09:09 -07:00