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”. I’ll drop the recording into this post once it’s published. What I want to do here is pull the whole thing together in writing: the demo kit, 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.
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 reports quickly. This post is that promised follow-up for SQL Server itself.
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. Visibility Is the Hard Part.
A coding assistant writes T-SQL out of the box. 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. Without help, it can’t answer Which sessions are blocked right now?, Where are the wait stats pointing?, or * Which indexes is the optimizer asking for? ** Those questions 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 to this unstructured system that is an LLM/Agent. Without that structure, an agent is just guessing. With it, and with the right guardrails around it, it becomes something closer to a junior DBA who never gets tired of running the same checklist 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. This 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 the reason is why MCP is a different than just 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/model does not have direct connections to the database server. The MCP server has only read access.
Two MCP Servers, Two Trust Boundaries
The demo repo runs two MCP servers side by side, on purpose, so the talk could show both patterns:
products-db(Data API Builder) — scoped to four tables inProductsDB, CRUD on application data. I tried DAB first for everything, and it’s the fastest path when you’re exposing a user database: point it at a table, get REST/GraphQL/MCP for free. 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 seperate server the sql-mcp-server,.sql-dba(custom,sql-mcp-server) — 34 tools, one per DMV-driven diagnostic,SELECT-only across the whole estate, enforced insafety.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.
Tools Without a Skill: The Agent Chains DMVs Itself
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. Here’s 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 mention of sys.dm_exec_requests or sys.dm_os_waiting_tasks, no hint at the T-SQL underneath. Just plain text about what the tool does, and the model matched that against my question well enough 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 in json from the MCP Server. If you’re doing this in a desktop client like VS Code it will present back to you 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.
The agent recommended a KILL <spid>, as a script. It didn’t, and couldn’t, run it. Maybe that’s the right thing to do, maybe its not. But it’s not the agents call. Its 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.
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 a 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:
- Persona — who the agent is, who it serves, one paragraph.
- Trigger conditions — when this skill applies, so the agent self-selects.
- Procedure — the exact tool sequence, as numbered steps.
- Thresholds — numbers, not adjectives. “Healthy” is a value.
- Decision rules — if/then, so the output is consistent run to run.
- 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 2 a.m. 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 likes like for YOUR workload.
With/Without: Availability, Live
Let’s check out a skill in action. Check out availability.instructions.md: from our repo. This defines the SOPs 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 queing 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
}
And here this are all almost back to normal, we’re SYNCHRONIZED so there’s no risk for data loss now:
{
"replica_server_name": "sqlserver2", "role_desc": "SECONDARY",
"synchronization_health_desc": "HEALTHY", "synchronization_state_desc": "SYNCHRONIZED",
"send_queue_mb": "0", "redo_queue_mb": "2873", "redo_rate_mb_per_sec": "43"
}
Same tool, same thresholds, same order. Healthy this time because the numbers say so, not because the model’s in a better mood. Redo is still draining behind a burst of churn I’d deliberately generated for the demo, but SYNCHRONIZED with a zero send queue already answers the actual question, “can I fail over without losing data.” The primary has confirmation the log is hardened on the secondary. Redo just hasn’t finished replaying it into the data pages yet. It will catch up shortly, but we’re in a happier place now.
With/Without: Backup and Recovery
Our MCP tool get_backup_status on its own returns timestamps. It doesn’t know your RPO policy. 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 from the get_backup_status tool it says that OrdersProdDB OK, because it’s had a full backup. The tool has no idea that database is Tier-1 and running in SIMPLE recovery, which means no point-in-time recovery no matter how recent that full backup is. And this violates our backup policy defined in our skill file backup-recovery.instructions.md where say say every database that’s Tier 1 has to be in FULL recovery model, have full and log backups.
That’s exactly the gap the skill closes: get_database_info adds the recovery model, the skill cross-references both against the policy define and this is the output from the Agent when it bumps the output of the MCP tool up against our Skill file’s backup policy:
| 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 |
get_backup_status didn’t change between the with-skill and without-skill runs. The skill changed 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: Detect, Never Remediate
This is the skill where the hard boundary matters most. I seeded four real findings, config drift, a rogue login added straight to sysadmin, an orphaned database user, and a failed-login spray, then 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-03T17: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-03T17: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, full stop.
Observability: The Same Process, Every Time
The value here isn’t a clever diagnosis, it’s consistency. get_wait_stats alone gives you a number. The observability skill runs the same seven-step sequence, in the same order, whether the server is healthy or on fire, and produces one incident report with a single severity line:
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 (16.0.4003.1)",
"edition": "Developer Edition",
"last_restart": "2026-09-04T08:22:14Z",
"uptime_hours": 5.2
}
// get_database_info
[
{ "database_name": "ProductsDB", "recovery_model": "FULL", "state": "ONLINE", "size_mb": 1024 },
{ "database_name": "msdb", "recovery_model": "SIMPLE", "state": "ONLINE", "size_mb": 56 }
]
// get_wait_stats (top 3)
[
{ "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)
[
{
"query_hash": "0x6B8F2A1C9E4D7F3A",
"total_cpu_ms": 3421,
"total_elapsed_ms": 4156,
"execution_count": 58,
"statement_text": "SELECT [ProductID],[ProductName],[UnitPrice] FROM [dbo].[Products] WHERE [Category]=@1 ORDER BY [ProductID] ASC"
}
]
// 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": "60",
"avg_user_impact_pct": 89,
"impact_score": 12.88,
"suggested_create_index": "CREATE INDEX [IX_Products_missing_4] ON [ProductsDB].[dbo].[Products] ([Category], [Discontinued]) INCLUDE ([UnitPrice])"
}
]
}
One honest note from actually running this back-to-back with the earlier blocking demo on the same server: get_wait_stats is cumulative since SQL Server last restarted, not since your last question. When I ran it here, LCK_M_S was sitting at 67% of total wait time, not from anything happening now, but as the fossil record of the blocking chain I’d seeded twenty minutes earlier for the tools demo. That’s not a bug in the tool. It’s a real DMV behavior every DBA has been bitten by at least once, and it’s exactly why the observability skill correlates wait stats against get_blocking_chains’s current state rather than trusting the cumulative number in isolation.
The report closes with “What I did NOT check”, so the scope is auditable even when nothing’s wrong. Config drift gets reported even on a healthy server, because drift is the finding regardless of whether it’s currently causing symptoms. And the skill has a hard boundary I like: don’t run the speculative deep-dive tools (memory, CPU history, tempdb) on a server that isn’t showing a symptom for them. A health snapshot should be six or seven calls, not fifteen. Cost discipline is part of the SOP too.
Guardrails: Six Layers That Don’t Depend on the Model’s Mood
Everything above only matters to a security team if none of it depends on the model behaving. Here’s the stack, bottom to top, and every layer is enforced in code or in a 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 |
Layer 2 is worth showing directly, because it’s the one that actually stops a model that’s decided, for whatever reason, to try something else. From sql-mcp-server/src/safety.ts:
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
// ...
];
That last pattern is the one I’d point out to a skeptical reviewer. It blocks DROP directly, sure, but it also blocks the semicolon trick that tries to smuggle a second statement in behind a legal-looking SELECT. And dba_monitor has no write grants at the SQL Server level either, so even a bypass here hits a wall at the login.
The question every reviewer asks in that room is the same one every time: “What can this agent do to my server without me approving it?” The honest answer is nothing that mutates state. Every fix across every demo, the KILL, the HADR RESUME, the security remediation I refused to draft, ended as a script for a human to read and run. Count the number of times the agent said “here’s the fix” and didn’t run it. That count is the pitch.
Why This Is Worth Building
Pulling it together, here’s what I think the actual value is, beyond the demo:
- 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 mental math I’d normally do by hand.
- 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.”
- Consistency you can compare across days and servers — The observability skill’s seven-step 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.
- 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.
- Control that survives a model that misbehaves — Every one of the six guardrail layers holds 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 SQL Server ×2 (plus a fleet profile for two more), Data API Builder, and the custom MCP server, all up, with Always On already configured. Then:
- Wire
mcp.jsonto point Copilot (or Claude, or Cursor, anything that takes MCP context) at:3001and:5001. - Copy the six
*.instructions.mdfiles into.github/instructions/. - 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:
- Giving AI Agents Visibility Into SQL Server with MCP
- Using Claude Code as a Database SRE Agent with the Everpure Fusion MCP Server
- Building a DBA Agent for Your SQL Server Estate with MCP (This Post)