diff --git a/Darling/Darling.Tests/CustomAlertTemplatesTests.cs b/Darling/Darling.Tests/CustomAlertTemplatesTests.cs
new file mode 100644
index 000000000..44445748b
--- /dev/null
+++ b/Darling/Darling.Tests/CustomAlertTemplatesTests.cs
@@ -0,0 +1,101 @@
+/*
+ * 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 System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using PerformanceMonitor.Darling.Service;
+using PerformanceMonitor.Darling.Service.Mcp;
+using Xunit;
+
+namespace Darling.Tests;
+
+///
+/// #3285 (plan Component 7): the starter custom-alert templates' drift-guard, the alert twin of
+/// ViewTemplatesTests. A stored rule is validated once at write time and never again, so a template
+/// naming a measure that later drifts out of the catalog would ship a rule that can never fire and nothing
+/// would grep it. The templates are CODE, so the SAME authority the evaluator and create/update run
+/// (, over the live MeasureCatalog) is applied to every
+/// one on every build — a drifted measure fails here rather than shipping dead.
+///
+public sealed class CustomAlertTemplatesTests
+{
+ [Fact]
+ public void EveryTemplate_ValidatesAgainstTheLiveRuleValidatorAndCatalog()
+ {
+ Assert.NotEmpty(CustomAlertTemplates.All);
+
+ foreach (var template in CustomAlertTemplates.All)
+ {
+ var (def, error) = CustomAlertRuleDefinition.TryParse(template.DefinitionJson);
+ Assert.True(
+ error is null && def is not null,
+ $"template '{template.Key}' no longer validates against the live rule-validator + catalog: {error}");
+ }
+ }
+
+ [Fact]
+ public void EveryTemplate_HasAUniqueKey_AndSaysWhatItIsFor()
+ {
+ var keys = CustomAlertTemplates.All.Select(t => t.Key).ToArray();
+
+ Assert.True(keys.Length >= 8, "expected the full starter set; found " + keys.Length);
+ Assert.Equal(keys.Length, keys.Distinct(StringComparer.Ordinal).Count());
+ Assert.All(CustomAlertTemplates.All, t =>
+ {
+ Assert.False(string.IsNullOrWhiteSpace(t.Key));
+ Assert.False(string.IsNullOrWhiteSpace(t.Name));
+ Assert.False(string.IsNullOrWhiteSpace(t.Description));
+ });
+ }
+
+ [Fact]
+ public void EveryTemplate_IsAScalarRuleWithAPredicate_AndBothSeverityTiers()
+ {
+ foreach (var template in CustomAlertTemplates.All)
+ {
+ var (def, error) = CustomAlertRuleDefinition.TryParse(template.DefinitionJson);
+ Assert.Null(error);
+ Assert.NotNull(def);
+
+ // A rule window is real and alert-appropriate; TryParse enforces the Scalar-only + predicate rules,
+ // and every starter ships a critical tier above warn (a user can drop it when they create the rule).
+ Assert.True(def!.WindowHours > 0);
+ Assert.NotNull(def.CriticalThreshold);
+ }
+ }
+
+ [Fact]
+ public async Task ListCustomAlertTemplatesTool_ReturnsEveryTemplate_WithAParsedDefinitionObject()
+ {
+ var json = await DarlingMcpCustomAlertTools.ListCustomAlertTemplates();
+ var root = (JsonObject)JsonNode.Parse(json)!;
+ var templates = (JsonArray)root["templates"]!;
+
+ Assert.Equal(CustomAlertTemplates.All.Count, templates.Count);
+
+ var toolKeys = templates.Select(t => (string?)((JsonObject)t!)["key"]).ToHashSet(StringComparer.Ordinal);
+ Assert.Equal(CustomAlertTemplates.All.Select(t => t.Key).ToHashSet(StringComparer.Ordinal), toolKeys);
+
+ foreach (var entry in templates)
+ {
+ var obj = (JsonObject)entry!;
+ Assert.False(string.IsNullOrWhiteSpace((string?)obj["name"]));
+ Assert.False(string.IsNullOrWhiteSpace((string?)obj["description"]));
+
+ // The definition is embedded as a JSON OBJECT (not an escaped string), ready to hand to
+ // create_custom_alert_rule / test_custom_alert_rule, and it still validates.
+ var definition = obj["definition"] as JsonObject;
+ Assert.NotNull(definition);
+ var (parsed, error) = CustomAlertRuleDefinition.TryParse(definition!.ToJsonString());
+ Assert.True(error is null && parsed is not null, error);
+ }
+ }
+}
diff --git a/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs b/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs
index e010ecda7..8aa786866 100644
--- a/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs
+++ b/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs
@@ -22,8 +22,9 @@ namespace Darling.Tests;
///
/// Ungated (no-live-store) contract for the #3285 custom-alert-rule MCP tools: the tool surface is EXACTLY the
-/// seven management tools (the six CRUD/validate tools plus #3299's test_custom_alert_rule evaluate-now; all
-/// static, on a [McpServerToolType] class, returning Task<string>), the advertised tools/list schema is
+/// eight management tools (the six CRUD/validate tools, #3299's test_custom_alert_rule evaluate-now, and #3285
+/// Component 7's list_custom_alert_templates; all static, on a [McpServerToolType] class, returning
+/// Task<string>), the advertised tools/list schema is
/// Gemini-clean (#1074) with the expected required-param set, and validate / create / update run the SAME
/// CustomAlertRuleDefinition.TryParse authority (the one the evaluator uses) BEFORE any persistence - an invalid
/// definition never reaches the store. The live CRUD round-trip is gated below.
@@ -52,6 +53,7 @@ public sealed class DarlingMcpCustomAlertToolsSurfaceTests
"delete_custom_alert_rule",
"get_custom_alert_rule",
"list_custom_alert_rules",
+ "list_custom_alert_templates",
"test_custom_alert_rule",
"update_custom_alert_rule",
"validate_custom_alert_rule",
@@ -63,7 +65,7 @@ private static MethodInfo[] ToolMethods() => typeof(DarlingMcpCustomAlertTools)
.ToArray();
[Fact]
- public void ToolSurface_IsExactlyTheSevenCustomAlertTools()
+ public void ToolSurface_IsExactlyTheEightCustomAlertTools()
{
var toolMethods = ToolMethods();
var names = toolMethods
@@ -87,10 +89,10 @@ public void ToolSurface_IsExactlyTheSevenCustomAlertTools()
}
[Fact]
- public void AdvertisedSchema_IsGeminiClean_ForAllSevenTools()
+ public void AdvertisedSchema_IsGeminiClean_ForAllEightTools()
{
var tools = BuildToolSchemas();
- Assert.Equal(7, tools.Count);
+ Assert.Equal(8, tools.Count);
var violations = tools.Values.SelectMany(t => DarlingMcpSchemaAssert.Violations(t.Name, t.InputSchema)).ToList();
Assert.True(violations.Count == 0, "Gemini-incompatible schema keywords leaked:\n" + string.Join("\n", violations));
}
@@ -104,6 +106,8 @@ public void AdvertisedSchema_IsGeminiClean_ForAllSevenTools()
[InlineData("delete_custom_alert_rule", "rule_id")]
/* #3299: both inputs are optional (the tool enforces "exactly one" at runtime, not via required-schema). */
[InlineData("test_custom_alert_rule", "")]
+ /* #3285 Component 7: the template list takes no parameters. */
+ [InlineData("list_custom_alert_templates", "")]
public void AdvertisedSchema_RequiredParams_MatchTheContract(string toolName, string expectedCsv)
{
var expected = expectedCsv.Length == 0 ? Array.Empty() : expectedCsv.Split(',');
diff --git a/Darling/Darling.Tests/DarlingWebEndpointsTests.cs b/Darling/Darling.Tests/DarlingWebEndpointsTests.cs
index fdb5f46fa..741f6b796 100644
--- a/Darling/Darling.Tests/DarlingWebEndpointsTests.cs
+++ b/Darling/Darling.Tests/DarlingWebEndpointsTests.cs
@@ -84,8 +84,9 @@ public void ExcludedToolNames_AreTheNonReadSurfaceTools()
{
/* The six original non-read tools (analyze_server, the mute write, the four analyze_*_plan), the eight
Custom Views tools (#1599 + describe_custom_view_catalog) served by /api/views + /api/compose/run +
- /api/catalog, the seven custom-alert-rule tools (#3285 — create/update/delete write, get/list/validate
- read against the compose catalog, and test_custom_alert_rule (#3299) evaluate-now, none a
+ /api/catalog, the eight custom-alert-rule tools (#3285 — create/update/delete write, get/list/validate
+ read against the compose catalog, test_custom_alert_rule (#3299) evaluate-now, and
+ list_custom_alert_templates (#3285 Component 7) starter templates, none a
/api/read/{tool} mirror), the three alert-tuning WRITE tools, and the two server-onboarding WRITE tools
(add_servers / remove_server) — all with no /api/read/{tool} 1:1 mirror, like mute_analysis_finding. */
Assert.Equal(
@@ -94,7 +95,7 @@ Custom Views tools (#1599 + describe_custom_view_catalog) served by /api/views +
"add_servers", "analyze_plan_xml", "analyze_procedure_plan", "analyze_query_plan", "analyze_query_store_plan",
"analyze_server", "create_custom_alert_rule", "create_custom_view", "create_mute_rule", "delete_custom_alert_rule",
"delete_custom_view", "delete_mute_rule", "describe_custom_view_catalog", "get_custom_alert_rule", "get_custom_view",
- "list_custom_alert_rules", "list_custom_views", "mute_analysis_finding", "remove_server", "run_custom_view_panel",
+ "list_custom_alert_rules", "list_custom_alert_templates", "list_custom_views", "mute_analysis_finding", "remove_server", "run_custom_view_panel",
"test_custom_alert_rule", "update_alert_settings", "update_custom_alert_rule", "update_custom_view", "validate_custom_alert_rule", "validate_custom_view",
},
DarlingWebEndpoints.ExcludedToolNames.OrderBy(n => n, StringComparer.Ordinal).ToArray());
diff --git a/Darling/PerformanceMonitor.Darling.Service/CustomAlertTemplates.cs b/Darling/PerformanceMonitor.Darling.Service/CustomAlertTemplates.cs
new file mode 100644
index 000000000..a26eea947
--- /dev/null
+++ b/Darling/PerformanceMonitor.Darling.Service/CustomAlertTemplates.cs
@@ -0,0 +1,141 @@
+/*
+ * 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.Collections.Generic;
+
+namespace PerformanceMonitor.Darling.Service;
+
+/// One starter custom-alert-rule template (#3285, plan Component 7 / #3282 Q2): a curated
+/// {metric + predicate + hysteresis} an operator can browse (list_custom_alert_templates) and create a
+/// real rule from (create_custom_alert_rule). is a genuine
+/// body: CODE, so the drift-guard re-validates it against the LIVE
+/// catalog on every build; a template naming a measure that later drifts out of the catalog fails the build
+/// rather than shipping a rule that can never fire.
+public sealed record CustomAlertTemplate(string Key, string Name, string Description, string DefinitionJson);
+
+///
+/// The starter custom-alert templates. #3282's answer to "the collected-but-unalerted signals": templates over
+/// new hardcoded evaluators, so the set generalizes to signals nobody has named yet. Each maps to a REAL
+/// measure (source + measure|ratio + aggregate + unit), with a deliberately
+/// conservative starter threshold + hysteresis the operator tunes; scope defaults to all servers.
+///
+/// Thresholds are STARTERS, not tuned bars: they are set where a value is unambiguous trouble on most
+/// fleets (a 30s+ block, a 40%+ signal wait), never at a value only a specific fleet's baseline could justify,
+/// and are documented per template. Every one fires only after 3 consecutive breaching evaluations (2 for the
+/// more urgent blocking signal) so a single spike does not page.
+///
+/// Deliberately not shipped: a PostgreSQL "storage growth" template. #3282 names it, but the
+/// catalog has no database-size GAUGE for PostgreSQL (pg_database_stats is transaction/block/temp
+/// COUNTERS, not size), so there is nothing honest to threshold on. The related disk-fill risk IS covered by
+/// the replication-slot-WAL-retention template (retained WAL is the PostgreSQL storage a stuck slot actually
+/// grows without bound). SQL Server storage is already covered by the built-in low-disk / file-growth alerts.
+///
+public static class CustomAlertTemplates
+{
+ /// The curated set. Ordered PostgreSQL-first (the signals #3282 flagged as unalerted), then the
+ /// obvious SQL Server ones.
+ public static readonly IReadOnlyList All = new[]
+ {
+ /* ── PostgreSQL (the #3282 collected-but-unalerted signals) ── */
+
+ new CustomAlertTemplate(
+ "pg-autovacuum-dead-tuples",
+ "PostgreSQL: dead tuples piling up",
+ "Fires when a table's estimated dead-tuple count stays high, the tell that autovacuum is falling "
+ + "behind the write rate (bloat and plan drift follow). Starter: warn at 1,000,000 dead tuples, "
+ + "critical at 10,000,000: table-grain, so it is the worst table in the window. Tune to your "
+ + "largest hot table; a small database should lower both.",
+ "{\"metric\":{\"source\":\"pg_autovacuum_stats\",\"measure\":\"pg_av_dead_tuples\",\"aggregate\":\"max\",\"unit\":\"count\",\"hours\":1}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":1000000,\"criticalThreshold\":10000000}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "pg-replication-lag",
+ "PostgreSQL: replica replay lag high",
+ "Fires when a standby's replay lag stays high: the replica is falling behind the primary, so a "
+ + "failover would lose more, and read replicas serve staler data. Starter: warn at 30s of replay "
+ + "lag, critical at 5 minutes. Lower it if your RPO is tight.",
+ "{\"metric\":{\"source\":\"pg_replication_stats\",\"measure\":\"pg_repl_replay_lag_ms\",\"aggregate\":\"max\",\"unit\":\"ms\",\"hours\":0.25}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":30000,\"criticalThreshold\":300000}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "pg-connection-saturation",
+ "PostgreSQL: connection count high",
+ "Fires when the total session count stays high: approaching max_connections means new connections "
+ + "start failing. Starter: warn at 200 sessions, critical at 500 (absolute, since the catalog does "
+ + "not carry max_connections); set both to a fraction of your configured max_connections.",
+ "{\"metric\":{\"source\":\"pg_session_states\",\"measure\":\"pg_sess_total_sessions\",\"aggregate\":\"max\",\"unit\":\"count\",\"hours\":0.25}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":200,\"criticalThreshold\":500}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "pg-table-bloat",
+ "PostgreSQL: table bloat high",
+ "Fires when a table's estimated bloat percentage stays high: wasted space and slower scans that a "
+ + "VACUUM FULL / pg_repack would reclaim. Starter: warn at 40% bloat, critical at 70%. Bloat is "
+ + "slow-moving, so this evaluates over an hour.",
+ "{\"metric\":{\"source\":\"pg_table_bloat_stats\",\"measure\":\"pg_tbl_bloat_pct\",\"aggregate\":\"max\",\"unit\":\"percent\",\"hours\":1}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":40,\"criticalThreshold\":70}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "pg-replication-slot-wal",
+ "PostgreSQL: replication slot retaining WAL",
+ "Fires when a replication slot's retained WAL stays large: a disconnected or slow consumer holds "
+ + "WAL that cannot be recycled, and the disk fills for the WHOLE instance. This is the storage risk "
+ + "#3282 flags (PostgreSQL has no database-size gauge to threshold directly). Starter: warn at ~10 GB "
+ + "retained, critical at ~50 GB. Set below your free disk headroom.",
+ "{\"metric\":{\"source\":\"pg_replication_slot_stats\",\"measure\":\"pg_slot_retained_wal_bytes\",\"aggregate\":\"max\",\"unit\":\"mb\",\"hours\":1}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":10240,\"criticalThreshold\":51200}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ /* ── SQL Server (the obvious ones) ── */
+
+ new CustomAlertTemplate(
+ "sqlserver-signal-wait-pct",
+ "SQL Server: high signal wait %",
+ "Fires when signal wait % stays high: threads are ready but waiting for a scheduler, the classic "
+ + "sign of CPU pressure / scheduler contention. Starter: warn at 25%, critical at 40% (the "
+ + "widely-used rule-of-thumb bars).",
+ "{\"metric\":{\"source\":\"wait_stats\",\"ratio\":\"signal_wait_pct\",\"unit\":\"percent\",\"hours\":0.25}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":25,\"criticalThreshold\":40}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "sqlserver-sustained-blocking",
+ "SQL Server: sustained blocking",
+ "Fires when the longest blocked-process report in the window crosses a duration bar: a session was "
+ + "blocked that long, not just momentary contention. Starter: warn at 30s blocked, critical at 2 "
+ + "minutes. Fires after 2 breaching evaluations (blocking is more urgent than the slow-moving "
+ + "signals). Requires the blocked-process report threshold to be configured on the instance.",
+ "{\"metric\":{\"source\":\"blocked_process_reports\",\"measure\":\"bpr_wait_time_ms\",\"aggregate\":\"max\",\"unit\":\"ms\",\"hours\":0.25}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":30000,\"criticalThreshold\":120000}," +
+ "\"hysteresis\":{\"breachSamples\":2,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "sqlserver-long-running-query",
+ "SQL Server: long-running query",
+ "Fires when the average query duration per execution stays high: a query (or plan regression) that "
+ + "is consistently slow, not one that ran long once. Starter: warn at a 10s average, critical at 60s. "
+ + "Tune to your workload's normal query time.",
+ "{\"metric\":{\"source\":\"query_stats\",\"ratio\":\"query_avg_elapsed_us\",\"unit\":\"ms\",\"hours\":0.25}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":10000,\"criticalThreshold\":60000}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+
+ new CustomAlertTemplate(
+ "sqlserver-tempdb-pressure",
+ "SQL Server: tempdb space growth",
+ "Fires when total tempdb reserved space stays high: spills, version store, or a runaway object "
+ + "eating tempdb, which can stall the whole instance if it runs out. Starter: warn at ~50 GB "
+ + "reserved, critical at ~100 GB (absolute); set both relative to your tempdb file sizes.",
+ "{\"metric\":{\"source\":\"tempdb_stats\",\"measure\":\"tempdb_total_reserved_mb\",\"aggregate\":\"max\",\"unit\":\"mb\",\"hours\":0.25}," +
+ "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":51200,\"criticalThreshold\":102400}," +
+ "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"),
+ };
+}
diff --git a/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs b/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs
index 1fecdd637..9c489072f 100644
--- a/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs
+++ b/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs
@@ -94,6 +94,7 @@ public static class DarlingWebEndpoints
"delete_custom_alert_rule",
"validate_custom_alert_rule",
"test_custom_alert_rule",
+ "list_custom_alert_templates",
};
/// The window (hours) the fleet card blocking / deadlock counts default to — the WPF Overview's window.
diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs
index 34c2317ad..a7fb34892 100644
--- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs
+++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs
@@ -378,6 +378,34 @@ public static async Task TestCustomAlertRule(
}
}
+ [McpServerTool(Name = "list_custom_alert_templates"), Description(
+ "Lists the built-in STARTER custom-alert-rule templates - curated {metric, predicate, hysteresis} " +
+ "definitions for common signals: PostgreSQL (dead-tuple pile-up / replica replay lag / connection " +
+ "count / table bloat / replication-slot WAL retention) and SQL Server (high signal-wait % / sustained " +
+ "blocking / long-running query / tempdb space). Each entry is {key, name, description, definition}, " +
+ "where definition is a ready-to-use rule body. Browse them, tweak the thresholds and scope to your " +
+ "fleet (the starters are deliberately conservative, not tuned), then pass the definition to " +
+ "create_custom_alert_rule to save it - or to test_custom_alert_rule to see what it would do right now. " +
+ "Read-only: this lists code-defined templates and touches no store.")]
+ public static Task ListCustomAlertTemplates()
+ {
+ var templates = new JsonArray();
+ foreach (var template in CustomAlertTemplates.All)
+ {
+ templates.Add(new JsonObject
+ {
+ ["key"] = template.Key,
+ ["name"] = template.Name,
+ ["description"] = template.Description,
+ // The definition embedded as a JSON object (NOT an escaped string), so a client can hand it
+ // straight to create_custom_alert_rule / test_custom_alert_rule after editing.
+ ["definition"] = JsonNode.Parse(template.DefinitionJson),
+ });
+ }
+
+ return Task.FromResult(new JsonObject { ["templates"] = templates }.ToJsonString(McpHelpers.JsonOptions));
+ }
+
/// The full single-rule wire shape (definition embedded as JSON, NOT an escaped string) - mirrors
/// , adding the enabled column that alert rules
/// carry and views do not. Returned by get / create / update.
diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs
index 4a7d3ab36..436635740 100644
--- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs
+++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs
@@ -62,7 +62,7 @@ public static string Build(DarlingPeerDirectory.Snapshot peers)
## Tool Reference
- This server exposes 146 tools. 86 are the same names Performance Monitor Lite exposes, spanning diagnostic analysis, plan analysis, data reads at core and diagnostic depth, resource contention + jobs, trends, system-health parse-on-read, alerts + health overview, and the Default Trace. The remaining 60 are unique to Darling: thirty-three are the PostgreSQL reads (Aurora/PostgreSQL targets only Darling's central store can hold), eight are the Custom Views tools (seven manage the saved views — the one view-authoring write surface — and `describe_custom_view_catalog` returns the read-only compose vocabulary those authoring tools draw from), seven are the custom-alert-rule tools (`create_custom_alert_rule` / `update_custom_alert_rule` / `delete_custom_alert_rule` manage the user-authored alert rules, the one alert-authoring write surface, while `get_custom_alert_rule` / `list_custom_alert_rules` read them back, `validate_custom_alert_rule` checks a rule definition against the same compose catalog without saving it, and `test_custom_alert_rule` evaluates a rule's metric NOW on each in-scope server and reports whether it would breach, without delivering or persisting anything), three are alert-tuning write tools (`update_alert_settings` tunes the alert engine's thresholds; `create_mute_rule` / `delete_mute_rule` manage the mute rules) that write only the shared alert configuration in the monitoring store, two are server-onboarding write tools (`add_servers` bulk-adds monitored servers; `remove_server` removes one) that add or remove rows in the monitoring store's monitored-server registry, `get_fleet_overview` and `get_ag_health` are the two cross-server reads only a central store can answer, `get_store_metrics` reads the monitoring store's OWN hourly size/compression/growth series for capacity forecasting, `get_store_log` reads what the monitoring store's OWN PostgreSQL server log recorded as a per-class census with its capture denominator (the self-monitoring that shows the store's half of a client-side symptom), `get_collector_cost` reads the tool's OWN per-collector cost on the monitored servers (the self-monitoring that flags a collector regressing into a hog), `get_collector_stall_probes` reads the out-of-band server-wide wait samples this tool takes while one of its own collectors is stalled mid-read — the only surface here that reports what a monitored instance was doing inside the window the sequential sweep records nothing in — and `get_blocking` is Darling's name for the blocked-process-report read that Lite exposes as `get_blocked_process_reports` — a naming difference, not a capability gap. Every data-read tool reads the data the collectors already captured into the store — a stored read, never a live query against the monitored server.
+ This server exposes 147 tools. 86 are the same names Performance Monitor Lite exposes, spanning diagnostic analysis, plan analysis, data reads at core and diagnostic depth, resource contention + jobs, trends, system-health parse-on-read, alerts + health overview, and the Default Trace. The remaining 61 are unique to Darling: thirty-three are the PostgreSQL reads (Aurora/PostgreSQL targets only Darling's central store can hold), eight are the Custom Views tools (seven manage the saved views — the one view-authoring write surface — and `describe_custom_view_catalog` returns the read-only compose vocabulary those authoring tools draw from), eight are the custom-alert-rule tools (`create_custom_alert_rule` / `update_custom_alert_rule` / `delete_custom_alert_rule` manage the user-authored alert rules, the one alert-authoring write surface, while `get_custom_alert_rule` / `list_custom_alert_rules` read them back, `validate_custom_alert_rule` checks a rule definition against the same compose catalog without saving it, `test_custom_alert_rule` evaluates a rule's metric NOW on each in-scope server and reports whether it would breach without delivering or persisting anything, and `list_custom_alert_templates` lists the built-in starter rule templates to browse and create from), three are alert-tuning write tools (`update_alert_settings` tunes the alert engine's thresholds; `create_mute_rule` / `delete_mute_rule` manage the mute rules) that write only the shared alert configuration in the monitoring store, two are server-onboarding write tools (`add_servers` bulk-adds monitored servers; `remove_server` removes one) that add or remove rows in the monitoring store's monitored-server registry, `get_fleet_overview` and `get_ag_health` are the two cross-server reads only a central store can answer, `get_store_metrics` reads the monitoring store's OWN hourly size/compression/growth series for capacity forecasting, `get_store_log` reads what the monitoring store's OWN PostgreSQL server log recorded as a per-class census with its capture denominator (the self-monitoring that shows the store's half of a client-side symptom), `get_collector_cost` reads the tool's OWN per-collector cost on the monitored servers (the self-monitoring that flags a collector regressing into a hog), `get_collector_stall_probes` reads the out-of-band server-wide wait samples this tool takes while one of its own collectors is stalled mid-read — the only surface here that reports what a monitored instance was doing inside the window the sequential sweep records nothing in — and `get_blocking` is Darling's name for the blocked-process-report read that Lite exposes as `get_blocked_process_reports` — a naming difference, not a capability gap. Every data-read tool reads the data the collectors already captured into the store — a stored read, never a live query against the monitored server.
### Reading an empty result
diff --git a/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs b/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs
index c44c2c0bd..cbf9e1f76 100644
--- a/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs
+++ b/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs
@@ -229,7 +229,8 @@ there is no Lite twin to port (same reasoning as the Custom Views + alert-tuning
config.custom_alert_rules in the central Postgres store; get_custom_alert_rule / list_custom_alert_rules
read them back; validate_custom_alert_rule checks a definition against the same compose catalog the
Custom Views tools draw from, without saving; test_custom_alert_rule (#3299) evaluates a rule's metric
- now on each in-scope server and reports whether it would breach, without delivering or persisting).
+ now on each in-scope server and reports whether it would breach, without delivering or persisting;
+ list_custom_alert_templates (#3285 Component 7) lists the code-defined starter rule templates).
Darling-ONLY by architecture, the same kind of entry as the Custom Views + alert-tuning tools above
rather than a "not ported yet": custom alert rules are a central-store feature the headless service
evaluates on its sweep, and Lite (a single-instance WPF app over local DuckDB with no central,
@@ -241,6 +242,7 @@ there is no Lite twin to port (same reasoning as the Custom Views + alert-tuning
"delete_custom_alert_rule",
"validate_custom_alert_rule",
"test_custom_alert_rule",
+ "list_custom_alert_templates",
};
[Fact]