Repository navigation
fix(dashboard): skip an unreadable global mcp.json during agent sync - #24187
KiroCrewCaptain merged 1 commit into
Conversation
UX Review (Opus 5.5, fork) — ⏭️ skippedRevision |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS It fixes the cause in the module it touches. Unfixed siblings are declared and deferred to #24093. Notes
What this change shipsInventory (3 items) — 3 justifiedIntent: an MCP install no longer answers 500 when the hand-edited global
[FIRST-PRINCIPLES-REVIEWED] 66dd8b6 Description read[DESCRIPTION-READ] cca88f5c4f5e106f70726f41da16bb5ad09e3e0cc7a8346c677a852856d03825 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 Nothing blocks: a narrow fix that sends two more read failures into the skip path that already existed. Notes
[DESIGN-REVIEWED] 66dd8b6 Description read[DESCRIPTION-READ] 115d66b4fa0edd5d82bbb5e25cdb9ba3246f2599856945d3e8e56786010aedcc 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 |
GPT 6.1 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
|
On the Design Review note about logging the skip: both reads now take the existing skip branch, which has never logged for a missing or malformed global |
The agent-spec sync copies a server's spec out of the user's hand-edited ~/.kiro/settings/mcp.json and means to skip the copy (or use an empty map in the batch form) when that file is unusable. It caught only FileNotFoundError and JSONDecodeError, so a file saved in a legacy code page (UnicodeDecodeError) or one the gateway may not read (PermissionError) escaped and failed the request, e.g. a capability MCP install answered 500 after the package was installed. Catch OSError and UnicodeDecodeError there too.
2ca0032 to
66dd8b6
Compare
KiroCrewCaptain
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 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: an unreadable or non-UTF-8 global mcp.json no longer fails the MCP install's agent-spec sync (closes #24186), with a test.
Problem / Motivation
Goal: An MCP install no longer fails when the user's global
mcp.jsonis not UTF-8 or not readable: the agent-spec sync skips that file, as it already does for a missing or malformed one.After the capability manager installs a server,
POST /api/capability/mcp/install(andPOST /api/mcp/discover/installfor the capability provider) runs_sync_mcp_to_agent(name, True), which copies the server's spec out of~/.kiro/settings/mcp.json. If that file was saved in a legacy code page, reading it raisesUnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9. If the gateway may not read it, reading raisesPermissionError. The endpoint answers500 {"error": "internal error"}for an install that already happened.Why it matters
~/.kiro/settings/mcp.jsonis a file people edit by hand, and on Windows Notepad's "ANSI" save is one accented character away from making it invalid UTF-8. The sync's ownexceptalready says what to do with a file it cannot use, which is skip it. Today only a missing or malformed file reaches that branch. Any other read error turns into a failed request after the earlier step has already taken effect.Not a goal
handlers/mcp.pyandhandlers/mcp_probe.py. They have the same narrowexcept, and the open fix(dashboard): read the global mcp.json off the event loop #24093 already edits those files. See Sibling sweep.kirocrew.jsonreads at the top of both sync functions (lines 119 and 303). That is our own file, and the user does not hand-edit it.What changed (motivation → approach → change)
An install fails when the global
mcp.jsoncannot be read as UTF-8 text. Root cause: both global-file reads indashboard/handlers/mcp_agent_sync.pycatch only(FileNotFoundError, json.JSONDecodeError).UnicodeDecodeErroris aValueErrorandPermissionErroris anOSErrorother thanFileNotFoundError, so neither reaches the skip branch. Change: both reads now catch(OSError, UnicodeDecodeError, json.JSONDecodeError)._sync_mcp_to_agent_unlocked, copy of one server's spec_sync_mcp_to_agent_batch_unlocked,gdatagdata = {}: servers already in the spec are still enabled, and servers that exist only in the global file are not copiedBackwards compatibility
Compatible: only exceptions that used to escape are caught now. A file that parsed before still parses the same way.
Removes nothing: no route, field, record or validator is removed or tightened.
Tests
Proof: PROVEN test/test_mcp_agent_sync_unreadable_global.py::test_single_sync_skips_the_copy[latin-1], test/test_mcp_agent_sync_unreadable_global.py::test_single_sync_skips_the_copy[permission-denied], test/test_mcp_agent_sync_unreadable_global.py::test_capability_install_answers_ok[latin-1], test/test_mcp_agent_sync_unreadable_global.py::test_capability_install_answers_ok[permission-denied], test/test_mcp_agent_sync_unreadable_global.py::test_batch_sync_still_syncs_the_servers_it_has[latin-1] (head 66dd8b6)
The new
test/test_mcp_agent_sync_unreadable_global.pyruns each test twice. One run uses a real latin-1mcp.json("Café"in an env value). The other uses a path whoseread_textraisesPermissionError. The sync calls are wrapped so that an escaping read error becomes apytest.fail, which makes the unfixed code fail on an assertion and not on a crash.test_single_sync_skips_the_copykirocrew.jsonunchanged and raises nothingtest_batch_sync_still_syncs_the_servers_it_hasdisabledand mounts a server that is already in the spec, and does not copy one that exists only in the unreadable filetest_capability_install_answers_okPOST /api/capability/mcp/installanswers 200 after the package installRed/green: with
mcp_agent_sync.pyreverted toorigin/main, all 6 fail. The sync tests fail withFailed: sync raised on an unreadable global mcp.json: UnicodeDecodeError(...)orPermissionError(13, ...), and the install test fails withAssertionError: {"error": "internal error", ...}. With the fix, all 6 pass.prove.py --per-hunkreverts each hunk on its own, and each revert is caught.Determinism: the tests use no clocks, sleeps or shared state, only
tmp_path. On the final head, the file passed 3/3 runs at-n 4.Local gates: black, isort, flake8, mypy (on the changed module),
check_black_formatting.py,check_sync_io_in_async.py,check_subprocess_encoding.py, comment-history, brand name,docs-lint.sh, the flake-pattern ratchet andsemgrep --config semgrep/(1.78.0, diff baseline: 0 findings) all pass. Inlocal-gate.py, the backend related set gives 124 passed. Its frontend lane could not start in this worktree becausenode_modulesis missing, and this diff has no frontend change.test_mcp_sync_agent.py,test_mcp_handler_facade.py,test_agent_home_isolation_derived_specs.pyandtest_secret_uri_launch_paths.pyplus the new file give 202 passed.Manual verification
N/A — unit coverage is sufficient. The tests call the real sync functions and the real install handler against real files in
tmp_path, and nothing else changed.Screenshots / video
N/A — no UI change.
Related Issues
Closes #24186
Sibling sweep
Swept:
git grep -nE "_GLOBAL_MCP_JSON\.read_text" -- src/Sibling: src/kiro_crew/dashboard/handlers/mcp_agent_sync.py -- changed here
Sibling: src/kiro_crew/dashboard/handlers/mcp.py -- not affected -- same gap at
:325,:526(toggle),:623,:697,:790,:880and:960, outside this PR's goal, and open #24093 edits this fileSibling: src/kiro_crew/dashboard/handlers/mcp_probe.py -- not affected -- same gap at
:374,:466,:600and:662, outside this PR's goal, and open #24093 edits this fileSibling: src/kiro_crew/dashboard/change_cards_readers.py:191 -- not affected -- already catches
(OSError, ValueError)The
mcp.pyandmcp_probe.pyreads are left for a follow-up once #24093 lands. The toggle (api_mcp_toggle,mcp.py:526) reads the file itself before it calls this sync, so a non-UTF-8 file still fails the toggle at that earlier read.Pattern harvest
Rule candidate: semgrep
Pattern:
<path>.read_text(encoding="utf-8")inside atrywhose handlers catchFileNotFoundErrorandjson.JSONDecodeErrorbut notOSErrorandUnicodeDecodeError(orValueError). For a user-edited file, both are the same "cannot use it" case. Onorigin/main,git grepfinds 12except (FileNotFoundError, json.JSONDecodeError)sites undersrc/kiro_crew/dashboard/handlers/, and 10 remain after this PR.Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)