From 3f647355ed7b6e49a7e99c9eb21a6db28e58b23e Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:12:00 -0400 Subject: [PATCH 1/5] Content PRs for the MCP reading guides can merge in any order (#3898) Refs #3898. Step 0: makes Phase 3's per-family conversion work parallel-safe before any content lane starts, with no Description text changes and a byte-identical tools/list on both products (329,494 B Darling / 148,216 B Lite, unchanged from dev). - McpToolsListBudget.txt splits into McpToolsListBudget/.txt, one file per tool class, one block per tool with a blank line between blocks, so two lanes editing different classes (or adjacent tools in the same class) never touch the same lines. The total ceiling is growth-only now; layout (ownership, canonical form) is checked separately from values, and every failure names the file, line and exact replacement. - Family head pins move out of McpToolGuideTests into their own McpToolGuideHeads..cs, copied from the health-parser pilot's; McpToolGuideTests keeps only generic pins lanes never need to touch. - McpToolGuideTopics becomes a partial class, one file per family (McpToolGuideTopics..cs), each pre-declaring its own topic array so a lane fills in an existing empty file instead of editing a shared one. - The per-family cross-SKU identity pin is replaced by one generic test that discovers every tool shared by both products and checks marker parity and head equality for all of them, present and future. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01M7r1CXmEcBmsrTV34CCLfF --- .../McpToolGuideHeads.HealthParser.cs | 126 ++++ Darling/Darling.Tests/McpToolGuideTests.cs | 332 ++++++--- Darling/Darling.Tests/McpToolsListBudget.txt | 662 ------------------ .../McpToolsListBudget/DarlingMcpAgTools.txt | 3 + .../DarlingMcpAlertTools.txt | 45 ++ .../DarlingMcpBlockingTools.txt | 43 ++ .../DarlingMcpCollectorCostTools.txt | 4 + .../DarlingMcpConfigHistoryTools.txt | 23 + .../DarlingMcpConfigTools.txt | 10 + .../DarlingMcpCustomAlertTools.txt | 31 + .../DarlingMcpCustomViewTools.txt | 28 + .../DarlingMcpDataTools.txt | 96 +++ .../DarlingMcpDefaultTraceTools.txt | 6 + .../DarlingMcpFleetSweepTools.txt | 6 + .../DarlingMcpFleetTools.txt | 3 + .../DarlingMcpHealthParserTools.txt | 54 ++ .../DarlingMcpHealthTools.txt | 12 + .../McpToolsListBudget/DarlingMcpJobTools.txt | 3 + .../DarlingMcpLatchSpinlockTools.txt | 12 + .../DarlingMcpLongQueryTools.txt | 6 + .../DarlingMcpMemoryGrantTools.txt | 15 + .../DarlingMcpObjectStatsTools.txt | 14 + .../DarlingMcpOversizedPlanBacklogTools.txt | 5 + .../DarlingMcpPgAutovacuumTools.txt | 6 + .../DarlingMcpPgBlockingTools.txt | 6 + .../DarlingMcpPgCpuUtilizationTools.txt | 5 + .../DarlingMcpPgDatabaseTools.txt | 6 + .../DarlingMcpPgDeadlockTools.txt | 11 + .../DarlingMcpPgIndexTools.txt | 13 + .../DarlingMcpPgIndexUsageTools.txt | 6 + .../DarlingMcpPgIoTools.txt | 6 + .../DarlingMcpPgKernelStatsTools.txt | 6 + .../DarlingMcpPgLogEventTools.txt | 8 + .../DarlingMcpPgLoggingAuditTools.txt | 3 + .../DarlingMcpPgPlanTools.txt | 13 + .../DarlingMcpPgPredicateTools.txt | 6 + .../DarlingMcpPgReplicationStatsTools.txt | 6 + .../DarlingMcpPgServerStateTools.txt | 35 + .../DarlingMcpPgSessionStatesTools.txt | 6 + .../DarlingMcpPgSlotTools.txt | 5 + .../DarlingMcpPgStatementTools.txt | 6 + .../DarlingMcpPgTableBloatTools.txt | 6 + .../DarlingMcpPgTrendTools.txt | 28 + .../DarlingMcpPgWaitSamplingTools.txt | 6 + .../DarlingMcpPgWaitTools.txt | 6 + .../DarlingMcpPgWraparoundTools.txt | 5 + .../DarlingMcpPgXminTools.txt | 5 + .../DarlingMcpPlanCacheSchedulerTools.txt | 10 + .../DarlingMcpPlanCorrectionTools.txt | 6 + .../DarlingMcpPlanTools.txt | 23 + .../McpToolsListBudget/DarlingMcpPvsTools.txt | 4 + .../DarlingMcpQueryHeatmapTools.txt | 9 + .../DarlingMcpQueryStoreClutterTools.txt | 7 + .../DarlingMcpQueryStoreRegressionTools.txt | 7 + .../DarlingMcpServerAdminTools.txt | 6 + .../DarlingMcpSessionTools.txt | 17 + .../DarlingMcpStallProbeTools.txt | 5 + .../DarlingMcpStoreLogTools.txt | 5 + .../DarlingMcpStoreMetricsTools.txt | 6 + .../DarlingMcpStoreQueryStatsTools.txt | 6 + .../DarlingMcpToolGuideTools.txt | 4 + .../McpToolsListBudget/DarlingMcpTools.txt | 32 + .../DarlingMcpTrendTools.txt | 44 ++ .../Darling.Tests/McpToolsListBudgetTests.cs | 341 +++++++-- Lite.Tests/McpToolGuideHeads.HealthParser.cs | 126 ++++ Lite.Tests/McpToolGuideTests.cs | 153 ++-- Lite.Tests/McpToolsListBudget.txt | 367 ---------- .../McpToolsListBudget/McpAlertTools.txt | 13 + .../McpToolsListBudget/McpAnalysisTools.txt | 32 + .../McpToolsListBudget/McpBlockingTools.txt | 40 ++ .../McpConfigHistoryTools.txt | 15 + .../McpToolsListBudget/McpConfigTools.txt | 18 + Lite.Tests/McpToolsListBudget/McpCpuTools.txt | 6 + .../McpDefaultTraceTools.txt | 6 + .../McpToolsListBudget/McpDiscoveryTools.txt | 2 + .../McpHealthParserTools.txt | 54 ++ .../McpToolsListBudget/McpHealthTools.txt | 35 + Lite.Tests/McpToolsListBudget/McpIoTools.txt | 10 + Lite.Tests/McpToolsListBudget/McpJobTools.txt | 3 + .../McpLatchSpinlockTools.txt | 12 + .../McpToolsListBudget/McpLongQueryTools.txt | 6 + .../McpToolsListBudget/McpMemoryTools.txt | 27 + .../McpObjectStatsTools.txt | 9 + .../McpToolsListBudget/McpPerfmonTools.txt | 12 + .../McpPlanCacheSchedulerTools.txt | 10 + .../McpPlanCorrectionTools.txt | 6 + .../McpToolsListBudget/McpPlanTools.txt | 20 + Lite.Tests/McpToolsListBudget/McpPvsTools.txt | 4 + .../McpToolsListBudget/McpQueryTools.txt | 63 ++ .../McpToolsListBudget/McpServerInfoTools.txt | 6 + .../McpToolsListBudget/McpSessionTools.txt | 11 + .../McpToolsListBudget/McpTempDbTools.txt | 6 + .../McpToolsListBudget/McpToolGuideTools.txt | 4 + .../McpToolsListBudget/McpWaitTools.txt | 24 + Lite.Tests/McpToolsListBudgetTests.cs | 341 +++++++-- .../Mcp/McpToolGuideTopics.Alerting.cs | 16 + .../Mcp/McpToolGuideTopics.Data.cs | 16 + .../Mcp/McpToolGuideTopics.HealthParser.cs | 44 ++ .../Mcp/McpToolGuideTopics.PgA.cs | 16 + .../Mcp/McpToolGuideTopics.PgB.cs | 16 + .../Mcp/McpToolGuideTopics.Platform.cs | 16 + .../Mcp/McpToolGuideTopics.SqlCore.cs | 16 + .../Mcp/McpToolGuideTopics.SqlTail.cs | 16 + .../Mcp/McpToolGuideTopics.cs | 53 +- 104 files changed, 2545 insertions(+), 1385 deletions(-) create mode 100644 Darling/Darling.Tests/McpToolGuideHeads.HealthParser.cs delete mode 100644 Darling/Darling.Tests/McpToolsListBudget.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAgTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAlertTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpBlockingTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCollectorCostTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigHistoryTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomAlertTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomViewTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDefaultTraceTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetSweepTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthParserTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpJobTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLatchSpinlockTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLongQueryTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpMemoryGrantTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpObjectStatsTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpOversizedPlanBacklogTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgAutovacuumTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgBlockingTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgCpuUtilizationTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDatabaseTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDeadlockTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexUsageTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIoTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgKernelStatsTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLogEventTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLoggingAuditTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPlanTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPredicateTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgReplicationStatsTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgServerStateTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSessionStatesTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSlotTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgStatementTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTableBloatTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTrendTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitSamplingTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWraparoundTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgXminTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCacheSchedulerTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCorrectionTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPvsTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryHeatmapTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreClutterTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreRegressionTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpServerAdminTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpSessionTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStallProbeTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreLogTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreMetricsTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreQueryStatsTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpToolGuideTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTools.txt create mode 100644 Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTrendTools.txt create mode 100644 Lite.Tests/McpToolGuideHeads.HealthParser.cs delete mode 100644 Lite.Tests/McpToolsListBudget.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpAlertTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpAnalysisTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpBlockingTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpConfigHistoryTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpConfigTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpCpuTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpDefaultTraceTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpHealthParserTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpHealthTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpIoTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpJobTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpLatchSpinlockTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpLongQueryTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpMemoryTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpObjectStatsTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpPlanCacheSchedulerTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpPlanCorrectionTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpPlanTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpPvsTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpQueryTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpSessionTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpTempDbTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpToolGuideTools.txt create mode 100644 Lite.Tests/McpToolsListBudget/McpWaitTools.txt create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Alerting.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.HealthParser.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgA.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgB.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Platform.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlCore.cs create mode 100644 PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlTail.cs diff --git a/Darling/Darling.Tests/McpToolGuideHeads.HealthParser.cs b/Darling/Darling.Tests/McpToolGuideHeads.HealthParser.cs new file mode 100644 index 0000000000..641963dd93 --- /dev/null +++ b/Darling/Darling.Tests/McpToolGuideHeads.HealthParser.cs @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; +using PerformanceMonitor.Common; +using Xunit; + +namespace Darling.Tests; + +/// +/// #3898 D3 head pins for the get_health_parser_* family (the D1 pilot). This is the pattern every +/// family converted under #3898 copies into its own McpToolGuideHeads.<Family>.cs: the family's +/// tool roster and guardrail facts, plus the two tests that pin them, using 's +/// shared helper. McpToolGuideTests itself is never edited to add +/// a family, so two families converting in parallel never conflict there. +/// +public sealed class McpToolGuideHeadsHealthParserTests +{ + private static readonly string[] HealthParserTools = + [ + "get_health_parser_cpu_tasks", + "get_health_parser_io_issues", + "get_health_parser_memory_broker", + "get_health_parser_memory_conditions", + "get_health_parser_memory_node_oom", + "get_health_parser_scheduler_issues", + "get_health_parser_severe_errors", + "get_health_parser_significant_waits", + "get_health_parser_system_health", + ]; + + /// The per-tool guardrail fact each head must state: the significance gate (a floor under what can + /// be listed at all) or its absence. + private static readonly (string Tool, string Fact)[] GateFacts = + [ + ("get_health_parser_system_health", "Ungated: every parsed event is returned."), + ("get_health_parser_memory_node_oom", "Ungated: every recorded OOM is returned."), + ("get_health_parser_severe_errors", "Gated: severity 19 or higher only, benign connection-reset error numbers excluded"), + ("get_health_parser_io_issues", "Gated: WARNING-state results only."), + ("get_health_parser_scheduler_issues", "Gated: WARNING-state results only."), + ("get_health_parser_memory_conditions", "Gated: only snapshots whose last notification is RESOURCE_MEMPHYSICAL_LOW."), + ("get_health_parser_cpu_tasks", "Gated: WARNING-state results with at least 10 pending tasks only."), + ("get_health_parser_memory_broker", "Gated: RESOURCE_MEMPHYSICAL_LOW notifications only."), + ("get_health_parser_significant_waits", "Floors: a real session, a non-BACKUP statement, at least 500 ms, and a wait type off the idle/background list; shorter waits are never listed."), + ]; + + /// D9: what an empty answer means, tied to the gate (or the floors, or the absence of one) beside + /// it rather than one identical sentence on all nine — a fresh reader given only the head misread the + /// ungated tools' empty answer as ambiguous when it was a real, healthy result (#4048 D9 round). + private const string GatedEmptyFact = + "An empty answer with status empty is a real result: nothing in this window passed the gate, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; + + private const string FloorsEmptyFact = + "An empty answer with status empty is a real result: nothing in this window passed the floors, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; + + private const string UngatedEmptyFact = + "An empty answer with status empty is a real result: nothing of this kind was recorded in this window. status unavailable with source_observed false is no evidence either way."; + + private static readonly (string Tool, string Fact)[] EmptyAnswerFacts = + [ + ("get_health_parser_system_health", UngatedEmptyFact), + ("get_health_parser_memory_node_oom", UngatedEmptyFact), + ("get_health_parser_significant_waits", FloorsEmptyFact), + ("get_health_parser_severe_errors", GatedEmptyFact), + ("get_health_parser_io_issues", GatedEmptyFact), + ("get_health_parser_scheduler_issues", GatedEmptyFact), + ("get_health_parser_memory_conditions", GatedEmptyFact), + ("get_health_parser_cpu_tasks", GatedEmptyFact), + ("get_health_parser_memory_broker", GatedEmptyFact), + ]; + + [Fact] + public void EveryPilotHead_ServesTheWindow_TheEmptyGuardrail_AndItsGate() + { + foreach (var tool in HealthParserTools) + { + var served = McpToolGuideTests.Served(tool); + Assert.NotNull(served.Tail); + /* The window is fixed on the event's own time, ends at as_of, newest first. */ + Assert.Contains("over an event_time window ending at as_of, newest first.", served.Served, StringComparison.Ordinal); + Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal); + /* D9: the floors sentence is the longest of the three variants; 620 leaves it headroom while + staying far under the D2 absolute cap of 1,000 (McpToolsListBudgetTests.ConvertedHeadCap). */ + Assert.True(served.Served.Length <= 620, $"{tool}: served head {served.Served.Length} is over the 620 target"); + } + + foreach (var (tool, fact) in GateFacts) + { + Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal); + } + + /* Zero is not a measurement here unless the witness says so (#3541 A12); D9: each tool's empty-answer + sentence ties to ITS gate (or floors, or absence of one) rather than one sentence on all nine, so a + reader given only the head does not have to guess which of the four rungs an empty answer landed on. */ + foreach (var (tool, fact) in EmptyAnswerFacts) + { + Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal); + } + + /* No required parameter, so the head need not name one: all four are optional in the served schema. */ + Assert.All(HealthParserTools, tool => Assert.All(McpToolGuideTests.Served(tool).ParameterDescriptionLengths, p => Assert.True(p.Length <= 200))); + } + + /// Nothing the old descriptions said was lost: the four-rung empty-window sentence every one of + /// them carried is the topic, and each guide carries the topic verbatim, so one call answers it. + [Fact] + public void EveryPilotGuide_CarriesTheEmptyWindowTopic_AndTheTopicCarriesEveryRung() + { + foreach (var tool in HealthParserTools) + { + Assert.EndsWith(McpToolGuideTopics.SystemHealthEmptyWindows, McpToolGuideTests.Served(tool).Tail!, StringComparison.Ordinal); + } + + var topic = McpToolGuideTopics.SystemHealthEmptyWindows; + foreach (var fact in new[] { "source_observed", "EVER", "last_captured_at", "gated out", "Captured before", "Never recorded", + "status unavailable", "not a clean bill", "events_in_window", "last_captured_of_type_at" }) + { + Assert.Contains(fact, topic, StringComparison.Ordinal); + } + } +} diff --git a/Darling/Darling.Tests/McpToolGuideTests.cs b/Darling/Darling.Tests/McpToolGuideTests.cs index 353212d72b..a27da117f5 100644 --- a/Darling/Darling.Tests/McpToolGuideTests.cs +++ b/Darling/Darling.Tests/McpToolGuideTests.cs @@ -7,12 +7,16 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; +using PerformanceMonitor.Alerting; using PerformanceMonitor.Common; using PerformanceMonitor.Darling.Service.Mcp; using Xunit; @@ -21,138 +25,123 @@ namespace Darling.Tests; /// /// #3898 D1 + D3 on Darling: get_tool_guide serves the tails the one-place split keeps off -/// tools/list, and the head pins for the pilot family (get_health_parser_*): every guardrail fact -/// a caller needs to read the answer is served in the HEAD, not left in the guide. Lite's twin is -/// Lite.Tests/McpToolGuideTests; the cross-SKU lockstep pins (identical heads, identical guide tool) -/// live here because this project already reads Lite's source. +/// tools/list, generic pins any converted family relies on, and the cross-SKU lockstep (identical heads, +/// identical guide tool) that no per-family file needs to repeat. Lite's twin is +/// Lite.Tests/McpToolGuideTests. A family's OWN head pins (its tool roster, its guardrail facts) live in +/// their own McpToolGuideHeads.<Family>.cs next to this file (see +/// for the pattern) — lanes converting a new family add a file +/// there and never edit this one, so two families converting in parallel never conflict here. /// public sealed class McpToolGuideTests { - private static readonly string[] HealthParserTools = - [ - "get_health_parser_cpu_tasks", - "get_health_parser_io_issues", - "get_health_parser_memory_broker", - "get_health_parser_memory_conditions", - "get_health_parser_memory_node_oom", - "get_health_parser_scheduler_issues", - "get_health_parser_severe_errors", - "get_health_parser_significant_waits", - "get_health_parser_system_health", - ]; - - /// The per-tool guardrail fact each head must state: the significance gate (a floor under what can - /// be listed at all) or its absence. - private static readonly (string Tool, string Fact)[] GateFacts = - [ - ("get_health_parser_system_health", "Ungated: every parsed event is returned."), - ("get_health_parser_memory_node_oom", "Ungated: every recorded OOM is returned."), - ("get_health_parser_severe_errors", "Gated: severity 19 or higher only, benign connection-reset error numbers excluded"), - ("get_health_parser_io_issues", "Gated: WARNING-state results only."), - ("get_health_parser_scheduler_issues", "Gated: WARNING-state results only."), - ("get_health_parser_memory_conditions", "Gated: only snapshots whose last notification is RESOURCE_MEMPHYSICAL_LOW."), - ("get_health_parser_cpu_tasks", "Gated: WARNING-state results with at least 10 pending tasks only."), - ("get_health_parser_memory_broker", "Gated: RESOURCE_MEMPHYSICAL_LOW notifications only."), - ("get_health_parser_significant_waits", "Floors: a real session, a non-BACKUP statement, at least 500 ms, and a wait type off the idle/background list; shorter waits are never listed."), - ]; - - /// D9: what an empty answer means, tied to the gate (or the floors, or the absence of one) beside - /// it rather than one identical sentence on all nine — a fresh reader given only the head misread the - /// ungated tools' empty answer as ambiguous when it was a real, healthy result (#4048 D9 round). - private const string GatedEmptyFact = - "An empty answer with status empty is a real result: nothing in this window passed the gate, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; - - private const string FloorsEmptyFact = - "An empty answer with status empty is a real result: nothing in this window passed the floors, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; - - private const string UngatedEmptyFact = - "An empty answer with status empty is a real result: nothing of this kind was recorded in this window. status unavailable with source_observed false is no evidence either way."; - - private static readonly (string Tool, string Fact)[] EmptyAnswerFacts = - [ - ("get_health_parser_system_health", UngatedEmptyFact), - ("get_health_parser_memory_node_oom", UngatedEmptyFact), - ("get_health_parser_significant_waits", FloorsEmptyFact), - ("get_health_parser_severe_errors", GatedEmptyFact), - ("get_health_parser_io_issues", GatedEmptyFact), - ("get_health_parser_scheduler_issues", GatedEmptyFact), - ("get_health_parser_memory_conditions", GatedEmptyFact), - ("get_health_parser_cpu_tasks", GatedEmptyFact), - ("get_health_parser_memory_broker", GatedEmptyFact), - ]; - - private static McpToolsListBudgetTests.MeasuredTool Served(string tool) => + internal static McpToolsListBudgetTests.MeasuredTool Served(string tool) => McpToolsListBudgetTests.Measure().Tools.Single(t => t.Name == tool); - /* ---------------- D3 head pins ---------------- */ + /* ---------------- generic D3 + D6 pins (every family, no hand-kept list) ---------------- */ + /// D3's absolute head-length target: every tool the split has actually converted (discovered from + /// the served list, not a hand-kept roster) stays at or under 620 served characters, pointer included. The + /// pilot pinned this per tool; this is the backstop that catches it for every family from here on, + /// including one whose own family file forgets to check it. [Fact] - public void EveryPilotHead_ServesTheWindow_TheEmptyGuardrail_AndItsGate() + public void EveryConvertedHead_StaysAtOrUnder620Characters() { - foreach (var tool in HealthParserTools) - { - var served = Served(tool); - Assert.NotNull(served.Tail); - /* The window is fixed on the event's own time, ends at as_of, newest first. */ - Assert.Contains("over an event_time window ending at as_of, newest first.", served.Served, StringComparison.Ordinal); - Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal); - /* D9: the floors sentence is the longest of the three variants; 620 leaves it headroom while - staying far under the D2 absolute cap of 1,000 (McpToolsListBudgetTests.ConvertedHeadCap). */ - Assert.True(served.Served.Length <= 620, $"{tool}: served head {served.Served.Length} is over the 620 target"); - } + var overLong = McpToolsListBudgetTests.Measure().Tools + .Where(t => t.Tail is not null && t.Served.Length > 620) + .Select(t => $"{t.Name}: {t.Served.Length}") + .ToList(); + Assert.True(overLong.Count == 0, "served head(s) over the 620 target: " + string.Join(", ", overLong)); + } - foreach (var (tool, fact) in GateFacts) - { - Assert.Contains(fact, Served(tool).Served, StringComparison.Ordinal); - } + /// + /// D6, generically: for every MCP tool name registered on BOTH SKUs, if either side's description carries + /// the marker, both must, and the served heads must be byte-identical. Tool names and marker presence are + /// discovered by scanning source (not a hand-kept roster), so a family converted on only one SKU fails here + /// without either SKU needing its own pin. Darling's own description text comes from + /// (already resolved by reflection) rather than a second, + /// fragile source-text parse: several unconverted Darling tools build their description from string + /// concatenation plus an embedded constant (get_collection_health's + /// ), which only matters for the marker-presence scan + /// (a plain substring search, safe regardless of how the string is built) — full resolution + /// () is only ever needed for Lite's side of a tool BOTH sides already agree + /// is converted, and every converted description is hand-authored literals a caller can resolve. + /// + [Fact] + public void EverySharedToolName_CarriesTheMarkerOnBothSkus_OrNeither_WithByteIdenticalHeads() + { + var (darlingFiles, darlingHasMarker) = ScanToolsUnder("Darling", "PerformanceMonitor.Darling.Service", "Mcp"); + var (liteFiles, liteHasMarker) = ScanToolsUnder("Lite", "Mcp"); + var darlingMeasured = McpToolsListBudgetTests.Measure().Tools.ToDictionary(t => t.Name, StringComparer.Ordinal); + var shared = darlingFiles.Keys.Intersect(liteFiles.Keys, StringComparer.Ordinal).OrderBy(n => n, StringComparer.Ordinal).ToList(); + var problems = new List(); - /* Zero is not a measurement here unless the witness says so (#3541 A12); D9: each tool's empty-answer - sentence ties to ITS gate (or floors, or absence of one) rather than one sentence on all nine, so a - reader given only the head does not have to guess which of the four rungs an empty answer landed on. */ - foreach (var (tool, fact) in EmptyAnswerFacts) + foreach (var tool in shared) { - Assert.Contains(fact, Served(tool).Served, StringComparison.Ordinal); + var darlingConverted = darlingHasMarker[tool]; + var liteConverted = liteHasMarker[tool]; + if (darlingConverted != liteConverted) + { + problems.Add($"{tool}: the {McpToolGuide.Marker} marker is on {(darlingConverted ? "Darling only" : "Lite only")}."); + continue; + } + + if (!darlingConverted) + { + continue; + } + + Assert.True(darlingMeasured.TryGetValue(tool, out var measured), $"{tool}: carries the marker but is not in Darling's measured tools/list."); + var darlingHead = McpToolGuide.Split(measured!.Description!).Head; + var liteHead = McpToolGuide.Split(DescriptionLiteral(File.ReadAllText(liteFiles[tool]), tool)).Head; + if (!string.Equals(darlingHead, liteHead, StringComparison.Ordinal)) + { + problems.Add($"{tool}: the served head differs between SKUs."); + } } - /* No required parameter, so the head need not name one: all four are optional in the served schema. */ - Assert.All(HealthParserTools, tool => Assert.All(Served(tool).ParameterDescriptionLengths, p => Assert.True(p.Length <= 200))); + Assert.True(problems.Count == 0, string.Join("\n", problems)); + Assert.Contains(shared, tool => darlingHasMarker[tool]); } - /// Nothing the old descriptions said was lost: the four-rung empty-window sentence every one of - /// them carried is the topic, and each guide carries the topic verbatim, so one call answers it. + /// D6: get_tool_guide's own description is byte-identical on both SKUs (it never carries the + /// marker itself, so the generic check above never compares it). [Fact] - public void EveryPilotGuide_CarriesTheEmptyWindowTopic_AndTheTopicCarriesEveryRung() + public void GetToolGuideDescription_IsByteIdenticalOnBothSkus() { - foreach (var tool in HealthParserTools) - { - Assert.EndsWith(McpToolGuideTopics.SystemHealthEmptyWindows, Served(tool).Tail!, StringComparison.Ordinal); - } + Assert.Equal( + DescriptionLiteral(File.ReadAllText(RepoPath("Darling", "PerformanceMonitor.Darling.Service", "Mcp", "DarlingMcpToolGuideTools.cs")), "get_tool_guide"), + DescriptionLiteral(File.ReadAllText(RepoPath("Lite", "Mcp", "McpToolGuideTools.cs")), "get_tool_guide")); + } - var topic = McpToolGuideTopics.SystemHealthEmptyWindows; - foreach (var fact in new[] { "source_observed", "EVER", "last_captured_at", "gated out", "Captured before", "Never recorded", - "status unavailable", "not a clean bill", "events_in_window", "last_captured_of_type_at" }) - { - Assert.Contains(fact, topic, StringComparison.Ordinal); - } + /* ---------------- topics: All aggregates every partial family array ---------------- */ + + [Fact] + public void AllTopics_HaveNoNullEntries() + { + Assert.All(McpToolGuideTopics.All, t => Assert.NotNull(t)); } - /// D6: the pilot heads are byte-identical on both SKUs (one shared parser, one shared gate), and so - /// is get_tool_guide's own description. Read from source, since Lite's assembly is not referenced here. [Fact] - public void PilotHeads_AndTheGuideTool_AreIdenticalOnBothSkus() + public void AllTopics_HaveUniqueNames() { - var darling = File.ReadAllText(RepoPath("Darling", "PerformanceMonitor.Darling.Service", "Mcp", "DarlingMcpHealthParserTools.cs")); - var lite = File.ReadAllText(RepoPath("Lite", "Mcp", "McpHealthParserTools.cs")); - foreach (var tool in HealthParserTools) - { - var darlingHead = HeadOf(darling, tool); - Assert.Equal(darlingHead, HeadOf(lite, tool)); - Assert.Equal(McpToolGuide.Split(Served(tool).Description!).Head, darlingHead); - } + var names = McpToolGuideTopics.All.Select(t => t.Name).ToList(); + Assert.Equal(names.Distinct(StringComparer.Ordinal).Count(), names.Count); + } - Assert.Equal( - DescriptionLiteral(File.ReadAllText(RepoPath("Darling", "PerformanceMonitor.Darling.Service", "Mcp", "DarlingMcpToolGuideTools.cs")), "get_tool_guide"), - DescriptionLiteral(File.ReadAllText(RepoPath("Lite", "Mcp", "McpToolGuideTools.cs")), "get_tool_guide")); + /// Each family's partial file declares its own private static readonly McpToolGuideTopic[] + /// field; reflected here so a family added and never wired into All's static constructor fails + /// loudly instead of silently serving no topics. + [Fact] + public void EveryPartialTopicArray_IsReflectedInAll() + { + var fields = typeof(McpToolGuideTopics) + .GetFields(BindingFlags.NonPublic | BindingFlags.Static) + .Where(f => f.FieldType == typeof(McpToolGuideTopic[])) + .ToList(); + Assert.True(fields.Count == 8, $"expected 8 per-family topic arrays, found {fields.Count}: {string.Join(", ", fields.Select(f => f.Name))}"); + + var totalFromFields = fields.Sum(f => ((McpToolGuideTopic[])f.GetValue(null)!).Length); + Assert.Equal(totalFromFields, McpToolGuideTopics.All.Count); } /* ---------------- get_tool_guide ---------------- */ @@ -181,9 +170,19 @@ public void GetToolGuide_ServesTails_AndTopics_AndReportsUnknownNames() [Fact] public void GetToolGuide_WithNoArguments_IsTheIndex() { + /* HostCatalog wires up exactly DarlingMcpHealthParserTools, DarlingMcpDataTools and + DarlingMcpToolGuideTools, so this fixture's own guide-bearing roster is a local, self-contained fact + about THIS test's host, not a family pin (that lives in McpToolGuideHeadsHealthParserTests). */ + var fixtureToolsWithGuides = new[] + { + "get_health_parser_cpu_tasks", "get_health_parser_io_issues", "get_health_parser_memory_broker", + "get_health_parser_memory_conditions", "get_health_parser_memory_node_oom", "get_health_parser_scheduler_issues", + "get_health_parser_severe_errors", "get_health_parser_significant_waits", "get_health_parser_system_health", + }; + using var doc = JsonDocument.Parse(DarlingMcpToolGuideTools.GetToolGuide(HostCatalog(), null, null)); var withGuides = doc.RootElement.GetProperty("tools_with_guides").EnumerateArray().Select(e => e.GetString()).ToArray(); - Assert.Equal(HealthParserTools, withGuides); + Assert.Equal(fixtureToolsWithGuides, withGuides); Assert.Contains(doc.RootElement.GetProperty("topics").EnumerateArray(), t => t.GetProperty("name").GetString() == McpToolGuideTopics.SystemHealthEmptyWindowsName); } @@ -235,11 +234,118 @@ private static McpToolGuideCatalog HostCatalog() /// The served head of a tool read from source: its Description literal, split at the marker. internal static string HeadOf(string source, string tool) => McpToolGuide.Split(DescriptionLiteral(source, tool)).Head; + /// + /// Reads a tool's Description attribute argument from source, handling both attribute forms + /// ([McpServerTool(Name = "x"), Description(...)] and [McpServerTool(Name = "x")]\n[Description(...)]), + /// a description written as several +-concatenated string literals across lines (the pilot's + /// single-literal-only pattern did not anticipate this; most tools outside the pilot use it), and + /// get_collection_health's one embedded constant reference + /// (, resolved rather than treated as a literal). Any + /// other non-literal piece fails, naming the tool and the unrecognized fragment, rather than returning a + /// silently truncated description. + /// private static string DescriptionLiteral(string source, string tool) { - var match = Regex.Match(source, @"\[McpServerTool\(Name = """ + tool + @"""\), Description\(""((?:[^""\\]|\\.)*)"""); - Assert.True(match.Success, $"no Description literal for {tool}"); - return Regex.Unescape(match.Groups[1].Value); + var anchor = Regex.Match(source, + @"\[McpServerTool\(Name\s*=\s*""" + Regex.Escape(tool) + @"""\)(?:\s*,\s*|\s*\]\s*\[\s*)Description\(", + RegexOptions.Singleline); + Assert.True(anchor.Success, $"no Description( immediately after the McpServerTool attribute for {tool}"); + + var i = anchor.Index + anchor.Length; + var sb = new StringBuilder(); + while (true) + { + while (i < source.Length && char.IsWhiteSpace(source[i])) + { + i++; + } + + if (i < source.Length && source[i] == '"') + { + i++; + var start = i; + while (source[i] != '"' || source[i - 1] == '\\') + { + i++; + } + + sb.Append(source[start..i]); + i++; + } + else + { + var known = Regex.Match(source[i..], @"^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)"); + var value = known.Success ? ResolveKnownConstant(known.Groups[1].Value, known.Groups[2].Value) : null; + Assert.True(value is not null, $"{tool}: unrecognized piece in the Description concatenation at offset {i} ('{source[i..Math.Min(i + 40, source.Length)]}...')"); + sb.Append(value); + i += known.Length; + } + + var afterPiece = i; + while (i < source.Length && char.IsWhiteSpace(source[i])) + { + i++; + } + + if (i < source.Length && source[i] == '+') + { + i++; + continue; + } + + i = afterPiece; + break; + } + + return Regex.Unescape(sb.ToString()); + } + + private static readonly Type[] KnownConstantClasses = [typeof(McpToolGuideTopics), typeof(McpToolGuide), typeof(AlertReadFailureCounter)]; + + /// Resolves a ClassName.MemberName piece of a concatenated Description to its compile-time + /// value, for the handful of shared constants (topic texts, the marker, the fleet-scoped-reads sentence) + /// tool descriptions embed instead of repeating. Returns null for anything else, so the caller fails naming + /// the tool rather than silently dropping an unrecognized piece. + private static string? ResolveKnownConstant(string className, string memberName) + { + var type = KnownConstantClasses.FirstOrDefault(t => t.Name == className); + if (type is null) + { + return null; + } + + var field = type.GetField(memberName, BindingFlags.Public | BindingFlags.Static); + if (field is not null) + { + return field.GetValue(null) as string; + } + + var property = type.GetProperty(memberName, BindingFlags.Public | BindingFlags.Static); + return property?.GetValue(null) as string; + } + + /// Every MCP tool name defined directly under a repo-relative Mcp directory, mapped to the file + /// that defines it (mirrors CrossAppMcpToolInventoryPinTests.ExtractToolNames's scan) and to whether + /// the marker appears anywhere between its attribute and the next tool's (or EOF) — a plain substring + /// search, so it needs no string-concatenation resolution and cannot be fooled by one. + private static (Dictionary Files, Dictionary HasMarker) ScanToolsUnder(params string[] segments) + { + var files = new Dictionary(StringComparer.Ordinal); + var hasMarker = new Dictionary(StringComparer.Ordinal); + foreach (var file in Directory.GetFiles(RepoPath(segments), "*.cs", SearchOption.TopDirectoryOnly)) + { + var src = File.ReadAllText(file); + var anchors = Regex.Matches(src, @"\[McpServerTool\(Name\s*=\s*""([a-z0-9_]+)""").OrderBy(m => m.Index).ToList(); + for (var idx = 0; idx < anchors.Count; idx++) + { + var name = anchors[idx].Groups[1].Value; + var end = idx + 1 < anchors.Count ? anchors[idx + 1].Index : src.Length; + Assert.True(files.TryAdd(name, file), $"duplicate MCP tool name '{name}' under {string.Join("/", segments)}"); + hasMarker[name] = src[anchors[idx].Index..end].Contains(McpToolGuide.Marker, StringComparison.Ordinal); + } + } + + return (files, hasMarker); } private static string RepoPath(params string[] segments) => Path.Combine(new[] { RepoRoot() }.Concat(segments).ToArray()); diff --git a/Darling/Darling.Tests/McpToolsListBudget.txt b/Darling/Darling.Tests/McpToolsListBudget.txt deleted file mode 100644 index 6a12888680..0000000000 --- a/Darling/Darling.Tests/McpToolsListBudget.txt +++ /dev/null @@ -1,662 +0,0 @@ -# #3898 Phase 0: Darling's tools/list budget. Ceilings only go down; see McpToolsListBudgetTests. -# 'tool ' is the served description; 'param . ' is a parameter description. -param add_servers.servers_json 395 -param analyze_plan_xml.plan_xml 25 -param analyze_procedure_plan.server_name 28 -param analyze_procedure_plan.sql_handle 52 -param analyze_query_plan.database_name 118 -param analyze_query_plan.query_hash 49 -param analyze_query_plan.server_name 28 -param analyze_query_store_plan.database_name 43 -param analyze_query_store_plan.plan_id 107 -param analyze_query_store_plan.query_id 38 -param analyze_query_store_plan.server_name 28 -param analyze_server.as_of 167 -param analyze_server.hours_back 104 -param analyze_server.server_name 28 -param audit_config.server_name 28 -param compare_analysis.as_of 167 -param compare_analysis.baseline_hours_back 211 -param compare_analysis.hours_back 57 -param compare_analysis.server_name 28 -param create_custom_alert_rule.definition 137 -param create_custom_alert_rule.description 36 -param create_custom_alert_rule.enabled 98 -param create_custom_alert_rule.name 60 -param create_custom_view.definition 149 -param create_custom_view.description 36 -param create_custom_view.name 60 -param create_mute_rule.database_pattern 89 -param create_mute_rule.expires_at 121 -param create_mute_rule.job_name_pattern 79 -param create_mute_rule.metric_name 110 -param create_mute_rule.query_text_pattern 83 -param create_mute_rule.reason 60 -param create_mute_rule.server_name 101 -param create_mute_rule.wait_type_pattern 86 -param delete_custom_alert_rule.rule_id 60 -param delete_custom_view.view_id 54 -param delete_mute_rule.rule_id 76 -param delete_notification_route.route_id 54 -param get_active_queries.as_of 167 -param get_active_queries.blocking_only 238 -param get_active_queries.database_name 156 -param get_active_queries.hours_back 37 -param get_active_queries.limit 149 -param get_active_queries.server_name 28 -param get_ag_health.server_name 118 -param get_alert_history.as_of 167 -param get_alert_history.hours_back 29 -param get_alert_history.include_dismissed 360 -param get_alert_history.limit 133 -param get_alert_history.server_name 90 -param get_analysis_facts.as_of 167 -param get_analysis_facts.hours_back 36 -param get_analysis_facts.min_severity 90 -param get_analysis_facts.server_name 28 -param get_analysis_facts.source 515 -param get_analysis_findings.as_of 167 -param get_analysis_findings.hours_back 49 -param get_analysis_findings.include_drilldown 235 -param get_analysis_findings.server_name 28 -param get_blocked_process_xml.as_of 167 -param get_blocked_process_xml.hours_back 29 -param get_blocked_process_xml.limit 113 -param get_blocked_process_xml.server_name 28 -param get_blocking.as_of 167 -param get_blocking.dedup_key 406 -param get_blocking.hours_back 29 -param get_blocking.limit 133 -param get_blocking.server_name 28 -param get_blocking_stats.as_of 167 -param get_blocking_stats.hours_back 191 -param get_blocking_stats.server_name 28 -param get_blocking_trend.as_of 167 -param get_blocking_trend.hours_back 29 -param get_blocking_trend.server_name 28 -param get_collection_health.server_name 28 -param get_collection_log.as_of 167 -param get_collection_log.collector_name 244 -param get_collection_log.hours_back 191 -param get_collection_log.limit 120 -param get_collection_log.min_duration_ms 382 -param get_collection_log.server_name 96 -param get_collection_log.status 481 -param get_collector_cost.collector_name 105 -param get_collector_cost.days_back 76 -param get_collector_stall_probes.days_back 64 -param get_collector_stall_probes.limit 42 -param get_collector_stall_probes.server_name 64 -param get_cpu_scheduler_pressure.as_of 167 -param get_cpu_scheduler_pressure.hours_back 63 -param get_cpu_scheduler_pressure.server_name 28 -param get_cpu_utilization.as_of 167 -param get_cpu_utilization.bucket_minutes 55 -param get_cpu_utilization.hours_back 28 -param get_cpu_utilization.server_name 28 -param get_current_waits_trend.as_of 167 -param get_current_waits_trend.database_name 73 -param get_current_waits_trend.hours_back 190 -param get_current_waits_trend.server_name 28 -param get_custom_alert_rule.rule_id 67 -param get_custom_view.view_id 61 -param get_daily_summary.server_name 28 -param get_daily_summary.summary_date 155 -param get_daily_summary_range.as_of 166 -param get_daily_summary_range.days_back 84 -param get_daily_summary_range.server_name 28 -param get_database_config.database_name 54 -param get_database_config.server_name 28 -param get_database_config_changes.as_of 167 -param get_database_config_changes.hours_back 51 -param get_database_config_changes.server_name 28 -param get_database_scoped_config.database_name 54 -param get_database_scoped_config.server_name 28 -param get_database_sizes.server_name 28 -param get_deadlock_detail.as_of 167 -param get_deadlock_detail.dedup_key 272 -param get_deadlock_detail.hours_back 29 -param get_deadlock_detail.limit 119 -param get_deadlock_detail.server_name 28 -param get_deadlock_trend.as_of 167 -param get_deadlock_trend.hours_back 29 -param get_deadlock_trend.server_name 28 -param get_deadlocks.as_of 167 -param get_deadlocks.dedup_key 406 -param get_deadlocks.hours_back 29 -param get_deadlocks.limit 133 -param get_deadlocks.server_name 28 -param get_default_trace_events.as_of 167 -param get_default_trace_events.hours_back 41 -param get_default_trace_events.limit 48 -param get_default_trace_events.server_name 28 -param get_file_io_stats.server_name 28 -param get_file_io_trend.as_of 167 -param get_file_io_trend.bucket_minutes 55 -param get_file_io_trend.database_name 56 -param get_file_io_trend.hours_back 29 -param get_file_io_trend.server_name 28 -param get_fleet_overview.hours_back 96 -param get_health_parser_cpu_tasks.as_of 167 -param get_health_parser_cpu_tasks.hours_back 41 -param get_health_parser_cpu_tasks.limit 38 -param get_health_parser_cpu_tasks.server_name 28 -param get_health_parser_io_issues.as_of 167 -param get_health_parser_io_issues.hours_back 41 -param get_health_parser_io_issues.limit 38 -param get_health_parser_io_issues.server_name 28 -param get_health_parser_memory_broker.as_of 167 -param get_health_parser_memory_broker.hours_back 41 -param get_health_parser_memory_broker.limit 38 -param get_health_parser_memory_broker.server_name 28 -param get_health_parser_memory_conditions.as_of 167 -param get_health_parser_memory_conditions.hours_back 41 -param get_health_parser_memory_conditions.limit 38 -param get_health_parser_memory_conditions.server_name 28 -param get_health_parser_memory_node_oom.as_of 167 -param get_health_parser_memory_node_oom.hours_back 41 -param get_health_parser_memory_node_oom.limit 38 -param get_health_parser_memory_node_oom.server_name 28 -param get_health_parser_scheduler_issues.as_of 167 -param get_health_parser_scheduler_issues.hours_back 41 -param get_health_parser_scheduler_issues.limit 38 -param get_health_parser_scheduler_issues.server_name 28 -param get_health_parser_severe_errors.as_of 167 -param get_health_parser_severe_errors.hours_back 41 -param get_health_parser_severe_errors.limit 38 -param get_health_parser_severe_errors.server_name 28 -param get_health_parser_significant_waits.as_of 167 -param get_health_parser_significant_waits.hours_back 41 -param get_health_parser_significant_waits.limit 38 -param get_health_parser_significant_waits.server_name 28 -param get_health_parser_system_health.as_of 167 -param get_health_parser_system_health.hours_back 41 -param get_health_parser_system_health.limit 38 -param get_health_parser_system_health.server_name 28 -param get_index_usage.database_name 123 -param get_index_usage.limit 36 -param get_index_usage.server_name 28 -param get_latch_stats.as_of 167 -param get_latch_stats.hours_back 37 -param get_latch_stats.server_name 28 -param get_latch_stats.top 50 -param get_lock_wait_trend.as_of 167 -param get_lock_wait_trend.bucket_minutes 55 -param get_lock_wait_trend.hours_back 29 -param get_lock_wait_trend.server_name 28 -param get_long_query_completions.as_of 167 -param get_long_query_completions.hours_back 29 -param get_long_query_completions.limit 134 -param get_long_query_completions.server_name 28 -param get_memory_clerks.server_name 28 -param get_memory_grants.as_of 167 -param get_memory_grants.hours_back 120 -param get_memory_grants.server_name 28 -param get_memory_pressure_events.as_of 167 -param get_memory_pressure_events.hours_back 29 -param get_memory_pressure_events.server_name 28 -param get_memory_stats.server_name 28 -param get_memory_trend.as_of 167 -param get_memory_trend.bucket_minutes 55 -param get_memory_trend.hours_back 29 -param get_memory_trend.server_name 28 -param get_mute_rules.enabled_only 54 -param get_object_locking.server_name 28 -param get_oversized_plan_backlog.include_rows 152 -param get_oversized_plan_backlog.limit 69 -param get_oversized_plan_backlog.server_name 64 -param get_perfmon_stats.counter_name 61 -param get_perfmon_stats.instance_name 57 -param get_perfmon_stats.server_name 28 -param get_perfmon_trend.as_of 167 -param get_perfmon_trend.bucket_minutes 55 -param get_perfmon_trend.counter_name 50 -param get_perfmon_trend.hours_back 29 -param get_perfmon_trend.server_name 28 -param get_pg_autovacuum_health.as_of 167 -param get_pg_autovacuum_health.hours_back 83 -param get_pg_autovacuum_health.limit 261 -param get_pg_autovacuum_health.server_name 28 -param get_pg_blocking.as_of 167 -param get_pg_blocking.hours_back 40 -param get_pg_blocking.limit 66 -param get_pg_blocking.server_name 28 -param get_pg_buffer_usage.as_of 167 -param get_pg_buffer_usage.hours_back 40 -param get_pg_buffer_usage.limit 35 -param get_pg_buffer_usage.server_name 28 -param get_pg_column_stats.as_of 167 -param get_pg_column_stats.hours_back 78 -param get_pg_column_stats.limit 35 -param get_pg_column_stats.server_name 28 -param get_pg_cpu_utilization.as_of 167 -param get_pg_cpu_utilization.hours_back 28 -param get_pg_cpu_utilization.server_name 28 -param get_pg_database_stats.as_of 167 -param get_pg_database_stats.hours_back 40 -param get_pg_database_stats.limit 229 -param get_pg_database_stats.server_name 28 -param get_pg_database_trend.as_of 167 -param get_pg_database_trend.bucket_minutes 55 -param get_pg_database_trend.database 152 -param get_pg_database_trend.hours_back 29 -param get_pg_database_trend.server_name 28 -param get_pg_deadlock_detail.deadlock_hash 71 -param get_pg_deadlock_detail.limit 58 -param get_pg_deadlock_detail.server_name 28 -param get_pg_deadlocks.as_of 167 -param get_pg_deadlocks.hours_back 40 -param get_pg_deadlocks.limit 245 -param get_pg_deadlocks.server_name 28 -param get_pg_extensions.as_of 167 -param get_pg_extensions.database_name 803 -param get_pg_extensions.hours_back 78 -param get_pg_extensions.limit 35 -param get_pg_extensions.server_name 28 -param get_pg_index_bloat.answered_only 563 -param get_pg_index_bloat.as_of 167 -param get_pg_index_bloat.hours_back 78 -param get_pg_index_bloat.limit 285 -param get_pg_index_bloat.server_name 28 -param get_pg_index_usage.as_of 167 -param get_pg_index_usage.hours_back 147 -param get_pg_index_usage.limit 257 -param get_pg_index_usage.server_name 28 -param get_pg_io_stats.as_of 167 -param get_pg_io_stats.hours_back 40 -param get_pg_io_stats.limit 235 -param get_pg_io_stats.server_name 28 -param get_pg_io_trend.as_of 167 -param get_pg_io_trend.backend_type 205 -param get_pg_io_trend.bucket_minutes 55 -param get_pg_io_trend.context 194 -param get_pg_io_trend.hours_back 29 -param get_pg_io_trend.server_name 28 -param get_pg_kernel_stats.as_of 167 -param get_pg_kernel_stats.hours_back 40 -param get_pg_kernel_stats.limit 204 -param get_pg_kernel_stats.server_name 28 -param get_pg_lock_stats.as_of 167 -param get_pg_lock_stats.hours_back 40 -param get_pg_lock_stats.limit 35 -param get_pg_lock_stats.server_name 28 -param get_pg_log_events.as_of 167 -param get_pg_log_events.family 109 -param get_pg_log_events.hours_back 40 -param get_pg_log_events.limit 123 -param get_pg_log_events.min_severity 233 -param get_pg_log_events.server_name 28 -param get_pg_logging_audit.server_name 28 -param get_pg_plan_capture_readiness.as_of 167 -param get_pg_plan_capture_readiness.hours_back 162 -param get_pg_plan_capture_readiness.limit 389 -param get_pg_plan_capture_readiness.server_name 28 -param get_pg_plans.as_of 167 -param get_pg_plans.hours_back 40 -param get_pg_plans.limit 235 -param get_pg_plans.query_id 320 -param get_pg_plans.server_name 28 -param get_pg_predicate_stats.as_of 167 -param get_pg_predicate_stats.hours_back 40 -param get_pg_predicate_stats.limit 35 -param get_pg_predicate_stats.server_name 28 -param get_pg_query_duration_trend.as_of 167 -param get_pg_query_duration_trend.bucket_minutes 55 -param get_pg_query_duration_trend.hours_back 29 -param get_pg_query_duration_trend.queryid 143 -param get_pg_query_duration_trend.server_name 28 -param get_pg_replication_slots.as_of 167 -param get_pg_replication_slots.hours_back 76 -param get_pg_replication_slots.server_name 28 -param get_pg_replication_stats.as_of 167 -param get_pg_replication_stats.hours_back 40 -param get_pg_replication_stats.limit 35 -param get_pg_replication_stats.server_name 28 -param get_pg_server_config.include_defaults 249 -param get_pg_server_config.limit 198 -param get_pg_server_config.server_name 28 -param get_pg_server_config_changes.as_of 167 -param get_pg_server_config_changes.hours_back 52 -param get_pg_server_config_changes.limit 39 -param get_pg_server_config_changes.server_name 28 -param get_pg_session_states.as_of 167 -param get_pg_session_states.hours_back 40 -param get_pg_session_states.limit 282 -param get_pg_session_states.server_name 28 -param get_pg_table_bloat.as_of 167 -param get_pg_table_bloat.hours_back 115 -param get_pg_table_bloat.limit 270 -param get_pg_table_bloat.server_name 28 -param get_pg_top_queries.as_of 167 -param get_pg_top_queries.hours_back 40 -param get_pg_top_queries.limit 228 -param get_pg_top_queries.server_name 28 -param get_pg_wait_sampling.as_of 167 -param get_pg_wait_sampling.hours_back 40 -param get_pg_wait_sampling.limit 200 -param get_pg_wait_sampling.server_name 28 -param get_pg_wait_stats.as_of 167 -param get_pg_wait_stats.hours_back 40 -param get_pg_wait_stats.limit 214 -param get_pg_wait_stats.server_name 28 -param get_pg_wait_trend.as_of 167 -param get_pg_wait_trend.hours_back 29 -param get_pg_wait_trend.server_name 28 -param get_pg_wait_trend.wait_event 108 -param get_pg_wraparound_risk.as_of 167 -param get_pg_wraparound_risk.hours_back 70 -param get_pg_wraparound_risk.server_name 28 -param get_pg_write_stats.as_of 167 -param get_pg_write_stats.hours_back 40 -param get_pg_write_stats.server_name 28 -param get_pg_xmin_horizon.as_of 167 -param get_pg_xmin_horizon.hours_back 74 -param get_pg_xmin_horizon.server_name 28 -param get_plan_cache_bloat.as_of 167 -param get_plan_cache_bloat.hours_back 63 -param get_plan_cache_bloat.server_name 28 -param get_plan_corrections.as_of 167 -param get_plan_corrections.hours_back 29 -param get_plan_corrections.limit 156 -param get_plan_corrections.server_name 28 -param get_plan_xml.database_name 118 -param get_plan_xml.query_hash 49 -param get_plan_xml.server_name 28 -param get_procedure_duration_trend.as_of 167 -param get_procedure_duration_trend.bucket_minutes 55 -param get_procedure_duration_trend.hours_back 29 -param get_procedure_duration_trend.server_name 28 -param get_pvs_stats.server_name 28 -param get_pvs_stats.trend_hours_back 98 -param get_query_duration_trend.as_of 167 -param get_query_duration_trend.bucket_minutes 55 -param get_query_duration_trend.hours_back 29 -param get_query_duration_trend.server_name 28 -param get_query_heatmap.as_of 167 -param get_query_heatmap.bucket_minutes 160 -param get_query_heatmap.database_name 46 -param get_query_heatmap.hours_back 43 -param get_query_heatmap.limit 195 -param get_query_heatmap.metric 123 -param get_query_heatmap.server_name 28 -param get_query_store_clutter.as_of 167 -param get_query_store_clutter.hours_back 200 -param get_query_store_clutter.include_fleet_median 322 -param get_query_store_clutter.limit 170 -param get_query_store_clutter.server_name 28 -param get_query_store_duration_trend.as_of 167 -param get_query_store_duration_trend.hours_back 29 -param get_query_store_duration_trend.server_name 28 -param get_query_store_health.database_name 54 -param get_query_store_health.server_name 28 -param get_query_store_regressions.as_of 167 -param get_query_store_regressions.database_name 46 -param get_query_store_regressions.hours_back 110 -param get_query_store_regressions.limit 86 -param get_query_store_regressions.server_name 28 -param get_query_store_top.as_of 167 -param get_query_store_top.database_name 30 -param get_query_store_top.hours_back 29 -param get_query_store_top.server_name 28 -param get_query_store_top.top 34 -param get_query_trend.as_of 167 -param get_query_trend.database_name 39 -param get_query_trend.hours_back 29 -param get_query_trend.query_hash 72 -param get_query_trend.server_name 28 -param get_resource_semaphore.as_of 167 -param get_resource_semaphore.hours_back 121 -param get_resource_semaphore.server_name 28 -param get_running_jobs.server_name 28 -param get_server_config.server_name 28 -param get_server_config_changes.as_of 167 -param get_server_config_changes.hours_back 51 -param get_server_config_changes.server_name 28 -param get_server_properties.server_name 28 -param get_server_summary.server_name 71 -param get_session_stats.server_name 28 -param get_spinlock_stats.as_of 167 -param get_spinlock_stats.hours_back 37 -param get_spinlock_stats.server_name 28 -param get_spinlock_stats.top 46 -param get_store_log.as_of 167 -param get_store_log.hours_back 38 -param get_store_log.limit 52 -param get_store_metrics.days_back 100 -param get_store_metrics.limit 36 -param get_store_metrics.object_kind 139 -param get_store_metrics.object_name 113 -param get_store_query_stats.full_text 95 -param get_store_query_stats.order_by 165 -param get_store_query_stats.role 83 -param get_store_query_stats.top 42 -param get_sweep_reports.as_of 167 -param get_sweep_reports.hours_back 80 -param get_sweep_reports.sweep_id 178 -param get_sweep_reports.watch_state 132 -param get_table_index_sizes.server_name 28 -param get_tempdb_trend.as_of 167 -param get_tempdb_trend.bucket_minutes 55 -param get_tempdb_trend.hours_back 29 -param get_tempdb_trend.server_name 28 -param get_tool_guide.tools 98 -param get_tool_guide.topics 98 -param get_top_procedures_by_cpu.as_of 167 -param get_top_procedures_by_cpu.database_name 30 -param get_top_procedures_by_cpu.hours_back 29 -param get_top_procedures_by_cpu.server_name 28 -param get_top_procedures_by_cpu.top 37 -param get_top_queries_by_cpu.as_of 167 -param get_top_queries_by_cpu.database_name 30 -param get_top_queries_by_cpu.group_by 616 -param get_top_queries_by_cpu.hours_back 29 -param get_top_queries_by_cpu.min_dop 132 -param get_top_queries_by_cpu.parallel_only 371 -param get_top_queries_by_cpu.server_name 28 -param get_top_queries_by_cpu.top 34 -param get_trace_flag_changes.as_of 167 -param get_trace_flag_changes.hours_back 51 -param get_trace_flag_changes.server_name 28 -param get_trace_flags.server_name 28 -param get_wait_stats.as_of 167 -param get_wait_stats.hours_back 40 -param get_wait_stats.limit 145 -param get_wait_stats.server_name 28 -param get_wait_trend.as_of 167 -param get_wait_trend.bucket_minutes 55 -param get_wait_trend.hours_back 29 -param get_wait_trend.server_name 28 -param get_wait_trend.wait_type 56 -param get_wait_types.as_of 167 -param get_wait_types.hours_back 29 -param get_wait_types.server_name 28 -param get_waiting_tasks.as_of 167 -param get_waiting_tasks.hours_back 28 -param get_waiting_tasks.limit 141 -param get_waiting_tasks.server_name 28 -param mute_analysis_finding.reason 27 -param mute_analysis_finding.server_name 50 -param mute_analysis_finding.story_path_hash 45 -param remove_server.server_name 204 -param run_custom_view_panel.spec 103 -param set_mute_rule_enabled.enabled 88 -param set_mute_rule_enabled.rule_id 87 -param set_notification_route_enabled.enabled 85 -param set_notification_route_enabled.route_id 65 -param test_custom_alert_rule.definition 117 -param test_custom_alert_rule.rule_id 100 -param update_alert_settings.settings_json 199 -param update_custom_alert_rule.definition 113 -param update_custom_alert_rule.description 106 -param update_custom_alert_rule.enabled 101 -param update_custom_alert_rule.name 74 -param update_custom_alert_rule.rule_id 60 -param update_custom_alert_rule.version 122 -param update_custom_view.definition 87 -param update_custom_view.description 111 -param update_custom_view.name 43 -param update_custom_view.version 116 -param update_custom_view.view_id 54 -param update_mute_rule.changes_json 196 -param update_mute_rule.rule_id 74 -param validate_custom_alert_rule.definition 53 -param validate_custom_view.definition 53 -tool add_servers 3285 -tool analyze_plan_xml 880 -tool analyze_procedure_plan 926 -tool analyze_query_plan 915 -tool analyze_query_store_plan 914 -tool analyze_server 4160 -tool audit_config 932 -tool compare_analysis 1647 -tool create_custom_alert_rule 447 -tool create_custom_view 331 -tool create_mute_rule 959 -tool delete_custom_alert_rule 499 -tool delete_custom_view 158 -tool delete_mute_rule 332 -tool delete_notification_route 496 -tool describe_custom_view_catalog 2589 -tool get_active_queries 1406 -tool get_ag_health 1529 -tool get_alert_history 4740 -tool get_alert_settings 7530 -tool get_analysis_facts 1825 -tool get_analysis_findings 2875 -tool get_blocked_process_xml 465 -tool get_blocking 1645 -tool get_blocking_stats 468 -tool get_blocking_trend 289 -tool get_collection_health 21026 -tool get_collection_log 6995 -tool get_collector_cost 1773 -tool get_collector_stall_probes 2678 -tool get_cpu_scheduler_pressure 640 -tool get_cpu_utilization 249 -tool get_current_waits_trend 481 -tool get_custom_alert_rule 308 -tool get_custom_view 271 -tool get_daily_summary 1414 -tool get_daily_summary_range 1903 -tool get_database_config 448 -tool get_database_config_changes 464 -tool get_database_scoped_config 288 -tool get_database_sizes 480 -tool get_deadlock_detail 492 -tool get_deadlock_trend 168 -tool get_deadlocks 822 -tool get_default_trace_events 866 -tool get_file_io_stats 690 -tool get_file_io_trend 585 -tool get_fleet_overview 3287 -tool get_health_parser_cpu_tasks 452 -tool get_health_parser_io_issues 419 -tool get_health_parser_memory_broker 420 -tool get_health_parser_memory_conditions 442 -tool get_health_parser_memory_node_oom 360 -tool get_health_parser_scheduler_issues 446 -tool get_health_parser_severe_errors 480 -tool get_health_parser_significant_waits 615 -tool get_health_parser_system_health 393 -tool get_index_usage 856 -tool get_latch_stats 1027 -tool get_lock_wait_trend 646 -tool get_long_query_completions 1037 -tool get_memory_clerks 232 -tool get_memory_grants 988 -tool get_memory_pressure_events 835 -tool get_memory_stats 480 -tool get_memory_trend 610 -tool get_mute_rules 274 -tool get_notification_routes 1746 -tool get_object_locking 661 -tool get_oversized_plan_backlog 2997 -tool get_perfmon_stats 1034 -tool get_perfmon_trend 1057 -tool get_pg_autovacuum_health 2215 -tool get_pg_blocking 1606 -tool get_pg_buffer_usage 684 -tool get_pg_column_stats 1105 -tool get_pg_cpu_utilization 1341 -tool get_pg_database_stats 2361 -tool get_pg_database_trend 1373 -tool get_pg_deadlock_detail 759 -tool get_pg_deadlocks 2839 -tool get_pg_extensions 1727 -tool get_pg_index_bloat 3491 -tool get_pg_index_usage 1471 -tool get_pg_io_stats 2045 -tool get_pg_io_trend 1596 -tool get_pg_kernel_stats 1298 -tool get_pg_lock_stats 722 -tool get_pg_log_events 4199 -tool get_pg_logging_audit 2086 -tool get_pg_plan_capture_readiness 1950 -tool get_pg_plans 1374 -tool get_pg_predicate_stats 841 -tool get_pg_query_duration_trend 513 -tool get_pg_replication_slots 803 -tool get_pg_replication_stats 925 -tool get_pg_server_config 2252 -tool get_pg_server_config_changes 1146 -tool get_pg_session_states 1617 -tool get_pg_table_bloat 1191 -tool get_pg_top_queries 2210 -tool get_pg_wait_sampling 2032 -tool get_pg_wait_stats 1483 -tool get_pg_wait_trend 792 -tool get_pg_wraparound_risk 792 -tool get_pg_write_stats 1509 -tool get_pg_xmin_horizon 1294 -tool get_plan_cache_bloat 471 -tool get_plan_corrections 1672 -tool get_plan_xml 176 -tool get_procedure_duration_trend 1180 -tool get_pvs_stats 1156 -tool get_query_duration_trend 1172 -tool get_query_heatmap 799 -tool get_query_store_clutter 5147 -tool get_query_store_duration_trend 1537 -tool get_query_store_health 2587 -tool get_query_store_regressions 1187 -tool get_query_store_top 609 -tool get_query_trend 798 -tool get_resource_semaphore 1310 -tool get_running_jobs 642 -tool get_server_config 571 -tool get_server_config_changes 405 -tool get_server_properties 894 -tool get_server_summary 648 -tool get_session_stats 391 -tool get_spinlock_stats 597 -tool get_store_log 2426 -tool get_store_metrics 7681 -tool get_store_query_stats 984 -tool get_sweep_reports 2274 -tool get_table_index_sizes 794 -tool get_tempdb_trend 254 -tool get_tool_guide 487 -tool get_top_procedures_by_cpu 821 -tool get_top_queries_by_cpu 1837 -tool get_trace_flag_changes 335 -tool get_trace_flags 333 -tool get_wait_stats 644 -tool get_wait_trend 448 -tool get_wait_types 172 -tool get_waiting_tasks 751 -tool list_custom_alert_rules 394 -tool list_custom_alert_templates 751 -tool list_custom_views 313 -tool list_servers 1228 -tool mute_analysis_finding 1444 -tool remove_server 1784 -tool run_custom_view_panel 1084 -tool set_mute_rule_enabled 1477 -tool set_notification_route_enabled 841 -tool test_custom_alert_rule 922 -tool update_alert_settings 8123 -tool update_custom_alert_rule 890 -tool update_custom_view 785 -tool update_mute_rule 2530 -tool validate_custom_alert_rule 2078 -tool validate_custom_view 1576 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAgTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAgTools.txt new file mode 100644 index 0000000000..813e89b03b --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAgTools.txt @@ -0,0 +1,3 @@ +# DarlingMcpAgTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_ag_health 1529 +param get_ag_health.server_name 118 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAlertTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAlertTools.txt new file mode 100644 index 0000000000..e4732face9 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpAlertTools.txt @@ -0,0 +1,45 @@ +# DarlingMcpAlertTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool create_mute_rule 959 +param create_mute_rule.database_pattern 89 +param create_mute_rule.expires_at 121 +param create_mute_rule.job_name_pattern 79 +param create_mute_rule.metric_name 110 +param create_mute_rule.query_text_pattern 83 +param create_mute_rule.reason 60 +param create_mute_rule.server_name 101 +param create_mute_rule.wait_type_pattern 86 + +tool delete_mute_rule 332 +param delete_mute_rule.rule_id 76 + +tool delete_notification_route 496 +param delete_notification_route.route_id 54 + +tool get_alert_history 4740 +param get_alert_history.as_of 167 +param get_alert_history.hours_back 29 +param get_alert_history.include_dismissed 360 +param get_alert_history.limit 133 +param get_alert_history.server_name 90 + +tool get_alert_settings 7530 + +tool get_mute_rules 274 +param get_mute_rules.enabled_only 54 + +tool get_notification_routes 1746 + +tool set_mute_rule_enabled 1477 +param set_mute_rule_enabled.enabled 88 +param set_mute_rule_enabled.rule_id 87 + +tool set_notification_route_enabled 841 +param set_notification_route_enabled.enabled 85 +param set_notification_route_enabled.route_id 65 + +tool update_alert_settings 8123 +param update_alert_settings.settings_json 199 + +tool update_mute_rule 2530 +param update_mute_rule.changes_json 196 +param update_mute_rule.rule_id 74 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpBlockingTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpBlockingTools.txt new file mode 100644 index 0000000000..c35529d69c --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpBlockingTools.txt @@ -0,0 +1,43 @@ +# DarlingMcpBlockingTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_blocked_process_xml 465 +param get_blocked_process_xml.as_of 167 +param get_blocked_process_xml.hours_back 29 +param get_blocked_process_xml.limit 113 +param get_blocked_process_xml.server_name 28 + +tool get_blocking 1645 +param get_blocking.as_of 167 +param get_blocking.dedup_key 406 +param get_blocking.hours_back 29 +param get_blocking.limit 133 +param get_blocking.server_name 28 + +tool get_blocking_trend 289 +param get_blocking_trend.as_of 167 +param get_blocking_trend.hours_back 29 +param get_blocking_trend.server_name 28 + +tool get_deadlock_detail 492 +param get_deadlock_detail.as_of 167 +param get_deadlock_detail.dedup_key 272 +param get_deadlock_detail.hours_back 29 +param get_deadlock_detail.limit 119 +param get_deadlock_detail.server_name 28 + +tool get_deadlock_trend 168 +param get_deadlock_trend.as_of 167 +param get_deadlock_trend.hours_back 29 +param get_deadlock_trend.server_name 28 + +tool get_deadlocks 822 +param get_deadlocks.as_of 167 +param get_deadlocks.dedup_key 406 +param get_deadlocks.hours_back 29 +param get_deadlocks.limit 133 +param get_deadlocks.server_name 28 + +tool get_lock_wait_trend 646 +param get_lock_wait_trend.as_of 167 +param get_lock_wait_trend.bucket_minutes 55 +param get_lock_wait_trend.hours_back 29 +param get_lock_wait_trend.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCollectorCostTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCollectorCostTools.txt new file mode 100644 index 0000000000..5eb2ddbd7e --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCollectorCostTools.txt @@ -0,0 +1,4 @@ +# DarlingMcpCollectorCostTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_collector_cost 1773 +param get_collector_cost.collector_name 105 +param get_collector_cost.days_back 76 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigHistoryTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigHistoryTools.txt new file mode 100644 index 0000000000..cb8431b16d --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigHistoryTools.txt @@ -0,0 +1,23 @@ +# DarlingMcpConfigHistoryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_database_config_changes 464 +param get_database_config_changes.as_of 167 +param get_database_config_changes.hours_back 51 +param get_database_config_changes.server_name 28 + +tool get_database_scoped_config 288 +param get_database_scoped_config.database_name 54 +param get_database_scoped_config.server_name 28 + +tool get_query_store_health 2587 +param get_query_store_health.database_name 54 +param get_query_store_health.server_name 28 + +tool get_server_config_changes 405 +param get_server_config_changes.as_of 167 +param get_server_config_changes.hours_back 51 +param get_server_config_changes.server_name 28 + +tool get_trace_flag_changes 335 +param get_trace_flag_changes.as_of 167 +param get_trace_flag_changes.hours_back 51 +param get_trace_flag_changes.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigTools.txt new file mode 100644 index 0000000000..85c3d4dd32 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpConfigTools.txt @@ -0,0 +1,10 @@ +# DarlingMcpConfigTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_database_config 448 +param get_database_config.database_name 54 +param get_database_config.server_name 28 + +tool get_server_config 571 +param get_server_config.server_name 28 + +tool get_trace_flags 333 +param get_trace_flags.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomAlertTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomAlertTools.txt new file mode 100644 index 0000000000..729d73f610 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomAlertTools.txt @@ -0,0 +1,31 @@ +# DarlingMcpCustomAlertTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool create_custom_alert_rule 447 +param create_custom_alert_rule.definition 137 +param create_custom_alert_rule.description 36 +param create_custom_alert_rule.enabled 98 +param create_custom_alert_rule.name 60 + +tool delete_custom_alert_rule 499 +param delete_custom_alert_rule.rule_id 60 + +tool get_custom_alert_rule 308 +param get_custom_alert_rule.rule_id 67 + +tool list_custom_alert_rules 394 + +tool list_custom_alert_templates 751 + +tool test_custom_alert_rule 922 +param test_custom_alert_rule.definition 117 +param test_custom_alert_rule.rule_id 100 + +tool update_custom_alert_rule 890 +param update_custom_alert_rule.definition 113 +param update_custom_alert_rule.description 106 +param update_custom_alert_rule.enabled 101 +param update_custom_alert_rule.name 74 +param update_custom_alert_rule.rule_id 60 +param update_custom_alert_rule.version 122 + +tool validate_custom_alert_rule 2078 +param validate_custom_alert_rule.definition 53 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomViewTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomViewTools.txt new file mode 100644 index 0000000000..d12c15bdf7 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpCustomViewTools.txt @@ -0,0 +1,28 @@ +# DarlingMcpCustomViewTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool create_custom_view 331 +param create_custom_view.definition 149 +param create_custom_view.description 36 +param create_custom_view.name 60 + +tool delete_custom_view 158 +param delete_custom_view.view_id 54 + +tool describe_custom_view_catalog 2589 + +tool get_custom_view 271 +param get_custom_view.view_id 61 + +tool list_custom_views 313 + +tool run_custom_view_panel 1084 +param run_custom_view_panel.spec 103 + +tool update_custom_view 785 +param update_custom_view.definition 87 +param update_custom_view.description 111 +param update_custom_view.name 43 +param update_custom_view.version 116 +param update_custom_view.view_id 54 + +tool validate_custom_view 1576 +param validate_custom_view.definition 53 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt new file mode 100644 index 0000000000..da470203cc --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt @@ -0,0 +1,96 @@ +# DarlingMcpDataTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_blocking_stats 468 +param get_blocking_stats.as_of 167 +param get_blocking_stats.hours_back 191 +param get_blocking_stats.server_name 28 + +tool get_collection_health 21026 +param get_collection_health.server_name 28 + +tool get_collection_log 6995 +param get_collection_log.as_of 167 +param get_collection_log.collector_name 244 +param get_collection_log.hours_back 191 +param get_collection_log.limit 120 +param get_collection_log.min_duration_ms 382 +param get_collection_log.server_name 96 +param get_collection_log.status 481 + +tool get_cpu_utilization 249 +param get_cpu_utilization.as_of 167 +param get_cpu_utilization.bucket_minutes 55 +param get_cpu_utilization.hours_back 28 +param get_cpu_utilization.server_name 28 + +tool get_current_waits_trend 481 +param get_current_waits_trend.as_of 167 +param get_current_waits_trend.database_name 73 +param get_current_waits_trend.hours_back 190 +param get_current_waits_trend.server_name 28 + +tool get_file_io_stats 690 +param get_file_io_stats.server_name 28 + +tool get_memory_clerks 232 +param get_memory_clerks.server_name 28 + +tool get_memory_stats 480 +param get_memory_stats.server_name 28 + +tool get_perfmon_stats 1034 +param get_perfmon_stats.counter_name 61 +param get_perfmon_stats.instance_name 57 +param get_perfmon_stats.server_name 28 + +tool get_query_store_top 609 +param get_query_store_top.as_of 167 +param get_query_store_top.database_name 30 +param get_query_store_top.hours_back 29 +param get_query_store_top.server_name 28 +param get_query_store_top.top 34 + +tool get_server_properties 894 +param get_server_properties.server_name 28 + +tool get_tempdb_trend 254 +param get_tempdb_trend.as_of 167 +param get_tempdb_trend.bucket_minutes 55 +param get_tempdb_trend.hours_back 29 +param get_tempdb_trend.server_name 28 + +tool get_top_procedures_by_cpu 821 +param get_top_procedures_by_cpu.as_of 167 +param get_top_procedures_by_cpu.database_name 30 +param get_top_procedures_by_cpu.hours_back 29 +param get_top_procedures_by_cpu.server_name 28 +param get_top_procedures_by_cpu.top 37 + +tool get_top_queries_by_cpu 1837 +param get_top_queries_by_cpu.as_of 167 +param get_top_queries_by_cpu.database_name 30 +param get_top_queries_by_cpu.group_by 616 +param get_top_queries_by_cpu.hours_back 29 +param get_top_queries_by_cpu.min_dop 132 +param get_top_queries_by_cpu.parallel_only 371 +param get_top_queries_by_cpu.server_name 28 +param get_top_queries_by_cpu.top 34 + +tool get_wait_stats 644 +param get_wait_stats.as_of 167 +param get_wait_stats.hours_back 40 +param get_wait_stats.limit 145 +param get_wait_stats.server_name 28 + +tool get_wait_trend 448 +param get_wait_trend.as_of 167 +param get_wait_trend.bucket_minutes 55 +param get_wait_trend.hours_back 29 +param get_wait_trend.server_name 28 +param get_wait_trend.wait_type 56 + +tool get_wait_types 172 +param get_wait_types.as_of 167 +param get_wait_types.hours_back 29 +param get_wait_types.server_name 28 + +tool list_servers 1228 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDefaultTraceTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDefaultTraceTools.txt new file mode 100644 index 0000000000..1a96c3fd3f --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDefaultTraceTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpDefaultTraceTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_default_trace_events 866 +param get_default_trace_events.as_of 167 +param get_default_trace_events.hours_back 41 +param get_default_trace_events.limit 48 +param get_default_trace_events.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetSweepTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetSweepTools.txt new file mode 100644 index 0000000000..a294b130e6 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetSweepTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpFleetSweepTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_sweep_reports 2274 +param get_sweep_reports.as_of 167 +param get_sweep_reports.hours_back 80 +param get_sweep_reports.sweep_id 178 +param get_sweep_reports.watch_state 132 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetTools.txt new file mode 100644 index 0000000000..ce3310198c --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpFleetTools.txt @@ -0,0 +1,3 @@ +# DarlingMcpFleetTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_fleet_overview 3287 +param get_fleet_overview.hours_back 96 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthParserTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthParserTools.txt new file mode 100644 index 0000000000..10e99f6ce5 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthParserTools.txt @@ -0,0 +1,54 @@ +# DarlingMcpHealthParserTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_health_parser_cpu_tasks 452 +param get_health_parser_cpu_tasks.as_of 167 +param get_health_parser_cpu_tasks.hours_back 41 +param get_health_parser_cpu_tasks.limit 38 +param get_health_parser_cpu_tasks.server_name 28 + +tool get_health_parser_io_issues 419 +param get_health_parser_io_issues.as_of 167 +param get_health_parser_io_issues.hours_back 41 +param get_health_parser_io_issues.limit 38 +param get_health_parser_io_issues.server_name 28 + +tool get_health_parser_memory_broker 420 +param get_health_parser_memory_broker.as_of 167 +param get_health_parser_memory_broker.hours_back 41 +param get_health_parser_memory_broker.limit 38 +param get_health_parser_memory_broker.server_name 28 + +tool get_health_parser_memory_conditions 442 +param get_health_parser_memory_conditions.as_of 167 +param get_health_parser_memory_conditions.hours_back 41 +param get_health_parser_memory_conditions.limit 38 +param get_health_parser_memory_conditions.server_name 28 + +tool get_health_parser_memory_node_oom 360 +param get_health_parser_memory_node_oom.as_of 167 +param get_health_parser_memory_node_oom.hours_back 41 +param get_health_parser_memory_node_oom.limit 38 +param get_health_parser_memory_node_oom.server_name 28 + +tool get_health_parser_scheduler_issues 446 +param get_health_parser_scheduler_issues.as_of 167 +param get_health_parser_scheduler_issues.hours_back 41 +param get_health_parser_scheduler_issues.limit 38 +param get_health_parser_scheduler_issues.server_name 28 + +tool get_health_parser_severe_errors 480 +param get_health_parser_severe_errors.as_of 167 +param get_health_parser_severe_errors.hours_back 41 +param get_health_parser_severe_errors.limit 38 +param get_health_parser_severe_errors.server_name 28 + +tool get_health_parser_significant_waits 615 +param get_health_parser_significant_waits.as_of 167 +param get_health_parser_significant_waits.hours_back 41 +param get_health_parser_significant_waits.limit 38 +param get_health_parser_significant_waits.server_name 28 + +tool get_health_parser_system_health 393 +param get_health_parser_system_health.as_of 167 +param get_health_parser_system_health.hours_back 41 +param get_health_parser_system_health.limit 38 +param get_health_parser_system_health.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthTools.txt new file mode 100644 index 0000000000..ab6f34aaf1 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpHealthTools.txt @@ -0,0 +1,12 @@ +# DarlingMcpHealthTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_daily_summary 1414 +param get_daily_summary.server_name 28 +param get_daily_summary.summary_date 155 + +tool get_daily_summary_range 1903 +param get_daily_summary_range.as_of 166 +param get_daily_summary_range.days_back 84 +param get_daily_summary_range.server_name 28 + +tool get_server_summary 648 +param get_server_summary.server_name 71 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpJobTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpJobTools.txt new file mode 100644 index 0000000000..1adf1c1c4a --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpJobTools.txt @@ -0,0 +1,3 @@ +# DarlingMcpJobTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_running_jobs 642 +param get_running_jobs.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLatchSpinlockTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLatchSpinlockTools.txt new file mode 100644 index 0000000000..008190584e --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLatchSpinlockTools.txt @@ -0,0 +1,12 @@ +# DarlingMcpLatchSpinlockTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_latch_stats 1027 +param get_latch_stats.as_of 167 +param get_latch_stats.hours_back 37 +param get_latch_stats.server_name 28 +param get_latch_stats.top 50 + +tool get_spinlock_stats 597 +param get_spinlock_stats.as_of 167 +param get_spinlock_stats.hours_back 37 +param get_spinlock_stats.server_name 28 +param get_spinlock_stats.top 46 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLongQueryTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLongQueryTools.txt new file mode 100644 index 0000000000..5af007fb21 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLongQueryTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpLongQueryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_long_query_completions 1037 +param get_long_query_completions.as_of 167 +param get_long_query_completions.hours_back 29 +param get_long_query_completions.limit 134 +param get_long_query_completions.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpMemoryGrantTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpMemoryGrantTools.txt new file mode 100644 index 0000000000..798c49f090 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpMemoryGrantTools.txt @@ -0,0 +1,15 @@ +# DarlingMcpMemoryGrantTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_memory_grants 988 +param get_memory_grants.as_of 167 +param get_memory_grants.hours_back 120 +param get_memory_grants.server_name 28 + +tool get_memory_pressure_events 835 +param get_memory_pressure_events.as_of 167 +param get_memory_pressure_events.hours_back 29 +param get_memory_pressure_events.server_name 28 + +tool get_resource_semaphore 1310 +param get_resource_semaphore.as_of 167 +param get_resource_semaphore.hours_back 121 +param get_resource_semaphore.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpObjectStatsTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpObjectStatsTools.txt new file mode 100644 index 0000000000..cfcfd167c4 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpObjectStatsTools.txt @@ -0,0 +1,14 @@ +# DarlingMcpObjectStatsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_database_sizes 480 +param get_database_sizes.server_name 28 + +tool get_index_usage 856 +param get_index_usage.database_name 123 +param get_index_usage.limit 36 +param get_index_usage.server_name 28 + +tool get_object_locking 661 +param get_object_locking.server_name 28 + +tool get_table_index_sizes 794 +param get_table_index_sizes.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpOversizedPlanBacklogTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpOversizedPlanBacklogTools.txt new file mode 100644 index 0000000000..a18657f52b --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpOversizedPlanBacklogTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpOversizedPlanBacklogTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_oversized_plan_backlog 2997 +param get_oversized_plan_backlog.include_rows 152 +param get_oversized_plan_backlog.limit 69 +param get_oversized_plan_backlog.server_name 64 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgAutovacuumTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgAutovacuumTools.txt new file mode 100644 index 0000000000..cb7ccc74e3 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgAutovacuumTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgAutovacuumTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_autovacuum_health 2215 +param get_pg_autovacuum_health.as_of 167 +param get_pg_autovacuum_health.hours_back 83 +param get_pg_autovacuum_health.limit 261 +param get_pg_autovacuum_health.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgBlockingTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgBlockingTools.txt new file mode 100644 index 0000000000..736d83ce11 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgBlockingTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgBlockingTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_blocking 1606 +param get_pg_blocking.as_of 167 +param get_pg_blocking.hours_back 40 +param get_pg_blocking.limit 66 +param get_pg_blocking.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgCpuUtilizationTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgCpuUtilizationTools.txt new file mode 100644 index 0000000000..6fc50f59ba --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgCpuUtilizationTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpPgCpuUtilizationTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_cpu_utilization 1341 +param get_pg_cpu_utilization.as_of 167 +param get_pg_cpu_utilization.hours_back 28 +param get_pg_cpu_utilization.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDatabaseTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDatabaseTools.txt new file mode 100644 index 0000000000..d2b79acedf --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDatabaseTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgDatabaseTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_database_stats 2361 +param get_pg_database_stats.as_of 167 +param get_pg_database_stats.hours_back 40 +param get_pg_database_stats.limit 229 +param get_pg_database_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDeadlockTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDeadlockTools.txt new file mode 100644 index 0000000000..2b831685ba --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgDeadlockTools.txt @@ -0,0 +1,11 @@ +# DarlingMcpPgDeadlockTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_deadlock_detail 759 +param get_pg_deadlock_detail.deadlock_hash 71 +param get_pg_deadlock_detail.limit 58 +param get_pg_deadlock_detail.server_name 28 + +tool get_pg_deadlocks 2839 +param get_pg_deadlocks.as_of 167 +param get_pg_deadlocks.hours_back 40 +param get_pg_deadlocks.limit 245 +param get_pg_deadlocks.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexTools.txt new file mode 100644 index 0000000000..ba8ae9ee36 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexTools.txt @@ -0,0 +1,13 @@ +# DarlingMcpPgIndexTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_column_stats 1105 +param get_pg_column_stats.as_of 167 +param get_pg_column_stats.hours_back 78 +param get_pg_column_stats.limit 35 +param get_pg_column_stats.server_name 28 + +tool get_pg_index_bloat 3491 +param get_pg_index_bloat.answered_only 563 +param get_pg_index_bloat.as_of 167 +param get_pg_index_bloat.hours_back 78 +param get_pg_index_bloat.limit 285 +param get_pg_index_bloat.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexUsageTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexUsageTools.txt new file mode 100644 index 0000000000..8e0527ef88 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIndexUsageTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgIndexUsageTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_index_usage 1471 +param get_pg_index_usage.as_of 167 +param get_pg_index_usage.hours_back 147 +param get_pg_index_usage.limit 257 +param get_pg_index_usage.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIoTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIoTools.txt new file mode 100644 index 0000000000..6f1ecdad96 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgIoTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgIoTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_io_stats 2045 +param get_pg_io_stats.as_of 167 +param get_pg_io_stats.hours_back 40 +param get_pg_io_stats.limit 235 +param get_pg_io_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgKernelStatsTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgKernelStatsTools.txt new file mode 100644 index 0000000000..f650767e43 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgKernelStatsTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgKernelStatsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_kernel_stats 1298 +param get_pg_kernel_stats.as_of 167 +param get_pg_kernel_stats.hours_back 40 +param get_pg_kernel_stats.limit 204 +param get_pg_kernel_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLogEventTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLogEventTools.txt new file mode 100644 index 0000000000..1e2bb3a81c --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLogEventTools.txt @@ -0,0 +1,8 @@ +# DarlingMcpPgLogEventTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_log_events 4199 +param get_pg_log_events.as_of 167 +param get_pg_log_events.family 109 +param get_pg_log_events.hours_back 40 +param get_pg_log_events.limit 123 +param get_pg_log_events.min_severity 233 +param get_pg_log_events.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLoggingAuditTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLoggingAuditTools.txt new file mode 100644 index 0000000000..ad283b5816 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgLoggingAuditTools.txt @@ -0,0 +1,3 @@ +# DarlingMcpPgLoggingAuditTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_logging_audit 2086 +param get_pg_logging_audit.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPlanTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPlanTools.txt new file mode 100644 index 0000000000..077fcb1971 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPlanTools.txt @@ -0,0 +1,13 @@ +# DarlingMcpPgPlanTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_plan_capture_readiness 1950 +param get_pg_plan_capture_readiness.as_of 167 +param get_pg_plan_capture_readiness.hours_back 162 +param get_pg_plan_capture_readiness.limit 389 +param get_pg_plan_capture_readiness.server_name 28 + +tool get_pg_plans 1374 +param get_pg_plans.as_of 167 +param get_pg_plans.hours_back 40 +param get_pg_plans.limit 235 +param get_pg_plans.query_id 320 +param get_pg_plans.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPredicateTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPredicateTools.txt new file mode 100644 index 0000000000..58e615f304 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgPredicateTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgPredicateTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_predicate_stats 841 +param get_pg_predicate_stats.as_of 167 +param get_pg_predicate_stats.hours_back 40 +param get_pg_predicate_stats.limit 35 +param get_pg_predicate_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgReplicationStatsTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgReplicationStatsTools.txt new file mode 100644 index 0000000000..5f0c353764 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgReplicationStatsTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgReplicationStatsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_replication_stats 925 +param get_pg_replication_stats.as_of 167 +param get_pg_replication_stats.hours_back 40 +param get_pg_replication_stats.limit 35 +param get_pg_replication_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgServerStateTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgServerStateTools.txt new file mode 100644 index 0000000000..8b19eb2e6f --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgServerStateTools.txt @@ -0,0 +1,35 @@ +# DarlingMcpPgServerStateTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_buffer_usage 684 +param get_pg_buffer_usage.as_of 167 +param get_pg_buffer_usage.hours_back 40 +param get_pg_buffer_usage.limit 35 +param get_pg_buffer_usage.server_name 28 + +tool get_pg_extensions 1727 +param get_pg_extensions.as_of 167 +param get_pg_extensions.database_name 803 +param get_pg_extensions.hours_back 78 +param get_pg_extensions.limit 35 +param get_pg_extensions.server_name 28 + +tool get_pg_lock_stats 722 +param get_pg_lock_stats.as_of 167 +param get_pg_lock_stats.hours_back 40 +param get_pg_lock_stats.limit 35 +param get_pg_lock_stats.server_name 28 + +tool get_pg_server_config 2252 +param get_pg_server_config.include_defaults 249 +param get_pg_server_config.limit 198 +param get_pg_server_config.server_name 28 + +tool get_pg_server_config_changes 1146 +param get_pg_server_config_changes.as_of 167 +param get_pg_server_config_changes.hours_back 52 +param get_pg_server_config_changes.limit 39 +param get_pg_server_config_changes.server_name 28 + +tool get_pg_write_stats 1509 +param get_pg_write_stats.as_of 167 +param get_pg_write_stats.hours_back 40 +param get_pg_write_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSessionStatesTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSessionStatesTools.txt new file mode 100644 index 0000000000..6e747234c7 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSessionStatesTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgSessionStatesTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_session_states 1617 +param get_pg_session_states.as_of 167 +param get_pg_session_states.hours_back 40 +param get_pg_session_states.limit 282 +param get_pg_session_states.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSlotTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSlotTools.txt new file mode 100644 index 0000000000..940e9d8d46 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgSlotTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpPgSlotTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_replication_slots 803 +param get_pg_replication_slots.as_of 167 +param get_pg_replication_slots.hours_back 76 +param get_pg_replication_slots.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgStatementTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgStatementTools.txt new file mode 100644 index 0000000000..f753a8e5aa --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgStatementTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgStatementTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_top_queries 2210 +param get_pg_top_queries.as_of 167 +param get_pg_top_queries.hours_back 40 +param get_pg_top_queries.limit 228 +param get_pg_top_queries.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTableBloatTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTableBloatTools.txt new file mode 100644 index 0000000000..c55f577dae --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTableBloatTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgTableBloatTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_table_bloat 1191 +param get_pg_table_bloat.as_of 167 +param get_pg_table_bloat.hours_back 115 +param get_pg_table_bloat.limit 270 +param get_pg_table_bloat.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTrendTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTrendTools.txt new file mode 100644 index 0000000000..0998fd0865 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgTrendTools.txt @@ -0,0 +1,28 @@ +# DarlingMcpPgTrendTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_database_trend 1373 +param get_pg_database_trend.as_of 167 +param get_pg_database_trend.bucket_minutes 55 +param get_pg_database_trend.database 152 +param get_pg_database_trend.hours_back 29 +param get_pg_database_trend.server_name 28 + +tool get_pg_io_trend 1596 +param get_pg_io_trend.as_of 167 +param get_pg_io_trend.backend_type 205 +param get_pg_io_trend.bucket_minutes 55 +param get_pg_io_trend.context 194 +param get_pg_io_trend.hours_back 29 +param get_pg_io_trend.server_name 28 + +tool get_pg_query_duration_trend 513 +param get_pg_query_duration_trend.as_of 167 +param get_pg_query_duration_trend.bucket_minutes 55 +param get_pg_query_duration_trend.hours_back 29 +param get_pg_query_duration_trend.queryid 143 +param get_pg_query_duration_trend.server_name 28 + +tool get_pg_wait_trend 792 +param get_pg_wait_trend.as_of 167 +param get_pg_wait_trend.hours_back 29 +param get_pg_wait_trend.server_name 28 +param get_pg_wait_trend.wait_event 108 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitSamplingTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitSamplingTools.txt new file mode 100644 index 0000000000..ef3f2c6696 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitSamplingTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgWaitSamplingTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_wait_sampling 2032 +param get_pg_wait_sampling.as_of 167 +param get_pg_wait_sampling.hours_back 40 +param get_pg_wait_sampling.limit 200 +param get_pg_wait_sampling.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitTools.txt new file mode 100644 index 0000000000..d30922509a --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWaitTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPgWaitTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_wait_stats 1483 +param get_pg_wait_stats.as_of 167 +param get_pg_wait_stats.hours_back 40 +param get_pg_wait_stats.limit 214 +param get_pg_wait_stats.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWraparoundTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWraparoundTools.txt new file mode 100644 index 0000000000..1506098711 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgWraparoundTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpPgWraparoundTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_wraparound_risk 792 +param get_pg_wraparound_risk.as_of 167 +param get_pg_wraparound_risk.hours_back 70 +param get_pg_wraparound_risk.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgXminTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgXminTools.txt new file mode 100644 index 0000000000..1011490f4e --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPgXminTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpPgXminTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pg_xmin_horizon 1294 +param get_pg_xmin_horizon.as_of 167 +param get_pg_xmin_horizon.hours_back 74 +param get_pg_xmin_horizon.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCacheSchedulerTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCacheSchedulerTools.txt new file mode 100644 index 0000000000..dd11984ff0 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCacheSchedulerTools.txt @@ -0,0 +1,10 @@ +# DarlingMcpPlanCacheSchedulerTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_cpu_scheduler_pressure 640 +param get_cpu_scheduler_pressure.as_of 167 +param get_cpu_scheduler_pressure.hours_back 63 +param get_cpu_scheduler_pressure.server_name 28 + +tool get_plan_cache_bloat 471 +param get_plan_cache_bloat.as_of 167 +param get_plan_cache_bloat.hours_back 63 +param get_plan_cache_bloat.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCorrectionTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCorrectionTools.txt new file mode 100644 index 0000000000..79380fc028 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCorrectionTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpPlanCorrectionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_plan_corrections 1672 +param get_plan_corrections.as_of 167 +param get_plan_corrections.hours_back 29 +param get_plan_corrections.limit 156 +param get_plan_corrections.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanTools.txt new file mode 100644 index 0000000000..f564b5dfde --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanTools.txt @@ -0,0 +1,23 @@ +# DarlingMcpPlanTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool analyze_plan_xml 880 +param analyze_plan_xml.plan_xml 25 + +tool analyze_procedure_plan 926 +param analyze_procedure_plan.server_name 28 +param analyze_procedure_plan.sql_handle 52 + +tool analyze_query_plan 915 +param analyze_query_plan.database_name 118 +param analyze_query_plan.query_hash 49 +param analyze_query_plan.server_name 28 + +tool analyze_query_store_plan 914 +param analyze_query_store_plan.database_name 43 +param analyze_query_store_plan.plan_id 107 +param analyze_query_store_plan.query_id 38 +param analyze_query_store_plan.server_name 28 + +tool get_plan_xml 176 +param get_plan_xml.database_name 118 +param get_plan_xml.query_hash 49 +param get_plan_xml.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPvsTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPvsTools.txt new file mode 100644 index 0000000000..0a33d418f0 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPvsTools.txt @@ -0,0 +1,4 @@ +# DarlingMcpPvsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pvs_stats 1156 +param get_pvs_stats.server_name 28 +param get_pvs_stats.trend_hours_back 98 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryHeatmapTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryHeatmapTools.txt new file mode 100644 index 0000000000..dc82523917 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryHeatmapTools.txt @@ -0,0 +1,9 @@ +# DarlingMcpQueryHeatmapTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_query_heatmap 799 +param get_query_heatmap.as_of 167 +param get_query_heatmap.bucket_minutes 160 +param get_query_heatmap.database_name 46 +param get_query_heatmap.hours_back 43 +param get_query_heatmap.limit 195 +param get_query_heatmap.metric 123 +param get_query_heatmap.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreClutterTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreClutterTools.txt new file mode 100644 index 0000000000..4c86e24d73 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreClutterTools.txt @@ -0,0 +1,7 @@ +# DarlingMcpQueryStoreClutterTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_query_store_clutter 5147 +param get_query_store_clutter.as_of 167 +param get_query_store_clutter.hours_back 200 +param get_query_store_clutter.include_fleet_median 322 +param get_query_store_clutter.limit 170 +param get_query_store_clutter.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreRegressionTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreRegressionTools.txt new file mode 100644 index 0000000000..82601f598c --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreRegressionTools.txt @@ -0,0 +1,7 @@ +# DarlingMcpQueryStoreRegressionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_query_store_regressions 1187 +param get_query_store_regressions.as_of 167 +param get_query_store_regressions.database_name 46 +param get_query_store_regressions.hours_back 110 +param get_query_store_regressions.limit 86 +param get_query_store_regressions.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpServerAdminTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpServerAdminTools.txt new file mode 100644 index 0000000000..d8f41abc84 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpServerAdminTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpServerAdminTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool add_servers 3285 +param add_servers.servers_json 395 + +tool remove_server 1784 +param remove_server.server_name 204 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpSessionTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpSessionTools.txt new file mode 100644 index 0000000000..ddb8c8a8e3 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpSessionTools.txt @@ -0,0 +1,17 @@ +# DarlingMcpSessionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_active_queries 1406 +param get_active_queries.as_of 167 +param get_active_queries.blocking_only 238 +param get_active_queries.database_name 156 +param get_active_queries.hours_back 37 +param get_active_queries.limit 149 +param get_active_queries.server_name 28 + +tool get_session_stats 391 +param get_session_stats.server_name 28 + +tool get_waiting_tasks 751 +param get_waiting_tasks.as_of 167 +param get_waiting_tasks.hours_back 28 +param get_waiting_tasks.limit 141 +param get_waiting_tasks.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStallProbeTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStallProbeTools.txt new file mode 100644 index 0000000000..a182312c0d --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStallProbeTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpStallProbeTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_collector_stall_probes 2678 +param get_collector_stall_probes.days_back 64 +param get_collector_stall_probes.limit 42 +param get_collector_stall_probes.server_name 64 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreLogTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreLogTools.txt new file mode 100644 index 0000000000..988339c050 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreLogTools.txt @@ -0,0 +1,5 @@ +# DarlingMcpStoreLogTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_store_log 2426 +param get_store_log.as_of 167 +param get_store_log.hours_back 38 +param get_store_log.limit 52 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreMetricsTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreMetricsTools.txt new file mode 100644 index 0000000000..64fcb62dad --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreMetricsTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpStoreMetricsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_store_metrics 7681 +param get_store_metrics.days_back 100 +param get_store_metrics.limit 36 +param get_store_metrics.object_kind 139 +param get_store_metrics.object_name 113 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreQueryStatsTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreQueryStatsTools.txt new file mode 100644 index 0000000000..aaee5403b9 --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpStoreQueryStatsTools.txt @@ -0,0 +1,6 @@ +# DarlingMcpStoreQueryStatsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_store_query_stats 984 +param get_store_query_stats.full_text 95 +param get_store_query_stats.order_by 165 +param get_store_query_stats.role 83 +param get_store_query_stats.top 42 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpToolGuideTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpToolGuideTools.txt new file mode 100644 index 0000000000..141a108ede --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpToolGuideTools.txt @@ -0,0 +1,4 @@ +# DarlingMcpToolGuideTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_tool_guide 487 +param get_tool_guide.tools 98 +param get_tool_guide.topics 98 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTools.txt new file mode 100644 index 0000000000..170104ca3d --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTools.txt @@ -0,0 +1,32 @@ +# DarlingMcpTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool analyze_server 4160 +param analyze_server.as_of 167 +param analyze_server.hours_back 104 +param analyze_server.server_name 28 + +tool audit_config 932 +param audit_config.server_name 28 + +tool compare_analysis 1647 +param compare_analysis.as_of 167 +param compare_analysis.baseline_hours_back 211 +param compare_analysis.hours_back 57 +param compare_analysis.server_name 28 + +tool get_analysis_facts 1825 +param get_analysis_facts.as_of 167 +param get_analysis_facts.hours_back 36 +param get_analysis_facts.min_severity 90 +param get_analysis_facts.server_name 28 +param get_analysis_facts.source 515 + +tool get_analysis_findings 2875 +param get_analysis_findings.as_of 167 +param get_analysis_findings.hours_back 49 +param get_analysis_findings.include_drilldown 235 +param get_analysis_findings.server_name 28 + +tool mute_analysis_finding 1444 +param mute_analysis_finding.reason 27 +param mute_analysis_finding.server_name 50 +param mute_analysis_finding.story_path_hash 45 diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTrendTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTrendTools.txt new file mode 100644 index 0000000000..f72a7b913e --- /dev/null +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTrendTools.txt @@ -0,0 +1,44 @@ +# DarlingMcpTrendTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_file_io_trend 585 +param get_file_io_trend.as_of 167 +param get_file_io_trend.bucket_minutes 55 +param get_file_io_trend.database_name 56 +param get_file_io_trend.hours_back 29 +param get_file_io_trend.server_name 28 + +tool get_memory_trend 610 +param get_memory_trend.as_of 167 +param get_memory_trend.bucket_minutes 55 +param get_memory_trend.hours_back 29 +param get_memory_trend.server_name 28 + +tool get_perfmon_trend 1057 +param get_perfmon_trend.as_of 167 +param get_perfmon_trend.bucket_minutes 55 +param get_perfmon_trend.counter_name 50 +param get_perfmon_trend.hours_back 29 +param get_perfmon_trend.server_name 28 + +tool get_procedure_duration_trend 1180 +param get_procedure_duration_trend.as_of 167 +param get_procedure_duration_trend.bucket_minutes 55 +param get_procedure_duration_trend.hours_back 29 +param get_procedure_duration_trend.server_name 28 + +tool get_query_duration_trend 1172 +param get_query_duration_trend.as_of 167 +param get_query_duration_trend.bucket_minutes 55 +param get_query_duration_trend.hours_back 29 +param get_query_duration_trend.server_name 28 + +tool get_query_store_duration_trend 1537 +param get_query_store_duration_trend.as_of 167 +param get_query_store_duration_trend.hours_back 29 +param get_query_store_duration_trend.server_name 28 + +tool get_query_trend 798 +param get_query_trend.as_of 167 +param get_query_trend.database_name 39 +param get_query_trend.hours_back 29 +param get_query_trend.query_hash 72 +param get_query_trend.server_name 28 diff --git a/Darling/Darling.Tests/McpToolsListBudgetTests.cs b/Darling/Darling.Tests/McpToolsListBudgetTests.cs index 02668bc767..08ed30d085 100644 --- a/Darling/Darling.Tests/McpToolsListBudgetTests.cs +++ b/Darling/Darling.Tests/McpToolsListBudgetTests.cs @@ -33,12 +33,25 @@ namespace Darling.Tests; /// same WithGeminiCompatibleTools path the host registers every tool with, over the tool types the host /// source registers. Lite's twin is Lite.Tests/McpToolsListBudgetTests (D6 lockstep). /// -/// The pins are ceilings that only go down. McpToolsListBudget.txt holds one line per tool -/// (its served description's length) and per advertised parameter (its description's length). A value over its -/// ceiling fails: put the new text after the tool's instead, where -/// get_tool_guide serves it. A value under its ceiling fails too, so a saving is banked the moment it -/// lands and cannot be spent later: lower the line. A new tool or parameter adds its line deliberately, with -/// the reason in the PR. The total's ceiling is below, with the reason for each change. +/// The pins live one file per tool class (#3898 step 0), under McpToolsListBudget/, named +/// for the class that declares the tools (e.g. DarlingMcpDataTools.txt). Inside a file: one block per +/// tool (a tool <name> <chars> line followed by that tool's sorted +/// param <tool>.<param> <chars> lines), blocks sorted by tool name, exactly one blank +/// line between blocks. Splitting the old single file this way means two content PRs that each touch a +/// different tool class never conflict, and a blank line between every two tools in the SAME class means two +/// PRs that each touch an adjacent tool in that class don't conflict either. +/// +/// The pins are ceilings that only go down. A value over its ceiling fails: put the new text after +/// the tool's instead, where get_tool_guide serves it. A value under +/// its ceiling fails too, so a saving is banked the moment it lands and cannot be spent later: lower the line. +/// A new tool or parameter adds its line deliberately, with the reason in the PR. Every failure names the file +/// and line and gives the exact replacement line. The directory is also checked for layout: every served tool +/// has exactly one block, in the file for the class that declares it; every file belongs to a registered class; +/// and every file's bytes match its own canonical re-serialization (order, blank lines, no duplicates). +/// +/// The total () is a growth-only ceiling; content PRs never lower +/// it (that happens once, in the #3898 PR that ships the last converted family). The measured total is also +/// written to the test output, so a run's log states it without re-deriving it. /// /// D2's absolute caps apply to converted tools (a description carrying the marker): the served head /// at most 1,000 characters (the target is 600), every parameter description at most 200, and every tail and @@ -46,9 +59,16 @@ namespace Darling.Tests; /// public sealed class McpToolsListBudgetTests { + private readonly ITestOutputHelper _output; + + public McpToolsListBudgetTests(ITestOutputHelper output) + { + _output = output; + } + /// - /// The serialized tools/list ceiling, in UTF-8 bytes. Change log (a ceiling only goes down, except for a - /// deliberate new tool or parameter, named here): + /// The serialized tools/list ceiling, in UTF-8 bytes. Growth-only; a content PR never lowers this (the + /// final #3898 PR does, once, after every family converts). Change log: /// /// #3898 Phase 0 + seam: pinned at the measured value, which includes the new get_tool_guide and the /// converted get_health_parser_* family. @@ -59,59 +79,180 @@ public sealed class McpToolsListBudgetTests /// private const int TotalCeilingBytes = 329_494; - /// How far under the total ceiling the measured size may sit before the ceiling must be lowered. - /// The per-tool and per-parameter lines are exact; this slack only spares every small edit a conflict on - /// one shared line. - private const int TotalBankingSlackBytes = 4_096; - private const int ConvertedHeadCap = 1_000; private const int ConvertedParameterCap = 200; - private const string BudgetFile = "McpToolsListBudget.txt"; + + /// The directory holding one budget file per tool class, next to this test. + private const string BudgetDir = "McpToolsListBudget"; + + private const string HeaderSuffix = + ": tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks."; + + private const string DebugPrefix = "darling-budget"; [Fact] - public void ToolsList_TotalBytes_IsAtOrUnderTheCeiling_AndSavingsAreBanked() + public void ToolsList_TotalBytes_IsAtOrUnderTheCeiling() { var measured = Measure(); + _output.WriteLine($"tools/list total: {measured.TotalBytes:N0} bytes (ceiling {TotalCeilingBytes:N0})."); Assert.True(measured.TotalBytes <= TotalCeilingBytes, $"tools/list is {measured.TotalBytes:N0} bytes, over its {TotalCeilingBytes:N0}-byte ceiling. Move reading guidance after the tool's {McpToolGuide.Marker} marker instead of growing the head."); - Assert.True(TotalCeilingBytes - measured.TotalBytes <= TotalBankingSlackBytes, - $"tools/list is {measured.TotalBytes:N0} bytes, {TotalCeilingBytes - measured.TotalBytes:N0} under its {TotalCeilingBytes:N0}-byte ceiling. Bank the saving: lower {nameof(TotalCeilingBytes)} to {measured.TotalBytes} and say why."); } [Fact] public void EveryServedDescription_AndEveryParameterDescription_MatchesItsCeiling() { var measured = Measure(); - var pinned = ReadBudget(); + var files = ReadBudgetFiles(); var problems = new List(); - foreach (var (key, value) in measured.Lines) + var pinned = new Dictionary(StringComparer.Ordinal); + foreach (var file in files) + { + foreach (var entry in file.Entries) + { + if (!pinned.TryAdd(entry.Key, (entry.Value, file.FileName, entry.LineNumber))) + { + var first = pinned[entry.Key]; + problems.Add($"{file.FileName}:{entry.LineNumber}: '{entry.Key}' duplicates the pin already at {first.FileName}:{first.LineNumber}. Remove one."); + } + } + } + + var measuredKeys = new SortedDictionary(StringComparer.Ordinal); + foreach (var tool in measured.Tools) + { + measuredKeys[$"tool {tool.Name}"] = (tool.Served.Length, tool.ClassName); + foreach (var (parameter, length) in tool.ParameterDescriptionLengths) + { + measuredKeys[$"param {tool.Name}.{parameter}"] = (length, tool.ClassName); + } + } + + foreach (var (key, measuredValue) in measuredKeys) { if (!pinned.TryGetValue(key, out var ceiling)) { - problems.Add($"{key}: {value} is not pinned. Add the line '{key} {value}' deliberately, with the reason in the PR."); + problems.Add($"{key}: {measuredValue.Value} is not pinned. Add the line '{key} {measuredValue.Value}' to {BudgetDir}/{measuredValue.ClassName}.txt."); + } + else if (measuredValue.Value > ceiling.Value) + { + problems.Add($"{ceiling.FileName}:{ceiling.LineNumber}: '{key}' is {measuredValue.Value}, over its ceiling {ceiling.Value}. Put the new text after the tool's {McpToolGuide.Marker} marker (get_tool_guide serves it) instead of raising the pin; if the growth is deliberate, replace the line with '{key} {measuredValue.Value}'."); + } + else if (measuredValue.Value < ceiling.Value) + { + problems.Add($"{ceiling.FileName}:{ceiling.LineNumber}: '{key}' is {measuredValue.Value}, under its ceiling {ceiling.Value}. Bank the saving: replace the line with '{key} {measuredValue.Value}'."); + } + } + + foreach (var (key, ceiling) in pinned) + { + if (!measuredKeys.ContainsKey(key)) + { + problems.Add($"{ceiling.FileName}:{ceiling.LineNumber}: '{key}' is pinned, but no such tool or parameter is served. Remove the line."); + } + } + + if (problems.Count > 0) + { + Assert.Fail($"{problems.Count} tools/list budget line(s) moved:\n" + + string.Join("\n", problems.OrderBy(p => p, StringComparer.Ordinal).Take(40))); + } + } + + /// + /// The directory's layout, independent of the values inside it: every served tool has exactly one block, + /// filed under the class that declares it; every file belongs to a class that is actually registered; and + /// every file's bytes are its own canonical re-serialization (blocks sorted by tool name, exactly one blank + /// line between them, no duplicates). A file that fails the last check gets its corrected form written to a + /// temp path named in the failure. + /// + [Fact] + public void EveryBudgetFile_MatchesItsRegisteredClass_AndIsInCanonicalForm() + { + var measured = Measure(); + var files = ReadBudgetFiles(); + var registeredClassNames = measured.ClassNames.ToHashSet(StringComparer.Ordinal); + var problems = new List(); + + var blockOwner = new Dictionary(StringComparer.Ordinal); + foreach (var file in files) + { + foreach (var entry in file.Entries) + { + var (tool, parameter) = ParseKey(entry.Key); + if (parameter is null) + { + blockOwner[tool] = file.ClassName; + } + } + } + + foreach (var tool in measured.Tools) + { + if (!blockOwner.TryGetValue(tool.Name, out var actualClass)) + { + problems.Add($"{tool.Name}: no block in any {BudgetDir} file. Add 'tool {tool.Name} {tool.Served.Length}' (and its param lines) to {BudgetDir}/{tool.ClassName}.txt."); + } + else if (!string.Equals(actualClass, tool.ClassName, StringComparison.Ordinal)) + { + problems.Add($"{tool.Name}: blocked in {BudgetDir}/{actualClass}.txt, but is declared on {tool.ClassName}. Move its block to {BudgetDir}/{tool.ClassName}.txt."); } - else if (value > ceiling) + } + + var measuredToolNames = measured.Tools.Select(t => t.Name).ToHashSet(StringComparer.Ordinal); + foreach (var (toolName, className) in blockOwner) + { + if (!measuredToolNames.Contains(toolName)) { - problems.Add($"{key}: {value} is over its ceiling {ceiling}. Put the new text after the tool's {McpToolGuide.Marker} marker (get_tool_guide serves it) instead of raising the pin."); + problems.Add($"{BudgetDir}/{className}.txt: block for '{toolName}', which is not a served tool. Remove it."); } - else if (value < ceiling) + } + + foreach (var file in files) + { + if (!registeredClassNames.Contains(file.ClassName)) { - problems.Add($"{key}: {value} is under its ceiling {ceiling}. Bank the saving: lower the line to '{key} {value}'."); + problems.Add($"{BudgetDir}/{file.FileName}: '{file.ClassName}' is not a registered tool class. Delete the file."); } } - foreach (var key in pinned.Keys.Where(k => !measured.Lines.ContainsKey(k))) + foreach (var file in files) { - problems.Add($"{key}: pinned, but no such tool or parameter is served. Remove the line."); + List canonical; + try + { + canonical = CanonicalLinesFromEntries(file.ClassName, file.Entries); + } + catch (InvalidOperationException ex) + { + problems.Add($"{BudgetDir}/{file.FileName}: {ex.Message}"); + continue; + } + + if (canonical.SequenceEqual(file.RawLines, StringComparer.Ordinal)) + { + continue; + } + + var firstDiff = 0; + while (firstDiff < canonical.Count && firstDiff < file.RawLines.Count + && string.Equals(canonical[firstDiff], file.RawLines[firstDiff], StringComparison.Ordinal)) + { + firstDiff++; + } + + var tempPath = Path.Combine(Path.GetTempPath(), $"{DebugPrefix}-{file.FileName}"); + File.WriteAllText(tempPath, JoinForDisk(canonical)); + var expected = firstDiff < canonical.Count ? canonical[firstDiff] : "(end of file)"; + var actual = firstDiff < file.RawLines.Count ? file.RawLines[firstDiff] : "(end of file)"; + problems.Add($"{BudgetDir}/{file.FileName}:{firstDiff + 1}: not in canonical form (blocks sorted by tool name, exactly one blank line between blocks, no duplicates). Expected '{expected}', found '{actual}'. Canonical file written to {tempPath}."); } if (problems.Count > 0) { - var path = Path.Combine(Path.GetTempPath(), "darling-" + BudgetFile); - File.WriteAllText(path, Render(measured)); - Assert.Fail($"{problems.Count} tools/list budget line(s) moved (measured file written to {path}):\n" - + string.Join("\n", problems.Take(40))); + Assert.Fail($"{problems.Count} budget-layout problem(s):\n" + + string.Join("\n", problems.OrderBy(p => p, StringComparer.Ordinal).Take(40))); } } @@ -181,12 +322,13 @@ public void NoDescriptionCarriesANearMissMarker() internal sealed record MeasuredTool( string Name, + string ClassName, string? Description, string Served, string? Tail, IReadOnlyList<(string Parameter, int Length)> ParameterDescriptionLengths); - internal sealed record Measurement(int TotalBytes, IReadOnlyList Tools, IReadOnlyDictionary Lines); + internal sealed record Measurement(int TotalBytes, IReadOnlyList Tools, IReadOnlyList ClassNames); private static Measurement? _cached; @@ -202,17 +344,19 @@ internal static Measurement Measure() var json = JsonSerializer.Serialize(new ListToolsResult { Tools = protocolTools }, McpJsonUtilities.DefaultOptions); var totalBytes = Encoding.UTF8.GetByteCount(json); - var descriptions = types - .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) - .Select(m => (Attr: m.GetCustomAttribute(), Method: m)) + var toolInfo = types + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).Select(m => (Type: t, Method: m))) + .Select(x => (Attr: x.Method.GetCustomAttribute(), x.Method, x.Type)) .Where(x => x.Attr is not null) - .ToDictionary(x => x.Attr!.Name!, x => x.Method.GetCustomAttribute()?.Description, StringComparer.Ordinal); + .ToDictionary( + x => x.Attr!.Name!, + x => (ClassName: x.Type.Name, Description: x.Method.GetCustomAttribute()?.Description), + StringComparer.Ordinal); var measuredTools = new List(); - var lines = new SortedDictionary(StringComparer.Ordinal); foreach (var tool in protocolTools) { - var description = descriptions[tool.Name]; + var (className, description) = toolInfo[tool.Name]; var served = tool.Description ?? string.Empty; var tail = description is null ? null : McpToolGuide.Split(description).Tail; var parameters = new List<(string, int)>(); @@ -222,15 +366,14 @@ internal static Measurement Measure() { var length = property.Value.TryGetProperty("description", out var d) ? d.GetString()!.Length : 0; parameters.Add((property.Name, length)); - lines[$"param {tool.Name}.{property.Name}"] = length; } } - lines[$"tool {tool.Name}"] = served.Length; - measuredTools.Add(new MeasuredTool(tool.Name, description, served, tail, parameters)); + measuredTools.Add(new MeasuredTool(tool.Name, className, description, served, tail, parameters)); } - _cached = new Measurement(totalBytes, measuredTools, lines); + var classNames = types.Select(t => t.Name).OrderBy(n => n, StringComparer.Ordinal).ToList(); + _cached = new Measurement(totalBytes, measuredTools, classNames); return _cached; } @@ -271,37 +414,121 @@ private static (List Tools, List Types) BuildServedTools() return (provider.GetServices().ToList(), types); } - private static Dictionary ReadBudget() + /* ---------------- per-class budget files ---------------- */ + + internal sealed record BudgetEntry(string Key, int Value, int LineNumber); + + internal sealed record BudgetFileContents(string ClassName, string FileName, IReadOnlyList RawLines, IReadOnlyList Entries); + + private static List ReadBudgetFiles() { - var result = new Dictionary(StringComparer.Ordinal); - foreach (var raw in File.ReadAllLines(RepoPath("Darling", "Darling.Tests", BudgetFile))) + var dir = RepoPath("Darling", "Darling.Tests", BudgetDir); + var files = new List(); + foreach (var filePath in Directory.GetFiles(dir, "*.txt").OrderBy(p => p, StringComparer.Ordinal)) { - var line = raw.Trim(); - if (line.Length == 0 || line.StartsWith('#')) + var className = Path.GetFileNameWithoutExtension(filePath); + var rawLines = File.ReadAllLines(filePath); + var entries = new List(); + for (var i = 0; i < rawLines.Length; i++) { - continue; + var trimmed = rawLines[i].Trim(); + if (trimmed.Length == 0 || trimmed.StartsWith('#')) + { + continue; + } + + var at = trimmed.LastIndexOf(' '); + entries.Add(new BudgetEntry(trimmed[..at], int.Parse(trimmed[(at + 1)..], CultureInfo.InvariantCulture), i + 1)); + } + + files.Add(new BudgetFileContents(className, Path.GetFileName(filePath), rawLines, entries)); + } + + return files; + } + + /// Splits a budget key into its tool name and, for a parameter line, the parameter name. + private static (string Tool, string? Parameter) ParseKey(string key) + { + if (key.StartsWith("tool ", StringComparison.Ordinal)) + { + return (key["tool ".Length..], null); + } + + if (key.StartsWith("param ", StringComparison.Ordinal)) + { + var rest = key["param ".Length..]; + var dot = rest.IndexOf('.', StringComparison.Ordinal); + return (rest[..dot], rest[(dot + 1)..]); + } + + throw new InvalidOperationException($"unrecognized budget key '{key}'"); + } + + private sealed record Block(string Tool, int ToolValue, IReadOnlyList<(string Parameter, int Length)> Parameters); + + /// The one formatter both the live-measurement renderer and the on-disk canonical-form checker + /// use, so a file this test generates always reads back as canonical. + private static List CanonicalLines(string className, IEnumerable blocks) + { + var lines = new List { "# " + className + HeaderSuffix }; + var first = true; + foreach (var block in blocks.OrderBy(b => b.Tool, StringComparer.Ordinal)) + { + if (!first) + { + lines.Add(string.Empty); } - var at = line.LastIndexOf(' '); - result.Add(line[..at], int.Parse(line[(at + 1)..], CultureInfo.InvariantCulture)); + first = false; + lines.Add($"tool {block.Tool} {block.ToolValue.ToString(CultureInfo.InvariantCulture)}"); + foreach (var (parameter, length) in block.Parameters.OrderBy(p => p.Parameter, StringComparer.Ordinal)) + { + lines.Add($"param {block.Tool}.{parameter} {length.ToString(CultureInfo.InvariantCulture)}"); + } } - return result; + return lines; } - private static string Render(Measurement measured) + internal static List CanonicalLinesFromMeasurement(string className, IEnumerable tools) => + CanonicalLines(className, tools.Select(t => new Block(t.Name, t.Served.Length, t.ParameterDescriptionLengths))); + + private static List CanonicalLinesFromEntries(string className, IReadOnlyList entries) { - var sb = new StringBuilder(); - sb.AppendLine("# #3898 Phase 0: Darling's tools/list budget. Ceilings only go down; see McpToolsListBudgetTests."); - sb.AppendLine("# 'tool ' is the served description; 'param . ' is a parameter description."); - foreach (var (key, value) in measured.Lines) + var toolValues = new Dictionary(StringComparer.Ordinal); + var paramsByTool = new Dictionary>(StringComparer.Ordinal); + foreach (var entry in entries) { - sb.Append(key).Append(' ').Append(value.ToString(CultureInfo.InvariantCulture)).AppendLine(); + var (tool, parameter) = ParseKey(entry.Key); + if (parameter is null) + { + toolValues[tool] = entry.Value; + } + else + { + if (!paramsByTool.TryGetValue(tool, out var list)) + { + list = new List<(string, int)>(); + paramsByTool[tool] = list; + } + + list.Add((parameter, entry.Value)); + } } - return sb.ToString(); + var orphans = paramsByTool.Keys.Where(t => !toolValues.ContainsKey(t)).OrderBy(t => t, StringComparer.Ordinal).ToList(); + if (orphans.Count > 0) + { + throw new InvalidOperationException($"parameter line(s) for tool(s) with no 'tool' line in this file: {string.Join(", ", orphans)}."); + } + + return CanonicalLines(className, toolValues.Select(kv => + new Block(kv.Key, kv.Value, paramsByTool.TryGetValue(kv.Key, out var ps) ? ps : new List<(string, int)>()))); } + private static string JoinForDisk(IEnumerable lines) => string.Join("\r\n", lines) + "\r\n"; + private static string RepoPath(params string[] segments) => Path.Combine(new[] { RepoRoot() }.Concat(segments).ToArray()); private static string RepoRoot([CallerFilePath] string thisFile = "") diff --git a/Lite.Tests/McpToolGuideHeads.HealthParser.cs b/Lite.Tests/McpToolGuideHeads.HealthParser.cs new file mode 100644 index 0000000000..ed084ecda6 --- /dev/null +++ b/Lite.Tests/McpToolGuideHeads.HealthParser.cs @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor Lite. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; +using PerformanceMonitor.Common; +using Xunit; + +namespace Lite.Tests; + +/// +/// #3898 D3 head pins for the get_health_parser_* family (the D1 pilot). This is the pattern every +/// family converted under #3898 copies into its own McpToolGuideHeads.<Family>.cs: the family's +/// tool roster and guardrail facts, plus the two tests that pin them, using 's +/// shared helper. McpToolGuideTests itself is never edited to add +/// a family, so two families converting in parallel never conflict there. +/// +public sealed class McpToolGuideHeadsHealthParserTests +{ + private static readonly string[] HealthParserTools = + [ + "get_health_parser_cpu_tasks", + "get_health_parser_io_issues", + "get_health_parser_memory_broker", + "get_health_parser_memory_conditions", + "get_health_parser_memory_node_oom", + "get_health_parser_scheduler_issues", + "get_health_parser_severe_errors", + "get_health_parser_significant_waits", + "get_health_parser_system_health", + ]; + + /// The per-tool guardrail fact each head must state: the significance gate (a floor under what can + /// be listed at all) or its absence. + private static readonly (string Tool, string Fact)[] GateFacts = + [ + ("get_health_parser_system_health", "Ungated: every parsed event is returned."), + ("get_health_parser_memory_node_oom", "Ungated: every recorded OOM is returned."), + ("get_health_parser_severe_errors", "Gated: severity 19 or higher only, benign connection-reset error numbers excluded"), + ("get_health_parser_io_issues", "Gated: WARNING-state results only."), + ("get_health_parser_scheduler_issues", "Gated: WARNING-state results only."), + ("get_health_parser_memory_conditions", "Gated: only snapshots whose last notification is RESOURCE_MEMPHYSICAL_LOW."), + ("get_health_parser_cpu_tasks", "Gated: WARNING-state results with at least 10 pending tasks only."), + ("get_health_parser_memory_broker", "Gated: RESOURCE_MEMPHYSICAL_LOW notifications only."), + ("get_health_parser_significant_waits", "Floors: a real session, a non-BACKUP statement, at least 500 ms, and a wait type off the idle/background list; shorter waits are never listed."), + ]; + + /// D9: what an empty answer means, tied to the gate (or the floors, or the absence of one) beside + /// it rather than one identical sentence on all nine — a fresh reader given only the head misread the + /// ungated tools' empty answer as ambiguous when it was a real, healthy result (#4048 D9 round). + private const string GatedEmptyFact = + "An empty answer with status empty is a real result: nothing in this window passed the gate, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; + + private const string FloorsEmptyFact = + "An empty answer with status empty is a real result: nothing in this window passed the floors, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; + + private const string UngatedEmptyFact = + "An empty answer with status empty is a real result: nothing of this kind was recorded in this window. status unavailable with source_observed false is no evidence either way."; + + private static readonly (string Tool, string Fact)[] EmptyAnswerFacts = + [ + ("get_health_parser_system_health", UngatedEmptyFact), + ("get_health_parser_memory_node_oom", UngatedEmptyFact), + ("get_health_parser_significant_waits", FloorsEmptyFact), + ("get_health_parser_severe_errors", GatedEmptyFact), + ("get_health_parser_io_issues", GatedEmptyFact), + ("get_health_parser_scheduler_issues", GatedEmptyFact), + ("get_health_parser_memory_conditions", GatedEmptyFact), + ("get_health_parser_cpu_tasks", GatedEmptyFact), + ("get_health_parser_memory_broker", GatedEmptyFact), + ]; + + [Fact] + public void EveryPilotHead_ServesTheWindow_TheEmptyGuardrail_AndItsGate() + { + foreach (var tool in HealthParserTools) + { + var served = McpToolGuideTests.Served(tool); + Assert.NotNull(served.Tail); + /* The window is fixed on the event's own time, ends at as_of, newest first. */ + Assert.Contains("over an event_time window ending at as_of, newest first.", served.Served, StringComparison.Ordinal); + Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal); + /* D9: the floors sentence is the longest of the three variants; 620 leaves it headroom while + staying far under the D2 absolute cap of 1,000 (McpToolsListBudgetTests.ConvertedHeadCap). */ + Assert.True(served.Served.Length <= 620, $"{tool}: served head {served.Served.Length} is over the 620 target"); + } + + foreach (var (tool, fact) in GateFacts) + { + Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal); + } + + /* Zero is not a measurement here unless the witness says so (#3541 A12); D9: each tool's empty-answer + sentence ties to ITS gate (or floors, or absence of one) rather than one sentence on all nine, so a + reader given only the head does not have to guess which of the four rungs an empty answer landed on. */ + foreach (var (tool, fact) in EmptyAnswerFacts) + { + Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal); + } + + /* No required parameter, so the head need not name one: all four are optional in the served schema. */ + Assert.All(HealthParserTools, tool => Assert.All(McpToolGuideTests.Served(tool).ParameterDescriptionLengths, p => Assert.True(p.Length <= 200))); + } + + /// Nothing the old descriptions said was lost: the four-rung empty-window sentence every one of + /// them carried is the topic, and each guide carries the topic verbatim, so one call answers it. + [Fact] + public void EveryPilotGuide_CarriesTheEmptyWindowTopic_AndTheTopicCarriesEveryRung() + { + foreach (var tool in HealthParserTools) + { + Assert.EndsWith(McpToolGuideTopics.SystemHealthEmptyWindows, McpToolGuideTests.Served(tool).Tail!, StringComparison.Ordinal); + } + + var topic = McpToolGuideTopics.SystemHealthEmptyWindows; + foreach (var fact in new[] { "source_observed", "EVER", "last_captured_at", "gated out", "Captured before", "Never recorded", + "status unavailable", "not a clean bill", "events_in_window", "last_captured_of_type_at" }) + { + Assert.Contains(fact, topic, StringComparison.Ordinal); + } + } +} diff --git a/Lite.Tests/McpToolGuideTests.cs b/Lite.Tests/McpToolGuideTests.cs index 20c3497814..da4a950bbf 100644 --- a/Lite.Tests/McpToolGuideTests.cs +++ b/Lite.Tests/McpToolGuideTests.cs @@ -9,6 +9,7 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.RegularExpressions; @@ -21,119 +22,63 @@ namespace Lite.Tests; /// /// #3898 D1 + D3 on Lite: get_tool_guide serves the tails the one-place split keeps off -/// tools/list, and the head pins for the pilot family (get_health_parser_*): every guardrail fact -/// a caller needs to read the answer is served in the HEAD, not left in the guide. Darling's twin is -/// Darling.Tests/McpToolGuideTests, which also holds the cross-SKU lockstep pins (identical heads, identical -/// guide tool), because that project already reads Lite's source. +/// tools/list, plus generic pins any converted family relies on. Darling's twin is +/// Darling.Tests/McpToolGuideTests, which also holds the cross-SKU lockstep pins (identical heads, +/// identical guide tool) because that project already reads Lite's source. A family's OWN head pins (its tool +/// roster, its guardrail facts) live in their own McpToolGuideHeads.<Family>.cs next to this file +/// (see for the pattern) — lanes converting a new family add a +/// file there and never edit this one, so two families converting in parallel never conflict here. /// public sealed class McpToolGuideTests { - private static readonly string[] HealthParserTools = - [ - "get_health_parser_cpu_tasks", - "get_health_parser_io_issues", - "get_health_parser_memory_broker", - "get_health_parser_memory_conditions", - "get_health_parser_memory_node_oom", - "get_health_parser_scheduler_issues", - "get_health_parser_severe_errors", - "get_health_parser_significant_waits", - "get_health_parser_system_health", - ]; - - /// The per-tool guardrail fact each head must state: the significance gate (a floor under what can - /// be listed at all) or its absence. - private static readonly (string Tool, string Fact)[] GateFacts = - [ - ("get_health_parser_system_health", "Ungated: every parsed event is returned."), - ("get_health_parser_memory_node_oom", "Ungated: every recorded OOM is returned."), - ("get_health_parser_severe_errors", "Gated: severity 19 or higher only, benign connection-reset error numbers excluded"), - ("get_health_parser_io_issues", "Gated: WARNING-state results only."), - ("get_health_parser_scheduler_issues", "Gated: WARNING-state results only."), - ("get_health_parser_memory_conditions", "Gated: only snapshots whose last notification is RESOURCE_MEMPHYSICAL_LOW."), - ("get_health_parser_cpu_tasks", "Gated: WARNING-state results with at least 10 pending tasks only."), - ("get_health_parser_memory_broker", "Gated: RESOURCE_MEMPHYSICAL_LOW notifications only."), - ("get_health_parser_significant_waits", "Floors: a real session, a non-BACKUP statement, at least 500 ms, and a wait type off the idle/background list; shorter waits are never listed."), - ]; - - /// D9: what an empty answer means, tied to the gate (or the floors, or the absence of one) beside - /// it rather than one identical sentence on all nine — a fresh reader given only the head misread the - /// ungated tools' empty answer as ambiguous when it was a real, healthy result (#4048 D9 round). - private const string GatedEmptyFact = - "An empty answer with status empty is a real result: nothing in this window passed the gate, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; - - private const string FloorsEmptyFact = - "An empty answer with status empty is a real result: nothing in this window passed the floors, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."; - - private const string UngatedEmptyFact = - "An empty answer with status empty is a real result: nothing of this kind was recorded in this window. status unavailable with source_observed false is no evidence either way."; - - private static readonly (string Tool, string Fact)[] EmptyAnswerFacts = - [ - ("get_health_parser_system_health", UngatedEmptyFact), - ("get_health_parser_memory_node_oom", UngatedEmptyFact), - ("get_health_parser_significant_waits", FloorsEmptyFact), - ("get_health_parser_severe_errors", GatedEmptyFact), - ("get_health_parser_io_issues", GatedEmptyFact), - ("get_health_parser_scheduler_issues", GatedEmptyFact), - ("get_health_parser_memory_conditions", GatedEmptyFact), - ("get_health_parser_cpu_tasks", GatedEmptyFact), - ("get_health_parser_memory_broker", GatedEmptyFact), - ]; - - private static McpToolsListBudgetTests.MeasuredTool Served(string tool) => + internal static McpToolsListBudgetTests.MeasuredTool Served(string tool) => McpToolsListBudgetTests.Measure().Tools.Single(t => t.Name == tool); - /* ---------------- D3 head pins ---------------- */ + /* ---------------- generic D3 pin (every family, no hand-kept list) ---------------- */ + /// D3's absolute head-length target: every tool the split has actually converted (discovered from + /// the served list, not a hand-kept roster) stays at or under 620 served characters, pointer included. The + /// pilot pinned this per tool; this is the backstop that catches it for every family from here on, + /// including one whose own family file forgets to check it. [Fact] - public void EveryPilotHead_ServesTheWindow_TheEmptyGuardrail_AndItsGate() + public void EveryConvertedHead_StaysAtOrUnder620Characters() { - foreach (var tool in HealthParserTools) - { - var served = Served(tool); - Assert.NotNull(served.Tail); - /* The window is fixed on the event's own time, ends at as_of, newest first. */ - Assert.Contains("over an event_time window ending at as_of, newest first.", served.Served, StringComparison.Ordinal); - Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal); - /* D9: the floors sentence is the longest of the three variants; 620 leaves it headroom while - staying far under the D2 absolute cap of 1,000 (McpToolsListBudgetTests.ConvertedHeadCap). */ - Assert.True(served.Served.Length <= 620, $"{tool}: served head {served.Served.Length} is over the 620 target"); - } - - foreach (var (tool, fact) in GateFacts) - { - Assert.Contains(fact, Served(tool).Served, StringComparison.Ordinal); - } + var overLong = McpToolsListBudgetTests.Measure().Tools + .Where(t => t.Tail is not null && t.Served.Length > 620) + .Select(t => $"{t.Name}: {t.Served.Length}") + .ToList(); + Assert.True(overLong.Count == 0, "served head(s) over the 620 target: " + string.Join(", ", overLong)); + } - /* Zero is not a measurement here unless the witness says so (#3541 A12); D9: each tool's empty-answer - sentence ties to ITS gate (or floors, or absence of one) rather than one sentence on all nine, so a - reader given only the head does not have to guess which of the four rungs an empty answer landed on. */ - foreach (var (tool, fact) in EmptyAnswerFacts) - { - Assert.Contains(fact, Served(tool).Served, StringComparison.Ordinal); - } + /* ---------------- topics: All aggregates every partial family array ---------------- */ - /* No required parameter, so the head need not name one: all four are optional in the served schema. */ - Assert.All(HealthParserTools, tool => Assert.All(Served(tool).ParameterDescriptionLengths, p => Assert.True(p.Length <= 200))); + [Fact] + public void AllTopics_HaveNoNullEntries() + { + Assert.All(McpToolGuideTopics.All, t => Assert.NotNull(t)); } - /// Nothing the old descriptions said was lost: the four-rung empty-window sentence every one of - /// them carried is the topic, and each guide carries the topic verbatim, so one call answers it. [Fact] - public void EveryPilotGuide_CarriesTheEmptyWindowTopic_AndTheTopicCarriesEveryRung() + public void AllTopics_HaveUniqueNames() { - foreach (var tool in HealthParserTools) - { - Assert.EndsWith(McpToolGuideTopics.SystemHealthEmptyWindows, Served(tool).Tail!, StringComparison.Ordinal); - } + var names = McpToolGuideTopics.All.Select(t => t.Name).ToList(); + Assert.Equal(names.Distinct(StringComparer.Ordinal).Count(), names.Count); + } - var topic = McpToolGuideTopics.SystemHealthEmptyWindows; - foreach (var fact in new[] { "source_observed", "EVER", "last_captured_at", "gated out", "Captured before", "Never recorded", - "status unavailable", "not a clean bill", "events_in_window", "last_captured_of_type_at" }) - { - Assert.Contains(fact, topic, StringComparison.Ordinal); - } + /// Each family's partial file declares its own private static readonly McpToolGuideTopic[] + /// field; reflected here so a family added and never wired into All's static constructor fails + /// loudly instead of silently serving no topics. + [Fact] + public void EveryPartialTopicArray_IsReflectedInAll() + { + var fields = typeof(McpToolGuideTopics) + .GetFields(BindingFlags.NonPublic | BindingFlags.Static) + .Where(f => f.FieldType == typeof(McpToolGuideTopic[])) + .ToList(); + Assert.True(fields.Count == 8, $"expected 8 per-family topic arrays, found {fields.Count}: {string.Join(", ", fields.Select(f => f.Name))}"); + + var totalFromFields = fields.Sum(f => ((McpToolGuideTopic[])f.GetValue(null)!).Length); + Assert.Equal(totalFromFields, McpToolGuideTopics.All.Count); } /* ---------------- get_tool_guide ---------------- */ @@ -162,9 +107,19 @@ public void GetToolGuide_ServesTails_AndTopics_AndReportsUnknownNames() [Fact] public void GetToolGuide_WithNoArguments_IsTheIndex() { + /* HostCatalog wires up exactly McpHealthParserTools, McpDiscoveryTools and McpToolGuideTools, so this + fixture's own guide-bearing roster is a local, self-contained fact about THIS test's host, not a + family pin (that lives in McpToolGuideHeadsHealthParserTests). */ + var fixtureToolsWithGuides = new[] + { + "get_health_parser_cpu_tasks", "get_health_parser_io_issues", "get_health_parser_memory_broker", + "get_health_parser_memory_conditions", "get_health_parser_memory_node_oom", "get_health_parser_scheduler_issues", + "get_health_parser_severe_errors", "get_health_parser_significant_waits", "get_health_parser_system_health", + }; + using var doc = JsonDocument.Parse(McpToolGuideTools.GetToolGuide(HostCatalog(), null, null)); var withGuides = doc.RootElement.GetProperty("tools_with_guides").EnumerateArray().Select(e => e.GetString()).ToArray(); - Assert.Equal(HealthParserTools, withGuides); + Assert.Equal(fixtureToolsWithGuides, withGuides); Assert.Contains(doc.RootElement.GetProperty("topics").EnumerateArray(), t => t.GetProperty("name").GetString() == McpToolGuideTopics.SystemHealthEmptyWindowsName); } diff --git a/Lite.Tests/McpToolsListBudget.txt b/Lite.Tests/McpToolsListBudget.txt deleted file mode 100644 index bdc032c41c..0000000000 --- a/Lite.Tests/McpToolsListBudget.txt +++ /dev/null @@ -1,367 +0,0 @@ -# #3898 Phase 0: Lite's tools/list budget. Ceilings only go down; see McpToolsListBudgetTests. -# 'tool ' is the served description; 'param . ' is a parameter description. -param analyze_plan_xml.plan_xml 25 -param analyze_procedure_plan.plan_handle 53 -param analyze_procedure_plan.server_name 28 -param analyze_query_plan.query_hash 49 -param analyze_query_plan.server_name 28 -param analyze_query_store_plan.database_name 43 -param analyze_query_store_plan.plan_id 37 -param analyze_query_store_plan.server_name 28 -param analyze_server.as_of 167 -param analyze_server.hours_back 104 -param analyze_server.server_name 28 -param audit_config.server_name 28 -param compare_analysis.as_of 167 -param compare_analysis.baseline_hours_back 211 -param compare_analysis.hours_back 57 -param compare_analysis.server_name 28 -param get_active_queries.as_of 167 -param get_active_queries.blocking_only 238 -param get_active_queries.database_name 156 -param get_active_queries.hours_back 37 -param get_active_queries.limit 149 -param get_active_queries.server_name 28 -param get_alert_history.as_of 167 -param get_alert_history.hours_back 29 -param get_alert_history.include_dismissed 357 -param get_alert_history.limit 133 -param get_analysis_facts.as_of 167 -param get_analysis_facts.hours_back 36 -param get_analysis_facts.min_severity 90 -param get_analysis_facts.server_name 28 -param get_analysis_facts.source 515 -param get_analysis_findings.as_of 167 -param get_analysis_findings.hours_back 49 -param get_analysis_findings.include_drilldown 235 -param get_analysis_findings.server_name 28 -param get_blocked_process_reports.as_of 167 -param get_blocked_process_reports.hours_back 29 -param get_blocked_process_reports.limit 133 -param get_blocked_process_reports.server_name 28 -param get_blocked_process_xml.as_of 167 -param get_blocked_process_xml.hours_back 29 -param get_blocked_process_xml.limit 113 -param get_blocked_process_xml.server_name 28 -param get_blocking_stats.as_of 167 -param get_blocking_stats.hours_back 191 -param get_blocking_stats.server_name 28 -param get_blocking_trend.as_of 167 -param get_blocking_trend.hours_back 29 -param get_blocking_trend.server_name 28 -param get_collection_health.server_name 28 -param get_collection_log.as_of 167 -param get_collection_log.collector_name 244 -param get_collection_log.hours_back 191 -param get_collection_log.limit 120 -param get_collection_log.min_duration_ms 382 -param get_collection_log.server_name 28 -param get_collection_log.status 481 -param get_cpu_scheduler_pressure.as_of 167 -param get_cpu_scheduler_pressure.hours_back 63 -param get_cpu_scheduler_pressure.server_name 28 -param get_cpu_utilization.as_of 167 -param get_cpu_utilization.bucket_minutes 55 -param get_cpu_utilization.hours_back 28 -param get_cpu_utilization.server_name 28 -param get_current_waits_trend.as_of 167 -param get_current_waits_trend.database_name 73 -param get_current_waits_trend.hours_back 190 -param get_current_waits_trend.server_name 28 -param get_daily_summary.server_name 28 -param get_daily_summary.summary_date 155 -param get_daily_summary_range.as_of 166 -param get_daily_summary_range.days_back 84 -param get_daily_summary_range.server_name 28 -param get_database_config.database_name 54 -param get_database_config.server_name 28 -param get_database_config_changes.as_of 167 -param get_database_config_changes.hours_back 51 -param get_database_config_changes.server_name 28 -param get_database_scoped_config.database_name 54 -param get_database_scoped_config.server_name 28 -param get_database_sizes.server_name 28 -param get_deadlock_detail.as_of 167 -param get_deadlock_detail.hours_back 29 -param get_deadlock_detail.limit 119 -param get_deadlock_detail.server_name 28 -param get_deadlock_trend.as_of 167 -param get_deadlock_trend.hours_back 29 -param get_deadlock_trend.server_name 28 -param get_deadlocks.as_of 167 -param get_deadlocks.hours_back 29 -param get_deadlocks.limit 133 -param get_deadlocks.server_name 28 -param get_default_trace_events.as_of 167 -param get_default_trace_events.hours_back 41 -param get_default_trace_events.limit 48 -param get_default_trace_events.server_name 28 -param get_file_io_stats.server_name 28 -param get_file_io_trend.as_of 167 -param get_file_io_trend.bucket_minutes 55 -param get_file_io_trend.database_name 56 -param get_file_io_trend.hours_back 29 -param get_file_io_trend.server_name 28 -param get_health_parser_cpu_tasks.as_of 167 -param get_health_parser_cpu_tasks.hours_back 41 -param get_health_parser_cpu_tasks.limit 38 -param get_health_parser_cpu_tasks.server_name 28 -param get_health_parser_io_issues.as_of 167 -param get_health_parser_io_issues.hours_back 41 -param get_health_parser_io_issues.limit 38 -param get_health_parser_io_issues.server_name 28 -param get_health_parser_memory_broker.as_of 167 -param get_health_parser_memory_broker.hours_back 41 -param get_health_parser_memory_broker.limit 38 -param get_health_parser_memory_broker.server_name 28 -param get_health_parser_memory_conditions.as_of 167 -param get_health_parser_memory_conditions.hours_back 41 -param get_health_parser_memory_conditions.limit 38 -param get_health_parser_memory_conditions.server_name 28 -param get_health_parser_memory_node_oom.as_of 167 -param get_health_parser_memory_node_oom.hours_back 41 -param get_health_parser_memory_node_oom.limit 38 -param get_health_parser_memory_node_oom.server_name 28 -param get_health_parser_scheduler_issues.as_of 167 -param get_health_parser_scheduler_issues.hours_back 41 -param get_health_parser_scheduler_issues.limit 38 -param get_health_parser_scheduler_issues.server_name 28 -param get_health_parser_severe_errors.as_of 167 -param get_health_parser_severe_errors.hours_back 41 -param get_health_parser_severe_errors.limit 38 -param get_health_parser_severe_errors.server_name 28 -param get_health_parser_significant_waits.as_of 167 -param get_health_parser_significant_waits.hours_back 41 -param get_health_parser_significant_waits.limit 38 -param get_health_parser_significant_waits.server_name 28 -param get_health_parser_system_health.as_of 167 -param get_health_parser_system_health.hours_back 41 -param get_health_parser_system_health.limit 38 -param get_health_parser_system_health.server_name 28 -param get_index_usage.server_name 28 -param get_latch_stats.as_of 167 -param get_latch_stats.hours_back 63 -param get_latch_stats.limit 194 -param get_latch_stats.server_name 28 -param get_lock_wait_trend.as_of 167 -param get_lock_wait_trend.bucket_minutes 55 -param get_lock_wait_trend.hours_back 29 -param get_lock_wait_trend.server_name 28 -param get_long_query_completions.as_of 167 -param get_long_query_completions.hours_back 29 -param get_long_query_completions.limit 134 -param get_long_query_completions.server_name 28 -param get_memory_clerks.server_name 28 -param get_memory_grants.as_of 167 -param get_memory_grants.hours_back 120 -param get_memory_grants.server_name 28 -param get_memory_pressure_events.as_of 167 -param get_memory_pressure_events.hours_back 29 -param get_memory_pressure_events.server_name 28 -param get_memory_stats.server_name 28 -param get_memory_trend.as_of 167 -param get_memory_trend.bucket_minutes 55 -param get_memory_trend.hours_back 29 -param get_memory_trend.server_name 28 -param get_mute_rules.enabled_only 41 -param get_object_locking.server_name 28 -param get_perfmon_stats.counter_name 61 -param get_perfmon_stats.instance_name 57 -param get_perfmon_stats.server_name 28 -param get_perfmon_trend.as_of 167 -param get_perfmon_trend.bucket_minutes 55 -param get_perfmon_trend.counter_name 50 -param get_perfmon_trend.hours_back 29 -param get_perfmon_trend.server_name 28 -param get_plan_cache_bloat.as_of 167 -param get_plan_cache_bloat.hours_back 63 -param get_plan_cache_bloat.server_name 28 -param get_plan_corrections.as_of 167 -param get_plan_corrections.hours_back 29 -param get_plan_corrections.limit 156 -param get_plan_corrections.server_name 28 -param get_plan_xml.query_hash 49 -param get_plan_xml.server_name 28 -param get_procedure_duration_trend.as_of 167 -param get_procedure_duration_trend.bucket_minutes 55 -param get_procedure_duration_trend.hours_back 29 -param get_procedure_duration_trend.server_name 28 -param get_pvs_stats.server_name 28 -param get_pvs_stats.trend_hours_back 98 -param get_query_duration_trend.as_of 167 -param get_query_duration_trend.bucket_minutes 55 -param get_query_duration_trend.hours_back 29 -param get_query_duration_trend.server_name 28 -param get_query_heatmap.as_of 167 -param get_query_heatmap.bucket_minutes 160 -param get_query_heatmap.database_name 46 -param get_query_heatmap.hours_back 43 -param get_query_heatmap.limit 195 -param get_query_heatmap.metric 123 -param get_query_heatmap.server_name 28 -param get_query_store_duration_trend.as_of 167 -param get_query_store_duration_trend.hours_back 29 -param get_query_store_duration_trend.server_name 28 -param get_query_store_health.database_name 54 -param get_query_store_health.server_name 28 -param get_query_store_regressions.as_of 167 -param get_query_store_regressions.database_name 46 -param get_query_store_regressions.hours_back 110 -param get_query_store_regressions.limit 86 -param get_query_store_regressions.server_name 28 -param get_query_store_top.as_of 167 -param get_query_store_top.database_name 30 -param get_query_store_top.hours_back 29 -param get_query_store_top.server_name 28 -param get_query_store_top.top 34 -param get_query_trend.as_of 167 -param get_query_trend.database_name 39 -param get_query_trend.hours_back 29 -param get_query_trend.query_hash 72 -param get_query_trend.server_name 28 -param get_resource_semaphore.as_of 167 -param get_resource_semaphore.hours_back 121 -param get_resource_semaphore.server_name 28 -param get_running_jobs.server_name 28 -param get_server_config.server_name 28 -param get_server_config_changes.as_of 167 -param get_server_config_changes.hours_back 51 -param get_server_config_changes.server_name 28 -param get_server_properties.server_name 28 -param get_server_summary.server_name 71 -param get_session_stats.server_name 28 -param get_spinlock_stats.as_of 167 -param get_spinlock_stats.hours_back 63 -param get_spinlock_stats.limit 195 -param get_spinlock_stats.server_name 28 -param get_table_index_sizes.server_name 28 -param get_tempdb_trend.as_of 167 -param get_tempdb_trend.bucket_minutes 55 -param get_tempdb_trend.hours_back 29 -param get_tempdb_trend.server_name 28 -param get_tool_guide.tools 98 -param get_tool_guide.topics 98 -param get_top_procedures_by_cpu.as_of 167 -param get_top_procedures_by_cpu.database_name 30 -param get_top_procedures_by_cpu.hours_back 29 -param get_top_procedures_by_cpu.server_name 28 -param get_top_procedures_by_cpu.top 37 -param get_top_queries_by_cpu.as_of 167 -param get_top_queries_by_cpu.database_name 30 -param get_top_queries_by_cpu.hours_back 29 -param get_top_queries_by_cpu.min_dop 132 -param get_top_queries_by_cpu.parallel_only 371 -param get_top_queries_by_cpu.server_name 28 -param get_top_queries_by_cpu.top 34 -param get_trace_flag_changes.as_of 167 -param get_trace_flag_changes.hours_back 51 -param get_trace_flag_changes.server_name 28 -param get_trace_flags.server_name 28 -param get_wait_stats.as_of 167 -param get_wait_stats.hours_back 40 -param get_wait_stats.limit 145 -param get_wait_stats.server_name 28 -param get_wait_trend.as_of 167 -param get_wait_trend.bucket_minutes 55 -param get_wait_trend.hours_back 29 -param get_wait_trend.server_name 28 -param get_wait_trend.wait_type 56 -param get_wait_types.as_of 167 -param get_wait_types.hours_back 29 -param get_wait_types.server_name 28 -param get_waiting_tasks.as_of 167 -param get_waiting_tasks.hours_back 28 -param get_waiting_tasks.limit 141 -param get_waiting_tasks.server_name 28 -param mute_analysis_finding.reason 27 -param mute_analysis_finding.server_name 50 -param mute_analysis_finding.story_path_hash 45 -tool analyze_plan_xml 880 -tool analyze_procedure_plan 912 -tool analyze_query_plan 903 -tool analyze_query_store_plan 210 -tool analyze_server 4160 -tool audit_config 431 -tool compare_analysis 1647 -tool get_active_queries 1421 -tool get_alert_history 3573 -tool get_alert_settings 3954 -tool get_analysis_facts 1825 -tool get_analysis_findings 2875 -tool get_blocked_process_reports 1340 -tool get_blocked_process_xml 480 -tool get_blocking_stats 381 -tool get_blocking_trend 199 -tool get_collection_health 21026 -tool get_collection_log 4405 -tool get_cpu_scheduler_pressure 640 -tool get_cpu_utilization 249 -tool get_current_waits_trend 443 -tool get_daily_summary 1034 -tool get_daily_summary_range 1474 -tool get_database_config 448 -tool get_database_config_changes 378 -tool get_database_scoped_config 288 -tool get_database_sizes 480 -tool get_deadlock_detail 492 -tool get_deadlock_trend 157 -tool get_deadlocks 694 -tool get_default_trace_events 702 -tool get_file_io_stats 690 -tool get_file_io_trend 585 -tool get_health_parser_cpu_tasks 452 -tool get_health_parser_io_issues 419 -tool get_health_parser_memory_broker 420 -tool get_health_parser_memory_conditions 442 -tool get_health_parser_memory_node_oom 360 -tool get_health_parser_scheduler_issues 446 -tool get_health_parser_severe_errors 480 -tool get_health_parser_significant_waits 615 -tool get_health_parser_system_health 393 -tool get_index_usage 493 -tool get_latch_stats 1372 -tool get_lock_wait_trend 646 -tool get_long_query_completions 1037 -tool get_memory_clerks 232 -tool get_memory_grants 988 -tool get_memory_pressure_events 1018 -tool get_memory_stats 480 -tool get_memory_trend 610 -tool get_mute_rules 109 -tool get_notification_routes 600 -tool get_object_locking 661 -tool get_perfmon_stats 1034 -tool get_perfmon_trend 1057 -tool get_plan_cache_bloat 520 -tool get_plan_corrections 1672 -tool get_plan_xml 169 -tool get_procedure_duration_trend 1180 -tool get_pvs_stats 1156 -tool get_query_duration_trend 1136 -tool get_query_heatmap 799 -tool get_query_store_duration_trend 1276 -tool get_query_store_health 2587 -tool get_query_store_regressions 1187 -tool get_query_store_top 215 -tool get_query_trend 798 -tool get_resource_semaphore 1310 -tool get_running_jobs 642 -tool get_server_config 571 -tool get_server_config_changes 379 -tool get_server_properties 894 -tool get_server_summary 648 -tool get_session_stats 391 -tool get_spinlock_stats 1306 -tool get_table_index_sizes 794 -tool get_tempdb_trend 254 -tool get_tool_guide 487 -tool get_top_procedures_by_cpu 821 -tool get_top_queries_by_cpu 1606 -tool get_trace_flag_changes 311 -tool get_trace_flags 333 -tool get_wait_stats 644 -tool get_wait_trend 445 -tool get_wait_types 150 -tool get_waiting_tasks 751 -tool list_servers 160 -tool mute_analysis_finding 1382 diff --git a/Lite.Tests/McpToolsListBudget/McpAlertTools.txt b/Lite.Tests/McpToolsListBudget/McpAlertTools.txt new file mode 100644 index 0000000000..afea35bac9 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpAlertTools.txt @@ -0,0 +1,13 @@ +# McpAlertTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_alert_history 3573 +param get_alert_history.as_of 167 +param get_alert_history.hours_back 29 +param get_alert_history.include_dismissed 357 +param get_alert_history.limit 133 + +tool get_alert_settings 3954 + +tool get_mute_rules 109 +param get_mute_rules.enabled_only 41 + +tool get_notification_routes 600 diff --git a/Lite.Tests/McpToolsListBudget/McpAnalysisTools.txt b/Lite.Tests/McpToolsListBudget/McpAnalysisTools.txt new file mode 100644 index 0000000000..898d3f0fd9 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpAnalysisTools.txt @@ -0,0 +1,32 @@ +# McpAnalysisTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool analyze_server 4160 +param analyze_server.as_of 167 +param analyze_server.hours_back 104 +param analyze_server.server_name 28 + +tool audit_config 431 +param audit_config.server_name 28 + +tool compare_analysis 1647 +param compare_analysis.as_of 167 +param compare_analysis.baseline_hours_back 211 +param compare_analysis.hours_back 57 +param compare_analysis.server_name 28 + +tool get_analysis_facts 1825 +param get_analysis_facts.as_of 167 +param get_analysis_facts.hours_back 36 +param get_analysis_facts.min_severity 90 +param get_analysis_facts.server_name 28 +param get_analysis_facts.source 515 + +tool get_analysis_findings 2875 +param get_analysis_findings.as_of 167 +param get_analysis_findings.hours_back 49 +param get_analysis_findings.include_drilldown 235 +param get_analysis_findings.server_name 28 + +tool mute_analysis_finding 1382 +param mute_analysis_finding.reason 27 +param mute_analysis_finding.server_name 50 +param mute_analysis_finding.story_path_hash 45 diff --git a/Lite.Tests/McpToolsListBudget/McpBlockingTools.txt b/Lite.Tests/McpToolsListBudget/McpBlockingTools.txt new file mode 100644 index 0000000000..0406c2616a --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpBlockingTools.txt @@ -0,0 +1,40 @@ +# McpBlockingTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_blocked_process_reports 1340 +param get_blocked_process_reports.as_of 167 +param get_blocked_process_reports.hours_back 29 +param get_blocked_process_reports.limit 133 +param get_blocked_process_reports.server_name 28 + +tool get_blocked_process_xml 480 +param get_blocked_process_xml.as_of 167 +param get_blocked_process_xml.hours_back 29 +param get_blocked_process_xml.limit 113 +param get_blocked_process_xml.server_name 28 + +tool get_blocking_trend 199 +param get_blocking_trend.as_of 167 +param get_blocking_trend.hours_back 29 +param get_blocking_trend.server_name 28 + +tool get_deadlock_detail 492 +param get_deadlock_detail.as_of 167 +param get_deadlock_detail.hours_back 29 +param get_deadlock_detail.limit 119 +param get_deadlock_detail.server_name 28 + +tool get_deadlock_trend 157 +param get_deadlock_trend.as_of 167 +param get_deadlock_trend.hours_back 29 +param get_deadlock_trend.server_name 28 + +tool get_deadlocks 694 +param get_deadlocks.as_of 167 +param get_deadlocks.hours_back 29 +param get_deadlocks.limit 133 +param get_deadlocks.server_name 28 + +tool get_lock_wait_trend 646 +param get_lock_wait_trend.as_of 167 +param get_lock_wait_trend.bucket_minutes 55 +param get_lock_wait_trend.hours_back 29 +param get_lock_wait_trend.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpConfigHistoryTools.txt b/Lite.Tests/McpToolsListBudget/McpConfigHistoryTools.txt new file mode 100644 index 0000000000..3cd6f89853 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpConfigHistoryTools.txt @@ -0,0 +1,15 @@ +# McpConfigHistoryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_database_config_changes 378 +param get_database_config_changes.as_of 167 +param get_database_config_changes.hours_back 51 +param get_database_config_changes.server_name 28 + +tool get_server_config_changes 379 +param get_server_config_changes.as_of 167 +param get_server_config_changes.hours_back 51 +param get_server_config_changes.server_name 28 + +tool get_trace_flag_changes 311 +param get_trace_flag_changes.as_of 167 +param get_trace_flag_changes.hours_back 51 +param get_trace_flag_changes.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpConfigTools.txt b/Lite.Tests/McpToolsListBudget/McpConfigTools.txt new file mode 100644 index 0000000000..5e825cb263 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpConfigTools.txt @@ -0,0 +1,18 @@ +# McpConfigTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_database_config 448 +param get_database_config.database_name 54 +param get_database_config.server_name 28 + +tool get_database_scoped_config 288 +param get_database_scoped_config.database_name 54 +param get_database_scoped_config.server_name 28 + +tool get_query_store_health 2587 +param get_query_store_health.database_name 54 +param get_query_store_health.server_name 28 + +tool get_server_config 571 +param get_server_config.server_name 28 + +tool get_trace_flags 333 +param get_trace_flags.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpCpuTools.txt b/Lite.Tests/McpToolsListBudget/McpCpuTools.txt new file mode 100644 index 0000000000..ce3c61c55a --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpCpuTools.txt @@ -0,0 +1,6 @@ +# McpCpuTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_cpu_utilization 249 +param get_cpu_utilization.as_of 167 +param get_cpu_utilization.bucket_minutes 55 +param get_cpu_utilization.hours_back 28 +param get_cpu_utilization.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpDefaultTraceTools.txt b/Lite.Tests/McpToolsListBudget/McpDefaultTraceTools.txt new file mode 100644 index 0000000000..0f1014b005 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpDefaultTraceTools.txt @@ -0,0 +1,6 @@ +# McpDefaultTraceTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_default_trace_events 702 +param get_default_trace_events.as_of 167 +param get_default_trace_events.hours_back 41 +param get_default_trace_events.limit 48 +param get_default_trace_events.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt b/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt new file mode 100644 index 0000000000..54f8663714 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt @@ -0,0 +1,2 @@ +# McpDiscoveryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool list_servers 160 diff --git a/Lite.Tests/McpToolsListBudget/McpHealthParserTools.txt b/Lite.Tests/McpToolsListBudget/McpHealthParserTools.txt new file mode 100644 index 0000000000..e784049bf8 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpHealthParserTools.txt @@ -0,0 +1,54 @@ +# McpHealthParserTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_health_parser_cpu_tasks 452 +param get_health_parser_cpu_tasks.as_of 167 +param get_health_parser_cpu_tasks.hours_back 41 +param get_health_parser_cpu_tasks.limit 38 +param get_health_parser_cpu_tasks.server_name 28 + +tool get_health_parser_io_issues 419 +param get_health_parser_io_issues.as_of 167 +param get_health_parser_io_issues.hours_back 41 +param get_health_parser_io_issues.limit 38 +param get_health_parser_io_issues.server_name 28 + +tool get_health_parser_memory_broker 420 +param get_health_parser_memory_broker.as_of 167 +param get_health_parser_memory_broker.hours_back 41 +param get_health_parser_memory_broker.limit 38 +param get_health_parser_memory_broker.server_name 28 + +tool get_health_parser_memory_conditions 442 +param get_health_parser_memory_conditions.as_of 167 +param get_health_parser_memory_conditions.hours_back 41 +param get_health_parser_memory_conditions.limit 38 +param get_health_parser_memory_conditions.server_name 28 + +tool get_health_parser_memory_node_oom 360 +param get_health_parser_memory_node_oom.as_of 167 +param get_health_parser_memory_node_oom.hours_back 41 +param get_health_parser_memory_node_oom.limit 38 +param get_health_parser_memory_node_oom.server_name 28 + +tool get_health_parser_scheduler_issues 446 +param get_health_parser_scheduler_issues.as_of 167 +param get_health_parser_scheduler_issues.hours_back 41 +param get_health_parser_scheduler_issues.limit 38 +param get_health_parser_scheduler_issues.server_name 28 + +tool get_health_parser_severe_errors 480 +param get_health_parser_severe_errors.as_of 167 +param get_health_parser_severe_errors.hours_back 41 +param get_health_parser_severe_errors.limit 38 +param get_health_parser_severe_errors.server_name 28 + +tool get_health_parser_significant_waits 615 +param get_health_parser_significant_waits.as_of 167 +param get_health_parser_significant_waits.hours_back 41 +param get_health_parser_significant_waits.limit 38 +param get_health_parser_significant_waits.server_name 28 + +tool get_health_parser_system_health 393 +param get_health_parser_system_health.as_of 167 +param get_health_parser_system_health.hours_back 41 +param get_health_parser_system_health.limit 38 +param get_health_parser_system_health.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpHealthTools.txt b/Lite.Tests/McpToolsListBudget/McpHealthTools.txt new file mode 100644 index 0000000000..a44092d521 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpHealthTools.txt @@ -0,0 +1,35 @@ +# McpHealthTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_blocking_stats 381 +param get_blocking_stats.as_of 167 +param get_blocking_stats.hours_back 191 +param get_blocking_stats.server_name 28 + +tool get_collection_health 21026 +param get_collection_health.server_name 28 + +tool get_collection_log 4405 +param get_collection_log.as_of 167 +param get_collection_log.collector_name 244 +param get_collection_log.hours_back 191 +param get_collection_log.limit 120 +param get_collection_log.min_duration_ms 382 +param get_collection_log.server_name 28 +param get_collection_log.status 481 + +tool get_current_waits_trend 443 +param get_current_waits_trend.as_of 167 +param get_current_waits_trend.database_name 73 +param get_current_waits_trend.hours_back 190 +param get_current_waits_trend.server_name 28 + +tool get_daily_summary 1034 +param get_daily_summary.server_name 28 +param get_daily_summary.summary_date 155 + +tool get_daily_summary_range 1474 +param get_daily_summary_range.as_of 166 +param get_daily_summary_range.days_back 84 +param get_daily_summary_range.server_name 28 + +tool get_server_summary 648 +param get_server_summary.server_name 71 diff --git a/Lite.Tests/McpToolsListBudget/McpIoTools.txt b/Lite.Tests/McpToolsListBudget/McpIoTools.txt new file mode 100644 index 0000000000..a8920b9e25 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpIoTools.txt @@ -0,0 +1,10 @@ +# McpIoTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_file_io_stats 690 +param get_file_io_stats.server_name 28 + +tool get_file_io_trend 585 +param get_file_io_trend.as_of 167 +param get_file_io_trend.bucket_minutes 55 +param get_file_io_trend.database_name 56 +param get_file_io_trend.hours_back 29 +param get_file_io_trend.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpJobTools.txt b/Lite.Tests/McpToolsListBudget/McpJobTools.txt new file mode 100644 index 0000000000..1380cd4294 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpJobTools.txt @@ -0,0 +1,3 @@ +# McpJobTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_running_jobs 642 +param get_running_jobs.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpLatchSpinlockTools.txt b/Lite.Tests/McpToolsListBudget/McpLatchSpinlockTools.txt new file mode 100644 index 0000000000..f30f90a480 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpLatchSpinlockTools.txt @@ -0,0 +1,12 @@ +# McpLatchSpinlockTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_latch_stats 1372 +param get_latch_stats.as_of 167 +param get_latch_stats.hours_back 63 +param get_latch_stats.limit 194 +param get_latch_stats.server_name 28 + +tool get_spinlock_stats 1306 +param get_spinlock_stats.as_of 167 +param get_spinlock_stats.hours_back 63 +param get_spinlock_stats.limit 195 +param get_spinlock_stats.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpLongQueryTools.txt b/Lite.Tests/McpToolsListBudget/McpLongQueryTools.txt new file mode 100644 index 0000000000..3b837202bc --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpLongQueryTools.txt @@ -0,0 +1,6 @@ +# McpLongQueryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_long_query_completions 1037 +param get_long_query_completions.as_of 167 +param get_long_query_completions.hours_back 29 +param get_long_query_completions.limit 134 +param get_long_query_completions.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpMemoryTools.txt b/Lite.Tests/McpToolsListBudget/McpMemoryTools.txt new file mode 100644 index 0000000000..d00dc2872b --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpMemoryTools.txt @@ -0,0 +1,27 @@ +# McpMemoryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_memory_clerks 232 +param get_memory_clerks.server_name 28 + +tool get_memory_grants 988 +param get_memory_grants.as_of 167 +param get_memory_grants.hours_back 120 +param get_memory_grants.server_name 28 + +tool get_memory_pressure_events 1018 +param get_memory_pressure_events.as_of 167 +param get_memory_pressure_events.hours_back 29 +param get_memory_pressure_events.server_name 28 + +tool get_memory_stats 480 +param get_memory_stats.server_name 28 + +tool get_memory_trend 610 +param get_memory_trend.as_of 167 +param get_memory_trend.bucket_minutes 55 +param get_memory_trend.hours_back 29 +param get_memory_trend.server_name 28 + +tool get_resource_semaphore 1310 +param get_resource_semaphore.as_of 167 +param get_resource_semaphore.hours_back 121 +param get_resource_semaphore.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpObjectStatsTools.txt b/Lite.Tests/McpToolsListBudget/McpObjectStatsTools.txt new file mode 100644 index 0000000000..070ef03993 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpObjectStatsTools.txt @@ -0,0 +1,9 @@ +# McpObjectStatsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_index_usage 493 +param get_index_usage.server_name 28 + +tool get_object_locking 661 +param get_object_locking.server_name 28 + +tool get_table_index_sizes 794 +param get_table_index_sizes.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt b/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt new file mode 100644 index 0000000000..51c832c58b --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt @@ -0,0 +1,12 @@ +# McpPerfmonTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_perfmon_stats 1034 +param get_perfmon_stats.counter_name 61 +param get_perfmon_stats.instance_name 57 +param get_perfmon_stats.server_name 28 + +tool get_perfmon_trend 1057 +param get_perfmon_trend.as_of 167 +param get_perfmon_trend.bucket_minutes 55 +param get_perfmon_trend.counter_name 50 +param get_perfmon_trend.hours_back 29 +param get_perfmon_trend.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpPlanCacheSchedulerTools.txt b/Lite.Tests/McpToolsListBudget/McpPlanCacheSchedulerTools.txt new file mode 100644 index 0000000000..eb7d9cfd20 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpPlanCacheSchedulerTools.txt @@ -0,0 +1,10 @@ +# McpPlanCacheSchedulerTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_cpu_scheduler_pressure 640 +param get_cpu_scheduler_pressure.as_of 167 +param get_cpu_scheduler_pressure.hours_back 63 +param get_cpu_scheduler_pressure.server_name 28 + +tool get_plan_cache_bloat 520 +param get_plan_cache_bloat.as_of 167 +param get_plan_cache_bloat.hours_back 63 +param get_plan_cache_bloat.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpPlanCorrectionTools.txt b/Lite.Tests/McpToolsListBudget/McpPlanCorrectionTools.txt new file mode 100644 index 0000000000..e46805b6ea --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpPlanCorrectionTools.txt @@ -0,0 +1,6 @@ +# McpPlanCorrectionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_plan_corrections 1672 +param get_plan_corrections.as_of 167 +param get_plan_corrections.hours_back 29 +param get_plan_corrections.limit 156 +param get_plan_corrections.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpPlanTools.txt b/Lite.Tests/McpToolsListBudget/McpPlanTools.txt new file mode 100644 index 0000000000..a915c56454 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpPlanTools.txt @@ -0,0 +1,20 @@ +# McpPlanTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool analyze_plan_xml 880 +param analyze_plan_xml.plan_xml 25 + +tool analyze_procedure_plan 912 +param analyze_procedure_plan.plan_handle 53 +param analyze_procedure_plan.server_name 28 + +tool analyze_query_plan 903 +param analyze_query_plan.query_hash 49 +param analyze_query_plan.server_name 28 + +tool analyze_query_store_plan 210 +param analyze_query_store_plan.database_name 43 +param analyze_query_store_plan.plan_id 37 +param analyze_query_store_plan.server_name 28 + +tool get_plan_xml 169 +param get_plan_xml.query_hash 49 +param get_plan_xml.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpPvsTools.txt b/Lite.Tests/McpToolsListBudget/McpPvsTools.txt new file mode 100644 index 0000000000..51adad3d11 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpPvsTools.txt @@ -0,0 +1,4 @@ +# McpPvsTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_pvs_stats 1156 +param get_pvs_stats.server_name 28 +param get_pvs_stats.trend_hours_back 98 diff --git a/Lite.Tests/McpToolsListBudget/McpQueryTools.txt b/Lite.Tests/McpToolsListBudget/McpQueryTools.txt new file mode 100644 index 0000000000..4d0ac4712a --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpQueryTools.txt @@ -0,0 +1,63 @@ +# McpQueryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_procedure_duration_trend 1180 +param get_procedure_duration_trend.as_of 167 +param get_procedure_duration_trend.bucket_minutes 55 +param get_procedure_duration_trend.hours_back 29 +param get_procedure_duration_trend.server_name 28 + +tool get_query_duration_trend 1136 +param get_query_duration_trend.as_of 167 +param get_query_duration_trend.bucket_minutes 55 +param get_query_duration_trend.hours_back 29 +param get_query_duration_trend.server_name 28 + +tool get_query_heatmap 799 +param get_query_heatmap.as_of 167 +param get_query_heatmap.bucket_minutes 160 +param get_query_heatmap.database_name 46 +param get_query_heatmap.hours_back 43 +param get_query_heatmap.limit 195 +param get_query_heatmap.metric 123 +param get_query_heatmap.server_name 28 + +tool get_query_store_duration_trend 1276 +param get_query_store_duration_trend.as_of 167 +param get_query_store_duration_trend.hours_back 29 +param get_query_store_duration_trend.server_name 28 + +tool get_query_store_regressions 1187 +param get_query_store_regressions.as_of 167 +param get_query_store_regressions.database_name 46 +param get_query_store_regressions.hours_back 110 +param get_query_store_regressions.limit 86 +param get_query_store_regressions.server_name 28 + +tool get_query_store_top 215 +param get_query_store_top.as_of 167 +param get_query_store_top.database_name 30 +param get_query_store_top.hours_back 29 +param get_query_store_top.server_name 28 +param get_query_store_top.top 34 + +tool get_query_trend 798 +param get_query_trend.as_of 167 +param get_query_trend.database_name 39 +param get_query_trend.hours_back 29 +param get_query_trend.query_hash 72 +param get_query_trend.server_name 28 + +tool get_top_procedures_by_cpu 821 +param get_top_procedures_by_cpu.as_of 167 +param get_top_procedures_by_cpu.database_name 30 +param get_top_procedures_by_cpu.hours_back 29 +param get_top_procedures_by_cpu.server_name 28 +param get_top_procedures_by_cpu.top 37 + +tool get_top_queries_by_cpu 1606 +param get_top_queries_by_cpu.as_of 167 +param get_top_queries_by_cpu.database_name 30 +param get_top_queries_by_cpu.hours_back 29 +param get_top_queries_by_cpu.min_dop 132 +param get_top_queries_by_cpu.parallel_only 371 +param get_top_queries_by_cpu.server_name 28 +param get_top_queries_by_cpu.top 34 diff --git a/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt b/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt new file mode 100644 index 0000000000..f13b3229c7 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt @@ -0,0 +1,6 @@ +# McpServerInfoTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_database_sizes 480 +param get_database_sizes.server_name 28 + +tool get_server_properties 894 +param get_server_properties.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpSessionTools.txt b/Lite.Tests/McpToolsListBudget/McpSessionTools.txt new file mode 100644 index 0000000000..0df445de4c --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpSessionTools.txt @@ -0,0 +1,11 @@ +# McpSessionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_active_queries 1421 +param get_active_queries.as_of 167 +param get_active_queries.blocking_only 238 +param get_active_queries.database_name 156 +param get_active_queries.hours_back 37 +param get_active_queries.limit 149 +param get_active_queries.server_name 28 + +tool get_session_stats 391 +param get_session_stats.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpTempDbTools.txt b/Lite.Tests/McpToolsListBudget/McpTempDbTools.txt new file mode 100644 index 0000000000..360befeb23 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpTempDbTools.txt @@ -0,0 +1,6 @@ +# McpTempDbTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_tempdb_trend 254 +param get_tempdb_trend.as_of 167 +param get_tempdb_trend.bucket_minutes 55 +param get_tempdb_trend.hours_back 29 +param get_tempdb_trend.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpToolGuideTools.txt b/Lite.Tests/McpToolsListBudget/McpToolGuideTools.txt new file mode 100644 index 0000000000..a9332797cc --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpToolGuideTools.txt @@ -0,0 +1,4 @@ +# McpToolGuideTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_tool_guide 487 +param get_tool_guide.tools 98 +param get_tool_guide.topics 98 diff --git a/Lite.Tests/McpToolsListBudget/McpWaitTools.txt b/Lite.Tests/McpToolsListBudget/McpWaitTools.txt new file mode 100644 index 0000000000..7750117d84 --- /dev/null +++ b/Lite.Tests/McpToolsListBudget/McpWaitTools.txt @@ -0,0 +1,24 @@ +# McpWaitTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. +tool get_wait_stats 644 +param get_wait_stats.as_of 167 +param get_wait_stats.hours_back 40 +param get_wait_stats.limit 145 +param get_wait_stats.server_name 28 + +tool get_wait_trend 445 +param get_wait_trend.as_of 167 +param get_wait_trend.bucket_minutes 55 +param get_wait_trend.hours_back 29 +param get_wait_trend.server_name 28 +param get_wait_trend.wait_type 56 + +tool get_wait_types 150 +param get_wait_types.as_of 167 +param get_wait_types.hours_back 29 +param get_wait_types.server_name 28 + +tool get_waiting_tasks 751 +param get_waiting_tasks.as_of 167 +param get_waiting_tasks.hours_back 28 +param get_waiting_tasks.limit 141 +param get_waiting_tasks.server_name 28 diff --git a/Lite.Tests/McpToolsListBudgetTests.cs b/Lite.Tests/McpToolsListBudgetTests.cs index a298158e1f..00e22b7e77 100644 --- a/Lite.Tests/McpToolsListBudgetTests.cs +++ b/Lite.Tests/McpToolsListBudgetTests.cs @@ -33,12 +33,25 @@ namespace Lite.Tests; /// same WithGeminiCompatibleTools path the host registers every tool with, over the tool types the host /// source registers. Darling's twin is Darling.Tests/McpToolsListBudgetTests (D6 lockstep). /// -/// The pins are ceilings that only go down. McpToolsListBudget.txt holds one line per tool -/// (its served description's length) and per advertised parameter (its description's length). A value over its -/// ceiling fails: put the new text after the tool's instead, where -/// get_tool_guide serves it. A value under its ceiling fails too, so a saving is banked the moment it -/// lands and cannot be spent later: lower the line. A new tool or parameter adds its line deliberately, with -/// the reason in the PR. The total's ceiling is below, with the reason for each change. +/// The pins live one file per tool class (#3898 step 0), under McpToolsListBudget/, named +/// for the class that declares the tools (e.g. McpWaitTools.txt). Inside a file: one block per +/// tool (a tool <name> <chars> line followed by that tool's sorted +/// param <tool>.<param> <chars> lines), blocks sorted by tool name, exactly one blank +/// line between blocks. Splitting the old single file this way means two content PRs that each touch a +/// different tool class never conflict, and a blank line between every two tools in the SAME class means two +/// PRs that each touch an adjacent tool in that class don't conflict either. +/// +/// The pins are ceilings that only go down. A value over its ceiling fails: put the new text after +/// the tool's instead, where get_tool_guide serves it. A value under +/// its ceiling fails too, so a saving is banked the moment it lands and cannot be spent later: lower the line. +/// A new tool or parameter adds its line deliberately, with the reason in the PR. Every failure names the file +/// and line and gives the exact replacement line. The directory is also checked for layout: every served tool +/// has exactly one block, in the file for the class that declares it; every file belongs to a registered class; +/// and every file's bytes match its own canonical re-serialization (order, blank lines, no duplicates). +/// +/// The total () is a growth-only ceiling; content PRs never lower +/// it (that happens once, in the #3898 PR that ships the last converted family). The measured total is also +/// written to the test output, so a run's log states it without re-deriving it. /// /// D2's absolute caps apply to converted tools (a description carrying the marker): the served head /// at most 1,000 characters (the target is 600), every parameter description at most 200, and every tail and @@ -46,9 +59,16 @@ namespace Lite.Tests; /// public sealed class McpToolsListBudgetTests { + private readonly ITestOutputHelper _output; + + public McpToolsListBudgetTests(ITestOutputHelper output) + { + _output = output; + } + /// - /// The serialized tools/list ceiling, in UTF-8 bytes. Change log (a ceiling only goes down, except for a - /// deliberate new tool or parameter, named here): + /// The serialized tools/list ceiling, in UTF-8 bytes. Growth-only; a content PR never lowers this (the + /// final #3898 PR does, once, after every family converts). Change log: /// /// #3898 Phase 0 + seam: pinned at the measured value, which includes the new get_tool_guide and the /// converted get_health_parser_* family. @@ -59,59 +79,180 @@ public sealed class McpToolsListBudgetTests /// private const int TotalCeilingBytes = 148_216; - /// How far under the total ceiling the measured size may sit before the ceiling must be lowered. - /// The per-tool and per-parameter lines are exact; this slack only spares every small edit a conflict on - /// one shared line. - private const int TotalBankingSlackBytes = 4_096; - private const int ConvertedHeadCap = 1_000; private const int ConvertedParameterCap = 200; - private const string BudgetFile = "McpToolsListBudget.txt"; + + /// The directory holding one budget file per tool class, next to this test. + private const string BudgetDir = "McpToolsListBudget"; + + private const string HeaderSuffix = + ": tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks."; + + private const string DebugPrefix = "lite-budget"; [Fact] - public void ToolsList_TotalBytes_IsAtOrUnderTheCeiling_AndSavingsAreBanked() + public void ToolsList_TotalBytes_IsAtOrUnderTheCeiling() { var measured = Measure(); + _output.WriteLine($"tools/list total: {measured.TotalBytes:N0} bytes (ceiling {TotalCeilingBytes:N0})."); Assert.True(measured.TotalBytes <= TotalCeilingBytes, $"tools/list is {measured.TotalBytes:N0} bytes, over its {TotalCeilingBytes:N0}-byte ceiling. Move reading guidance after the tool's {McpToolGuide.Marker} marker instead of growing the head."); - Assert.True(TotalCeilingBytes - measured.TotalBytes <= TotalBankingSlackBytes, - $"tools/list is {measured.TotalBytes:N0} bytes, {TotalCeilingBytes - measured.TotalBytes:N0} under its {TotalCeilingBytes:N0}-byte ceiling. Bank the saving: lower {nameof(TotalCeilingBytes)} to {measured.TotalBytes} and say why."); } [Fact] public void EveryServedDescription_AndEveryParameterDescription_MatchesItsCeiling() { var measured = Measure(); - var pinned = ReadBudget(); + var files = ReadBudgetFiles(); var problems = new List(); - foreach (var (key, value) in measured.Lines) + var pinned = new Dictionary(StringComparer.Ordinal); + foreach (var file in files) + { + foreach (var entry in file.Entries) + { + if (!pinned.TryAdd(entry.Key, (entry.Value, file.FileName, entry.LineNumber))) + { + var first = pinned[entry.Key]; + problems.Add($"{file.FileName}:{entry.LineNumber}: '{entry.Key}' duplicates the pin already at {first.FileName}:{first.LineNumber}. Remove one."); + } + } + } + + var measuredKeys = new SortedDictionary(StringComparer.Ordinal); + foreach (var tool in measured.Tools) + { + measuredKeys[$"tool {tool.Name}"] = (tool.Served.Length, tool.ClassName); + foreach (var (parameter, length) in tool.ParameterDescriptionLengths) + { + measuredKeys[$"param {tool.Name}.{parameter}"] = (length, tool.ClassName); + } + } + + foreach (var (key, measuredValue) in measuredKeys) { if (!pinned.TryGetValue(key, out var ceiling)) { - problems.Add($"{key}: {value} is not pinned. Add the line '{key} {value}' deliberately, with the reason in the PR."); + problems.Add($"{key}: {measuredValue.Value} is not pinned. Add the line '{key} {measuredValue.Value}' to {BudgetDir}/{measuredValue.ClassName}.txt."); + } + else if (measuredValue.Value > ceiling.Value) + { + problems.Add($"{ceiling.FileName}:{ceiling.LineNumber}: '{key}' is {measuredValue.Value}, over its ceiling {ceiling.Value}. Put the new text after the tool's {McpToolGuide.Marker} marker (get_tool_guide serves it) instead of raising the pin; if the growth is deliberate, replace the line with '{key} {measuredValue.Value}'."); + } + else if (measuredValue.Value < ceiling.Value) + { + problems.Add($"{ceiling.FileName}:{ceiling.LineNumber}: '{key}' is {measuredValue.Value}, under its ceiling {ceiling.Value}. Bank the saving: replace the line with '{key} {measuredValue.Value}'."); + } + } + + foreach (var (key, ceiling) in pinned) + { + if (!measuredKeys.ContainsKey(key)) + { + problems.Add($"{ceiling.FileName}:{ceiling.LineNumber}: '{key}' is pinned, but no such tool or parameter is served. Remove the line."); + } + } + + if (problems.Count > 0) + { + Assert.Fail($"{problems.Count} tools/list budget line(s) moved:\n" + + string.Join("\n", problems.OrderBy(p => p, StringComparer.Ordinal).Take(40))); + } + } + + /// + /// The directory's layout, independent of the values inside it: every served tool has exactly one block, + /// filed under the class that declares it; every file belongs to a class that is actually registered; and + /// every file's bytes are its own canonical re-serialization (blocks sorted by tool name, exactly one blank + /// line between them, no duplicates). A file that fails the last check gets its corrected form written to a + /// temp path named in the failure. + /// + [Fact] + public void EveryBudgetFile_MatchesItsRegisteredClass_AndIsInCanonicalForm() + { + var measured = Measure(); + var files = ReadBudgetFiles(); + var registeredClassNames = measured.ClassNames.ToHashSet(StringComparer.Ordinal); + var problems = new List(); + + var blockOwner = new Dictionary(StringComparer.Ordinal); + foreach (var file in files) + { + foreach (var entry in file.Entries) + { + var (tool, parameter) = ParseKey(entry.Key); + if (parameter is null) + { + blockOwner[tool] = file.ClassName; + } + } + } + + foreach (var tool in measured.Tools) + { + if (!blockOwner.TryGetValue(tool.Name, out var actualClass)) + { + problems.Add($"{tool.Name}: no block in any {BudgetDir} file. Add 'tool {tool.Name} {tool.Served.Length}' (and its param lines) to {BudgetDir}/{tool.ClassName}.txt."); + } + else if (!string.Equals(actualClass, tool.ClassName, StringComparison.Ordinal)) + { + problems.Add($"{tool.Name}: blocked in {BudgetDir}/{actualClass}.txt, but is declared on {tool.ClassName}. Move its block to {BudgetDir}/{tool.ClassName}.txt."); } - else if (value > ceiling) + } + + var measuredToolNames = measured.Tools.Select(t => t.Name).ToHashSet(StringComparer.Ordinal); + foreach (var (toolName, className) in blockOwner) + { + if (!measuredToolNames.Contains(toolName)) { - problems.Add($"{key}: {value} is over its ceiling {ceiling}. Put the new text after the tool's {McpToolGuide.Marker} marker (get_tool_guide serves it) instead of raising the pin."); + problems.Add($"{BudgetDir}/{className}.txt: block for '{toolName}', which is not a served tool. Remove it."); } - else if (value < ceiling) + } + + foreach (var file in files) + { + if (!registeredClassNames.Contains(file.ClassName)) { - problems.Add($"{key}: {value} is under its ceiling {ceiling}. Bank the saving: lower the line to '{key} {value}'."); + problems.Add($"{BudgetDir}/{file.FileName}: '{file.ClassName}' is not a registered tool class. Delete the file."); } } - foreach (var key in pinned.Keys.Where(k => !measured.Lines.ContainsKey(k))) + foreach (var file in files) { - problems.Add($"{key}: pinned, but no such tool or parameter is served. Remove the line."); + List canonical; + try + { + canonical = CanonicalLinesFromEntries(file.ClassName, file.Entries); + } + catch (InvalidOperationException ex) + { + problems.Add($"{BudgetDir}/{file.FileName}: {ex.Message}"); + continue; + } + + if (canonical.SequenceEqual(file.RawLines, StringComparer.Ordinal)) + { + continue; + } + + var firstDiff = 0; + while (firstDiff < canonical.Count && firstDiff < file.RawLines.Count + && string.Equals(canonical[firstDiff], file.RawLines[firstDiff], StringComparison.Ordinal)) + { + firstDiff++; + } + + var tempPath = Path.Combine(Path.GetTempPath(), $"{DebugPrefix}-{file.FileName}"); + File.WriteAllText(tempPath, JoinForDisk(canonical)); + var expected = firstDiff < canonical.Count ? canonical[firstDiff] : "(end of file)"; + var actual = firstDiff < file.RawLines.Count ? file.RawLines[firstDiff] : "(end of file)"; + problems.Add($"{BudgetDir}/{file.FileName}:{firstDiff + 1}: not in canonical form (blocks sorted by tool name, exactly one blank line between blocks, no duplicates). Expected '{expected}', found '{actual}'. Canonical file written to {tempPath}."); } if (problems.Count > 0) { - var path = Path.Combine(Path.GetTempPath(), "lite-" + BudgetFile); - File.WriteAllText(path, Render(measured)); - Assert.Fail($"{problems.Count} tools/list budget line(s) moved (measured file written to {path}):\n" - + string.Join("\n", problems.Take(40))); + Assert.Fail($"{problems.Count} budget-layout problem(s):\n" + + string.Join("\n", problems.OrderBy(p => p, StringComparer.Ordinal).Take(40))); } } @@ -181,12 +322,13 @@ public void NoDescriptionCarriesANearMissMarker() internal sealed record MeasuredTool( string Name, + string ClassName, string? Description, string Served, string? Tail, IReadOnlyList<(string Parameter, int Length)> ParameterDescriptionLengths); - internal sealed record Measurement(int TotalBytes, IReadOnlyList Tools, IReadOnlyDictionary Lines); + internal sealed record Measurement(int TotalBytes, IReadOnlyList Tools, IReadOnlyList ClassNames); private static Measurement? _cached; @@ -202,17 +344,19 @@ internal static Measurement Measure() var json = JsonSerializer.Serialize(new ListToolsResult { Tools = protocolTools }, McpJsonUtilities.DefaultOptions); var totalBytes = Encoding.UTF8.GetByteCount(json); - var descriptions = types - .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) - .Select(m => (Attr: m.GetCustomAttribute(), Method: m)) + var toolInfo = types + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).Select(m => (Type: t, Method: m))) + .Select(x => (Attr: x.Method.GetCustomAttribute(), x.Method, x.Type)) .Where(x => x.Attr is not null) - .ToDictionary(x => x.Attr!.Name!, x => x.Method.GetCustomAttribute()?.Description, StringComparer.Ordinal); + .ToDictionary( + x => x.Attr!.Name!, + x => (ClassName: x.Type.Name, Description: x.Method.GetCustomAttribute()?.Description), + StringComparer.Ordinal); var measuredTools = new List(); - var lines = new SortedDictionary(StringComparer.Ordinal); foreach (var tool in protocolTools) { - var description = descriptions[tool.Name]; + var (className, description) = toolInfo[tool.Name]; var served = tool.Description ?? string.Empty; var tail = description is null ? null : McpToolGuide.Split(description).Tail; var parameters = new List<(string, int)>(); @@ -222,15 +366,14 @@ internal static Measurement Measure() { var length = property.Value.TryGetProperty("description", out var d) ? d.GetString()!.Length : 0; parameters.Add((property.Name, length)); - lines[$"param {tool.Name}.{property.Name}"] = length; } } - lines[$"tool {tool.Name}"] = served.Length; - measuredTools.Add(new MeasuredTool(tool.Name, description, served, tail, parameters)); + measuredTools.Add(new MeasuredTool(tool.Name, className, description, served, tail, parameters)); } - _cached = new Measurement(totalBytes, measuredTools, lines); + var classNames = types.Select(t => t.Name).OrderBy(n => n, StringComparer.Ordinal).ToList(); + _cached = new Measurement(totalBytes, measuredTools, classNames); return _cached; } @@ -271,37 +414,121 @@ private static (List Tools, List Types) BuildServedTools() return (provider.GetServices().ToList(), types); } - private static Dictionary ReadBudget() + /* ---------------- per-class budget files ---------------- */ + + internal sealed record BudgetEntry(string Key, int Value, int LineNumber); + + internal sealed record BudgetFileContents(string ClassName, string FileName, IReadOnlyList RawLines, IReadOnlyList Entries); + + private static List ReadBudgetFiles() { - var result = new Dictionary(StringComparer.Ordinal); - foreach (var raw in File.ReadAllLines(RepoPath("Lite.Tests", BudgetFile))) + var dir = RepoPath("Lite.Tests", BudgetDir); + var files = new List(); + foreach (var filePath in Directory.GetFiles(dir, "*.txt").OrderBy(p => p, StringComparer.Ordinal)) { - var line = raw.Trim(); - if (line.Length == 0 || line.StartsWith('#')) + var className = Path.GetFileNameWithoutExtension(filePath); + var rawLines = File.ReadAllLines(filePath); + var entries = new List(); + for (var i = 0; i < rawLines.Length; i++) { - continue; + var trimmed = rawLines[i].Trim(); + if (trimmed.Length == 0 || trimmed.StartsWith('#')) + { + continue; + } + + var at = trimmed.LastIndexOf(' '); + entries.Add(new BudgetEntry(trimmed[..at], int.Parse(trimmed[(at + 1)..], CultureInfo.InvariantCulture), i + 1)); + } + + files.Add(new BudgetFileContents(className, Path.GetFileName(filePath), rawLines, entries)); + } + + return files; + } + + /// Splits a budget key into its tool name and, for a parameter line, the parameter name. + private static (string Tool, string? Parameter) ParseKey(string key) + { + if (key.StartsWith("tool ", StringComparison.Ordinal)) + { + return (key["tool ".Length..], null); + } + + if (key.StartsWith("param ", StringComparison.Ordinal)) + { + var rest = key["param ".Length..]; + var dot = rest.IndexOf('.', StringComparison.Ordinal); + return (rest[..dot], rest[(dot + 1)..]); + } + + throw new InvalidOperationException($"unrecognized budget key '{key}'"); + } + + private sealed record Block(string Tool, int ToolValue, IReadOnlyList<(string Parameter, int Length)> Parameters); + + /// The one formatter both the live-measurement renderer and the on-disk canonical-form checker + /// use, so a file this test generates always reads back as canonical. + private static List CanonicalLines(string className, IEnumerable blocks) + { + var lines = new List { "# " + className + HeaderSuffix }; + var first = true; + foreach (var block in blocks.OrderBy(b => b.Tool, StringComparer.Ordinal)) + { + if (!first) + { + lines.Add(string.Empty); } - var at = line.LastIndexOf(' '); - result.Add(line[..at], int.Parse(line[(at + 1)..], CultureInfo.InvariantCulture)); + first = false; + lines.Add($"tool {block.Tool} {block.ToolValue.ToString(CultureInfo.InvariantCulture)}"); + foreach (var (parameter, length) in block.Parameters.OrderBy(p => p.Parameter, StringComparer.Ordinal)) + { + lines.Add($"param {block.Tool}.{parameter} {length.ToString(CultureInfo.InvariantCulture)}"); + } } - return result; + return lines; } - private static string Render(Measurement measured) + internal static List CanonicalLinesFromMeasurement(string className, IEnumerable tools) => + CanonicalLines(className, tools.Select(t => new Block(t.Name, t.Served.Length, t.ParameterDescriptionLengths))); + + private static List CanonicalLinesFromEntries(string className, IReadOnlyList entries) { - var sb = new StringBuilder(); - sb.AppendLine("# #3898 Phase 0: Lite's tools/list budget. Ceilings only go down; see McpToolsListBudgetTests."); - sb.AppendLine("# 'tool ' is the served description; 'param . ' is a parameter description."); - foreach (var (key, value) in measured.Lines) + var toolValues = new Dictionary(StringComparer.Ordinal); + var paramsByTool = new Dictionary>(StringComparer.Ordinal); + foreach (var entry in entries) { - sb.Append(key).Append(' ').Append(value.ToString(CultureInfo.InvariantCulture)).AppendLine(); + var (tool, parameter) = ParseKey(entry.Key); + if (parameter is null) + { + toolValues[tool] = entry.Value; + } + else + { + if (!paramsByTool.TryGetValue(tool, out var list)) + { + list = new List<(string, int)>(); + paramsByTool[tool] = list; + } + + list.Add((parameter, entry.Value)); + } } - return sb.ToString(); + var orphans = paramsByTool.Keys.Where(t => !toolValues.ContainsKey(t)).OrderBy(t => t, StringComparer.Ordinal).ToList(); + if (orphans.Count > 0) + { + throw new InvalidOperationException($"parameter line(s) for tool(s) with no 'tool' line in this file: {string.Join(", ", orphans)}."); + } + + return CanonicalLines(className, toolValues.Select(kv => + new Block(kv.Key, kv.Value, paramsByTool.TryGetValue(kv.Key, out var ps) ? ps : new List<(string, int)>()))); } + private static string JoinForDisk(IEnumerable lines) => string.Join("\r\n", lines) + "\r\n"; + private static string RepoPath(params string[] segments) => Path.Combine(new[] { RepoRoot() }.Concat(segments).ToArray()); private static string RepoRoot([CallerFilePath] string thisFile = "") diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Alerting.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Alerting.cs new file mode 100644 index 0000000000..d496ff623f --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Alerting.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The alerting family's topics (#3898). Empty until the alerting lane converts its tools and fills +/// this in; see 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_alerting = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs new file mode 100644 index 0000000000..df4831d73e --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The data family's topics (#3898). Empty until the data lane converts its tools and fills this in; +/// see 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_data = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.HealthParser.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.HealthParser.cs new file mode 100644 index 0000000000..eac8f5c8a2 --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.HealthParser.cs @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The health-parser family's topics (#3898 D1 pilot). See 's class +/// doc comment for the partial-file pattern every other family copies. +public static partial class McpToolGuideTopics +{ + /// The topic's name. + public const string SystemHealthEmptyWindowsName = "system_health_empty_windows"; + + /// + /// What zero rows means for a get_health_parser_* read: the four-rung ladder both SKUs' + /// EmptyAsync implement (#3541 A12). It was the same 400-character sentence in all nine + /// descriptions on both SKUs; the heads keep the guardrail (an empty answer is not a clean bill; read the + /// witness keys) and this carries the rungs. + /// + public const string SystemHealthEmptyWindows = + "Every get_health_parser_* answer carries source_observed (whether this server's system_health session has EVER " + + "been read into the store) and last_captured_at (the collector's newest capture), beside status on data and " + + "empty answers alike. Zero rows is one of four things. (1) Captured and gated out: events of the category were " + + "stored in the window and the significance gate kept none; healthy, and events_in_window says how many were " + + "captured. (2) Captured before, not in this window: a quiet window; last_captured_of_type_at says when the last " + + "one was stored, so move as_of or widen hours_back to reach it. (3) Never recorded, session alive: other " + + "categories are stored, so the ring buffer is being read and the engine has never recorded one of these; for a " + + "memory-node OOM or a severe error that is the healthy measurement. Those three answer status empty. (4) Nothing " + + "of any type, ever: a dead system_health session or a collector that never ran; status unavailable with " + + "source_observed false. That is not a clean bill of health; it is no evidence either way. An engine with no " + + "system_health session to read (Azure SQL Database) says so instead."; + + private static readonly McpToolGuideTopic[] s_healthParser = + [ + new( + SystemHealthEmptyWindowsName, + "What an empty get_health_parser_* answer means: four different nothings, one of them no evidence at all.", + SystemHealthEmptyWindows), + ]; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgA.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgA.cs new file mode 100644 index 0000000000..278b1e6a66 --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgA.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The pgA family's topics (#3898). Empty until the pgA lane converts its tools and fills this in; see +/// 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_pgA = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgB.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgB.cs new file mode 100644 index 0000000000..d2b4661e6b --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.PgB.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The pgB family's topics (#3898). Empty until the pgB lane converts its tools and fills this in; see +/// 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_pgB = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Platform.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Platform.cs new file mode 100644 index 0000000000..8ef0a9b570 --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Platform.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The platform family's topics (#3898). Empty until the platform lane converts its tools and fills +/// this in; see 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_platform = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlCore.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlCore.cs new file mode 100644 index 0000000000..42a0626f37 --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlCore.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The sqlCore family's topics (#3898). Empty until the sqlCore lane converts its tools and fills this +/// in; see 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_sqlCore = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlTail.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlTail.cs new file mode 100644 index 0000000000..0336a4da02 --- /dev/null +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlTail.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitor.Common; + +/// The sqlTail family's topics (#3898). Empty until the sqlTail lane converts its tools and fills this +/// in; see 's class doc comment for the partial-file pattern. +public static partial class McpToolGuideTopics +{ + private static readonly McpToolGuideTopic[] s_sqlTail = []; +} diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.cs index d5ec8bac70..3323493acc 100644 --- a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.cs +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.cs @@ -7,6 +7,7 @@ */ using System.Collections.Generic; +using System.Linq; namespace PerformanceMonitor.Common; @@ -16,37 +17,29 @@ namespace PerformanceMonitor.Common; /// const so a converted tool's tail can include one verbatim (attribute arguments must be constants), /// which saves the caller a second call without a second copy of the sentence. Both SKUs serve the same set /// (D6 lockstep). +/// +/// #3898 step 0: a partial class, one file per lane. Each family converted under #3898 gets its own +/// McpToolGuideTopics.<Family>.cs, declaring that family's own private static readonly +/// McpToolGuideTopic[] field ( is the pilot's). A lane fills in its own, +/// already-declared, already-empty array; it never edits this file or any other family's, so two families' +/// topic work never conflicts. +/// +/// Why is built in a static constructor. Static field initializers in different +/// partial-class files run in an unspecified order, so an initializer here that read another part's array could +/// observe it before that part's own initializer ran. A static constructor in the SAME type runs only after +/// every part's field initializers have run, regardless of their relative order, so it is the one place that +/// may safely read across parts. /// -public static class McpToolGuideTopics +public static partial class McpToolGuideTopics { - /// The topic's name. - public const string SystemHealthEmptyWindowsName = "system_health_empty_windows"; + /// Every topic, in the order the index lists them: every family's array, family arrays in a fixed + /// order, each family's own topics in its own declared order. + public static IReadOnlyList All { get; } - /// - /// What zero rows means for a get_health_parser_* read: the four-rung ladder both SKUs' - /// EmptyAsync implement (#3541 A12). It was the same 400-character sentence in all nine - /// descriptions on both SKUs; the heads keep the guardrail (an empty answer is not a clean bill; read the - /// witness keys) and this carries the rungs. - /// - public const string SystemHealthEmptyWindows = - "Every get_health_parser_* answer carries source_observed (whether this server's system_health session has EVER " + - "been read into the store) and last_captured_at (the collector's newest capture), beside status on data and " + - "empty answers alike. Zero rows is one of four things. (1) Captured and gated out: events of the category were " + - "stored in the window and the significance gate kept none; healthy, and events_in_window says how many were " + - "captured. (2) Captured before, not in this window: a quiet window; last_captured_of_type_at says when the last " + - "one was stored, so move as_of or widen hours_back to reach it. (3) Never recorded, session alive: other " + - "categories are stored, so the ring buffer is being read and the engine has never recorded one of these; for a " + - "memory-node OOM or a severe error that is the healthy measurement. Those three answer status empty. (4) Nothing " + - "of any type, ever: a dead system_health session or a collector that never ran; status unavailable with " + - "source_observed false. That is not a clean bill of health; it is no evidence either way. An engine with no " + - "system_health session to read (Azure SQL Database) says so instead."; - - /// Every topic, in the order the index lists them. - public static IReadOnlyList All { get; } = - [ - new( - SystemHealthEmptyWindowsName, - "What an empty get_health_parser_* answer means: four different nothings, one of them no evidence at all.", - SystemHealthEmptyWindows), - ]; + static McpToolGuideTopics() + { + All = new[] { s_healthParser, s_data, s_sqlCore, s_sqlTail, s_alerting, s_platform, s_pgA, s_pgB } + .SelectMany(topics => topics) + .ToList(); + } } From 7c4e3e54578dfc1bb87fbc09db41c9dcf0095b35 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:44:02 -0400 Subject: [PATCH 2/5] Data family: 7 MCP tool descriptions move reading guidance off tools/list (#3898) Converts get_wait_stats, get_file_io_stats, get_perfmon_stats, get_server_properties, get_top_procedures_by_cpu, get_top_queries_by_cpu and list_servers on both Darling and Lite: served heads shrink to guardrail facts, full original prose moves to each tool's get_tool_guide tail. A shared cpu_time_extremes_and_attribution topic replaces the identical sentence duplicated across the two top-by-cpu tools. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01M7r1CXmEcBmsrTV34CCLfF --- .../Darling.Tests/McpToolGuideHeads.Data.cs | 85 +++++++++++++++++++ Darling/Darling.Tests/McpToolGuideTests.cs | 7 +- .../DarlingMcpDataTools.txt | 18 ++-- .../Mcp/DarlingMcpDataTools.cs | 18 ++-- Lite.Tests/McpToolGuideHeads.Data.cs | 77 +++++++++++++++++ Lite.Tests/McpToolGuideTests.cs | 4 +- .../McpToolsListBudget/McpDiscoveryTools.txt | 2 +- Lite.Tests/McpToolsListBudget/McpIoTools.txt | 2 +- .../McpToolsListBudget/McpPerfmonTools.txt | 2 +- .../McpToolsListBudget/McpQueryTools.txt | 6 +- .../McpToolsListBudget/McpServerInfoTools.txt | 2 +- .../McpToolsListBudget/McpWaitTools.txt | 2 +- Lite/Mcp/McpDiscoveryTools.cs | 2 +- Lite/Mcp/McpIoTools.cs | 2 +- Lite/Mcp/McpPerfmonTools.cs | 2 +- Lite/Mcp/McpQueryTools.cs | 6 +- Lite/Mcp/McpServerInfoTools.cs | 2 +- Lite/Mcp/McpWaitTools.cs | 2 +- .../Mcp/McpToolGuideTopics.Data.cs | 28 +++++- 19 files changed, 228 insertions(+), 41 deletions(-) create mode 100644 Darling/Darling.Tests/McpToolGuideHeads.Data.cs create mode 100644 Lite.Tests/McpToolGuideHeads.Data.cs diff --git a/Darling/Darling.Tests/McpToolGuideHeads.Data.cs b/Darling/Darling.Tests/McpToolGuideHeads.Data.cs new file mode 100644 index 0000000000..c26c0684d0 --- /dev/null +++ b/Darling/Darling.Tests/McpToolGuideHeads.Data.cs @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; +using PerformanceMonitor.Common; +using Xunit; + +namespace Darling.Tests; + +/// +/// #3898 D3 head pins for the "Data" family: the core resource/query/discovery tools converted out of +/// DarlingMcpDataTools (Darling) and their Lite twins. Follows the pattern in +/// . +/// +public sealed class McpToolGuideHeadsDataTests +{ + private static readonly string[] ConvertedTools = + [ + "get_file_io_stats", + "get_perfmon_stats", + "get_server_properties", + "get_top_procedures_by_cpu", + "get_top_queries_by_cpu", + "get_wait_stats", + "list_servers", + ]; + + /// The per-tool guardrail phrase each head must state. + private static readonly (string Tool, string Fact)[] HeadFacts = + [ + ("get_file_io_stats", "Reads the newest snapshot, not a window"), + ("get_perfmon_stats", "Reads the newest snapshot, not a window"), + ("get_server_properties", "Reads the newest snapshot, not a window"), + ("get_top_procedures_by_cpu", "LIFETIME extremes, not windowed"), + ("get_top_queries_by_cpu", "LIFETIME extremes, not windowed"), + ("get_top_queries_by_cpu", "an empty page under it is the window's real answer, not a miss"), + ("get_wait_stats", "Bounded by limit"), + ("list_servers", "Darling has no live connection to monitored servers"), + ("list_servers", "Lite's status IS a live connection check"), + ]; + + [Fact] + public void EveryConvertedHead_CarriesItsGuardrailFact_AndThePointer() + { + foreach (var tool in ConvertedTools) + { + var served = McpToolGuideTests.Served(tool); + Assert.NotNull(served.Tail); + Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal); + Assert.True(served.Served.Length <= 620, $"{tool}: served head {served.Served.Length} is over the 620 target"); + Assert.All(served.ParameterDescriptionLengths, p => Assert.True(p.Length <= 200, $"{tool}.{p.Parameter}: {p.Length} > 200")); + } + + foreach (var (tool, fact) in HeadFacts) + { + Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal); + } + } + + /// Nothing dropped: the shared cpu-extremes/attribution topic rides on both top_queries and + /// top_procedures tails, and the zero-semantics sentence each carries in its own tail. + [Fact] + public void CpuTimeExtremesTopic_RidesOnBothTopByCpuTools() + { + Assert.EndsWith(McpToolGuideTopics.CpuTimeExtremesAndAttribution, McpToolGuideTests.Served("get_top_procedures_by_cpu").Tail!, StringComparison.Ordinal); + Assert.EndsWith(McpToolGuideTopics.CpuTimeExtremesAndAttribution, McpToolGuideTests.Served("get_top_queries_by_cpu").Tail!, StringComparison.Ordinal); + Assert.Contains("distinct_texts", McpToolGuideTests.Served("get_top_queries_by_cpu").Tail!, StringComparison.Ordinal); + } + + /// list_servers is D10's always-loaded entry tool; its Darling-only peer_fleets/engine_kind detail + /// lives in its own tail, never the shared head (D6: the head must be true on both SKUs). + [Fact] + public void ListServers_KeepsEngineAndPeerDetailOffTheSharedHead() + { + var served = McpToolGuideTests.Served("list_servers"); + Assert.DoesNotContain("peer_fleets", served.Served, StringComparison.Ordinal); + Assert.DoesNotContain("engine_kind", served.Served, StringComparison.Ordinal); + Assert.Contains("peer_fleets", served.Tail!, StringComparison.Ordinal); + } +} diff --git a/Darling/Darling.Tests/McpToolGuideTests.cs b/Darling/Darling.Tests/McpToolGuideTests.cs index a27da117f5..4d8a807340 100644 --- a/Darling/Darling.Tests/McpToolGuideTests.cs +++ b/Darling/Darling.Tests/McpToolGuideTests.cs @@ -152,7 +152,7 @@ public void GetToolGuide_ServesTails_AndTopics_AndReportsUnknownNames() var catalog = HostCatalog(); using var doc = JsonDocument.Parse(DarlingMcpToolGuideTools.GetToolGuide( catalog, - ["GET_HEALTH_PARSER_CPU_TASKS", "get_health_parser_cpu_tasks", "list_servers", "no_such_tool"], + ["GET_HEALTH_PARSER_CPU_TASKS", "get_health_parser_cpu_tasks", "get_cpu_utilization", "no_such_tool"], ["system_health_empty_windows", "no_such_topic"])); var root = doc.RootElement; @@ -161,7 +161,7 @@ public void GetToolGuide_ServesTails_AndTopics_AndReportsUnknownNames() Assert.Single(tools); Assert.Equal("get_health_parser_cpu_tasks", tools[0].GetProperty("name").GetString()); Assert.Equal(Served("get_health_parser_cpu_tasks").Tail, tools[0].GetProperty("guide").GetString()); - Assert.Equal(new[] { "list_servers" }, root.GetProperty("no_separate_guide").EnumerateArray().Select(e => e.GetString()).ToArray()); + Assert.Equal(new[] { "get_cpu_utilization" }, root.GetProperty("no_separate_guide").EnumerateArray().Select(e => e.GetString()).ToArray()); Assert.Equal(new[] { "no_such_tool" }, root.GetProperty("unknown_tools").EnumerateArray().Select(e => e.GetString()).ToArray()); Assert.Equal(new[] { "no_such_topic" }, root.GetProperty("unknown_topics").EnumerateArray().Select(e => e.GetString()).ToArray()); Assert.Equal(McpToolGuideTopics.SystemHealthEmptyWindows, root.GetProperty("topics")[0].GetProperty("guide").GetString()); @@ -175,9 +175,12 @@ public void GetToolGuide_WithNoArguments_IsTheIndex() about THIS test's host, not a family pin (that lives in McpToolGuideHeadsHealthParserTests). */ var fixtureToolsWithGuides = new[] { + "get_file_io_stats", "get_health_parser_cpu_tasks", "get_health_parser_io_issues", "get_health_parser_memory_broker", "get_health_parser_memory_conditions", "get_health_parser_memory_node_oom", "get_health_parser_scheduler_issues", "get_health_parser_severe_errors", "get_health_parser_significant_waits", "get_health_parser_system_health", + "get_perfmon_stats", "get_server_properties", "get_top_procedures_by_cpu", "get_top_queries_by_cpu", + "get_wait_stats", "list_servers", }; using var doc = JsonDocument.Parse(DarlingMcpToolGuideTools.GetToolGuide(HostCatalog(), null, null)); diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt index da470203cc..9ed95676fb 100644 --- a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt @@ -28,7 +28,7 @@ param get_current_waits_trend.database_name 73 param get_current_waits_trend.hours_back 190 param get_current_waits_trend.server_name 28 -tool get_file_io_stats 690 +tool get_file_io_stats 425 param get_file_io_stats.server_name 28 tool get_memory_clerks 232 @@ -37,7 +37,7 @@ param get_memory_clerks.server_name 28 tool get_memory_stats 480 param get_memory_stats.server_name 28 -tool get_perfmon_stats 1034 +tool get_perfmon_stats 509 param get_perfmon_stats.counter_name 61 param get_perfmon_stats.instance_name 57 param get_perfmon_stats.server_name 28 @@ -49,7 +49,7 @@ param get_query_store_top.hours_back 29 param get_query_store_top.server_name 28 param get_query_store_top.top 34 -tool get_server_properties 894 +tool get_server_properties 559 param get_server_properties.server_name 28 tool get_tempdb_trend 254 @@ -58,24 +58,24 @@ param get_tempdb_trend.bucket_minutes 55 param get_tempdb_trend.hours_back 29 param get_tempdb_trend.server_name 28 -tool get_top_procedures_by_cpu 821 +tool get_top_procedures_by_cpu 406 param get_top_procedures_by_cpu.as_of 167 param get_top_procedures_by_cpu.database_name 30 param get_top_procedures_by_cpu.hours_back 29 param get_top_procedures_by_cpu.server_name 28 param get_top_procedures_by_cpu.top 37 -tool get_top_queries_by_cpu 1837 +tool get_top_queries_by_cpu 474 param get_top_queries_by_cpu.as_of 167 param get_top_queries_by_cpu.database_name 30 -param get_top_queries_by_cpu.group_by 616 +param get_top_queries_by_cpu.group_by 167 param get_top_queries_by_cpu.hours_back 29 param get_top_queries_by_cpu.min_dop 132 -param get_top_queries_by_cpu.parallel_only 371 +param get_top_queries_by_cpu.parallel_only 160 param get_top_queries_by_cpu.server_name 28 param get_top_queries_by_cpu.top 34 -tool get_wait_stats 644 +tool get_wait_stats 316 param get_wait_stats.as_of 167 param get_wait_stats.hours_back 40 param get_wait_stats.limit 145 @@ -93,4 +93,4 @@ param get_wait_types.as_of 167 param get_wait_types.hours_back 29 param get_wait_types.server_name 28 -tool list_servers 1228 +tool list_servers 382 diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs index 2f593d6a3a..2f22404d2b 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs @@ -98,7 +98,7 @@ internal static async Task GetCpuUtilization( } } - [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] + [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] public static async Task GetWaitStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -372,7 +372,7 @@ with the read by construction and tell the caller nothing it did not already hav } } - [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] + [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. Reads the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0.")] public static async Task GetFileIoStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null) @@ -461,7 +461,7 @@ internal static async Task GetTempDbTrend( } } - [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] + [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). Reads the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] public static async Task GetPerfmonStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -512,16 +512,16 @@ public static async Task GetPerfmonStats( /* ═══════════════════════════ query performance ═══════════════════════════ */ - [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means only rows predating the text dimension). Set group_by='host_object' to roll all of a procedure's statements into one row — necessary when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. Also returns cpu_attribution: the returned rows' summed CPU-seconds against the SQL process's measured CPU-seconds for the window (avg cpu_utilization % x core count x window) - attributed_cpu_ratio says how much of the box the ranking explains; when the CPU series or core count is missing, or covers too little of the window, the ratio is omitted rather than invented.")] + [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means only rows predating the text dimension). 'host_object' rolls all of a procedure's statements into one row — use it when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Ad-hoc statements have no host object and stay grouped per hash in both modes. distinct_query_hashes reports how many hashes a row rolled up. Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopQueriesByCpu( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, [Description("Hours of history. Default 24.")] int hours_back = 24, [Description("Number of top queries. Default 20.")] int top = 20, [Description("Filter to a specific database.")] string? database_name = null, - [Description("If true, only return queries whose cached plan has EVER run at DOP > 1. Note: max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled. Confirm current parallelism with analyze_query_plan, which reads the actual plan.")] bool parallel_only = false, + [Description("If true, only return queries whose cached plan has EVER run at DOP > 1 (a LIFETIME max_dop; can read stale after a MAXDOP change). See the tool's reading guide.")] bool parallel_only = false, [Description("Minimum DOP to filter on. Implies parallel filtering. Filters the same lifetime-max value as parallel_only, not current parallelism.")] int min_dop = 0, - [Description("Grouping. 'query_hash' (default) is one row per (database, query_hash, host_object). 'host_object' rolls every statement of a hosting procedure/function into ONE row — use it when dynamic SQL built with per-value literals fragments one logical statement across many query_hash values, which makes top-N-by-hash structurally unable to surface it (measured at 21 fragments for one statement, whose combined CPU was the largest on the instance while no single fragment ranked). Ad-hoc statements have no host object and stay grouped per hash in both modes. distinct_query_hashes reports how many hashes a row rolled up.")] string group_by = "query_hash", + [Description("Grouping. 'query_hash' (default): one row per (database, query_hash, host_object). 'host_object': rolls a proc's statements into ONE row. See the tool's reading guide.")] string group_by = "query_hash", [Description(McpHelpers.AsOfDescription)] string? as_of = null) { var (resolved, error) = await DarlingServerResolver.ResolveOrErrorAsync(postgres, server_name); @@ -667,7 +667,7 @@ the two answer different questions and the rows look alike. */ } } - [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time. Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. Also returns cpu_attribution: the returned rows' summed CPU-seconds against the SQL process's measured CPU-seconds for the window (avg cpu_utilization % x core count x window) - attributed_cpu_ratio says how much of the box the ranking explains; when the CPU series or core count is missing, or covers too little of the window, the ratio is omitted rather than invented.")] + [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time over a window ending at as_of. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed (extremes_note flags a provably stale one); cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopProceduresByCpu( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -859,7 +859,7 @@ it stops. The trend family renamed the same fact the same way (DarlingMcpTrendTo /* ═══════════════════════════ discovery / health ═══════════════════════════ */ - [McpServerTool(Name = "list_servers"), Description("Lists all monitored servers — SQL Server and PostgreSQL — with their collection freshness status and last collection time. Use this first to see available servers before calling other tools. Each row says which engine it describes: engine_kind is the raw registry token (sqlserver, postgres, aurora-postgres; null when no connect has stamped the row — the same vocabulary get_fleet_overview uses) and engine_version is the engine-aware version label (\"SQL Server 2022\", \"PostgreSQL 18\"; empty when no version has been collected). sql_version is a DEPRECATED legacy alias carrying the same value as engine_version, kept so existing consumers keep working — read engine_version instead, and never infer the engine from that key's name: a PostgreSQL row's sql_version reads \"PostgreSQL 18\". The service has no live connection to the monitored servers, so status is derived from how recently each server was collected (Online = fresh, Warning = stale, Offline = no recent collection). The peer_fleets block names the SIBLING Darling stores that monitor the rest of a split fleet, with what each one covers — this server can only NAME them (no cross-store reads), and peer_note says what an empty peer_fleets does and does not prove.")] + [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored servers — SQL Server and PostgreSQL. Each row says which engine it describes: engine_kind is the raw registry token (sqlserver, postgres, aurora-postgres; null when no connect has stamped the row — the same vocabulary get_fleet_overview uses) and engine_version is the engine-aware version label (\"SQL Server 2022\", \"PostgreSQL 18\"; empty when no version has been collected). sql_version is a DEPRECATED legacy alias carrying the same value as engine_version, kept so existing consumers keep working — read engine_version instead, and never infer the engine from that key's name: a PostgreSQL row's sql_version reads \"PostgreSQL 18\". The peer_fleets block names the SIBLING Darling stores that monitor the rest of a split fleet, with what each one covers — this server can only NAME them (no cross-store reads), and peer_note says what an empty peer_fleets does and does not prove.")] public static async Task ListServers( NpgsqlDataSource postgres) { @@ -1390,7 +1390,7 @@ column of the statement behind it follows. */ } } - [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] + [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). Reads the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Use for capacity planning and edition-aware recommendations. utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. A core count or memory figure here is what the server reported AT that stamp.")] public static async Task GetServerProperties( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null) diff --git a/Lite.Tests/McpToolGuideHeads.Data.cs b/Lite.Tests/McpToolGuideHeads.Data.cs new file mode 100644 index 0000000000..4aaea1bd46 --- /dev/null +++ b/Lite.Tests/McpToolGuideHeads.Data.cs @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor Lite. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; +using PerformanceMonitor.Common; +using Xunit; + +namespace Lite.Tests; + +/// +/// #3898 D3 head pins for the "Data" family's Lite twins. Darling's twin is +/// Darling.Tests/McpToolGuideHeadsDataTests, which also holds the cross-SKU lockstep pin. +/// +public sealed class McpToolGuideHeadsDataTests +{ + private static readonly string[] ConvertedTools = + [ + "get_file_io_stats", + "get_perfmon_stats", + "get_server_properties", + "get_top_procedures_by_cpu", + "get_top_queries_by_cpu", + "get_wait_stats", + "list_servers", + ]; + + private static readonly (string Tool, string Fact)[] HeadFacts = + [ + ("get_file_io_stats", "Reads the newest snapshot, not a window"), + ("get_perfmon_stats", "Reads the newest snapshot, not a window"), + ("get_server_properties", "Reads the newest snapshot, not a window"), + ("get_top_procedures_by_cpu", "LIFETIME extremes, not windowed"), + ("get_top_queries_by_cpu", "LIFETIME extremes, not windowed"), + ("get_wait_stats", "Bounded by limit"), + ("list_servers", "Lite's status IS a live connection check"), + ]; + + [Fact] + public void EveryConvertedHead_CarriesItsGuardrailFact_AndThePointer() + { + foreach (var tool in ConvertedTools) + { + var served = McpToolGuideTests.Served(tool); + Assert.NotNull(served.Tail); + Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal); + Assert.True(served.Served.Length <= 620, $"{tool}: served head {served.Served.Length} is over the 620 target"); + Assert.All(served.ParameterDescriptionLengths, p => Assert.True(p.Length <= 200, $"{tool}.{p.Parameter}: {p.Length} > 200")); + } + + foreach (var (tool, fact) in HeadFacts) + { + Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal); + } + } + + [Fact] + public void CpuTimeExtremesTopic_RidesOnBothTopByCpuTools() + { + Assert.EndsWith(McpToolGuideTopics.CpuTimeExtremesAndAttribution, McpToolGuideTests.Served("get_top_procedures_by_cpu").Tail!, StringComparison.Ordinal); + Assert.EndsWith(McpToolGuideTopics.CpuTimeExtremesAndAttribution, McpToolGuideTests.Served("get_top_queries_by_cpu").Tail!, StringComparison.Ordinal); + } + + /// Lite's list_servers has no fleet/engine concept; its tail is just the original "use this first" + /// sentence, never the Darling-only peer_fleets/engine_kind prose (that lives only in Darling's own tail). + [Fact] + public void ListServers_TailHasNoDarlingOnlyFleetDetail() + { + var served = McpToolGuideTests.Served("list_servers"); + Assert.DoesNotContain("peer_fleets", served.Served, StringComparison.Ordinal); + Assert.DoesNotContain("peer_fleets", served.Tail!, StringComparison.Ordinal); + } +} diff --git a/Lite.Tests/McpToolGuideTests.cs b/Lite.Tests/McpToolGuideTests.cs index da4a950bbf..1b8298be00 100644 --- a/Lite.Tests/McpToolGuideTests.cs +++ b/Lite.Tests/McpToolGuideTests.cs @@ -89,7 +89,7 @@ public void GetToolGuide_ServesTails_AndTopics_AndReportsUnknownNames() var catalog = HostCatalog(); using var doc = JsonDocument.Parse(McpToolGuideTools.GetToolGuide( catalog, - ["GET_HEALTH_PARSER_CPU_TASKS", "get_health_parser_cpu_tasks", "list_servers", "no_such_tool"], + ["GET_HEALTH_PARSER_CPU_TASKS", "get_health_parser_cpu_tasks", "no_such_tool"], ["system_health_empty_windows", "no_such_topic"])); var root = doc.RootElement; @@ -98,7 +98,6 @@ public void GetToolGuide_ServesTails_AndTopics_AndReportsUnknownNames() Assert.Single(tools); Assert.Equal("get_health_parser_cpu_tasks", tools[0].GetProperty("name").GetString()); Assert.Equal(Served("get_health_parser_cpu_tasks").Tail, tools[0].GetProperty("guide").GetString()); - Assert.Equal(new[] { "list_servers" }, root.GetProperty("no_separate_guide").EnumerateArray().Select(e => e.GetString()).ToArray()); Assert.Equal(new[] { "no_such_tool" }, root.GetProperty("unknown_tools").EnumerateArray().Select(e => e.GetString()).ToArray()); Assert.Equal(new[] { "no_such_topic" }, root.GetProperty("unknown_topics").EnumerateArray().Select(e => e.GetString()).ToArray()); Assert.Equal(McpToolGuideTopics.SystemHealthEmptyWindows, root.GetProperty("topics")[0].GetProperty("guide").GetString()); @@ -115,6 +114,7 @@ family pin (that lives in McpToolGuideHeadsHealthParserTests). */ "get_health_parser_cpu_tasks", "get_health_parser_io_issues", "get_health_parser_memory_broker", "get_health_parser_memory_conditions", "get_health_parser_memory_node_oom", "get_health_parser_scheduler_issues", "get_health_parser_severe_errors", "get_health_parser_significant_waits", "get_health_parser_system_health", + "list_servers", }; using var doc = JsonDocument.Parse(McpToolGuideTools.GetToolGuide(HostCatalog(), null, null)); diff --git a/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt b/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt index 54f8663714..00590a8f67 100644 --- a/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpDiscoveryTools.txt @@ -1,2 +1,2 @@ # McpDiscoveryTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. -tool list_servers 160 +tool list_servers 382 diff --git a/Lite.Tests/McpToolsListBudget/McpIoTools.txt b/Lite.Tests/McpToolsListBudget/McpIoTools.txt index a8920b9e25..7decae6864 100644 --- a/Lite.Tests/McpToolsListBudget/McpIoTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpIoTools.txt @@ -1,5 +1,5 @@ # McpIoTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. -tool get_file_io_stats 690 +tool get_file_io_stats 425 param get_file_io_stats.server_name 28 tool get_file_io_trend 585 diff --git a/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt b/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt index 51c832c58b..b0f16ae38c 100644 --- a/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt @@ -1,5 +1,5 @@ # McpPerfmonTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. -tool get_perfmon_stats 1034 +tool get_perfmon_stats 509 param get_perfmon_stats.counter_name 61 param get_perfmon_stats.instance_name 57 param get_perfmon_stats.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpQueryTools.txt b/Lite.Tests/McpToolsListBudget/McpQueryTools.txt index 4d0ac4712a..2158749e6f 100644 --- a/Lite.Tests/McpToolsListBudget/McpQueryTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpQueryTools.txt @@ -46,18 +46,18 @@ param get_query_trend.hours_back 29 param get_query_trend.query_hash 72 param get_query_trend.server_name 28 -tool get_top_procedures_by_cpu 821 +tool get_top_procedures_by_cpu 406 param get_top_procedures_by_cpu.as_of 167 param get_top_procedures_by_cpu.database_name 30 param get_top_procedures_by_cpu.hours_back 29 param get_top_procedures_by_cpu.server_name 28 param get_top_procedures_by_cpu.top 37 -tool get_top_queries_by_cpu 1606 +tool get_top_queries_by_cpu 474 param get_top_queries_by_cpu.as_of 167 param get_top_queries_by_cpu.database_name 30 param get_top_queries_by_cpu.hours_back 29 param get_top_queries_by_cpu.min_dop 132 -param get_top_queries_by_cpu.parallel_only 371 +param get_top_queries_by_cpu.parallel_only 160 param get_top_queries_by_cpu.server_name 28 param get_top_queries_by_cpu.top 34 diff --git a/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt b/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt index f13b3229c7..759e7165d8 100644 --- a/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt @@ -2,5 +2,5 @@ tool get_database_sizes 480 param get_database_sizes.server_name 28 -tool get_server_properties 894 +tool get_server_properties 559 param get_server_properties.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpWaitTools.txt b/Lite.Tests/McpToolsListBudget/McpWaitTools.txt index 7750117d84..3fbb238a2a 100644 --- a/Lite.Tests/McpToolsListBudget/McpWaitTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpWaitTools.txt @@ -1,5 +1,5 @@ # McpWaitTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. -tool get_wait_stats 644 +tool get_wait_stats 316 param get_wait_stats.as_of 167 param get_wait_stats.hours_back 40 param get_wait_stats.limit 145 diff --git a/Lite/Mcp/McpDiscoveryTools.cs b/Lite/Mcp/McpDiscoveryTools.cs index a56e8e0084..712fad7f69 100644 --- a/Lite/Mcp/McpDiscoveryTools.cs +++ b/Lite/Mcp/McpDiscoveryTools.cs @@ -8,7 +8,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpDiscoveryTools { - [McpServerTool(Name = "list_servers"), Description("Lists all monitored SQL Server instances with their current status and last collection time. Use this first to see available servers before calling other tools.")] + [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored SQL Server instances.")] public static async Task ListServers(ServerManager serverManager, LocalDataService dataService) { var servers = serverManager.GetEnabledServers(); diff --git a/Lite/Mcp/McpIoTools.cs b/Lite/Mcp/McpIoTools.cs index d972d6bb7b..11eb31ca41 100644 --- a/Lite/Mcp/McpIoTools.cs +++ b/Lite/Mcp/McpIoTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpIoTools { - [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] + [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. Reads the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0.")] public static async Task GetFileIoStats( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpPerfmonTools.cs b/Lite/Mcp/McpPerfmonTools.cs index 14419ddf40..e12f746201 100644 --- a/Lite/Mcp/McpPerfmonTools.cs +++ b/Lite/Mcp/McpPerfmonTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpPerfmonTools { - [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] + [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). Reads the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] public static async Task GetPerfmonStats( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpQueryTools.cs b/Lite/Mcp/McpQueryTools.cs index ad02151c7a..73850487db 100644 --- a/Lite/Mcp/McpQueryTools.cs +++ b/Lite/Mcp/McpQueryTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpQueryTools { - [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means no stored text for the group). Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. Also returns cpu_attribution: the returned rows' summed CPU-seconds against the SQL process's measured CPU-seconds for the window (avg cpu_utilization % x core count x window) - attributed_cpu_ratio says how much of the box the ranking explains; when the CPU series or core count is missing, or covers too little of the window, the ratio is omitted rather than invented.")] + [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means no stored text for the group). Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopQueriesByCpu( LocalDataService dataService, ServerManager serverManager, @@ -18,7 +18,7 @@ public static async Task GetTopQueriesByCpu( [Description("Hours of history. Default 24.")] int hours_back = 24, [Description("Number of top queries. Default 20.")] int top = 20, [Description("Filter to a specific database.")] string? database_name = null, - [Description("If true, only return queries whose cached plan has EVER run at DOP > 1. Note: max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled. Confirm current parallelism with analyze_query_plan, which reads the actual plan.")] bool parallel_only = false, + [Description("If true, only return queries whose cached plan has EVER run at DOP > 1 (a LIFETIME max_dop; can read stale after a MAXDOP change). See the tool's reading guide.")] bool parallel_only = false, [Description("Minimum DOP to filter on. Implies parallel filtering. Filters the same lifetime-max value as parallel_only, not current parallelism.")] int min_dop = 0, [Description(McpHelpers.AsOfDescription)] string? as_of = null) { @@ -146,7 +146,7 @@ ratio is omitted rather than invented when a denominator piece is missing. One n } } - [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time. Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. Also returns cpu_attribution: the returned rows' summed CPU-seconds against the SQL process's measured CPU-seconds for the window (avg cpu_utilization % x core count x window) - attributed_cpu_ratio says how much of the box the ranking explains; when the CPU series or core count is missing, or covers too little of the window, the ratio is omitted rather than invented.")] + [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time over a window ending at as_of. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed (extremes_note flags a provably stale one); cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopProceduresByCpu( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpServerInfoTools.cs b/Lite/Mcp/McpServerInfoTools.cs index ca66178928..5fcc549b2a 100644 --- a/Lite/Mcp/McpServerInfoTools.cs +++ b/Lite/Mcp/McpServerInfoTools.cs @@ -9,7 +9,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpServerInfoTools { - [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] + [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). Reads the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Use for capacity planning and edition-aware recommendations. utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. A core count or memory figure here is what the server reported AT that stamp.")] public static async Task GetServerProperties( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpWaitTools.cs b/Lite/Mcp/McpWaitTools.cs index 704665d63a..e6a46d4249 100644 --- a/Lite/Mcp/McpWaitTools.cs +++ b/Lite/Mcp/McpWaitTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpWaitTools { - [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] + [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] public static async Task GetWaitStats( LocalDataService dataService, ServerManager serverManager, diff --git a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs index df4831d73e..60600d077b 100644 --- a/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs +++ b/PerformanceMonitor.Common/Mcp/McpToolGuideTopics.Data.cs @@ -8,9 +8,31 @@ namespace PerformanceMonitor.Common; -/// The data family's topics (#3898). Empty until the data lane converts its tools and fills this in; -/// see 's class doc comment for the partial-file pattern. +/// The data family's topics (#3898). public static partial class McpToolGuideTopics { - private static readonly McpToolGuideTopic[] s_data = []; + /// The topic's name. + public const string CpuTimeExtremesAndAttributionName = "cpu_time_extremes_and_attribution"; + + /// + /// get_top_queries_by_cpu and get_top_procedures_by_cpu carried this identical sentence: min/max_cpu_ms + /// and min/max_elapsed_ms are lifetime extremes rather than windowed, and cpu_attribution's ratio is + /// omitted, never invented, when its inputs are missing. + /// + public const string CpuTimeExtremesAndAttribution = + "min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same " + + "semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme " + + "provably predates the window carry extremes_note. Also returns cpu_attribution: the returned rows' " + + "summed CPU-seconds against the SQL process's measured CPU-seconds for the window (avg cpu_utilization " + + "% x core count x window) - attributed_cpu_ratio says how much of the box the ranking explains; when " + + "the CPU series or core count is missing, or covers too little of the window, the ratio is omitted " + + "rather than invented."; + + private static readonly McpToolGuideTopic[] s_data = + [ + new( + CpuTimeExtremesAndAttributionName, + "How min/max CPU/elapsed ms and cpu_attribution's ratio read on the top_queries/top_procedures CPU rankings.", + CpuTimeExtremesAndAttribution), + ]; } From 644a8ee3c6adb0d361672857fbc95bdfc159a6c0 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:07:19 -0400 Subject: [PATCH 3/5] WIP: restore dropped sentences and LATEST IS A TIME head wording (#3898) --- .../Mcp/DarlingMcpDataTools.cs | 12 ++++++------ Lite/Mcp/McpDiscoveryTools.cs | 2 +- Lite/Mcp/McpIoTools.cs | 2 +- Lite/Mcp/McpPerfmonTools.cs | 2 +- Lite/Mcp/McpQueryTools.cs | 2 +- Lite/Mcp/McpServerInfoTools.cs | 2 +- Lite/Mcp/McpWaitTools.cs | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs index 2f22404d2b..8344a2ae18 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs @@ -98,7 +98,7 @@ internal static async Task GetCpuUtilization( } } - [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] + [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] public static async Task GetWaitStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -372,7 +372,7 @@ with the read by construction and tell the caller nothing it did not already hav } } - [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. Reads the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0.")] + [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] public static async Task GetFileIoStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null) @@ -461,7 +461,7 @@ internal static async Task GetTempDbTrend( } } - [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). Reads the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] + [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] public static async Task GetPerfmonStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -512,7 +512,7 @@ public static async Task GetPerfmonStats( /* ═══════════════════════════ query performance ═══════════════════════════ */ - [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means only rows predating the text dimension). 'host_object' rolls all of a procedure's statements into one row — use it when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Ad-hoc statements have no host object and stay grouped per hash in both modes. distinct_query_hashes reports how many hashes a row rolled up. Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] + [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means only rows predating the text dimension). 'host_object' rolls all of a procedure's statements into one row — use it when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Ad-hoc statements have no host object and stay grouped per hash in both modes. distinct_query_hashes reports how many hashes a row rolled up. Set group_by='host_object' to roll all of a procedure's statements into one row — necessary when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopQueriesByCpu( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -859,7 +859,7 @@ it stops. The trend family renamed the same fact the same way (DarlingMcpTrendTo /* ═══════════════════════════ discovery / health ═══════════════════════════ */ - [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored servers — SQL Server and PostgreSQL. Each row says which engine it describes: engine_kind is the raw registry token (sqlserver, postgres, aurora-postgres; null when no connect has stamped the row — the same vocabulary get_fleet_overview uses) and engine_version is the engine-aware version label (\"SQL Server 2022\", \"PostgreSQL 18\"; empty when no version has been collected). sql_version is a DEPRECATED legacy alias carrying the same value as engine_version, kept so existing consumers keep working — read engine_version instead, and never infer the engine from that key's name: a PostgreSQL row's sql_version reads \"PostgreSQL 18\". The peer_fleets block names the SIBLING Darling stores that monitor the rest of a split fleet, with what each one covers — this server can only NAME them (no cross-store reads), and peer_note says what an empty peer_fleets does and does not prove.")] + [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored servers — SQL Server and PostgreSQL — with their collection freshness status and last collection time. Each row says which engine it describes: engine_kind is the raw registry token (sqlserver, postgres, aurora-postgres; null when no connect has stamped the row — the same vocabulary get_fleet_overview uses) and engine_version is the engine-aware version label (\"SQL Server 2022\", \"PostgreSQL 18\"; empty when no version has been collected). sql_version is a DEPRECATED legacy alias carrying the same value as engine_version, kept so existing consumers keep working — read engine_version instead, and never infer the engine from that key's name: a PostgreSQL row's sql_version reads \"PostgreSQL 18\". The service has no live connection to the monitored servers, so status is derived from how recently each server was collected (Online = fresh, Warning = stale, Offline = no recent collection). The peer_fleets block names the SIBLING Darling stores that monitor the rest of a split fleet, with what each one covers — this server can only NAME them (no cross-store reads), and peer_note says what an empty peer_fleets does and does not prove.")] public static async Task ListServers( NpgsqlDataSource postgres) { @@ -1390,7 +1390,7 @@ column of the statement behind it follows. */ } } - [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). Reads the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Use for capacity planning and edition-aware recommendations. utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. A core count or memory figure here is what the server reported AT that stamp.")] + [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] public static async Task GetServerProperties( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null) diff --git a/Lite/Mcp/McpDiscoveryTools.cs b/Lite/Mcp/McpDiscoveryTools.cs index 712fad7f69..82f02d6048 100644 --- a/Lite/Mcp/McpDiscoveryTools.cs +++ b/Lite/Mcp/McpDiscoveryTools.cs @@ -8,7 +8,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpDiscoveryTools { - [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored SQL Server instances.")] + [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored SQL Server instances with their current status and last collection time.")] public static async Task ListServers(ServerManager serverManager, LocalDataService dataService) { var servers = serverManager.GetEnabledServers(); diff --git a/Lite/Mcp/McpIoTools.cs b/Lite/Mcp/McpIoTools.cs index 11eb31ca41..4f321b4520 100644 --- a/Lite/Mcp/McpIoTools.cs +++ b/Lite/Mcp/McpIoTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpIoTools { - [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. Reads the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0.")] + [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] public static async Task GetFileIoStats( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpPerfmonTools.cs b/Lite/Mcp/McpPerfmonTools.cs index e12f746201..1498f988e8 100644 --- a/Lite/Mcp/McpPerfmonTools.cs +++ b/Lite/Mcp/McpPerfmonTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpPerfmonTools { - [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). Reads the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] + [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] public static async Task GetPerfmonStats( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpQueryTools.cs b/Lite/Mcp/McpQueryTools.cs index 73850487db..0edb0fc43c 100644 --- a/Lite/Mcp/McpQueryTools.cs +++ b/Lite/Mcp/McpQueryTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpQueryTools { - [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means no stored text for the group). Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] + [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means no stored text for the group). Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopQueriesByCpu( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpServerInfoTools.cs b/Lite/Mcp/McpServerInfoTools.cs index 5fcc549b2a..61bca17e7b 100644 --- a/Lite/Mcp/McpServerInfoTools.cs +++ b/Lite/Mcp/McpServerInfoTools.cs @@ -9,7 +9,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpServerInfoTools { - [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). Reads the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Use for capacity planning and edition-aware recommendations. utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. A core count or memory figure here is what the server reported AT that stamp.")] + [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] public static async Task GetServerProperties( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpWaitTools.cs b/Lite/Mcp/McpWaitTools.cs index e6a46d4249..a9348f928d 100644 --- a/Lite/Mcp/McpWaitTools.cs +++ b/Lite/Mcp/McpWaitTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpWaitTools { - [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] + [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] public static async Task GetWaitStats( LocalDataService dataService, ServerManager serverManager, From c457e2f182a806575c0fc5e9f30a12489de16801 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:19:31 -0400 Subject: [PATCH 4/5] Fix round 1 (#4055): restore dropped sentences, LATEST IS A TIME head wording, and two source-parsing test helpers that assumed a single description literal (#3898) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01M7r1CXmEcBmsrTV34CCLfF --- .../McpLatestSnapshotStampTests.cs | 52 +++++++++++++++---- Darling/Darling.Tests/McpPageContractTests.cs | 34 +++++++++++- .../Darling.Tests/McpToolGuideHeads.Data.cs | 6 +-- .../DarlingMcpDataTools.txt | 6 +-- Lite.Tests/McpToolGuideHeads.Data.cs | 6 +-- Lite.Tests/McpToolsListBudget/McpIoTools.txt | 2 +- .../McpToolsListBudget/McpPerfmonTools.txt | 2 +- .../McpToolsListBudget/McpServerInfoTools.txt | 2 +- 8 files changed, 86 insertions(+), 24 deletions(-) diff --git a/Darling/Darling.Tests/McpLatestSnapshotStampTests.cs b/Darling/Darling.Tests/McpLatestSnapshotStampTests.cs index 2bac509ee3..86046b4b5c 100644 --- a/Darling/Darling.Tests/McpLatestSnapshotStampTests.cs +++ b/Darling/Darling.Tests/McpLatestSnapshotStampTests.cs @@ -741,20 +741,50 @@ private static string DescriptionOf(string body, string label) further down can never be read as the tool's — which is exactly what a bare `Description(` search did on the const-described tools when this file was first executed. */ var attribute = Regex.Match(body, @"\A\[McpServerTool\(Name = ""[a-z_0-9]+""\), Description\((?:\s*)(?:""((?:[^""\\]|\\.)*)""|(\w+))\)\]"); - Assert.True(attribute.Success, $"{label}: could not locate the tool's Description on its McpServerTool attribute"); - if (attribute.Groups[1].Success) + if (attribute.Success) { - return attribute.Groups[1].Value; - } + if (attribute.Groups[1].Success) + { + return attribute.Groups[1].Value; + } - var constName = attribute.Groups[2].Value; + var constName = attribute.Groups[2].Value; - /* The const lives in the same file, above the attribute; the body slice starts AT the attribute, so it - is not in the body — resolve it from the file the label points at. */ - var (type, toolName, liteFile, _) = LatestTools.Single(t => label.EndsWith(t.ToolName, StringComparison.Ordinal)); - return label.StartsWith("Darling", StringComparison.Ordinal) - ? (string)type.GetField(constName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)!.GetRawConstantValue()! - : LiteConstLiteral(liteFile, constName); + /* The const lives in the same file, above the attribute; the body slice starts AT the attribute, so + it is not in the body — resolve it from the file the label points at. */ + var (type, toolName, liteFile, _) = LatestTools.Single(t => label.EndsWith(t.ToolName, StringComparison.Ordinal)); + return label.StartsWith("Darling", StringComparison.Ordinal) + ? (string)type.GetField(constName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)!.GetRawConstantValue()! + : LiteConstLiteral(liteFile, constName); + } + + /* #3898: a converted tool's Description is a "head" + McpToolGuide.Marker + "tail" concatenation — + neither a single literal nor a single const. Join every quoted literal chunk in source order, + skipping the identifiers between them (the marker, McpToolGuideTopics.* consts); this test only + needs the plain prose, never the reading-guide plumbing those identifiers resolve to. */ + var anchor = Regex.Match(body, @"\A\[McpServerTool\(Name = ""[a-z_0-9]+""\), Description\("); + Assert.True(anchor.Success, $"{label}: could not locate the tool's Description on its McpServerTool attribute"); + var i = anchor.Length; + var sb = new System.Text.StringBuilder(); + while (i < body.Length && !(body[i] == ')' && i + 1 < body.Length && body[i + 1] == ']')) + { + if (body[i] == '"') + { + i++; + while (i < body.Length && body[i] != '"') + { + if (body[i] == '\\' && i + 1 < body.Length) { sb.Append(body[i + 1]); i += 2; } + else { sb.Append(body[i]); i++; } + } + i++; + } + else + { + i++; + } + } + Assert.True(sb.Length > 0, $"{label}: Description( had no literal text before its closing )]"); + return sb.ToString(); } /// A Lite const string's literal, read from source: internal const string Name =\n "…";. diff --git a/Darling/Darling.Tests/McpPageContractTests.cs b/Darling/Darling.Tests/McpPageContractTests.cs index f0d6e5545a..fbab485846 100644 --- a/Darling/Darling.Tests/McpPageContractTests.cs +++ b/Darling/Darling.Tests/McpPageContractTests.cs @@ -330,7 +330,7 @@ public void EveryLitePagedTool_SaysWhatBoundsItsPage_InItsDescription() foreach (var (_, _, liteFile, liteName) in PagedTools) { var body = ToolBody(ReadRepoFileLf(liteFile.Split('/')), liteName); - var description = Regex.Match(body, @"Description\((?:\s*)""((?:[^""\\]|\\.)*)""\)\]").Groups[1].Value; + var description = DescriptionLiteralConcat(body); Assert.False(string.IsNullOrEmpty(description), $"{liteFile}: could not locate {liteName}'s Description"); Assert.Contains("truncated", description, StringComparison.Ordinal); Assert.Contains("limit", description, StringComparison.Ordinal); @@ -1179,6 +1179,38 @@ private static string ToolBody(string source, string toolName) return next < 0 ? source[start..] : source[start..next]; } + /// The tool's Description, from source: the FIRST Description( in a + /// slice is always the tool's own (a parameter's own [Description("...")] comes later, on the + /// method signature). #3898: a converted tool's Description is a "head" + McpToolGuide.Marker + + /// "tail" concatenation, not one literal, so this joins every quoted chunk up to the attribute's + /// closing )] and skips the identifiers between them (the marker, McpToolGuideTopics.* consts) — + /// this reads the prose only, never the reading-guide plumbing those identifiers resolve to. + private static string DescriptionLiteralConcat(string body) + { + var start = body.IndexOf("Description(", StringComparison.Ordinal); + Assert.True(start >= 0, "no Description( found in body"); + var i = start + "Description(".Length; + var sb = new System.Text.StringBuilder(); + while (i < body.Length && !(body[i] == ')' && i + 1 < body.Length && body[i + 1] == ']')) + { + if (body[i] == '"') + { + i++; + while (i < body.Length && body[i] != '"') + { + if (body[i] == '\\' && i + 1 < body.Length) { sb.Append(body[i + 1]); i += 2; } + else { sb.Append(body[i]); i++; } + } + i++; + } + else + { + i++; + } + } + return sb.ToString(); + } + /// A non-tool member's span (LF source): from its static <type> Name( declaration to the /// first line that is exactly the class-member closing brace. The wire-shape builders this file needs are /// methods at member indent whose bodies close that way; a member that does not would return its first diff --git a/Darling/Darling.Tests/McpToolGuideHeads.Data.cs b/Darling/Darling.Tests/McpToolGuideHeads.Data.cs index c26c0684d0..bd99ff727f 100644 --- a/Darling/Darling.Tests/McpToolGuideHeads.Data.cs +++ b/Darling/Darling.Tests/McpToolGuideHeads.Data.cs @@ -33,9 +33,9 @@ public sealed class McpToolGuideHeadsDataTests /// The per-tool guardrail phrase each head must state. private static readonly (string Tool, string Fact)[] HeadFacts = [ - ("get_file_io_stats", "Reads the newest snapshot, not a window"), - ("get_perfmon_stats", "Reads the newest snapshot, not a window"), - ("get_server_properties", "Reads the newest snapshot, not a window"), + ("get_file_io_stats", "LATEST IS A TIME: the newest snapshot, not a window"), + ("get_perfmon_stats", "LATEST IS A TIME: the newest snapshot, not a window"), + ("get_server_properties", "LATEST IS A TIME: the newest snapshot, not a window"), ("get_top_procedures_by_cpu", "LIFETIME extremes, not windowed"), ("get_top_queries_by_cpu", "LIFETIME extremes, not windowed"), ("get_top_queries_by_cpu", "an empty page under it is the window's real answer, not a miss"), diff --git a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt index 9ed95676fb..96f8169a7a 100644 --- a/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt +++ b/Darling/Darling.Tests/McpToolsListBudget/DarlingMcpDataTools.txt @@ -28,7 +28,7 @@ param get_current_waits_trend.database_name 73 param get_current_waits_trend.hours_back 190 param get_current_waits_trend.server_name 28 -tool get_file_io_stats 425 +tool get_file_io_stats 437 param get_file_io_stats.server_name 28 tool get_memory_clerks 232 @@ -37,7 +37,7 @@ param get_memory_clerks.server_name 28 tool get_memory_stats 480 param get_memory_stats.server_name 28 -tool get_perfmon_stats 509 +tool get_perfmon_stats 521 param get_perfmon_stats.counter_name 61 param get_perfmon_stats.instance_name 57 param get_perfmon_stats.server_name 28 @@ -49,7 +49,7 @@ param get_query_store_top.hours_back 29 param get_query_store_top.server_name 28 param get_query_store_top.top 34 -tool get_server_properties 559 +tool get_server_properties 571 param get_server_properties.server_name 28 tool get_tempdb_trend 254 diff --git a/Lite.Tests/McpToolGuideHeads.Data.cs b/Lite.Tests/McpToolGuideHeads.Data.cs index 4aaea1bd46..364665a7d3 100644 --- a/Lite.Tests/McpToolGuideHeads.Data.cs +++ b/Lite.Tests/McpToolGuideHeads.Data.cs @@ -31,9 +31,9 @@ public sealed class McpToolGuideHeadsDataTests private static readonly (string Tool, string Fact)[] HeadFacts = [ - ("get_file_io_stats", "Reads the newest snapshot, not a window"), - ("get_perfmon_stats", "Reads the newest snapshot, not a window"), - ("get_server_properties", "Reads the newest snapshot, not a window"), + ("get_file_io_stats", "LATEST IS A TIME: the newest snapshot, not a window"), + ("get_perfmon_stats", "LATEST IS A TIME: the newest snapshot, not a window"), + ("get_server_properties", "LATEST IS A TIME: the newest snapshot, not a window"), ("get_top_procedures_by_cpu", "LIFETIME extremes, not windowed"), ("get_top_queries_by_cpu", "LIFETIME extremes, not windowed"), ("get_wait_stats", "Bounded by limit"), diff --git a/Lite.Tests/McpToolsListBudget/McpIoTools.txt b/Lite.Tests/McpToolsListBudget/McpIoTools.txt index 7decae6864..6997d7ebe1 100644 --- a/Lite.Tests/McpToolsListBudget/McpIoTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpIoTools.txt @@ -1,5 +1,5 @@ # McpIoTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. -tool get_file_io_stats 425 +tool get_file_io_stats 437 param get_file_io_stats.server_name 28 tool get_file_io_trend 585 diff --git a/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt b/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt index b0f16ae38c..8adfb9cfc2 100644 --- a/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt @@ -1,5 +1,5 @@ # McpPerfmonTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks. -tool get_perfmon_stats 509 +tool get_perfmon_stats 521 param get_perfmon_stats.counter_name 61 param get_perfmon_stats.instance_name 57 param get_perfmon_stats.server_name 28 diff --git a/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt b/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt index 759e7165d8..c32fdb2684 100644 --- a/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt +++ b/Lite.Tests/McpToolsListBudget/McpServerInfoTools.txt @@ -2,5 +2,5 @@ tool get_database_sizes 480 param get_database_sizes.server_name 28 -tool get_server_properties 559 +tool get_server_properties 571 param get_server_properties.server_name 28 From 806d97eb2d35289515a03e1d0b7c120a548eb8e1 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:23:55 -0400 Subject: [PATCH 5/5] Data family markers are written inside the Description literals, and a concatenated marker now fails (#3898) The seven Data tools wrote the split as "..." + McpToolGuide.Marker + "...". That compiles to the same string, but the cross-SKU identity scan (and grep <>) reads source text, so it saw these tools as unconverted and silently skipped comparing their heads. Rewritten as the literal <> (Split trims both sides, so served text is byte-identical), and ScanToolsUnder now refuses a concatenated marker. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M7r1CXmEcBmsrTV34CCLfF --- Darling/Darling.Tests/McpToolGuideTests.cs | 6 ++++++ .../Mcp/DarlingMcpDataTools.cs | 14 +++++++------- Lite/Mcp/McpDiscoveryTools.cs | 2 +- Lite/Mcp/McpIoTools.cs | 2 +- Lite/Mcp/McpPerfmonTools.cs | 2 +- Lite/Mcp/McpQueryTools.cs | 4 ++-- Lite/Mcp/McpServerInfoTools.cs | 2 +- Lite/Mcp/McpWaitTools.cs | 2 +- 8 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Darling/Darling.Tests/McpToolGuideTests.cs b/Darling/Darling.Tests/McpToolGuideTests.cs index 4d8a807340..99aecae6c0 100644 --- a/Darling/Darling.Tests/McpToolGuideTests.cs +++ b/Darling/Darling.Tests/McpToolGuideTests.cs @@ -345,6 +345,12 @@ private static (Dictionary Files, Dictionary HasMa var end = idx + 1 < anchors.Count ? anchors[idx + 1].Index : src.Length; Assert.True(files.TryAdd(name, file), $"duplicate MCP tool name '{name}' under {string.Join("/", segments)}"); hasMarker[name] = src[anchors[idx].Index..end].Contains(McpToolGuide.Marker, StringComparison.Ordinal); + /* The marker is written INSIDE the Description literal (D3). A concatenated McpToolGuide.Marker + compiles to the same string, but this scan (and `grep <>`) would read the tool as + unconverted, so the cross-SKU identity check above would silently skip it. */ + Assert.False( + Regex.IsMatch(src[anchors[idx].Index..end], @"\+\s*McpToolGuide\.Marker|McpToolGuide\.Marker\s*\+"), + $"{name}: write {McpToolGuide.Marker} inside the Description literal; a concatenated McpToolGuide.Marker hides the tool from the source scans."); } } diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs index 8344a2ae18..50c81ccb53 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpDataTools.cs @@ -98,7 +98,7 @@ internal static async Task GetCpuUtilization( } } - [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] + [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server. <> Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] public static async Task GetWaitStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -372,7 +372,7 @@ with the read by construction and tell the caller nothing it did not already hav } } - [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] + [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0. <> Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] public static async Task GetFileIoStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null) @@ -461,7 +461,7 @@ internal static async Task GetTempDbTrend( } } - [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] + [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate). <> Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] public static async Task GetPerfmonStats( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -512,7 +512,7 @@ public static async Task GetPerfmonStats( /* ═══════════════════════════ query performance ═══════════════════════════ */ - [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means only rows predating the text dimension). 'host_object' rolls all of a procedure's statements into one row — use it when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Ad-hoc statements have no host object and stay grouped per hash in both modes. distinct_query_hashes reports how many hashes a row rolled up. Set group_by='host_object' to roll all of a procedure's statements into one row — necessary when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] + [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing. <> Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means only rows predating the text dimension). 'host_object' rolls all of a procedure's statements into one row — use it when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Ad-hoc statements have no host object and stay grouped per hash in both modes. distinct_query_hashes reports how many hashes a row rolled up. Set group_by='host_object' to roll all of a procedure's statements into one row — necessary when dynamic SQL with per-value literals fragments one statement across many hashes, which no top-N-by-hash ranking can surface. Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopQueriesByCpu( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -667,7 +667,7 @@ the two answer different questions and the rows look alike. */ } } - [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time over a window ending at as_of. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed (extremes_note flags a provably stale one); cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] + [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time over a window ending at as_of. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed (extremes_note flags a provably stale one); cpu_attribution's ratio is omitted, not invented, when its inputs are missing. <> Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopProceduresByCpu( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null, @@ -859,7 +859,7 @@ it stops. The trend family renamed the same fact the same way (DarlingMcpTrendTo /* ═══════════════════════════ discovery / health ═══════════════════════════ */ - [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored servers — SQL Server and PostgreSQL — with their collection freshness status and last collection time. Each row says which engine it describes: engine_kind is the raw registry token (sqlserver, postgres, aurora-postgres; null when no connect has stamped the row — the same vocabulary get_fleet_overview uses) and engine_version is the engine-aware version label (\"SQL Server 2022\", \"PostgreSQL 18\"; empty when no version has been collected). sql_version is a DEPRECATED legacy alias carrying the same value as engine_version, kept so existing consumers keep working — read engine_version instead, and never infer the engine from that key's name: a PostgreSQL row's sql_version reads \"PostgreSQL 18\". The service has no live connection to the monitored servers, so status is derived from how recently each server was collected (Online = fresh, Warning = stale, Offline = no recent collection). The peer_fleets block names the SIBLING Darling stores that monitor the rest of a split fleet, with what each one covers — this server can only NAME them (no cross-store reads), and peer_note says what an empty peer_fleets does and does not prove.")] + [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected. <> Use this first to see available servers before calling other tools. Lists all monitored servers — SQL Server and PostgreSQL — with their collection freshness status and last collection time. Each row says which engine it describes: engine_kind is the raw registry token (sqlserver, postgres, aurora-postgres; null when no connect has stamped the row — the same vocabulary get_fleet_overview uses) and engine_version is the engine-aware version label (\"SQL Server 2022\", \"PostgreSQL 18\"; empty when no version has been collected). sql_version is a DEPRECATED legacy alias carrying the same value as engine_version, kept so existing consumers keep working — read engine_version instead, and never infer the engine from that key's name: a PostgreSQL row's sql_version reads \"PostgreSQL 18\". The service has no live connection to the monitored servers, so status is derived from how recently each server was collected (Online = fresh, Warning = stale, Offline = no recent collection). The peer_fleets block names the SIBLING Darling stores that monitor the rest of a split fleet, with what each one covers — this server can only NAME them (no cross-store reads), and peer_note says what an empty peer_fleets does and does not prove.")] public static async Task ListServers( NpgsqlDataSource postgres) { @@ -1390,7 +1390,7 @@ column of the statement behind it follows. */ } } - [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] + [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off. <> Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] public static async Task GetServerProperties( NpgsqlDataSource postgres, [Description("Server name or display name.")] string? server_name = null) diff --git a/Lite/Mcp/McpDiscoveryTools.cs b/Lite/Mcp/McpDiscoveryTools.cs index 82f02d6048..a323508141 100644 --- a/Lite/Mcp/McpDiscoveryTools.cs +++ b/Lite/Mcp/McpDiscoveryTools.cs @@ -8,7 +8,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpDiscoveryTools { - [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected." + McpToolGuide.Marker + " Use this first to see available servers before calling other tools. Lists all monitored SQL Server instances with their current status and last collection time.")] + [McpServerTool(Name = "list_servers"), Description("Lists monitored servers with collection freshness status and last collection time. Darling has no live connection to monitored servers: status is derived from how recently each was collected (Online = fresh, Warning = stale, Offline = no recent collection). Lite's status IS a live connection check, independent of whether anything is being collected. <> Use this first to see available servers before calling other tools. Lists all monitored SQL Server instances with their current status and last collection time.")] public static async Task ListServers(ServerManager serverManager, LocalDataService dataService) { var servers = serverManager.GetEnabledServers(); diff --git a/Lite/Mcp/McpIoTools.cs b/Lite/Mcp/McpIoTools.cs index 4f321b4520..ef44c9da85 100644 --- a/Lite/Mcp/McpIoTools.cs +++ b/Lite/Mcp/McpIoTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpIoTools { - [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0." + McpToolGuide.Marker + " Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] + [McpServerTool(Name = "get_file_io_stats"), Description("Gets the latest per-database-file I/O stats: read/write counts, bytes, stall times, calculated latency. LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and the deltas cover the sample_interval_seconds ending there. sample_interval_seconds 0 means no delta was knowable for that file (first sighting, counter reset, a gap) and that row's latencies are null, not 0. <> Gets the latest file I/O statistics per database file: read/write counts, bytes, stall times, and calculated latency. High read latency (>20ms) or write latency (>10ms for data, >2ms for log) often indicates storage bottlenecks. Each row carries sample_interval_seconds, the measured seconds its deltas accrued over; a 0 means no delta was knowable for that file at this collection (first sighting, counter reset, or a gap past the delta policy — typically a restart) and its latencies are null rather than 0. LATEST IS A TIME: this reads the newest file-I/O snapshot, not a window, and captured_at is the instant it was collected; the deltas cover the sample_interval_seconds ending there.")] public static async Task GetFileIoStats( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpPerfmonTools.cs b/Lite/Mcp/McpPerfmonTools.cs index 1498f988e8..7d48c52f9a 100644 --- a/Lite/Mcp/McpPerfmonTools.cs +++ b/Lite/Mcp/McpPerfmonTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpPerfmonTools { - [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate)." + McpToolGuide.Marker + " Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] + [McpServerTool(Name = "get_perfmon_stats"), Description("Gets the latest SQL Server performance counter values (batch requests/sec, compilations/sec, deadlocks/sec, and more). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected; use get_perfmon_trend for history. counter_kind: gauge = value IS the reading, delta_value null; rate = value is cumulative, delta_value its per-interval change; other = a non-rate per-interval change; null counter_kind predates the column, classify by name (ends in /sec = rate). <> Gets the latest SQL Server performance counter values: batch requests/sec, compilations/sec, deadlocks/sec, and more. Provides throughput context to distinguish a busy server from a sick one. Use counter_name or instance_name to filter results. LATEST IS A TIME: this reads the newest counter snapshot, not a window, and captured_at is the instant it was collected; use get_perfmon_trend for a counter over time. Each row carries counter_kind from the stored cntr_type: 'gauge' means value IS the reading (a level such as Total Server Memory (KB); delta_value is null because a level has no delta), 'rate' means value is a cumulative count and delta_value is its change over the last collection interval (get_perfmon_trend carries the sample_interval_seconds to divide it by for a per-second figure), 'other' means an average/fraction numerator whose delta_value is a per-interval change and not a rate; null counter_kind is a row written before the type was stored — classify it by name (a counter whose name ends in /sec is a rate).")] public static async Task GetPerfmonStats( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpQueryTools.cs b/Lite/Mcp/McpQueryTools.cs index 0edb0fc43c..e152100ef8 100644 --- a/Lite/Mcp/McpQueryTools.cs +++ b/Lite/Mcp/McpQueryTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpQueryTools { - [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means no stored text for the group). Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] + [McpServerTool(Name = "get_top_queries_by_cpu"), Description("Gets expensive cached queries from sys.dm_exec_query_stats, ranked by CPU over a window ending at as_of. Filters (database_name, parallel_only, min_dop) apply before the top-N cap: filter_applied names the floor in force, and an empty page under it is the window's real answer, not a miss. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed; cpu_attribution's ratio is omitted, not invented, when its inputs are missing. <> Gets expensive queries from sys.dm_exec_query_stats (plan cache). Best for: currently cached queries with detailed per-execution stats, DOP, spills, and query_hash for trending. Returns query_hash, query_plan_hash, sql_handle, plan_handle, and host_object (the hosting procedure/function for proc-hosted statements, null for ad-hoc) — groups key on (database, query_hash, host_object), so INSERT...EXEC callers in different procedures report separately with their own text. distinct_texts counts statement texts merged into a group (>1 = ad-hoc literal variants or pre-upgrade history; query_text is one representative, 0 means no stored text for the group). Supports database and parallelism filtering; every filter is applied IN the query before the ranking and the cap, so the page is the top-N of the FILTERED population (filter_applied names the parallelism floor in force, null when none), and an empty page under parallel_only/min_dop is the window's answer rather than a page artefact. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes for the plan's time in cache (same semantics as max_dop), not windowed — totals and avgs are windowed deltas; rows where an extreme provably predates the window carry extremes_note. max_dop comes from sys.dm_exec_query_stats and is a lifetime-max for the plan's time in cache, so a plan compiled before MAXDOP was lowered keeps reporting the old higher value until it is evicted or recompiled; confirm current parallelism with analyze_query_plan, which reads the actual plan. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopQueriesByCpu( LocalDataService dataService, ServerManager serverManager, @@ -146,7 +146,7 @@ ratio is omitted rather than invented when a denominator piece is missing. One n } } - [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time over a window ending at as_of. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed (extremes_note flags a provably stale one); cpu_attribution's ratio is omitted, not invented, when its inputs are missing." + McpToolGuide.Marker + " Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] + [McpServerTool(Name = "get_top_procedures_by_cpu"), Description("Gets the most expensive stored procedures ranked by total CPU time over a window ending at as_of. Delta-based: requires ~30 minutes after adding a new server before data appears. min/max_cpu_ms and min/max_elapsed_ms are LIFETIME extremes, not windowed (extremes_note flags a provably stale one); cpu_attribution's ratio is omitted, not invented, when its inputs are missing. <> Shows execution counts, CPU/elapsed times, and I/O metrics. Delta-based: requires ~30 minutes after adding a new server before data appears. " + McpToolGuideTopics.CpuTimeExtremesAndAttribution)] public static async Task GetTopProceduresByCpu( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpServerInfoTools.cs b/Lite/Mcp/McpServerInfoTools.cs index 61bca17e7b..6108e750dc 100644 --- a/Lite/Mcp/McpServerInfoTools.cs +++ b/Lite/Mcp/McpServerInfoTools.cs @@ -9,7 +9,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpServerInfoTools { - [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off." + McpToolGuide.Marker + " Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] + [McpServerTool(Name = "get_server_properties"), Description("Gets SQL Server instance properties: edition, version, CPU count, memory, socket/core topology, HADR, clustering, and the clock (utc_offset_minutes, time_zone_id). LATEST IS A TIME: the newest snapshot, not a window; captured_at is when it was collected, and on a stalled collector it is the only sign of staleness. time_zone_id is CURRENT_TIMEZONE_ID() (SQL Server 2022+/Azure SQL only); null means a pre-2022 engine, so only the offset in force at captured_at is known, and an instant across a DST transition from it can read an hour off. <> Gets SQL Server instance properties: edition, version, CPU count, physical memory, socket/core topology, HADR status, clustering, and the server's clock: utc_offset_minutes is the UTC offset in force when the snapshot was collected, and time_zone_id is the engine's own time-zone name (CURRENT_TIMEZONE_ID(), SQL Server 2022+ and Azure SQL only) - a null time_zone_id means a pre-2022 engine, where only the offset is known and any instant on the far side of a DST transition from the snapshot is placed an hour off by that offset. Use for capacity planning and edition-aware recommendations. LATEST IS A TIME: this reads the newest properties snapshot, not a window, and captured_at is the instant it was collected - a core count or memory figure here is what the server reported AT that stamp, and on a server whose collector has stalled the stamp is the only thing that says how stale it is.")] public static async Task GetServerProperties( LocalDataService dataService, ServerManager serverManager, diff --git a/Lite/Mcp/McpWaitTools.cs b/Lite/Mcp/McpWaitTools.cs index a9348f928d..b4e5210703 100644 --- a/Lite/Mcp/McpWaitTools.cs +++ b/Lite/Mcp/McpWaitTools.cs @@ -10,7 +10,7 @@ namespace PerformanceMonitorLite.Mcp; [McpServerToolType] public sealed class McpWaitTools { - [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server." + McpToolGuide.Marker + " Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] + [McpServerTool(Name = "get_wait_stats"), Description("Gets the top SQL Server wait types over a time period ending at as_of, heaviest total wait first. Bounded by limit: wait_types_returned is how many you got; truncated means the window held more. The page holds only the heaviest waits — a sum over the page is not a sum over the server. <> Gets the top SQL Server wait types aggregated over a time period, heaviest total wait first. Wait stats reveal what SQL Server spends time waiting on — high signal waits indicate CPU pressure, high resource waits indicate I/O or lock contention. Use this first to identify the dominant wait category, then drill into specific tools based on the wait type. THE PAGE IS BOUNDED BY limit: wait_types_returned is how many wait types you got and truncated says the window observed more than limit — the rows you have are the heaviest, and the ones past the cap are lighter, but a sum over the page is a sum over the page rather than over the server.")] public static async Task GetWaitStats( LocalDataService dataService, ServerManager serverManager,