Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Mark failed PR tool calls as isError instead of successful text - #3163

Closed
noQbot wants to merge 1 commit into
Graphify-Labs:v8from
VinvAI:fix-pr-tools-iserror
Closed

Mark failed PR tool calls as isError instead of successful text#3163
noQbot wants to merge 1 commit into
Graphify-Labs:v8from
VinvAI:fix-pr-tools-iserror

Conversation

@noQbot

@noQbot noQbot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Mark failed MCP PR tool calls as errors instead of returning the failure message as a successful text result. list_prs, triage_prs, and get_pr_impact returned "gh CLI not found or not authenticated" / "PR #N not found" as ordinary text with isError unset, so a client that checks isError read a genuine failure as success.

Closes #3162.

What changed

  • Added a module-level ToolError exception that a tool handler raises to signal an error result.
  • The PR tools now raise it for the failure cases: list_prs / triage_prs on the gh-missing RuntimeError, and get_pr_impact when the PR can't be resolved (pr_data is None).
  • The dispatch propagates it: call_tool re-raises ToolError (the mcp 1.x decorator wraps a raised exception into an error result), and the 2.x _on_call_tool catches it and returns CallToolResult(content=…, isError=True).

Normal not-found text (missing node/community) and get_pr_impact's "no changed files" empty result are unchanged, so only genuine failures flip isError.

Why

The PR tools were inconsistent with the rest of the server: input-validation errors already surface as isError: true, but a runtime failure inside these handlers (gh missing / unresolved PR) was swallowed into an ordinary text result.
A client relying on isError to branch on failure would treat those as success.

Testing

  • New HTTP test: list_prs with fetch_prs mocked to fail returns isError:true and the gh message.
  • New HTTP test: a normal graph_stats result is not isError.
  • Full serve/PR suite passes: test_serve.py, test_serve_http.py, test_prs.py (240 tests). ruff clean.

Notes

  • I extended the fix to triage_prs as well — it had the identical return f"Error: {e}" bug — so all three PR tools behave consistently. Happy to narrow to list_prs/get_pr_impact if you'd prefer.
  • Root cause and fix were identified using Cursor (Composer 2.5) + Vinv AI

list_prs, triage_prs, and get_pr_impact returned their failure messages ('gh
CLI not found or not authenticated', 'PR #N not found') as ordinary text with
isError unset, so an MCP client that only checks isError read a genuine failure
as success (ADR-0001 finding 4).

Add a ToolError exception a handler raises to signal an error result. The PR
tools raise it for the gh-missing / unresolved-PR cases. call_tool re-raises it
(the mcp 1.x decorator wraps a raised exception into an error result) and the
2.x _on_call_tool catches it and returns CallToolResult(..., isError=True).

Normal not-found text (missing node/community) and get_pr_impact's 'no changed
files' empty result are unchanged, so only genuine failures flip isError.

Tests: a PR tool with gh mocked to fail returns isError:true; a normal tool
result is not isError.

Co-Authored-By: Vinv-AI <[email protected]>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds a ToolError exception that PR tool handlers raise when gh is missing/unauthenticated or a PR can't be resolved, so those cases surface as tool results with isError: true instead of being returned as ordinary successful text; the _on_call_tool (mcp 2.x) path catches it into a CallToolResult with isError=True, while the mcp 1.x path re-raises so its decorator marks the error. Normal string returns (including benign "no changed files" cases) stay successful results. Tests cover both the failing PR path setting isError and a successful tool result staying unmarked.

Worth a look

  • Test monkeypatches the wrong fetch_prs referencetests/test_serve_http.py:371 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 491 functions depend on the 160 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 122 callees
  • new: _query_graph_text() — 20 callers, 9 callees
  • new: _score_query() — 15 callers, 5 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: run_benchmark() — 16 callers, 3 callees
  • new: _build_server() — 2 callers, 16 callees
  • new: _load_graph() — 9 callers, 3 callees
  • new: _query_subgraph_tokens() — 7 callers, 3 callees
  • …and 9 more — each is listed as a finding

Verification — 491 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 324 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous

· 1 grounded finding(s) anchored inline below; 16 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py
"""


def _load_graph(graph_path: str) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_load_graph()

9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@safishamsi

Copy link
Copy Markdown
Collaborator

Shipped in v0.9.52 via authorship-preserving cherry-pick so you keep contributor-graph credit. Thanks @noQbot! Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.52

@safishamsi safishamsi closed this Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP PR tools report gh/auth failures as successful results (isError unset)

2 participants