Skip to content

fix(serve): MCP orientation refreshes the strict hook's freshness stamp - #3042

Open
Azeem1985 wants to merge 1 commit into
Graphify-Labs:v8from
TelB-io:upstream/mcp-stamp-touch
Open

fix(serve): MCP orientation refreshes the strict hook's freshness stamp#3042
Azeem1985 wants to merge 1 commit into
Graphify-Labs:v8from
TelB-io:upstream/mcp-stamp-touch

Conversation

@Azeem1985

Copy link
Copy Markdown
Contributor

Fixes #3039.

The strict read guard admits an agent only while the "recently oriented" stamp (graphify-out/cache/last_query_stamp) is fresh; the CLI's query / path / explain each touch it, but their MCP twins never did — so an agent that oriented through the server was still treated as blind and had its first raw read denied (measured on a live deployment; details in the issue).

call_tool now touches the stamp after every successful call to one of the four orientation tools, chosen by strict parity with the CLI's stamping commands:

CLI (stamps) MCP twin stamps now
graphify query query_graph yes
graphify path shortest_path yes
graphify explain get_node + get_neighbors (explain's split) yes
graphify god-nodes (no stamp) god_nodes no
graph_stats, get_community, PR tools no

Two deliberate choices:

  • Stamp on any successful call, including "No node matching" answers — the CLI explain's no-stamp-on-no-match is an accident of its early sys.exit(0), not a contract; a no-match answer is still a graph consult, and the guard's premise ("asked the graph first") holds.
  • Stamp next to the graph that answered — a project_path call stamps THAT project's graphify-out/cache, the graph whose reads the guard gates. A failed call stamps nothing.

The stamp writer is the CLI's own fail-silent _touch_query_stamp (lazy import; cli imports serve only inside handlers, so no cycle).

Tests: stamp created by query_graph; each orientation tool refreshes an aged stamp (parametrized); browsing tools leave an aged stamp untouched; project_path stamps the serving project, not the default; failed call stamps nothing. serve/serve_http/hook_strict families green on top of v8 @ 0.9.49; ruff clean.

🤖 Generated with Claude Code

@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 2 advisory finding(s) below merit a look before merge.

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


Graphify review — findings

Makes the MCP orientation tools refresh the strict read hook's "recently oriented" stamp, matching the CLI, so an agent that consults the graph via query_graph, shortest_path, get_node, or get_neighbors isn't treated as blind and denied its next raw file read. The stamp is written after a successful call next to the graph that actually answered — so a project_path query stamps that project's graphify-out/cache — while browsing tools (graph_stats, god_nodes, get_community), PR tools, and failed calls stamp nothing. Stamping is fail-silent, so a stamp write failure never breaks the tool result.

Worth a look

  • Post-call stamp reads shared active_graph_path after another concurrent tool call may have rebound itgraphify/serve.py:2068 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Orientation stamp uses shared active_graph_path after tool executiongraphify/serve.py:2072 · 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 — 686 functions depend on the 361 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, 17 callees
  • new: _load_graph() — 9 callers, 3 callees
  • new: _query_subgraph_tokens() — 7 callers, 3 callees
  • …and 14 more — each is listed as a finding

Verification — 686 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: 525 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

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

Comment thread tests/test_serve_http.py
os.utime(stamp, (1000, 1000))


def test_mcp_query_graph_touches_orientation_stamp(tmp_path):

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 regressiontest_mcp_query_graph_touches_orientation_stamp()

fans out to 6 callees (efferent coupling).

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

Comment thread tests/test_serve_http.py
("shortest_path", {"source": "Alpha", "target": "Beta"}),
],
)
def test_mcp_orientation_tools_refresh_aged_stamp(tmp_path, tool, arguments):

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 regressiontest_mcp_orientation_tools_refresh_aged_stamp()

fans out to 7 callees (efferent coupling).

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

Comment thread tests/test_serve_http.py
("get_community", {"community_id": 0}),
],
)
def test_mcp_non_orientation_tools_leave_stamp_alone(tmp_path, tool, arguments):

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 regressiontest_mcp_non_orientation_tools_leave_stamp_alone()

fans out to 7 callees (efferent coupling).

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

Comment thread tests/test_serve_http.py
assert stamp.stat().st_mtime == 1000, f"{tool} unexpectedly refreshed the stamp"


def test_mcp_project_path_query_stamps_that_project(tmp_path):

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 regressiontest_mcp_project_path_query_stamps_that_project()

fans out to 8 callees (efferent coupling).

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

Comment thread tests/test_serve_http.py
assert not _stamp_path(default_graph).exists()


def test_mcp_failed_call_does_not_stamp(tmp_path):

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 regressiontest_mcp_failed_call_does_not_stamp()

fans out to 6 callees (efferent coupling).

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

@Azeem1985

Copy link
Copy Markdown
Contributor Author

Thanks for the grounded review. The flagged fan-out is in the end-to-end test functions — each deliberately exercises the full server lifecycle (spawn, handshake, call, stat the stamp/cache) inline so the test reads as one narrative; happy to factor the shared steps into a fixture if maintainers prefer lower per-test fan-out. No production-code findings noted. — Azeem1985/TelB-io fork

🤖 Generated with Claude Code

The strict read guard admits an agent only while the 'recently oriented'
stamp (graphify-out/cache/last_query_stamp) is fresh. The CLI's query /
path / explain each touch it (cli._touch_query_stamp), but their MCP
twins never did — an agent that oriented through the server via
query_graph was still treated as blind and had its first raw read
denied.

call_tool now touches the stamp after every successful call to one of
the four orientation tools: query_graph (= query), shortest_path
(= path), get_node + get_neighbors (the MCP split of explain). The
stamp lands next to the graph that answered, so a project_path call
stamps THAT project's graphify-out/cache — the graph whose reads the
guard gates. Browsing tools (graph_stats, god_nodes, get_community) and
the PR tools don't stamp on the CLI and still don't over MCP; a failed
call stamps nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Azeem1985
Azeem1985 force-pushed the upstream/mcp-stamp-touch branch from fd76abc to 4c3c76b Compare August 25, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant