Skip to content

get_ag_health/get_store_query_stats move reading guidance off tools/list (#3898) - #4114

Merged
erikdarlingdata merged 5 commits into
devfrom
feature/3898-content-agstore
Sep 24, 2026
Merged

erikdarlingdata merged 5 commits into
devfrom
feature/3898-content-agstore

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Why

get_ag_health (1,529 served characters) and get_store_query_stats (984 served characters) each put a long,
guardrail-heavy description on every tools/list call. Both are Darling-only reads with no Lite twin. This PR
moves the reading guidance off the wire, into a short head plus a get_tool_guide tail, per #3898.

What changes

  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAgTools.cs: get_ag_health gets a 585-character head
    (616 served) followed by <<GUIDE>> and the original description, kept verbatim, as the tail.
  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpStoreQueryStatsTools.cs: get_store_query_stats gets
    a 583-character head (614 served) followed by <<GUIDE>> and the original description, kept verbatim, as the
    tail.
  • Darling/Darling.Tests/McpToolGuideHeads.AgStore.cs (new): head pins for both tools.
  • Both tools' lines in Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAgTools.txt and
    .../DarlingMcpStoreQueryStatsTools.txt updated to the new served sizes.
  • No parameter descriptions changed: all four are already under the 200-character cap (order_by is the
    longest, at 165).
  • Two guardrail facts below are new text. The original description never stated them, though nothing in it was
    wrong, and I verified both against the tool bodies:
    • get_ag_health's head now says a call scoped to one server returns not_collected when that server's engine
      can never run AG collection (DarlingEngineCapability.NotCollectedStatusAsync, DarlingMcpAgTools.cs:86).
      The original description covered only the fleet-wide empty case.
    • get_store_query_stats's head now says a zero-match result is a normal JSON payload with empty arrays, not
      a status: "empty" envelope. The tool has no empty or unavailable branch at all. I checked every return path
      in GetStoreQueryStats to confirm this.

Served characters (Darling only, no Lite twin)

Tool Darling before Darling after
get_ag_health 1,529 616
get_store_query_stats 984 614

Corrections

None. Both original descriptions were accurate. The two additions above are new guardrail facts, not fixes to
wrong ones.

D4 removals

None. Neither served description contained an issue reference or an anecdote to remove.

Re-pointed pins

Darling/Darling.Tests/DarlingMcpAgToolsTests.cs has one existing pin on this tool's description text:
Description_SurfacesTheReadersTwoDocumentedTraps. It reads the raw DescriptionAttribute.Description, which is
the head, the marker and the tail joined together, not the served head alone. Every phrase it checks for is still
present, verbatim, in the tail. That list is NO row for a server with no AGs, last non-empty snapshot,
lag and queue depth are NOT banded, secondary_lag_seconds, is_suspended,
0 lag while data movement is suspended, and populated only for the LOCAL replica. No change was needed. I ran
this test and confirmed it still passes.

I found no other test, in Darling.Tests or Lite.Tests, that pins either tool's description text. Other files that
mention get_ag_health or get_store_query_stats only check tool-name presence, parameter names, or catalog and
dispatch membership. Those files are DarlingMcpStoreMetricsToolsTests.cs, DarlingWebEndpointsTests.cs,
EngineCapabilityMissTests.cs, PgTargetMcpSurfaceTests.cs, and Lite.Tests/CrossAppMcpToolInventoryPinTests.cs.

D9 traps

  • Q: get_ag_health lists an AG once in the result. Does that mean it has only one monitored replica?
    Correct: Not necessarily. Each row is one monitored server's VIEW of the AG, so a multi-replica AG appears
    once per monitored replica.
    Prevents it: "One row per REPLICA's view: a multi-replica AG appears once per replica, not merged."
  • Q: A secondary's severity reads healthy. Does that mean its lag and queue depth are fine too?
    Correct: No. Severities restate the DMVs' own state and health strings only. Lag and queue depth are never
    folded in, so a badly lagging secondary can still carry a healthy severity.
    Prevents it: "Severities restate DMV verdicts only, un-banded on lag/queue depth"
  • Q: secondary_lag_seconds reads 0. Is the secondary caught up?
    Correct: Not necessarily. The DMV reports 0 lag while data movement is suspended, so a 0 must be read
    together with is_suspended.
    Prevents it: "lag reads 0 while suspended, so check secondary_lag_seconds and is_suspended yourself."
  • Q: A group's collection_time looks old. Does that always mean a live collection problem?
    Correct: Not necessarily. A server whose AGs were dropped keeps returning its last non-empty snapshot, so
    an old collection_time can be that case instead of a live reading.
    Prevents it: "collection_time can be stale after an AG is dropped."
  • Q: A fleet-wide call (no server_name) returns status empty. Does that mean the AG collectors are broken
    everywhere?
    Correct: No. Empty on a fleet-wide call is real: no Availability Groups exist on any monitored server.
    That is a normal topology, not a collection gap.
    Prevents it: "Empty: none collected fleet-wide or on the server."
  • Q: Scoped to one server, get_ag_health returns status not_collected. Does that mean the server just has not
    reported its AGs yet?
    Correct: No. not_collected here means that server's engine can never run AG collection at all, for example
    Azure SQL Database or a PostgreSQL target. That is a permanent gap, not a timing gap.
    Prevents it: "Scoped to a server whose engine never runs AG collection: not_collected."
  • Q: Does a fleet-wide call (no server_name) ever return not_collected?
    Correct: No. not_collected is checked only when the caller scoped to one server. A fleet-wide miss always
    reads as empty, because no single engine speaks for the whole fleet.
    Prevents it: "Empty: none collected fleet-wide or on the server. Scoped to a server whose engine never runs
    AG collection: not_collected."
  • Q: Can I pass server_name to scope get_store_query_stats to one monitored server?
    Correct: No. There is no server_name parameter. The tool has no monitored-server scope at all, because the
    store itself, not a monitored server, is the subject.
    Prevents it: "No server_name: the store is the subject."
  • Q: Does get_store_query_stats rank a monitored SQL Server's expensive queries?
    Correct: No. It ranks the monitoring STORE's own SQL statements, from pg_stat_statements on the store's
    PostgreSQL backend, not a monitored server's queries.
    Prevents it: "Ranks the STORE's own SQL statements by server-side cost, from pg_stat_statements: not a
    monitored server's queries"
  • Q: A role's by_role share is high. Does that mean that role's calls are the slowest?
    Correct: Not necessarily. by_role is each role's share of the total recorded time. A role can lead by
    running many cheap calls rather than slow ones.
    Prevents it: "by_role is each role's TIME SHARE, not how slow it felt."
  • Q: statements_returned comes back 0. Is that a status empty result?
    Correct: No. This tool has no empty status. Zero matching statements is a normal JSON payload with empty
    by_role and statements arrays.
    Prevents it: "Zero matches: a normal payload with empty arrays, not status empty."
  • Q: The call returns status precondition. Does that mean the store has no query activity to show?
    Correct: No. precondition is a setup gap, not an activity gap. pg_stat_statements is missing, not loaded
    or too old, its reader is not built yet, or this role has no grant.
    Prevents it: "Gated: status precondition, with the remedy, when pg_stat_statements is missing, not loaded
    or too old, its reader isn't built yet, or this role has no grant."

Status routes

Tool Status Condition File:line
get_ag_health not_collected Scoped to one server (server_name given) whose engine can never run AG collection DarlingMcpAgTools.cs:86
get_ag_health empty AvailabilityGroupCount == 0, fleet-wide or scoped to a server whose engine CAN collect AGs DarlingMcpAgTools.cs:96-97
get_ag_health error Unhandled exception during the read DarlingMcpAgTools.cs:107
get_store_query_stats precondition pg_stat_statements missing, too old, not built yet, role not granted, or not loaded DarlingMcpStoreQueryStatsTools.cs:220-222
get_store_query_stats error Unhandled exception during the read DarlingMcpStoreQueryStatsTools.cs:328
get_store_query_stats (none; normal JSON) Zero matching statements, no precondition failure n/a

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: Build succeeded, 0 Warning(s), 0 Error(s).
  • dropcheck.py origin/dev HEAD get_ag_health get_store_query_stats: both missing=0.
  • Targeted: McpToolGuideHeadsAgStoreTests (new file), 5/5 pass.
  • Targeted: McpToolGuideTests, McpToolsListBudgetTests, McpZeroIsAMeasurementTests,
    McpPayloadContractCensusTests, DarlingMcpAgToolsTests, StoreStatementStatsTests,
    DarlingMcpStoreMetricsToolsTests, DarlingWebEndpointsTests, EngineCapabilityMissTests,
    PgTargetMcpSurfaceTests: 246/246 pass, 0 failed.
  • Merged origin/dev (through DarlingInstallLocationTests: the #4052 checks pass when run non-elevated #4111), clean merge, no conflicts.
  • Full suite, once, after the merge: Darling.Tests.exe. Total 13,340, errors 0, failed 0, skipped 636
    (live PostgreSQL classes, no rig). Not Run: 1, a pre-existing runner artifact unrelated to either tool.
    No [FAIL] lines appear in the run. The two DarlingInstallLocationTests this wave's brief calls out as
    known local failures did not fail here: DarlingInstallLocationTests: the #4052 checks pass when run non-elevated #4111, merged in, fixes them.
  • Lite full suite: not run. This lane touched no Lite or PerformanceMonitor.Common file (neither tool has a
    Lite twin), so the wave rules do not call for it.
  • Plain-English checker on this PR body: check.py final pass found 25 hit(s). All are the allowed D9 labels
    (**Correct:**, **Prevents it:**) or a semicolon inside a table cell.

Coordinator verification

  • Checked every head fact against the code. get_ag_health is correct as written. It returns empty when nothing
    was collected. It returns not_collected only when scoped to one server whose engine never runs AG collection
    (DarlingMcpAgTools.cs:79-101).
  • Fixed 1 fact in the get_store_query_stats head. The precondition clause named 3 of the 5 routes in
    PreconditionReason. It now names all five: missing, not loaded, too old, reader not built yet, and no grant for
    this role. To make room, the head drops "answering which one is slow" (the tail still says it) and says
    "carries its role" instead of "is attributed to its role". Served size stays 614, under the 620 cap, so the
    budget line is unchanged. The pin in McpToolGuideHeads.AgStore.cs checks the new clause.
  • D9, one round (haiku, one Read): 1 misread, on a question I added about the precondition scope. The fix above
    addresses it. One other answer was "not stated", which does not count as a misread.
  • After the fix: build succeeded with 0 warnings. The targeted classes passed, 124 of 124.

Handoff

Nothing deferred. Both of this lane's tools are fully converted and green.

erikdarlingdata and others added 5 commits September 23, 2026 22:49
…ist (#3898)

Refs #3898. Moves both Darling-only tools' guardrail-heavy descriptions to a
620-character head plus a get_tool_guide tail, adding two guardrail facts
verified against source that the original prose never stated: get_ag_health's
scoped not_collected route, and get_store_query_stats' zero-match payload
shape. Adds Darling/Darling.Tests/McpToolGuideHeads.AgStore.cs and updates
both tools' budget lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
The precondition clause named 3 of the 5 routes in PreconditionReason. It now
also names the reader-not-built route and the no-grant route. The pin checks
the new clause. Served size stays 614.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 03:25
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 03:25
@erikdarlingdata
erikdarlingdata merged commit 03f9f7b into dev Sep 24, 2026
16 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-agstore branch September 24, 2026 04:13
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
* Add 48 buffered CHANGELOG entries in one splice

Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran.

Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

* Changelog splice: add #4137's entry, which merged before the splice but was missed

#4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its
body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right
after #4136's, and #4136's last sentence now points to it instead of saying plan capture is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant