Summary
The MCP list_prs, get_pr_impact, and triage_prs tools return their failure messages ("gh CLI not found or not authenticated", "PR #N not found") as ordinary text content with isError left unset (i.e. false). A client that checks the isError field to distinguish failure from success reads a genuine failure as a successful tool call.
Steps to reproduce
- Run the MCP server without
gh installed / authenticated.
- Call
list_prs (or get_pr_impact({"pr_number": 1})).
- Inspect the tool result. The
content carries the error text, but the result has isError: false (or isError absent) — it looks like a success.
Expected behavior
A failed tool call — gh missing / not authenticated, or a PR that cannot be resolved — should return a result with isError: true, so clients can tell a real failure from a valid result. (Input-validation errors on other tools already return isError: true; the PR tools are inconsistent with that.)
Actual behavior
isError is never set. Under the hood graphify.prs.fetch_prs raises RuntimeError, and the tool handler turns it into an ordinary string:
_tool_list_prs / _tool_triage_prs (graphify/serve.py): except RuntimeError as e: return f"Error: {e}"
_tool_get_pr_impact: if pr_data is None: return f"PR #{number} not found …"
call_tool then wraps the string as a normal TextContent, and the transport returns CallToolResult(content=…) with no isError. So the MCP contract shows success for every one of these failures.
Root cause
The PR tool handlers return error strings instead of signalling an error, and the dispatch layer (call_tool / the HTTP _on_call_tool) never sets isError.
Fix
Introduce an error signal a handler raises (e.g. a ToolError) for the gh-missing / unresolved-PR cases; propagate it through call_tool so the mcp 1.x decorator wraps it as an error result, and have the 2.x _on_call_tool return CallToolResult(content=…, isError=True). Normal not-found text (missing node/community) and a PR that exists but touched no files stay isError: false.
Summary
The MCP
list_prs,get_pr_impact, andtriage_prstools return their failure messages ("gh CLI not found or not authenticated", "PR #N not found") as ordinary text content withisErrorleft unset (i.e. false). A client that checks theisErrorfield to distinguish failure from success reads a genuine failure as a successful tool call.Steps to reproduce
ghinstalled / authenticated.list_prs(orget_pr_impact({"pr_number": 1})).contentcarries the error text, but the result hasisError: false(orisErrorabsent) — it looks like a success.Expected behavior
A failed tool call —
ghmissing / not authenticated, or a PR that cannot be resolved — should return a result withisError: true, so clients can tell a real failure from a valid result. (Input-validation errors on other tools already returnisError: true; the PR tools are inconsistent with that.)Actual behavior
isErroris never set. Under the hoodgraphify.prs.fetch_prsraisesRuntimeError, and the tool handler turns it into an ordinary string:_tool_list_prs/_tool_triage_prs(graphify/serve.py):except RuntimeError as e: return f"Error: {e}"_tool_get_pr_impact:if pr_data is None: return f"PR #{number} not found …"call_toolthen wraps the string as a normalTextContent, and the transport returnsCallToolResult(content=…)with noisError. So the MCP contract shows success for every one of these failures.Root cause
The PR tool handlers return error strings instead of signalling an error, and the dispatch layer (
call_tool/ the HTTP_on_call_tool) never setsisError.Fix
Introduce an error signal a handler raises (e.g. a
ToolError) for the gh-missing / unresolved-PR cases; propagate it throughcall_toolso the mcp 1.x decorator wraps it as an error result, and have the 2.x_on_call_toolreturnCallToolResult(content=…, isError=True). Normal not-found text (missing node/community) and a PR that exists but touched no files stayisError: false.