fix: resolve stale MCP runtime EOF follow-ups (#531)
- fail closed when runtime diagnostics fail unexpectedly - move MCP connection healthcheck under scripts/ - document #543 as the remaining IDE namespace proof - keep namespace binding test isolated from mocked git metadata
This commit is contained in:
@@ -9,3 +9,4 @@ Pending architectural and workflow decisions:
|
|||||||
- **Operation-scoped role selection (#228)** — Move from launcher-profile-dependent routing to per-operation profile resolution.
|
- **Operation-scoped role selection (#228)** — Move from launcher-profile-dependent routing to per-operation profile resolution.
|
||||||
- **Gitea-Tools wiki for dadeschools remote** — This bootstrap covers `prgs`; dadeschools instance wiki parity is undecided.
|
- **Gitea-Tools wiki for dadeschools remote** — This bootstrap covers `prgs`; dadeschools instance wiki parity is undecided.
|
||||||
- **Server-side self-merge block** — Complement tool gates with Gitea branch protection where available.
|
- **Server-side self-merge block** — Complement tool gates with Gitea branch protection where available.
|
||||||
|
- **IDE-namespace callable-tool proof (#543)** — Remaining follow-up. PR #545 (stale MCP runtime race-control) adds runtime staleness guards before mutations and improves diagnostics, but does not include a full cross-namespace callable tool proof for IDE-spawned MCP sessions. Track completion of direct "callable from IDE namespace" verification under #543.
|
||||||
|
|||||||
+14
-4
@@ -694,8 +694,13 @@ def _verify_role_mutation_workspace(
|
|||||||
raise RuntimeError("; ".join(runtime_reasons))
|
raise RuntimeError("; ".join(runtime_reasons))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if "stale-runtime:" in str(exc):
|
if "stale-runtime:" in str(exc):
|
||||||
raise RuntimeError(str(exc))
|
raise RuntimeError(str(exc)) from exc
|
||||||
pass
|
# Fail-closed: unexpected error while performing stale-runtime guard must not be swallowed.
|
||||||
|
# Surface an actionable diagnostic instead of allowing a mutation under unknown runtime state.
|
||||||
|
raise RuntimeError(
|
||||||
|
f"stale-runtime: runtime diagnostic check failed unexpectedly ({type(exc).__name__}: {exc}). "
|
||||||
|
"Failing closed. Verify MCP server processes, profile configuration, and OS process visibility; retry after full MCP server restart."
|
||||||
|
) from exc
|
||||||
|
|
||||||
role = _effective_workspace_role()
|
role = _effective_workspace_role()
|
||||||
git_state = issue_lock_worktree.read_worktree_git_state(
|
git_state = issue_lock_worktree.read_worktree_git_state(
|
||||||
@@ -8338,7 +8343,12 @@ def gitea_route_task_session(
|
|||||||
|
|
||||||
|
|
||||||
def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) -> list[str]:
|
def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) -> list[str]:
|
||||||
"""Check running runtimes and return errors if they are missing or stale."""
|
"""Check running runtimes and return errors if they are missing or stale.
|
||||||
|
|
||||||
|
NOTE: IDE-namespace callable-tool proof for #543 is a documented follow-up
|
||||||
|
if this change does not fully close the gap for tools invoked from
|
||||||
|
IDE-launched MCP client namespaces. See docs/wiki/Open-Decisions.md.
|
||||||
|
"""
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -8407,7 +8417,7 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
|||||||
if self_stale:
|
if self_stale:
|
||||||
reasons.append(
|
reasons.append(
|
||||||
"stale-runtime: The active Gitea MCP server process is stale (running code from before changes were merged). "
|
"stale-runtime: The active Gitea MCP server process is stale (running code from before changes were merged). "
|
||||||
"Please fully restart the Gitea MCP server (e.g. touch /Users/jasonwalker/.gemini/config/mcp_config.json) and retry."
|
"Please fully restart the Gitea MCP server (for the affected profile) and retry."
|
||||||
)
|
)
|
||||||
|
|
||||||
if matching_profiles:
|
if matching_profiles:
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Live health-check script to verify Gitea MCP namespace connections.
|
"""Live health-check script to verify Gitea MCP namespace connections.
|
||||||
|
|
||||||
Spawns the MCP server processes as defined in the IDE's global config,
|
Spawns the MCP server processes as defined in a config file,
|
||||||
performs the JSON-RPC handshake, and queries the tools list to verify
|
performs the JSON-RPC handshake, and queries the tools list to verify
|
||||||
that the connection is fully operational and doesn't return EOF.
|
that the connection is fully operational and doesn't return EOF.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/test_mcp_conn.py [config.json]
|
||||||
|
|
||||||
|
Env:
|
||||||
|
GITEA_MCP_HEALTHCHECK_CONFIG Path to mcpServers JSON config (e.g. ~/.gemini/config/mcp_config.json)
|
||||||
|
|
||||||
|
The config file must contain an object with "mcpServers": { "gitea-author": {...}, ... }
|
||||||
|
Each server entry has "command", optional "args", optional "env".
|
||||||
"""
|
"""
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -103,12 +113,26 @@ def test_connection(name, config):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
config_path = "/Users/jasonwalker/.gemini/config/mcp_config.json"
|
parser = argparse.ArgumentParser(description="Gitea MCP connection health check")
|
||||||
|
parser.add_argument(
|
||||||
|
"config",
|
||||||
|
nargs="?",
|
||||||
|
default=os.environ.get("GITEA_MCP_HEALTHCHECK_CONFIG"),
|
||||||
|
help="Path to MCP config JSON with mcpServers (or set GITEA_MCP_HEALTHCHECK_CONFIG)"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config_path = args.config
|
||||||
|
if not config_path:
|
||||||
|
print("ERROR: No config path provided. Pass as argument or set GITEA_MCP_HEALTHCHECK_CONFIG.")
|
||||||
|
print("Example: GITEA_MCP_HEALTHCHECK_CONFIG=/path/to/mcp_config.json python scripts/test_mcp_conn.py")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_path) as f:
|
with open(config_path) as f:
|
||||||
mcp_config = json.load(f)
|
mcp_config = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to load mcp_config.json: {e}")
|
print(f"Failed to load config {config_path}: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
servers = mcp_config.get("mcpServers", {})
|
servers = mcp_config.get("mcpServers", {})
|
||||||
@@ -118,7 +142,7 @@ def main():
|
|||||||
if not test_connection(name, servers[name]):
|
if not test_connection(name, servers[name]):
|
||||||
failed = True
|
failed = True
|
||||||
else:
|
else:
|
||||||
print(f"Server '{name}' not found in mcp_config.json")
|
print(f"Server '{name}' not found in config")
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -127,3 +151,4 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
@@ -234,7 +234,20 @@ class TestNamespaceWorkspaceIntegration(unittest.TestCase):
|
|||||||
mock_run.return_value = MagicMock(returncode=0, stdout=f"{CONTROL_ROOT}/.git\n")
|
mock_run.return_value = MagicMock(returncode=0, stdout=f"{CONTROL_ROOT}/.git\n")
|
||||||
os.environ[nwb.AUTHOR_WORKTREE_ENV] = AUTHOR_DIRTY
|
os.environ[nwb.AUTHOR_WORKTREE_ENV] = AUTHOR_DIRTY
|
||||||
srv._preflight_resolved_role = "reviewer"
|
srv._preflight_resolved_role = "reviewer"
|
||||||
|
clean_git_state = {
|
||||||
|
"current_branch": "master",
|
||||||
|
"porcelain_status": "",
|
||||||
|
"head_sha": "abc123",
|
||||||
|
}
|
||||||
with mock.patch.object(srv, "PROJECT_ROOT", MCP_PROCESS_ROOT):
|
with mock.patch.object(srv, "PROJECT_ROOT", MCP_PROCESS_ROOT):
|
||||||
with mock.patch("gitea_mcp_server.get_profile", return_value=self._merger_profile()):
|
with mock.patch("gitea_mcp_server.get_profile", return_value=self._merger_profile()):
|
||||||
resolved = srv._verify_role_mutation_workspace("prgs")
|
with mock.patch(
|
||||||
|
"gitea_mcp_server.issue_lock_worktree.read_worktree_git_state",
|
||||||
|
return_value=clean_git_state,
|
||||||
|
):
|
||||||
|
with mock.patch(
|
||||||
|
"gitea_mcp_server.root_checkout_guard.resolve_remote_master_sha",
|
||||||
|
return_value="abc123",
|
||||||
|
):
|
||||||
|
resolved = srv._verify_role_mutation_workspace("prgs")
|
||||||
self.assertEqual(resolved, os.path.realpath(MCP_PROCESS_ROOT))
|
self.assertEqual(resolved, os.path.realpath(MCP_PROCESS_ROOT))
|
||||||
Reference in New Issue
Block a user