Skip to content

Release notes ClusterControl MCP

The ClusterControl MCP Server is versioned and released independently of the main ClusterControl components, as clustercontrol-mcp-1.0.0-N builds. This page is the running release history for the MCP Server. The initial launch (clustercontrol-mcp-1.0.0-5, May 4th, 2026) is documented in the v2.4.0 release notes.

Feature Release: Workload advice, PostgreSQL switchover, minor upgrades and guarded point-in-time recovery

  • Build:
    • clustercontrol-mcp-1.0.0-213

This release lets an agent go from finding a problem to recovering from it. A new advisor names what in a cluster's workload needs attention, and new procedures switch a PostgreSQL primary over to a standby, apply minor upgrades to MySQL, Galera and PostgreSQL clusters, and restore a cluster to a point in time. Every new procedure is checked by the server before anyone is asked to approve it. Point-in-time recovery always requires an approval, and can now require one from a second, independently authenticated person.

⚠ Read before upgrading

No tool, argument or setting was removed or renamed, but some calls now refuse what they used to accept, and one setting now refuses values it used to accept. Each change is described under Behaviour changes below. Match a refusal's code, not its sentence.

  • MCP_HUMAN_APPROVAL now accepts only off, elicitation (and its earlier spellings on, enforced, 1 and true) or out_of_band. Any other value stops the server at startup, where it used to switch approval on. Check the value in /etc/default/cmon-mcp before upgrading.
  • create_job now refuses an upgrade_cluster whose job_data carries anything but clusterid (and, on PostgreSQL, upgrade_to_version), a PostgreSQL promotion of anything but a caught-up standby, a PostgreSQL rebuild that does not name a registered standby, and point-in-time recovery outside the admitted route.
  • With MCP_TOOL_ALLOW set, the two new tools are out of scope until you add them. A profile copied into /etc/default/cmon-mcp keeps refusing them, and agentic-operate copies keep refusing restore_backup, until you copy the new assignments.
  • get_cluster host lines end with a new version= field, and the finding catalog moves from version 19 to 24.
  • advise_workload — what in a cluster's workload needs attention. On MySQL-family clusters it reads the controller's top query digests, its process list and its recorded long-running transactions; on PostgreSQL, the process list. Codes: QUERY_HOTSPOT (one digest holds at least 25% of the summed wait, once that sum reaches 10 seconds), QUERY_FULL_SCAN_SUSPECTED (1,000 rows examined per row sent over at least 100 executions, a heuristic), LONG_RUNNING_QUERY (30 seconds), IDLE_IN_TRANSACTION (PostgreSQL, at least 20 seconds), LOCK_WAITS_RECENT (transactions the controller recorded in the last hour), QUERY_MONITOR_DISABLED and WORKLOAD_DATA_UNAVAILABLE. Missing, stale or partial data is never reported as clean, and only the cluster's own database nodes are judged, not its load balancers. It reports no latency percentiles or query plans, and no statement text beyond a digest's normalised pattern. (CLUS-8999)
  • list_available_upgrades — the package upgrades the controller last found for each host of a cluster, with the age of that package check and the hosts it did not cover. These are the controller's recorded candidates; the package repositories can change before an upgrade installs them. (CLUS-9022)
  • sop_switchover_postgresql — switches a running PostgreSQL 14 to 18 primary over to a chosen standby. It reads the standby twice, at least two minutes apart, and stops unless every standby is streaming and the chosen one is caught up, with replay not paused and a lag of at most 5 seconds. It also stops on an active maintenance window. The approval asks whether an external failover manager such as Patroni manages the hosts, which ClusterControl does not detect, and names the primary that will be stopped and the standby that will be promoted. Afterwards it checks that the old primary rejoined as a standby. sop_failover_drill now routes PostgreSQL clusters to it. (CLUS-9000, CLUS-9021)
  • sop_minor_upgrade and sop_minor_upgrade_postgresql — upgrade a MySQL replication or Galera cluster, or a PostgreSQL 14 to 18 cluster, to the newest minor release its package repository offers. Each stops on a topology the controller cannot upgrade this way (MYSQL_SINGLE, GROUP_REPL, POSTGRESQL_LOGICAL) and, before approval, requires a finished package check under 24 hours old. The approval says what restarts: the replication primary in place with writes refused and no switchover, and every Galera and PostgreSQL node, even with nothing to upgrade. (CLUS-9001)
  • sop_pitr_restore — restores a cluster to a point in time from one of its own completed backups, through restore_backup. MySQL and Galera: Oracle MySQL MYSQL_SINGLE and REPLICATION, and healthy Percona, Codership or MariaDB Galera clusters, from physical full backups, physical incremental chains (the whole chain is checked) and complete gzip logical dumps. PostgreSQL POSTGRESQL_SINGLE: pg_basebackup, pg_basebackupincr (PostgreSQL 17 or later), pgbackrestfull, pgbackrestincr, pgbackrestdiff, walgfull and walgincr. The preview states the target and what the restore replaces. After approval the server checks permissions, the backup and the topology again and submits the restore once; neither a confirmation nor an approval can override a failed final check. (CLUS-9002, CLUS-9033, CLUS-9040)
  • Independent approval (MCP_HUMAN_APPROVAL=out_of_band) — a destructive call waits for a decision from a separately authenticated approver whose controller user is not the requester's. Approvers answer on a separate loopback listener (MCP_APPROVAL_BIND_ADDRESS), are listed in an operator-owned file (MCP_APPROVER_MAP_FILE) by controller user and token hash, and their tokens must differ from every caller's. A request expires after two minutes. The mode requires the identity map, the digest key and the audit log, and the server does not start without them. elicitation remains available, and the packaged agentic-operate profile keeps it. (CLUS-8997, CLUS-9002)
  • The tool registry grows from 84 to 86 tools (57 pure-read, 26 CMON-mutating, 3 local-filesystem-writing; 22 flagged destructive), adding the two read-only tools above, and the procedure prompts grow from six to ten. The 21 resource surfaces are unchanged. The finding catalog moves from version 19 to 24.
  • MCP_HUMAN_APPROVAL takes named values. off or empty turns approval off, elicitation (or on, enforced, 1, true) asks the calling session as before, and out_of_band selects independent approval. Any other value stops the server at startup with MCP_HUMAN_APPROVAL must be off, elicitation or out_of_band. (CLUS-8997)
  • Point-in-time recovery runs only through an admitted restore_backup. A restore_backup call, the dedicated tool or create_job, that carries pitr, pitr_stop_time, pitr_stop_log or pitr_stop_pos is checked before the preview and again before submission, and needs an approval model switched on (PITR_APPROVAL_MODEL_REQUIRED). Refused with PITR_NOT_ADMITTED: the standalone pitr command, other recovery options on any command, unknown keys, and a backup, method or topology outside the admitted set. A restore_backup with none of these keys is an ordinary restore, as before. (CLUS-9002, CLUS-9033, CLUS-9040)
  • create_job holds upgrade_cluster to two shapes. job_data may carry only clusterid and, on POSTGRESQL_SINGLE, upgrade_to_version as a bare major version. Every other key is refused, among them version, mysql_version, mongodb_version and server_version, which could turn a minor upgrade into a major one. So is upgrade_cluster on MYSQL_SINGLE, GROUP_REPL, the SQL Server types, ELASTIC, CLICKHOUSE and POSTGRESQL_LOGICAL. The check runs before the preview and again before submission. (CLUS-9001)
  • A PostgreSQL promotion must name a caught-up standby of the one primary. promote_replication_slave on POSTGRESQL_SINGLE is refused unless it names, with an explicit port, an online standby that is streaming from the cluster's online primary with replay not paused and a lag of at most 5 seconds; force is refused. Under MCP_EXECUTION_GUARD the confirmation binds both the standby and the primary the preview named. A MySQL promotion is unchanged. (CLUS-9000, CLUS-9021)
  • A PostgreSQL rebuild must name a registered standby and a different primary. stage_replication_slave on POSTGRESQL_SINGLE checks both roles before the preview and again before submission; a down or failed standby can still be rebuilt. Registrations of other services on different ports no longer block promotion or rebuild admission. (CLUS-9019, CLUS-9020)
  • The query, process and transaction tools check cluster access first. get_top_queries, get_sql_processes, get_tx_deadlocks and the query and process resources ask for the cluster before reading, which adds one request, and refuse a cluster the identity cannot read exactly as one that does not exist. A cluster the controller holds but is not running now reads "Cluster was not found." too. (CLUS-8998)
  • Answers change shape. get_cluster host lines end with version=<v>, or version=unknown. FAILOVER_READY's PostgreSQL recommendation names sop_switchover_postgresql. sop_major_upgrade says this server checks upgrade_cluster's keys. (CLUS-9000, CLUS-9001)
  • The deployment profiles allow the new tools. Both add advise_workload and list_available_upgrades. agentic-operate also allows the dedicated restore_backup tool, and its job command policy adds restore_backup to promote_replication_slave, stage_replication_slave, upgrade_cluster and verify_backup. Copy the new assignments into /etc/default/cmon-mcp and restart. (CLUS-8999, CLUS-9002, CLUS-9022)
  • A registered name can no longer add lines to tool output. Cluster names, hostnames, roles, statuses and types that hold control characters or Unicode line separators are now quoted in get_cluster and related listings, so a crafted name cannot forge an extra line an agent would read as the server's. Ordinary text and structured JSON are unchanged. (CLUS-9023)
  • get_tx_deadlocks works against ClusterControl 2.5.0. The controller refused its request, which carried no offset; the request now always carries offset: 0. (CLUS-9004)
  • PostgreSQL sessions show their real process id, user, database, node and time. get_sql_processes returned a process id of 0 and empty fields for every PostgreSQL session, so a session could not be identified or ended. (CLUS-9005)
  • One unrelated host entry no longer blocks a promotion or rebuild. A registration with a port above 65535, or of another service on a different port, made the whole cluster refuse. (CLUS-9019)
  • Point-in-time recovery is admitted more widely than it was run live. Live runs on ClusterControl 2.5.0 covered PostgreSQL 18 native full backups with local WAL, and physical full, physical incremental and logical restores on Oracle MySQL 8.4 replication and MariaDB 10.11 Galera. pgBackRest, WAL-G, native PostgreSQL incrementals, other timezones and timelines, and MySQL positional, encrypted, cloud and streaming cases are admitted without a live run.
  • On MySQL and Galera, check the restored data, not only the job's state. A finished restore job does not by itself show that the data reached the target time. Before resuming writes, check on every member that rows written before the target are present and rows written after it are absent; the procedure's verify step does this, and it stops before the preview when it cannot confirm the backup and binary logs it will replay.
  • A PostgreSQL point-in-time recovery can finish after falling back to ordinary recovery. Native recovery, a local pgBackRest repository and WAL-G can replay all available WAL when the target cannot be reached, so a finished job does not show the requested point was reached; the procedure reports the job, any fallback and the data separately.
  • Checks and approvals apply to jobs the MCP Server submits. The ClusterControl GUI, the s9s CLI and other clients do not pass through them, and a preview binds the backup's catalog record and the topology, not the backup's bytes.
  • An approval proves who answered, not that a person read the request. elicitation shows that the requesting session answered; out_of_band that a different authenticated user decided. Neither stops an operator from giving one agent both credentials.
  • A switchover judges the controller's last sample. The server checks the standby's last reading before the preview and again just before submission, but the standby can fall behind after that, and the controller itself does not check it. (CLUS-9025)
  • On Debian and Ubuntu, a MySQL replication minor upgrade can restart a server once more while the packages install, before ClusterControl restarts it. The approval says so. (CLUS-9024)
  • advise_workload has no PostgreSQL query digests or lock waits, which it reports as WORKLOAD_DATA_UNAVAILABLE, and get_fleet_posture does not run it.
  • Packages are x86_64 only in this release.

Feature Release: Guided remediation — a fleet view, recovery procedures and PostgreSQL coverage

  • Build:
    • clustercontrol-mcp-1.0.0-194

Where the previous feature release let the server advise, this one helps act on the advice. Findings now name their cluster and what to run next, a fleet view ranks what needs attention across every cluster you can read, and four new procedures rebuild a broken MySQL replica or PostgreSQL standby and prove that a backup restores. Every new tool is read-only: a procedure changes a cluster only through create_job, which now checks a job before anyone is asked to approve it and can be limited to the job commands you allow.

⚠ Read before upgrading

No tool, argument or setting was removed or renamed, but some calls now refuse what they used to accept and some answers change shape. Each change is described under Behaviour changes below. Match a refusal's code, not its sentence.

  • With MCP_TOOL_ALLOW set, the four new tools are out of scope until you add them. With MCP_TOOL_DENY keeping backup data or controller settings from an agent, also deny advise_backup_posture, advise_failover_readiness and get_fleet_posture, which read them.
  • create_job now refuses a job on a cluster where the identity lacks read and execute access, a job_data that is not one JSON object with unique keys, the create_maintenance command, a replica rebuild aimed at a primary, and a verification of a backup the cluster does not own.
  • The maintenance tools require write access to the cluster, in the dry_run=true preview too.
  • Every finding gains cluster_id and may carry next_action, and catalog_version moves from 7 to 19.
  • list_backups rows end with a verified: field, and IPv6 endpoints are written [address]:port.
  • Findings name their cluster and what to run next — every advisor finding carries cluster_id, and a finding code can name a next_action: a read-only tool or a procedure prompt, with its arguments filled in from the finding. REPL_LINK_DOWN names the rebuild procedure for its engine, BACKUP_NOT_VERIFIED names sop_backup_verify, and BACKUP_LAST_FAILED names get_job_log for the job that ran the failed backup. (CLUS-8816, CLUS-8817, CLUS-8818, CLUS-8819, CLUS-8894)
  • get_fleet_posture — which clusters need attention first, and why. It runs six advisors (replication, failover readiness, backups, SSL, release end of life and disk capacity) over every cluster this identity can read, and returns their warn and critical findings ranked, the worst cluster first, with per-cluster critical, warn and unknown counts. A cluster whose data could not be read or is stale counts as unknown, never as clean. A call has a 45-second budget and returns at most 50 findings, and its text is held within 64 KiB by cutting whole clusters from the bottom; complete and truncation say what was cut. A cluster this identity cannot read is not listed. (CLUS-8821, CLUS-8962)
  • advise_backup_posture — will the backup restore, and is the recovery point met? For any engine: BACKUP_NONE, BACKUP_LAST_FAILED, BACKUP_VERIFICATION_FAILED, BACKUP_NOT_VERIFIED, BACKUP_STALE (no restorable backup in 26 hours), BACKUP_ON_DB_HOST, BACKUP_NO_SCHEDULE, BACKUP_SCHEDULE_PAUSED and BACKUP_OK. A backup counts as restorable only when it completed and has a storage location, and a location the controller did not report is treated as unknown, never as empty. (CLUS-8817)
  • advise_failover_readiness — if the primary died now, could ClusterControl fail over, and to which replica or standby? It judges Oracle MySQL and Percona replication clusters, and PostgreSQL 14 to 18 POSTGRESQL_SINGLE clusters of the community vendor, against the controller's own election rules for each engine: FAILOVER_READY, FAILOVER_CANDIDATE_BLOCKED with the first failing rule, FAILOVER_READINESS_UNKNOWN, NO_FAILOVER_CANDIDATE (critical) and FAILOVER_AUTORECOVERY_OFF. On PostgreSQL the summary names the projected winner, the ready standby furthest ahead in replay. MariaDB is refused with UNSUPPORTED_ENGINE. (CLUS-8820, CLUS-8892)
  • check_capacity — which database host fills up first. It judges each database host on its fullest mount's newest disk sample against the controller's diskspace_warning and diskspace_critical settings, or the defaults of 80% and 90% when those cannot be read, and says which it used: DISK_USAGE_CRITICAL, DISK_USAGE_WARNING, DISK_USAGE_OK, CAPACITY_DATA_STALE (a sample older than three collection intervals) and CAPACITY_DATA_UNAVAILABLE. Stale, missing or malformed data is never reported as clean. (CLUS-8823, CLUS-8962)
  • check_version_eol assesses community PostgreSQL — by major version, against the single community end-of-support date of each major from 9.6 to 18. PostgreSQL 14 reads MAINTENANCE_ENDING_SOON through November 12th, 2026, and VERSION_EOL after it. PostgreSQL takes no support entitlement, and EnterpriseDB and distribution-packaged builds are still not assessed. The bundled lifecycle table grows from 30 to 40 releases. (CLUS-8957)
  • sop_rebuild_replica — rebuilds a MySQL-family replica whose replication link is down with SQL error 1062 or 1032, or IO error 1236. Each procedure in this release previews before it acts, asks for approval, and carries one branch for an operation nobody approved and another for a failure part-way through. This one stops on the primary, an intermediate primary, an IPv6 host or an active maintenance window. (CLUS-8818, CLUS-8816)
  • sop_rebuild_standby — rebuilds a PostgreSQL 14 to 18 standby that cannot follow its primary. Before advising that a down standby be started, it reads that standby's own rebuild history. The controller keeps a standby's last replication positions even after a failed rebuild has erased its data directory, so the procedure hands the standby to a person when that history shows an erase, shows a rebuild still running, or cannot be read in full. (CLUS-8894, CLUS-8900, CLUS-8961)
  • sop_backup_verify — proves a MySQL-family backup restores, onto a spare host that a person chooses. It admits only the backup advise_backup_posture names, stops on a spare that belongs to a visible cluster or is the controller, and reads the outcome from the job's final state. (CLUS-8819, CLUS-8902, CLUS-8906)
  • sop_backup_verify_postgresql — the same for PostgreSQL 14 to 18 pg_basebackup and pgdumpall backups. Its approval step states that ClusterControl turns the cluster's automatic failover and node recovery off while it restores the backup, and what the spare host is left with. (CLUS-8893)
  • A job command policy (MCP_JOB_COMMAND_ALLOW, MCP_JOB_COMMAND_DENY, or the -job-command-allow and -job-command-deny flags) — names the job commands that create_job and the dedicated job tools may submit; deny wins, and unset changes nothing. A refused command answers JOB_COMMAND_NOT_PERMITTED before any preview or approval request. It is checked again before submission, and a tool that submits one fixed command is hidden when that command is not permitted. get_session_info reports the effective policy as job_command_policy. (CLUS-8956)
  • Two deployment profiles for agents — agentic-observe (read-only, audit on, an allow list) and agentic-operate (audit on, the execution guard enforced, approval over elicitation, no export directory, the same allow list plus create_job, and a job command policy of the four commands its procedures submit). They are installed as /usr/share/cmon-mcp/profiles/agentic-observe.env and agentic-operate.env, and nothing reads them in place: copy the assignments into /etc/default/cmon-mcp, and copy them again after an upgrade that changes them. agentic-operate needs the HTTP transport and an identity map. (CLUS-8822, CLUS-8829, CLUS-8956)
  • The tool registry grows from 80 to 84 tools (55 pure-read, 26 CMON-mutating, 3 local-filesystem-writing; 22 flagged destructive), adding the four read-only tools above, and the procedure prompts grow from two to six. The 21 resource surfaces are unchanged. The finding catalog moves from version 7 to 19.
  • create_job checks a job before anyone is asked to approve it. Admission freezes the call's arguments and requires read and execute access (r and x) on the cluster before any preview, confirmation or approval request. It runs again on the frozen arguments just before the job is submitted, so a privilege removed while a person was deciding still stops the job. (CLUS-8883)
  • create_job accepts job_data only as a string holding one JSON object with unique, printable keys; an empty string still omits it. A job_data sent as an object, array, number, null or bool used to be dropped, so the job ran without it, and a repeated key was sent with its last value. The refusal names the key at fault, in the dry_run=true preview too. (CLUS-8819)
  • Replica rebuilds and backup verifications are checked against the cluster. A stage_replication_slave job must name a replica as its target and a source in the same cluster, with explicit ports and the right roles. A primary, writable or read-only, or an intermediate primary as the target is refused before the preview. A backup verification requires a backup the named cluster owns, and refuses an ambiguous selector or a file path with no backup record. (CLUS-8884, CLUS-8926)
  • The maintenance tools require write access, and create_job refuses create_maintenance. add_maintenance_window, remove_maintenance_window and remove_maintenance_windows_by_filter refuse, in the preview as on execution, unless the identity holds w on the cluster and on every readable cluster that lists the host as its own. They also refuse the controller's host and Prometheus hosts. create_job refuses the create_maintenance command and points to add_maintenance_window. list_all_maintenance shows only the windows of clusters this identity can read, by cluster id or by a host's hostname or IP, and list_maintenance heads a cluster-wide window cluster #<id>. (CLUS-8827)
  • More tools confirm the identity can read the cluster before answering. These are list_maintenance, get_db_growth, the cluster maintenance resource, get_job_log, get_job_checkpoint, and the check_no_primary_keys, check_redundant_indexes and check_myisam_tables advisors. A cluster the identity cannot read gets the controller's own refusal, and a job of such a cluster is answered as not found. (CLUS-8816, CLUS-8827, CLUS-8832)
  • The MySQL procedures cover the MySQL family. sop_rebuild_replica, sop_backup_verify and sop_failover_drill continue on a MYSQL_SINGLE or REPLICATION cluster of vendor oracle, percona or mariadb, with no version gate, and stop on anything else; Galera is not covered. sop_failover_drill now opens with an eligibility step and stops a MYSQL_SINGLE cluster before approval. (CLUS-8907, CLUS-8911)
  • The approval tier says what it checks. It accepts an answer from the calling session that carries the approval code it sent. The startup lines, the HUMAN_APPROVAL_REQUIRED and HUMAN_APPROVAL_UNAVAILABLE refusals and the procedures no longer say the call is put to a person. Every refusal still begins with its code; the text after the code has changed. (CLUS-8887)
  • Answers change shape. REPL_LINK_DOWN evidence carries the host's hostname, port and engine, and a PostgreSQL standby's finding names sop_rebuild_standby instead of the MySQL procedure. list_backups rows end with verified: <status> <time>, or verified: unknown. get_session_info gains identity_note and job_command_policy. Every findings tool's description now opens with the question it answers. (CLUS-8818, CLUS-8819, CLUS-8821, CLUS-8882, CLUS-8894, CLUS-8956)
  • NO_FAILOVER_CANDIDATE is now emitted, by advise_failover_readiness at critical. Previously it was catalogued under advise_replication_topology and never emitted, so a client filtering on a finding's tool or severity sees it move. (CLUS-8820)
  • A down PostgreSQL standby with no upstream is now named in advise_replication_topology's summary, whose replica count now reads "with a reported upstream". (CLUS-8901)
  • The approval prompt shows a job's job_data key by key. It used to be one string cut at 96 bytes, so the end of a long job_data could be submitted without being shown to the approver. Each value is now cut on its own, and credential-named keys holding a string or a composite value are redacted. Numbers are shown and sent exactly as written: 9007199254740993 used to be sent as 9007199254740992. (CLUS-8819)
  • A major-upgrade preview carries the high-risk warning. upgrade_cluster was missing from the commands whose preview warns, although a major upgrade stops and unregisters the primary and cannot be rolled back once started. verify_backup now previews with the warning too, because a MySQL verification replaces the MySQL software on the host it names. (CLUS-8746, CLUS-8819)
  • A procedure's hostname must name its host literally. IPv4 in any but canonical form (192.168.139.010, 3232271112, 0xc0.0xa8.0x8b.0x4f) is refused, because a resolver reads those as a different host than the text says. Loopback addresses and names are refused too. (CLUS-8819)
  • create_backup_schedule accepts a plain cron line. The recurrence was sent with a prefix the controller read as part of the minute field, so 0 2 * * * was refused with "Failed to parse recurrence." It is now sent as written, and a line with no TZ= token runs in UTC. Both schedule tools now check the line before the preview, and refuse a line the controller would run at other times than written, naming the field at fault. (CLUS-8882)
  • update_backup_schedule no longer loses the schedule it replaces. It deleted the old schedule before creating the new one, so a replacement the controller refused left no schedule at all. It now creates the replacement first, and refuses a job_id that is not one of the named cluster's schedules. (CLUS-8882)
  • get_session_info without an identity map no longer reads as "not connected". The new identity_note says that every call runs as the server's configured user. (CLUS-8882)
  • audit_db_privileges no longer audits MariaDB's built-in mariadb.sys@localhost account. (CLUS-8882)
  • A reply the controller sends while it is stopping is an error, not an empty success. Every tool read it as a successful reply with no data — no windows, no rows, a "report" holding the reply. It now reports cmon: cmon is stopping. (requestStatus ObjectNotFound). (CLUS-8858)
  • check_ssl_posture no longer fails on a host listed twice, which dropped the request over HTTP and ended the server process over stdio. A cluster listing the controller does not answer Ok is now an error, rather than an empty or partial list. (CLUS-8821)
  • IPv6 endpoints are written [2001:db8::5]:3306. They were written 2001:db8::5:3306, which is itself a valid, different address, in get_cluster, list_nodes, get_cluster_topology, the replication and SSL advisors and the incident bundle. (CLUS-8818)
  • advise_replication_topology no longer fails on a node with no hostname. It names the node by role and port, and still counts it, so it raises no false SINGLE_NODE_PRIMARY. (CLUS-8818)
  • Backup-verification spares and upgrade candidates are no longer judged as database nodes. check_ssl_posture, advise_replication_topology and check_version_eol raised SSL findings for them, counted them as a second node, and listed them among the hosts. (CLUS-8828)
  • sop_failover_drill can no longer promote twice after a timed-out submit. It looked for the job in one default page of list_jobs and, finding none, submitted again. It now asks for at least 200 jobs, goes on only with exactly one match, and stops for a person otherwise. (CLUS-8816)
  • The approval tier cannot see a person. It checks an answer from the calling session carrying the approval code, which is a person's decision only if the client shows the request to a person the agent cannot answer for. So far only the Python SDK with an elicitation_callback has been shown approving.
  • Admitted is wider than qualified. The MySQL procedures were run end to end against a live controller on Oracle MySQL 8.0 and 8.4 REPLICATION clusters only (CLUS-8958). Percona Server, MariaDB, MYSQL_SINGLE, the failover drill and the major upgrade are admitted without that run. The PostgreSQL procedures and the PostgreSQL failover judgement were run on PostgreSQL 18 only, and the pgdumpall verification loop has not been run through the server.
  • Admission, ownership and command-policy checks apply to jobs the MCP Server submits.
  • A command policy governs job submission, not effects. set_controller_config, set_node_config, unset_node_config and add_maintenance_window reach the same effects as some job commands through other requests. Scope those tools too, or prefer the allow list; the server names each such case at startup.
  • advise_failover_readiness describes ClusterControl-managed failover only. It does not consider Patroni. It judges no MariaDB, MYSQL_SINGLE or POSTGRESQL_LOGICAL cluster, and refuses IPv6 entries in a failover list. It says so, rather than reading a maintenance window as protection, because ClusterControl 2.5.0 fails a PostgreSQL primary over during maintenance.
  • get_fleet_posture has no cursor. Past 64 KiB of text the least pressing clusters are counted but not listed; call an advisor on one of them for its findings.
  • check_capacity judges every mount, whether or not it holds the data directory. It measures sample age on the MCP server's clock, and predicts no time until full.
  • advise_backup_posture judges one page of schedules, at most 100 scheduled jobs of every command, and the controller can answer a failed backup read as an empty list. (CLUS-8830)
  • A verification leaves files on the spare host. A PostgreSQL spare keeps a wal_archive directory, which a person removes, and a backup kept only in cloud storage is downloaded to the spare.
  • A failed standby rebuild does not always drop the standby's upstream. On ClusterControl 2.5.0 it has been seen both ways, and the advisor and sop_rebuild_standby handle both.
  • The scope-overlap warning over-names. Denying one of the newly access-checked tools warns that some twenty cluster tools reach the same data, which they do not. The warning errs towards naming too much, never too little.
  • Packages are x86_64 only in this release.

Maintenance Release: September 30th, 2026

  • Build:
    • clustercontrol-mcp-1.0.0-124

This release fixes how the server reads backup records, so the backup tools list real backups again. The tools, their arguments and the server's settings are unchanged from 1.0.0-122.

  • Backups are listed with their real id, method, status, timestamps and size again. list_backups, get_latest_backup and the clustercontrol://clusters/{cluster_id}/backups resource showed a backup that exists as #0 with a size of 0 B and no method, status or timestamps, so a restore or a delete could not be pointed at a real backup id. The server asks the controller for version-2 backup records, which the controller returns nested under metadata, but it read them in the older flat shape. It now reads both shapes, and the output format is unchanged. Every earlier build was affected, against ClusterControl 2.4.0 and 2.5.0 controllers alike. (CLUS-8812)

Feature Release: Advisors that report findings, and Terraform export

  • Build:
    • clustercontrol-mcp-1.0.0-122

Where the previous release made the server safe to operate, this one lets it advise. Six new tools inspect a cluster and report what is wrong or risky — as machine-readable findings a client can act on, not prose — and an existing cluster can now be exported as Terraform. Every new tool is read-only, so read-only mode, tool scoping and the audit log apply to them exactly as to any other read tool.

⚠ Read before upgrading

No tool, argument or setting was removed or renamed. Three changes can still affect a client that parses output strictly; all three are described under Behaviour changes below.

  • lag=0s becomes lag=unknown in topology and incident-bundle text wherever the controller reported no lag.
  • Cluster host JSON gains keys.
  • With MCP_TOOL_ALLOW set, the six new tools are out of scope until you add them.
  • One findings contract for every advisor — each advisor returns {findings, summary, catalog_version}, both as structuredContent with a published outputSchema and as the same JSON in the text content, so a client can key on a finding's code instead of parsing prose. Codes, severities (info, warn, critical), recommendations and thresholds come from a versioned catalog that ships inside the binary (version 7), so changing a threshold is a catalog version bump rather than a silent change in behaviour. A healthy cluster produces no warn or critical findings. (CLUS-8116)
  • advise_replication_topology — broken replication links (REPL_LINK_DOWN), replicas more than 60 seconds behind (REPL_LAG_HIGH), and a cluster with a single database node (SINGLE_NODE_PRIMARY). MySQL replication and PostgreSQL streaming replication are each judged by their own model, and an unknown lag is reported as unknown, never as zero. (CLUS-8113)
  • audit_db_privileges — accounts holding every global privilege (PRIV_GLOBAL_ALL), accounts that accept connections from any host (PRIV_WILDCARD_HOST), and more than three superuser accounts (PRIV_SUPERUSER_COUNT_HIGH), on MySQL-family and PostgreSQL clusters. System accounts are excluded. (CLUS-8116)
  • check_config_drift — a configuration parameter whose value differs between nodes of the same role (CONFIG_DRIFT), with each node's value and the node that is off the majority. Parameters that differ per node by design — server_id, addresses, paths — are never compared. (CLUS-8117)
  • check_ssl_posture — SSL turned off on a database host (SSL_DISABLED), and the state of each of the cluster's certificates from ClusterControl's own CA: revoked, expired, expiring within 30 days, valid, or unknown. On MySQL-family hosts it asks the database server itself whether TLS is available rather than trusting the controller's flag alone — see Bug fixes. (CLUS-8115, CLUS-8762)
  • check_version_eol — whether each database release is supported, ending within 90 days, past community maintenance, or end of life, resolved per engine against your support entitlement. The lifecycle table covers 30 releases of MySQL, MariaDB and MongoDB, ships inside the package and is never fetched, so the check works in an air-gapped installation; every result carries the table's date so its age is visible. (CLUS-8118, CLUS-8767)
  • Support entitlement settings (MCP_SUPPORT_ENTITLEMENT, MCP_EOL_TABLE_FILE) — read only by check_version_eol, and both unset by default. The entitlement is given per engine and optionally per cluster, for example mysql=premier,mariadb=community,cluster:7=extended; unset means each engine's free community stream. The table file replaces the bundled lifecycle table, for testing or to carry newer dates before the next release. Both fail fast: a value the engine does not accept, or a table file that cannot be read, stops the server at startup with an error naming the entry. (CLUS-8118)
  • export_terraform — an existing cluster as Terraform HCL for the severalnines/clustercontrol provider v0.2.25, returned in the tool result; the server writes no file. Credentials appear only as var.* variables. MySQL replication, Galera and PostgreSQL replication clusters are exported; an engine or layout the provider cannot express is refused with EXPORT_UNSUPPORTED_ENGINE or EXPORT_UNSUPPORTED_TOPOLOGY rather than exported as a different cluster. (CLUS-8119)
  • The documentation bundle that search_docs serves offline is refreshed from the current ClusterControl documentation. (CLUS-8769)
  • The tool registry grows from 74 to 80 tools (51 pure-read, 26 CMON-mutating, 3 local-filesystem-writing; 22 flagged destructive), adding the six read-only tools above. The 21 resource surfaces are unchanged.
  • lag=0s becomes lag=unknown in get_cluster_topology and get_incident_bundle text wherever the controller reported no lag, and the topology view gains a lag_unknown field. Anything that read lag=0s as "caught up" was being told something the controller never said. (CLUS-8113)
  • Cluster host JSON gains keys where the controller reports them: version, hostname_data, hostname_internal, ip, synchronous, sync_state, and the replication link status with its IO/SQL error numbers and states. A client that rejects unknown keys needs to accept them. (CLUS-8113, CLUS-8119)
  • With MCP_TOOL_ALLOW set, the new tools are out of scope until they are added to the list. That is the scope working as designed.
  • The configuration readers hide more parameters — every parameter whose name contains conninfo is now hidden. See Security fixes. (CLUS-8766)
  • The configuration readers no longer return replication credentials in clear text. get_node_config, get_cluster_config and the configuration resource returned the wsrep-provider-options and loose-wsrep-provider-options spellings, and PostgreSQL primary_conninfo, unmasked — values that can carry a replication password. Parameter names are now normalised before the masking decision, and every name containing conninfo is hidden, which deliberately also hides some non-secret parameters. (CLUS-8766)
  • set_node_config works on MySQL-family clusters. It sent no configuration group, so outside CCX mode the controller rejected the change with "Group is missing.", and unset_node_config named no section of my.cnf. Both tools now take an optional group; when it is omitted, a MySQL, Galera or Group Replication node gets mysqld and any other node gets none. (CLUS-8760)
  • check_ssl_posture no longer reports SSL enabled on a MySQL-family server that refuses TLS. The controller derives its SSL flag from tls_version, which lists protocols even when the server cannot negotiate TLS; this was reproduced on MySQL 8.0, MySQL 8.4 and MariaDB 11.4. Where the controller reports a MySQL-family host enabled, the tool now reads the server's own have_ssl, or on MySQL 8.4 whether a certificate is loaded, and reports SSL_DISABLED when the server says TLS is off. The check only ever turns enabled into disabled; a host that does not answer keeps the controller's flag and is named in the summary. (CLUS-8762)
  • Reading a PostgreSQL cluster no longer fails on an unusual lag value. A replication lag reported as "--" or as a fraction broke the whole cluster read for every tool, and an empty lag no longer reads as 0. (CLUS-8113)
  • The advisors read the controller's sampled state. The replication advisor sees the controller's last sample, taken about every 10 seconds by default, and REPL_LAG_HIGH uses its own 60-second threshold, separate from the controller's max_replication_lag alarm. Galera is judged for redundancy only: REPL_LINK_DOWN and REPL_LAG_HIGH are never raised on a Galera cluster, and the summary says so.
  • Two replication codes are reserved but never emitted. NO_FAILOVER_CANDIDATE and REPL_MONITOR_GRANT_MISSING are in the catalog, but the controller exposes no clean source for either, so promotability and the monitoring account's grants are not assessed.
  • check_config_drift compares configuration files, not running servers. A parameter changed with SET GLOBAL, or a file edited but not reloaded, is not seen as the server has it, and memory-sized settings legitimately differ between nodes on different hardware. Hidden parameters are never compared.
  • check_version_eol assesses database nodes, never the controller, and covers 30 releases. PostgreSQL, Percona, Redis/Valkey, SQL Server, and releases the table does not list return EOL_DATA_UNAVAILABLE. The table's dates change only with a new package. Oracle Sustaining Support does not count as supported, so MySQL 8.0 reads VERSION_EOL even under mysql=extended.
  • audit_db_privileges is literal. A stock MySQL cluster always reports PRIV_GLOBAL_ALL, and because accounts are counted per user@host it can exceed the superuser limit. A single-node MySQL cluster is refused, because the controller does not list its accounts; PostgreSQL host rules are not assessed for PRIV_WILDCARD_HOST; and privileges held only through a granted role, and dynamic privileges, are not seen.
  • check_ssl_posture reads certificate expiry from ClusterControl's CA, so a certificate it did not issue or import is reported SSL_CERT_EXPIRY_UNKNOWN.
  • export_terraform describes how to recreate a cluster, not how to manage it. The provider cannot import an existing cluster, so terraform plan against an export is never a no-op; applying it against a different controller deploys onto the same hosts, even while they run this cluster; and the Terraform state holds the passwords you supply in plaintext. ClickHouse clusters and load balancers are not exported. No export from this release has been applied against a controller.
  • ClickHouse is not covered by the advisors in this release.
  • Packages are x86_64 only in this release.

Feature Release: Per-session identity, a confirmation step, and job forensics

  • Build:
    • clustercontrol-mcp-1.0.0-108

Where the previous release let the server refuse actions, this one lets it tell two callers apart. Each assistant can now hold its own CMON identity, a destructive call can be made to require a confirmation — or a person — before it runs, and a failed job can be asked what it was doing when it stopped.

⚠ Breaking changes

Three changes alter behaviour a working deployment may depend on. All three are described under Behaviour changes below.

  • On the HTTP transport, session IDs the server did not issue are now refused, and outstanding IDs do not survive a restart. Two instances behind a load balancer need sticky routing.
  • create_job now refuses a command that is not in this build's catalogue.
  • get_node_config and get_controller_config hide more parameters than before.
  • Per-session identity (MCP_IDENTITY_MAP_FILE) — an operator-owned map binds each bearer token to its own CMON username and credential, so the controller's audit log attributes each assistant's actions to a different user. A token that is not in the map resolves to nothing: there is no default entry and no fall back to the shared credentials, so an unmapped token is refused rather than arriving as somebody else. A map that does not parse, or that is ambiguous, is a startup error rather than a server that silently dropped an entry. The map requires the HTTP transport — identity rides on the Authorization header, and selecting a map on stdio is a startup refusal. (CLUS-8102, CLUS-8646)
  • get_session_info now reports whether that identity actually works. It returns one of no_identity_bound, not_attempted, verified or failed, plus identity_last_error carrying the controller's own sentence when it refused. Previously the tool reported the configured name and nothing else, so a map naming a CMON user that does not exist looked healthy until the first call failed. Credentials are still not probed at startup — the verdict is what CMON answered on a call the server has already made. (CLUS-8738)
  • Two-step execution guard (MCP_EXECUTION_GUARD=enforced) — a destructive call is admitted only against a confirmation_token the server minted on a preview of the same operation. The confirmation covers one identity, one session, one tool and one set of arguments; it is single-use, it expires, and a restart revokes every outstanding one. A preview is never refused, and is where the confirmation comes from. (CLUS-8108)
  • Human-approval tier (MCP_HUMAN_APPROVAL=elicitation) — above the guard, and usable on its own. A destructive execution is put to a person over the calling session's elicitation channel, carrying the operation's digest and a single-use code the answer must return. Where an approval cannot be sought at all — no bound identity, or a session that cannot be elicited — the call is refused with HUMAN_APPROVAL_UNAVAILABLE rather than proceeding or hanging. (CLUS-8108)
  • Standard operating procedure prompts — sop_failover_drill and sop_major_upgrade, served as MCP prompts. Each previews before it acts and carries an explicit branch for a failure part-way through. sop_major_upgrade opens with an engine guard (PostgreSQL only) and a backup check; sop_failover_drill opens with a preflight. Both name a human-approval step, which this server enforces only when MCP_HUMAN_APPROVAL is on — it says so at startup when it is not. (CLUS-8109, CLUS-8110)
  • get_job_checkpoint — where one job actually got to: its command, status, whether this build recognises that status at all, and the tail of its log. Fetched by job ID alone, so a job older than one page of list_jobs is still answered for. It never suggests re-submitting a job: CMON jobs are not idempotent, so every hint it gives is a query or a wait. (CLUS-8111)
  • get_incident_bundle can be anchored on a job. Passing job_id adds a failed_job section and widens the jobs window to reach that job — never narrowing it below what you asked for. Only the jobs section is job-scoped, and the other sections say why they cannot be. (CLUS-8112)
  • Client capability probe — the server records what each client declared at connection time and consults it when a control needs to reach a person, so an approval-required call is refused on a session that cannot be elicited rather than waiting for an answer that cannot arrive. (CLUS-8107)
  • The tool registry grows from 72 to 74 tools (45 pure-read, 26 CMON-mutating, 3 local-filesystem-writing; 22 flagged destructive), adding get_session_info and get_job_checkpoint. The 21 resource surfaces are unchanged.
  • Session IDs are validated for issuance. On the HTTP transport the server now admits only the Mcp-Session-Id values it issued, where previously any correctly shaped value was served. Two consequences: outstanding IDs do not survive a restart, so clients get 404 and must re-initialize; and two server instances require sticky routing for a session's full lifetime, because a session established against one is not known to the other. (CLUS-8646)
  • With an identity map configured, /sse and /message are not served. SSE's separate session mechanism sits outside the identity binding, so it is refused rather than left as a way around it. Deployments using SSE should stay on the shared-token configuration until their clients speak the streamable HTTP transport. (CLUS-8102)
  • create_job refuses a command outside this build's catalogue. Previously such a call was accepted and reported a job ID — CMON creates a job titled Unknown Command, runs nothing and fails it — so a caller was told work had started that never would. The catalogue is a snapshot taken from the controller source at build time, so a command added by a newer controller is also refused; the refusal says the command is not in this build's catalogue, and never that it is not a real CMON command. (CLUS-8736)
  • get_node_config and get_controller_config hide more parameters. The rule that decides what counts as a credential is now shared with the approval prompt and matches key and auth as substrings, so operational parameters whose names contain them — key_buffer_size, foreign_key_checks, authentication_policy and similar — are now returned as <hidden>. This over-matching is deliberate: the alternative is a hand-maintained exemption list whose failure mode is disclosure. (CLUS-8740)
  • Task status notifications no longer reach every connected client. A background job's progress was broadcast to all sessions, so one assistant could observe another's activity. Notifications are now scoped per session when an identity map is configured; without a map every caller is the same CMON principal, which is the pre-existing behaviour. (CLUS-8737)
  • get_node_config no longer returns credentials verbatim. wsrep_sst_auth carries sstuser:<password> and matched none of the previous rules, so Galera SST credentials were returned in clear text. The read path is now no wider than the approval prompt, from one shared list. (CLUS-8740)
  • The destructive-operation warning now fires for the commands it names. Four of the nine job commands it escalated on were names CMON does not accept — shutdown, remove_node, drop_cluster and rebuild_replication_slave — so the extra warning could never appear for stopping or removing a cluster. The same fabricated names appeared in the operator-facing scope warning. Both are corrected against the controller's own command list. (CLUS-8736, CLUS-8741)
  • A tool declaring that it writes to the filesystem can no longer bypass the export directory. The build-time check enforced only that a tool declaring no local writes made none; a tool declaring the opposite could call the filesystem directly. Both directions are now enforced. (CLUS-8707)
  • A non-credential refusal is recorded as a credential failure. identity_verification reads failed whenever the controller answers AccessDenied, and CMON answers AccessDenied both for a wrong password and for a licensing refusal or a controller whose database is unavailable. An identity whose credentials are correct can therefore read failed while the controller is starting up. (CLUS-8752)
  • Scoping removes a tool, not a data class. Several tools read the same data, so denying one can leave a sibling serving the same rows. The server names each case at startup as tool scope: WARNING: ….
  • create_job still reaches job commands no tool exposes. It is now bounded — the command must be in the committed catalogue — but that catalogue is wider than the tool surface and includes destructive commands such as remove_cluster, removenode and failover. The startup warning about this is narrower than the exposure: it appears only when a tool scope is configured, read-only mode is off, create_job is in scope, and that scope denies a job-backed tool. A deployment with no scope configured has the exposure in full and gets no warning.
  • Symlink protection covers the final path component only. For the digest key and the audit log, a hard link at the path itself, or a symlink at any parent directory, is followed normally. Whoever can write a directory on those paths controls what is at the end of them.
  • Packages are x86_64 only in this release.

Feature Release: Access controls, audit logging and offline docs

  • Build:
    • clustercontrol-mcp-1.0.0-90

This release adds the controls needed to connect an AI assistant to a production controller: the server can now refuse actions itself, rather than relying on the client to respect an advisory hint.

  • Global read-only mode (MCP_READ_ONLY) — refuses all 29 state-changing tools for the lifetime of the process. Enforced on two independent layers: the write tools are hidden from the client's tool list, and a direct call on one is refused, so a client that calls a tool it was never shown is still refused. (CLUS-8099)
  • Per-tool and per-resource scoping (MCP_TOOL_ALLOW, MCP_TOOL_DENY) — expose only the tools you name. Deny wins over allow, the two compose with read-only mode, and resource surfaces follow their tools automatically. An entry matching no tool is a startup error rather than a silent no-op. (CLUS-8100)
  • Audit log (MCP_AUDIT_LOG) — every tool call and resource read recorded twice as JSONL, an intent record before the action and a completion after, with a third acceptance record on the background-task path. Raw arguments are never written; what is recorded is a keyed digest, so a reader of the log cannot work backwards to a password. Auditing is fail-closed: if the intent record cannot be written, the action does not run. (CLUS-8103)
  • Export directory containment (MCP_EXPORT_DIR) — the three tools that write to the MCP Server's own filesystem are confined to one directory, whatever path the client asks for. (CLUS-8581)
  • search_docs — ranked excerpts from a ClusterControl documentation bundle shipped inside the package. No network calls, so it works in an air-gapped installation. (CLUS-8104, CLUS-8105)
  • get_incident_bundle — one read-only call returning alarms, recent jobs, collected log tails, a metric summary with outlier flags, and topology. Every section is always present and explicitly marked [error], [empty] or [truncated], so a section missing because a source failed cannot be mistaken for one that was empty. (CLUS-8106)
  • The tool registry grows from 70 to 72 tools (43 pure-read, 26 CMON-mutating, 3 local-filesystem-writing; 22 flagged destructive), with 21 resource surfaces unchanged.
  • Local file writes are confined by default. A server whose configuration is untouched will begin refusing destination paths outside /var/lib/cmon-mcp/exports — including paths such as /tmp that previously worked. Set MCP_EXPORT_DIR to the directory your callers already use to keep their existing paths valid. (CLUS-8581)
  • A fresh install is audited by default. First installs provision a digest key and switch auditing on. Upgrades enable nothing, because a host that has never been audited has no key and a server told to audit without one refuses to start. Set MCP_AUDIT_LOG=off in /etc/default/cmon-mcp to opt out. (CLUS-8583)
  • The configuration file is /etc/default/cmon-mcp. It follows the service and binary names; the package is still clustercontrol-mcp. On RPM hosts an earlier build could leave settings behind in a .rpmsave file during this rename — the installer now reports exactly what it found and where. (CLUS-8601, CLUS-8648)
  • A flag-parse error no longer reprints the CMON password and MCP bearer token to the journal. (CLUS-8587)
  • /etc/default/cmon-mcp, which holds CMON_PASSWORD and MCP_AUTH_TOKEN, now ships mode 0600 rather than world-readable, and the installer tightens every copy of it the package manager leaves behind. (CLUS-8594)
  • export_cluster_log is no longer an arbitrary-file write as root; every client-supplied path is confined to the export directory and lands through a staged rename, so a symlink at the destination is never written through. (CLUS-8581)
  • get_controller_config no longer returns live Vault and OpenBao tokens in clear text. (CLUS-8650)
  • Credentials and key material are held where Go's formatting verbs cannot reach them, so no log line or error message can print them by accident. (CLUS-8595)
  • The digest key file is no longer symlink-swappable, and governed mode refuses to write its marker through a dangling symlink. (CLUS-8576, CLUS-8647)
  • Every session runs as one CMON identity. The server authenticates as the single configured CMON_USERNAME, and the audit log attributes every action to it, so two assistants cannot be told apart from the record. Per-session identity is planned for a later release.
  • Scoping removes a tool, not a data class. Several tools read the same data, so denying one can leave a sibling serving the same rows. The server names each case at startup as tool scope: WARNING: ….
  • create_job reaches the whole job surface. Its free-form command argument reaches CMON job commands that have no tool of their own and that no deny list can name.
  • Packages are x86_64 only in this release.

Maintenance Release: September 1st, 2026

  • Build:
    • clustercontrol-mcp-1.0.0-60

⚠ Breaking change

get_cluster_log no longer accepts output_path and no longer writes files — it is now a pure read that returns log content (the tail of the last 200 lines by default). Callers that used output_path to save a log file locally must switch to the new export_cluster_log tool, which requires output_path and writes the file with mode 0600. (CLUS-8098)

  • export_cluster_log — exports a collected cluster log file in full to a local output_path on the MCP Server host. The file is written with mode 0600, including when overwriting an existing file that had looser permissions. (CLUS-8098)
  • Tool annotations — all 70 tools now serve the standard MCP readOnlyHint and destructiveHint annotations. The annotations are generated at tool registration from a committed, human-reviewed manifest, so the served hints cannot drift from the manifest. Note that annotations are advisory metadata for MCP clients — clients can use them to auto-approve reads and require confirmation before destructive calls — they are not server-side enforcement, and dry_run previews remain a client-side safeguard. (CLUS-8098)
  • The tool registry grows from 69 to 70 tools (41 pure-read, 26 CMON-mutating, 3 local-filesystem-writing; 22 flagged destructive), with 21 resource surfaces (3 static + 18 templates). (CLUS-8098)