Building a DBA Agent for Your SQL Server Estate with MCP

Page content

Yesterday I gave a talk for MSSQLTips called “Building a DBA Agent for Your SQL Server Estate”. Which you can watch here. What I want to do here is pull the whole thing together in writing: the demos, what I actually ran against a live SQL Server estate, and the argument I’m making when I say an AI agent can be trusted near production.

If you’ve been following along, this is the third post in what’s turned into a series. In Giving AI Agents Visibility Into SQL Server with MCP, I built sql-mcp-server, a custom MCP server that gives an agent real DMV access. I said at the end of that post that skill files were coming in a follow-up also in Using Claude Code as a Database SRE Agent, I showed a skill file catching a silently unprotected DR instance against the Everpure Fusion fleet API and how to build compliance and observability reports quickly. So here we are.

Here’s the repo from the session, dba-agent-talk-kit, that stands up the whole thing with one docker compose command, well really a script but you get the idea. Let’s dig in.

Writing T-SQL Is the Easy Part.

A coding assistant/LLM can certainly write T-SQL. Ask it for a query, and it’ll hand you one. Sometimes that query is correct; sometimes it has a syntax error or uses the wrong column name, and the agent has to retry, burning up your tokens. It might be able answer Which sessions are blocked right now?, Where are the wait stats pointing?, or Which indexes is the optimizer asking for? today and it might stuggle answering those questions tomorrow. That’s the nature of LLMs and their non-determinism. To answer those questions, agents would also require a live connection into a running instance, and the standard tool-calling interface for wiring that connection up is MCP (Model Context Protocol). And this is literally my favorite thing about MCP. It gives structure and determinisim to the unstructured and non-deterministic system that is an LLM/Agent. Without that structure, an agent is just guessing. And add in the right guardrails around it, it becomes something closer to a junior DBA who never gets tired of running the same checklists written by a senior.

An Agent Isn’t a Chatbot With a Clever Prompt

Here’s the architecture of the system. An agent is a model in a loop with tools. It decides which tool to call and in what order, from your question alone. You never name a DMV. It doesn’t try to write the code. It picks the tool from the tool’s description, calls it, reads the result, and decides what to call next. The tool has the implementation of the DMV query that YOU control.

        ┌─────────────────────────────────────────────────────────┐
        │   You: "Are there any blocking sessions right now?"     │
        └────────────────────────────┬────────────────────────────┘
                                     ▼
                         ┌───────────────────────┐
                         │   Language Model      │
                         │   (decides WHAT to    │
                         │    call, not HOW)     │
                         └───────────┬───────────┘
                                     │ tool call
                                     ▼
                         ┌───────────────────────┐
                         │   MCP Client          │◄──── mcp.json names
                         │   (Copilot Chat)      │        the servers
                         └───────────┬───────────┘
                                     │ HTTP
                                     ▼
                         ┌───────────────────────┐
                         │   MCP Server          │
                         │   (sql-mcp-server)    │
                         │   runs the real T-SQL │
                         └───────────┬───────────┘
                                     │ TDS
                                     ▼
                         ┌───────────────────────┐
                         │   SQL Server          │
                         │   (the DMVs)          │
                         └───────────────────────┘

The model never touches the database. It calls the tool. The tool server runs the query, against a SQL login that only has VIEW SERVER STATE and read access, never write. Above is the whole control story in one diagram: the LLM picks what to call, and you, in the tool’s implementation, control how it’s actually done.

That’s why MCP is different from giving an agent a connection string and a system prompt telling it to behave. A prompt is a request. A tool that doesn’t exist can’t be called, and a tool whose query is SELECT-only can’t do anything else no matter how the model is feeling that day. The agent doesn’t have a direct connection to the database server. The sql-dba MCP server has only read access.

Two MCP Servers, Two Diffferent Jobs

The demo repo runs two MCP servers side by side, on purpose, so the I could show both patterns application access and DBA access:

  • products-db (Data API Builder) — in our demo, its scoped to four tables in ProductsDB, with CRUD access to a user database. I tried DAB first for everything, it wasn’t built to be pointed at system DMVs, since those tables use data types and query shapes DAB doesn’t support, so I built a separate server, sql-mcp-server.
  • sql-dba (custom, sql-mcp-server) — 34 tools, one per DMV-driven diagnostic, SELECT-only across the whole estate, enforced in safety.ts, not by a prompt.
┌─────────────────────────────────────────────────────────────────────┐
│                       Docker Compose network                        │
│  ┌────────────────┐        CRUD         ┌───────────────────────┐   │
│  │  sqlserver1    │◄────────────────────│  products-db (DAB)    │   │
│  │  ProductsDB    │                     │  :5001: 4 tables      │   │
│  │  :1433         │    SELECT only      ├───────────────────────┤   │
│  │                │◄────────────────────│  sql-dba (custom)     │   │
│  └────────────────┘                     │  :3001: 34 DMV tools  │   │
│          ▲                              │  safety.ts allowlist  │   │
│  ┌─────────────────┐  SELECT only       └───────────────────────┘   │
│  │  sqlserver2,3,4 │────────────────────────────▲                   │
│  │  (secondary)    │      multi-instance fan-out                    │
│  └─────────────────┘                                                │
└─────────────────────────────────────────────────────────────────────┘

Neither server knows the other exists. The agent is the only thing that spans both, and it still can’t do anything either server disallows.

Agents Without Skills are Just Tools

Let’s look at a tool in action. I seeded a real blocking chain (demos/sql/seed-blocking.sh, an open transaction with WAITFOR DELAY holding an exclusive lock, plus a victim SELECT queued behind it) and asked:

Are there any blocking sessions right now on SqlServer1? Who is blocking whom, how long has the block been in place, and what SQL is running on both sides? What should I do about it?

I never told it to call get_blocking_chains. It picked that tool from the description alone. Below is that description, straight from tools.ts, the same text the model sees when it’s deciding what to call:

Show all current blocking chains — which sessions are blocked and which session is causing the blockage. Includes the SQL text of both the blocked and blocking session, wait time, and lock details. Returns a message if there is no blocking.

That’s the entire interface. No attempts to dynamically build a query calling sys.dm_exec_requests or sys.dm_os_waiting_tasks. Just a plain text description about what the tool does, and the model matched that against my question to call it correctly on the first try. That’s what LLMs are good at, finding similar things. And this is the tool most similar to the prompt I passed in.

Here’s the actual response from the live MCP server. The response is JSON from the MCP server. If you’re doing this in a desktop client like VS Code, it will be presented in a more human-readable way.

{
  "blocking_chains": [
    {
      "blocked_session_id": 65,
      "blocking_session_id": 62,
      "wait_type": "LCK_M_S",
      "wait_seconds": 9.023,
      "database_name": "ProductsDB",
      "blocked_login": "sa",
      "blocked_program": "SQLCMD",
      "blocked_statement": "SELECT [ProductID],[ProductName],[UnitPrice] FROM [dbo].[Products] WHERE [Category]=@1 ORDER BY [ProductID] ASC",
      "blocker_login": "sa",
      "blocker_program": "SQLCMD",
      "blocker_sql_text": "BEGIN TRANSACTION;\nUPDATE dbo.Products SET UnitPrice = UnitPrice * 1.01 WHERE Category = 'Electronics';\nWAITFOR DELAY '00:08:00';\nROLLBACK;\n",
      "blocker_last_request_start": "2026-09-04T11:45:32.303Z"
    },
    {
      "blocked_session_id": 67,
      "blocking_session_id": 62,
      "wait_type": "LCK_M_S",
      "wait_seconds": 4.345,
      "database_name": "ProductsDB",
      "blocked_login": "dab_app",
      "blocked_program": "dab_oss_2.0.1",
      "blocked_statement": "SELECT TOP 101 [dbo_Products].[ProductID] ... FROM [dbo].[Products] AS [dbo_Products] WHERE 1 = 1 ORDER BY [dbo_Products].[ProductID] ASC FOR JSON PATH, INCLUDE_NULL_VALUES",
      "blocker_login": "sa",
      "blocker_program": "SQLCMD",
      "blocker_sql_text": "BEGIN TRANSACTION;\nUPDATE dbo.Products SET UnitPrice = UnitPrice * 1.01 WHERE Category = 'Electronics';\nWAITFOR DELAY '00:08:00';\nROLLBACK;\n",
      "blocker_last_request_start": "2026-09-04T11:45:32.303Z"
    }
  ]
}

Notice the second entry: dab_app, the DAB server’s own login, queued behind the exact same blocker as the plain SELECT. Two separate MCP servers, two different logins, both stuck behind one open transaction on sqlserver1, and one tool call surfaced both of them at once.

Where the query came from: the head-blocker SQL text in that result comes from dm_exec_connections.most_recent_sql_handle, a pattern lifted straight from Brent Ozar’s First Responder Kit (sp_Blitz.sql), which is MIT-licensed. That’s true of several tools in sql-mcp-server. I didn’t want to reinvent DBA diagnostics for this project. I wanted community-vetted queries, the same ones thousands of DBAs already run manually, into tools an agent can call on its own. If you pick this project up for your environment, sub you own queries and maybe even extend it to make additional tools for your use case.

The agent recommended a KILL <spid> as a script. It didn’t, and couldn’t, run it. Maybe killing that spid is the right thing to do, maybe it’s not. But it’s not the agent’s call. It’s my call. sql-dba’s SQL login has no write permissions, and safety.ts blocks anything that isn’t SELECT, WITH, or DECLARE before the query ever reaches SQL Server.

Agents with Skills: The Runbook Your DBA Already Has in Their Head

Tools alone get you access to the data needed. A skill file is what turns that data into something meaningful to the DBA, and it’s the part of this I think matters most for how a DBA team actually operates day to day.

A skill file is plain markdown, no code, six parts in order:

  1. Persona — who the agent is, who it serves, one paragraph.
  2. Trigger conditions — when this skill applies, so the agent self-selects.
  3. Procedure — the exact tool sequence, as numbered steps.
  4. Thresholds — numbers, not adjectives. “Healthy” is a value.
  5. Decision rules — if/then, so the output is consistent run to run.
  6. Hard boundaries — what the agent must never do, and when to hand off.

Here’s the example skill from the repo, .github/instructions/availability.instructions.md:

---
applyTo: "**"
description: "Availability management SOP: Always On AG health checks, sync state evaluation, failover readiness for the SQL Server estate"
---

applyTo scopes which files the skill attaches to, and description is what Copilot reads to select the right skill for your prompt without you attaching anything by hand. And the thresholds table underneath it:

Metric Healthy Warning Critical
synchronization_health HEALTHY PARTIALLY_HEALTHY NOT_HEALTHY
send_queue_mb (sync replica) < 1 1-10 > 10 or growing 3 checks in a row
redo_queue_mb < 10 10-100 > 100
VLF count per database < 300 300-1,000 > 1,000
Estimated data loss (async) < 15 s 15-60 s > 60 s

You define what good looks like. The agent doesn’t guess what “behind” means. You tell it once, in a file, and every future run bumps the data up against the same number.

Why this matters for a DBA team specifically: this is how a senior DBA’s experience can quickly be transferred to a junior, an on-call rotation, or the agent itself can execute identically at 2AM. Today that knowledge lives in a senior’s head, or scattered across a wiki nobody updates. A skill file is that same knowledge, written down once, version-controlled next to the code, and applied the same way every single time it’s invoked. It’s a runbook that actually gets followed, because the thing following it doesn’t get tired, skip steps, or forget the threshold or what good looks like for YOUR workload.

Agents Helping With Availability

Let’s check out a skill in action. Check out availability.instructions.md: from our repo. This defines the operations and thresholds for our Availability Group posture. To highlight how this works, I seeded AG lag (demos/sql/seed-ag-lag.sh suspends the secondary and churns the log on the primary) and asked the agent which now knows about the skill:

Is my availability group healthy? If I had to fail over to the secondary right now, could I do it without losing data? Give me the full picture.

And here’s what it gave back to me…it runs the get_ag_health tool, which again is a structure query defined in the MCP Server. Not dynamically generated T-SQL by the LLM.

{
  "ag_health": [
    { "replica_server_name": "sqlserver1", "role_desc": "PRIMARY",   "connected_state_desc": "CONNECTED",    "synchronization_health_desc": "HEALTHY",     "synchronization_state_desc": "SYNCHRONIZED" },
    { "replica_server_name": "sqlserver2", "role_desc": "SECONDARY", "connected_state_desc": "CONNECTED",    "synchronization_health_desc": "NOT_HEALTHY", "synchronization_state_desc": "NOT SYNCHRONIZING", "is_suspended": true, "suspend_reason_desc": "SUSPEND_FROM_USER" }
  ]
}

The skill checks the primary first, not the secondary, because I wrote that order into the procedure. is_suspended: true with suspend_reason_desc: "SUSPEND_FROM_USER" maps straight onto the skill’s decision rule for exactly this state: SUSPENDED data movement → report as CRITICAL… check get_database_files for free-space/growth settings before recommending resume. It answered the failover question directly: no, not safely, with ALTER DATABASE [ProductsDB] SET HADR RESUME drafted for a human to run. It didn’t run it.

After resuming data movement (demos/sql/resume-ag.sh) and re-asking a few seconds later, mid-catch-up you can see a little bit of queueing on the primary and secondary:

{
  "replica_server_name": "sqlserver2", "role_desc": "SECONDARY",
  "synchronization_health_desc": "PARTIALLY_HEALTHY", "synchronization_state_desc": "SYNCHRONIZING",
  "send_queue_mb": "2315", "send_rate_mb_per_sec": "153",
  "redo_queue_mb": "1444", "redo_rate_mb_per_sec": "21",
  "estimated_recovery_seconds": 67
}

After the churn drained, both queues reached zero. This demo uses synchronous commit, and we’re SYNCHRONIZED, so acknowledged transactions are hardened on the secondary:

{
  "replica_server_name": "sqlserver2", "role_desc": "SECONDARY",
  "synchronization_health_desc": "HEALTHY", "synchronization_state_desc": "SYNCHRONIZED",
  "send_queue_mb": "0", "redo_queue_mb": "0"
}

With both queues at zero, the secondary has caught up with the primary. In synchronous commit, SYNCHRONIZED indicates that acknowledged log records are hardened on the secondary. A planned failover still needs the normal readiness checks, and a forced failover can still lose data depending on the commit mode and replica state.

Nobody Cares About Backups, They Care About Data Loss

get_backup_status on its own returns timestamps. It doesn’t know your RPO policy. But let’s look at how we can bridge timestamps to something meaningful to the business. I seeded three backup violations and one clean control (demos/sql/seed-backup-gaps.sh) and asked, with the backup-recovery skill attached:

What’s the backup situation across this instance? Treat PaymentsDB, ClaimsDB, and OrdersProdDB as Tier-1. If we lost the server right now, what would we actually lose, database by database?

{
  "backup_status": [
    { "database_name": "ClaimsDB",     "recovery_model_desc": "FULL",   "last_full_backup": null,                    "last_log_backup": null, "backup_health": "NEVER_BACKED_UP" },
    { "database_name": "PaymentsDB",   "recovery_model_desc": "FULL",   "last_full_backup": "2026-09-04T11:46:25Z",  "last_log_backup": null, "backup_health": "NO_LOG_BACKUPS" },
    { "database_name": "OrdersProdDB", "recovery_model_desc": "SIMPLE", "last_full_backup": "2026-09-04T11:46:26Z",  "last_log_backup": null, "backup_health": "OK" },
    { "database_name": "InventoryDB",  "recovery_model_desc": "FULL",   "last_full_backup": "2026-09-04T11:46:26Z",  "last_log_backup": "2026-09-04T11:46:26Z", "backup_health": "OK" }
  ]
}

Looking at the JSON output returned from the get_backup_status tool, it says that OrdersProdDB is OK because it has a full backup. The tool has no idea that the database is Tier 1 and running in SIMPLE recovery, which means no point-in-time recovery no matter how recent that full backup is. That violates the backup policy defined in our skill file, backup-recovery.instructions.md, which says every Tier 1 database must use the FULL recovery model and have full and log backups.

That’s exactly the gap the skill closes: get_database_info adds the recovery model, and the skill cross-references both against the policy. From the JSON output above, when the LLM/Agent combines that with the policy defined it the skill file it produces the table below that shows the Agent’s interpreted results compared with the backup policy. The data loss estimates use the elapsed time since the last applicable backup at the time of the demo:

Database Tier RPO Policy Data Loss NOW Status
ClaimsDB 1 15 min ALL (unrecoverable) CRITICAL
PaymentsDB 1 15 min ~1 hour CRITICAL
OrdersProdDB 1 15 min ~1 hour (no PIT possible) CRITICAL
InventoryDB — — ~1 min OK

The skill changes what the answer means: minutes of data loss against a policy, which is the number the business actually understands, instead of a raw timestamp they’d have to do math on themselves.

Security and Auditing: Accelerating Those TPS Reports

This is the skill where the hard boundary matters most. I seeded some badness into our environment with demos/sql/seed-security-issues.sh: config drift, a rogue login added straight to sysadmin, an orphaned database user, and a failed-login spray. Then I asked:

Run a security review of SqlServer1. Check configuration drift, privileged role membership, failed logins in the last 24 hours, and orphaned database users. Tell me what’s wrong and how bad it is.

Four tools get called based on the prompt’s request and the Procedure defined in security-audit.instructions.md:

// get_failed_logins (SEC-004)
{
  "failed_logins": [
    { "login_name": "sa",          "attempts": 5, "first_seen": "2026-09-04T11:45:56Z" },
    { "login_name": "admin_probe", "attempts": 3, "first_seen": "2026-09-04T11:45:57Z" }
  ]
}

// get_sysadmin_members (SEC-002)
{
  "privileged_members": [
    { "member_name": "sa",             "login_type": "SQL_LOGIN", "sa_enabled_flag": 1, "recently_modified_flag": 1 },
    { "member_name": "svc_reporting",  "login_type": "SQL_LOGIN", "create_date": "2026-09-04T17:42:41Z", "recently_modified_flag": 1 }
  ]
}

// get_security_config_drift (SEC-001)
{
  "config_drift": [
    { "name": "Ad Hoc Distributed Queries", "current_value": 1, "required_value": 0, "compliant": 0 },
    { "name": "clr enabled",                "current_value": 1, "required_value": 0, "compliant": 0 },
    { "name": "remote access",              "current_value": 1, "required_value": 0, "compliant": 0 }
  ]
}

// get_orphaned_users (SEC-003)
{
  "orphaned_users": [
    { "database_name": "ProductsDB", "user_name": "temp_migration_login", "create_date": "2026-09-04T17:42:42Z" }
  ]
}

The skill’s decision rules rank findings: active-attack indicators first, then privileged-access violations, then surface-area drift, then hygiene. That’s why the failed-login spray against admin_probe, a login that doesn’t exist, a recon pattern, leads the report, ahead of three configuration options that have quietly drifted since the box was built. That order is written down, not improvised.

The skill’s persona line is explicit: You detect and report; you never remediate. No DROP LOGIN. No sp_configure ... 0. Findings come back as facts plus baseline deltas, login X was added to sysadmin on <date>, never accusations. That skill file is also where I found the sentence I’d put in front of any security team on its own: findings are evidence for an audit trail, and remediation goes through change control.

Observability: The Same Process, Every Time for Incident Response

The value here isn’t a clever diagnosis, it’s consistency. get_wait_stats alone gives you data. The observability skill runs the same six-tool sequence, in the same order, whether the server is healthy or you’re in a sev 1, and produces one incident report:

Pull a full health snapshot of SqlServer1: server info, databases, wait stats, top queries by CPU, any blocking, and missing indexes. Write it up as an incident report.

// get_server_info
{
  "server_name": "sqlserver1",
  "sql_version": "SQL Server 2025 (RTM-CU8) (17.0.4075.5)",
  "edition": "Enterprise Developer Edition (64-bit)",
  "last_restart": "2026-09-04T13:20:10Z",
  "uptime_hours": 0.5,
  "configuration": {
    "max_degree_of_parallelism": 0,
    "cost_threshold_for_parallelism": 5,
    "max_server_memory_mb": 2147483647,
    "ad_hoc_distributed_queries": 1
  }
}

// get_database_info
[
  { "database_name": "ProductsDB", "recovery_model": "FULL", "state": "ONLINE", "log_reuse_wait": "LOG_BACKUP", "size_mb": 2960.0 },
  { "database_name": "msdb", "recovery_model": "SIMPLE", "state": "ONLINE", "log_reuse_wait": "OLDEST_PAGE", "size_mb": 23.7 }
]

// get_wait_stats (top 3, cumulative since the 2026-09-04T13:20:10Z restart)
[
  { "wait_type": "LCK_M_S", "wait_time_ms": 15342, "signal_wait_time_ms": 1203, "wait_pct": 67.2 },
  { "wait_type": "PAGEIOLATCH_SH", "wait_time_ms": 4120, "signal_wait_time_ms": 1891, "wait_pct": 18.1 },
  { "wait_type": "SOS_SCHEDULER_YIELD", "wait_time_ms": 2504, "signal_wait_time_ms": 1634, "wait_pct": 11.0 }
]

// get_top_queries (by CPU)
[
  {
    "execution_count": 91,
    "total_cpu_ms": 489,
    "total_elapsed_ms": 515,
    "database_name": null,
    "query_text": "SELECT TOP 101 [dbo_Products].[ProductID] AS [ProductID], [dbo_Products].[ProductName] AS [ProductName], [dbo_Products].[Category] ..."
  }
]

// get_blocking_chains
{
  "blocking_chains": []
}

// get_missing_indexes (after ~20k skewed rows and a run of category-filter queries)
{
  "missing_indexes": [
    {
      "database_name": "ProductsDB",
      "table_name": "Products",
      "equality_columns": "[Category], [Discontinued]",
      "included_columns": "[UnitPrice]",
      "user_seeks": "30",
      "avg_user_impact_pct": 93.17,
      "impact_score": 27.95,
      "suggested_create_index": "CREATE INDEX [IX_Products_missing_4] ON [ProductsDB].[dbo].[Products] ([Category], [Discontinued]) INCLUDE ([UnitPrice])"
    }
  ]
}

The skill reports ad_hoc_distributed_queries: 1 as configuration drift against the baseline. LOG_BACKUP is different: it says the log currently needs a log backup before SQL Server can reuse that space. It isn’t a failure by itself, but it gives the DBA a concrete follow-up to check against the backup policy.

One note from this run: get_wait_stats is cumulative since SQL Server last restarted (or the waits were cleared), not since your last question. If you want historical performance data, you’ll need to store that somewhere and re-load it for comparisions if/when needed. I’m still working on how I want to handle this architecturally in an agentic system, but I imagine having it land in time series database like Prometheus or InfluxDB will be the answer there.

Guardrails: Layers of Protection

This system is nothing if it depends on the model behaving. Here’s the stack, bottom to top, and every layer is enforced in code or in a skill file you can read, not in a prompt you hope gets followed:

Layer Guardrail Enforced by
6 Audit trail every tool call logged server-side
5 Human approval the KILL, the AG resume, every fix; drafted, never run
4 Skill judgment never force failover; detect, never remediate
3 Scoped access dab-config.json: four tables, explicit verbs
2 Query allowlist safety.ts: SELECT / WITH / DECLARE only
1 Least privilege dba_monitor: VIEW SERVER STATE, no writes

The fan_out_query tool runs the same T-SQL across multiple SQL Server instances in parallel. This is a pretty powerful feature of the MCP Server. Pretty straight forward if I tell the model exactly what query to run across my SQL Servers, but it could also try to build and run a query it self based off a prompt or while in an agentic loop. So we have to put a control mechanisim on this. An that’s what we’re doing here in Layer 2 with the Query Allow List, controlling which T-SQL commands can be run using this tool.

In sql-mcp-server/src/safety.ts you’ll find the BLOCKED_PATTERNS list blocking certain T-SQL commands:

const ALLOWED_START = /^\s*(SELECT|WITH|DECLARE)\b/i;

const BLOCKED_PATTERNS = [
  /\bINSERT\s+INTO\b/i,
  /\bUPDATE\s+\w/i,
  /\bDELETE\s+FROM\b/i,
  /\bDROP\s+(TABLE|DATABASE|INDEX|VIEW|PROCEDURE|FUNCTION|TRIGGER|LOGIN|USER)\b/i,
  /\bXP_CMDSHELL\b/i,
  /;\s*(EXEC|EXECUTE)\b/i,    // multi-statement to bypass ALLOWED_START check
  // ...
];

Why You Should Try This

Pulling it together, here’s what I think the actual value is, beyond the demo:

  1. Acceleration to a verdict — Going from is my AG healthy to a ranked, thresholds-based CRITICAL/HEALTHY answer with a drafted fix took one prompt instead of the three-to-five manual DMV queries and the math I’d normally do by hand.
  2. Institutional knowledge as an artifact, not a memory — A skill file is a senior DBA’s judgment, written down once, so a junior, an on-call engineer, or the agent itself runs the exact same procedure at the exact same thresholds, every time, instead of ask whoever’s been here the longest.
  3. Consistency you can compare across days and servers — The observability skill’s six-tool sequence doesn’t change with the model’s mood, doesn’t skip a step under pressure, and produces a report shape you can diff week over week.
  4. Community-vetted diagnostics, not reinvented ones — The DMV logic underneath these tools is Brent Ozar’s First Responder Kit, MIT-licensed and already trusted by a huge share of the SQL Server community. I wired it into tools an agent can call, I didn’t rewrite it from scratch.
  5. Control that survives a model that misbehaves — The six guardrail layers hold even if the model tries something dumb. That’s the difference between enforced in code and asked nicely in a prompt, and it’s the only argument that actually lands with a security team.

Getting Started Using This Stack

Here’s the whole thing, start to finish:

git clone https://github.com/nocentino/dba-agent-talk-kit.git
cd dba-agent-talk-kit
cp .env.example .env   # edit SA_PASSWORD at minimum
./compose/startup.sh

That’s four SQL Servers, One AG, a Data API Builder, and the DBA MCP server, all configured. Then:

  1. Wire mcp.json to point Copilot (or Claude, or Cursor, anything that takes MCP context) at :3001 and :5001.
  2. Copy the six *.instructions.md files into .github/instructions/.
  3. Write your own: persona → triggers → procedure → thresholds → decision rules → hard boundaries.

The architecture is a handful of markdown files and an HTTP endpoint. That’s the whole thing.

Wrapping Up

Tools give an agent visibility. Skills turn that visibility into your standard of practice, written down once and followed exactly, every time. Guardrails make sure none of it depends on the model’s mood. That’s the whole pitch, and it’s the same pitch whether the room is a room full of DBAs or a room full of security reviewers, because it’s true for both audiences at once.

Clone dba-agent-talk-kit, point it at your own estate, write your own skill files, and let me know how it works for you. And if you want just the MCP server without the demo scaffolding, it’s sql-mcp-server on its own.

MCP + SQL Server series:

  1. Giving AI Agents Visibility Into SQL Server with MCP
  2. Using Claude Code as a Database SRE Agent with the Everpure Fusion MCP Server
  3. Building a DBA Agent for Your SQL Server Estate with MCP (This Post)