Repository navigation
fix(dashboard): read the global mcp.json off the event loop - #24093
KiroCrewCaptain merged 1 commit into
Conversation
The background MCP probe and the GET /api/mcp, GET /api/mcp/active
and POST /api/mcp/probe routes each read ~/.kiro/settings/mcp.json
with a bare read_text() inside the async def. A slow or network home
directory stalled every session while the MCP panel loaded or a
probe ran.
One sync helper, _read_global_mcps(), now does the read, parse and
.get("mcpServers", {}) with the same FileNotFoundError /
JSONDecodeError fallback, and all four sites await it through
asyncio.to_thread. The sync-io baseline entry for mcp_probe.py is
gone.
Closes kirodotdev#24092
UX Review (Opus 5.5, fork) — ⏭️ skippedRevision |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks. This is a mechanism-level fix of a baselined sync-io debt. The seven remaining What this change shipsInventory (4 items) — 4 justifiedIntent: keep a slow home directory from stalling every session while the MCP panel loads or a probe runs. This is a FIX.
[FIRST-PRINCIPLES-REVIEWED] e74f94b Description read[DESCRIPTION-READ] 934cda65c85c135f74e038e3bdf4c41b48fb79f4982a571449dc164b30134f30 That sha256 names the PR description this verdict read. Recompute it with |
Design Review (Opus 5.5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS No design-level concerns. The change moves one blocking read into a worker thread, the same way the store read next to it already works, and it keeps the same fallback. [DESIGN-REVIEWED] e74f94b Description read[DESCRIPTION-READ] 3e5f863f8ae3fbfcec46b3244bf87ff542cca6cfc3236f992c90240dcc288cca That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
GPT 6.1 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
KiroCrewCaptain
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (4 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, security checklist all-NO, no reviewer-lane findings, readiness signal fresh for this head SHA, declared category matches the diff. Category: bug fix that moves the four reads of the global mcp.json in the MCP probe and listing handlers into one helper called through a worker thread, drops the file from the sync-I/O baseline, and adds a test.
Problem / Motivation
Goal: Loading the MCP panel or running an MCP probe no longer stalls every other session while
~/.kiro/settings/mcp.jsonis read.The background probe (
_run_mcp_probe) and theGET /api/mcp,GET /api/mcp/activeandPOST /api/mcp/proberoutes insrc/kiro_crew/dashboard/handlers/mcp_probe.pyeach read the Kiro-globalmcp.jsonwith a bareread_text()inside theasync def. On a slow or network-mounted home directory every session's turn waits on that read, and a long stall trips the loop watchdog (#3057, #1572). The sync-io gate lists all four (4 src/kiro_crew/dashboard/handlers/mcp_probe.py).Why it matters
GET /api/mcpruns on every MCP panel load, so the stall hits anyone who opens the panel, not only people who probe. The Kiro Crew store read on the next line already runs in a thread; only the global file was left on the loop.Not a goal
handlers/mcp.py(toggle, remove, sync, detail). They read the same file under_get_mcp_lock(), and moving them means threading the whole read-modify-write, not one read.PermissionErroron these reads (dashboard: unlocked config-file GET handlers return HTTP 500 on Windows when a read lands in a writer's atomic replace (PermissionError) #15554). The exception set stays exactly as it was.What changed (motivation → approach → change)
The global file was read on the loop at four sites, while the store read beside each already used
asyncio.to_thread. A new sync helper,_read_global_mcps(), does the read and the parse and returnsmcpServers, falling back to{}on the sameFileNotFoundError/JSONDecodeErroras before. All four sites nowawait asyncio.to_thread(_read_global_mcps). The helper is re-exported from thehandlers/mcp.pyfacade like the module's other names. Themcp_probe.pyline is removed from.github/sync-io-in-async-fs-baseline.txtwith--update-baseline.Backwards compatibility
Compatible: the four routes return the same payloads, and a missing or unparsable file still reads as
{}. Only the thread doing the read changes.Removes nothing: the only removal is a sync-io baseline line for calls that no longer exist.
Tests
test/test_mcp_global_read_off_loop.pyswaps_GLOBAL_MCP_JSONfor a file that records the thread that read it. It drives each of the four entry points and asserts that the read happened and did not run on the loop thread. It is modelled ontest/test_mcp_probe_spawn_off_loop.py.Proof: PROVEN test/test_mcp_global_read_off_loop.py::test_global_mcp_json_is_read_off_the_loop_thread[_run_mcp_probe], test/test_mcp_global_read_off_loop.py::test_global_mcp_json_is_read_off_the_loop_thread[api_mcp_servers], test/test_mcp_global_read_off_loop.py::test_global_mcp_json_is_read_off_the_loop_thread[api_mcp_active], test/test_mcp_global_read_off_loop.py::test_global_mcp_json_is_read_off_the_loop_thread[api_mcp_probe] (head e74f94b)
Local: 536 passed across the MCP handler, probe, quarantine, facade and sync-io gate suites.
check_sync_io_in_async.py, black, isort, flake8,mypy src/kiro_crew, the comment-history and brand gates, anddocs-lint.shall pass.Manual verification
N/A: unit coverage is sufficient. The change only moves a file read into a worker thread, and the test pins the thread for every entry point.
Related Issues
Closes #24092
Sibling sweep
Swept: git grep -n "_GLOBAL_MCP_JSON.read_text" -- src/ (classified by enclosing function with ast)
Sibling: src/kiro_crew/dashboard/handlers/mcp_probe.py -- changed here
Sibling: src/kiro_crew/dashboard/handlers/mcp.py -- not affected -- read-modify-writes under _get_mcp_lock (api_mcp_sync/toggle/toggle_tool/toggle_all/remove/server_detail); fixing them means threading the whole RMW, and they stay in the sync-io baseline (8)
Sibling: src/kiro_crew/dashboard/handlers/mcp_agent_sync.py -- not affected -- reads are inside sync helpers (_sync_mcp_to_agent_unlocked, _sync_mcp_to_agent_batch_unlocked), not an async def; the gate does not list them
Pattern harvest
Not generalizable: the sync-io-in-async gate and its baseline already catch this class; this PR only works one baselined file down.
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)