[{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tr/series/azure-sre-agent-icin-sql-mcp/","section":"Series","summary":"","title":"Azure SRE Agent Için SQL MCP","type":"series"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/azure-sre-agent/","section":"Tags","summary":"","title":"Azure-Sre-Agent","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/dab/","section":"Tags","summary":"","title":"Dab","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/entra-id/","section":"Tags","summary":"","title":"Entra-Id","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/gmsa/","section":"Tags","summary":"","title":"Gmsa","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/kerberos/","section":"Tags","summary":"","title":"Kerberos","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/mcp/","section":"Tags","summary":"","title":"Mcp","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"We wanted Azure SRE Agent to answer \u0026ldquo;what is the top wait on sql02?\u0026rdquo; and \u0026ldquo;how many user sessions are there right now?\u0026rdquo; without ever handing it a SQL prompt. No sysadmin, no free-form query, no helper database on the SQL Servers, and no public endpoint. This post is how we built that with Data API builder (DAB) 2.1.5 and its MCP endpoint, and what broke along the way.\n✅ = proven in our lab · 📄 = documented only (Microsoft Learn or DAB source code), not tested by us.\nGoal and audience # Goal. At the end, the agent reads seven SQL Server DMV views (waits, counters, memory, sessions, requests, query stats) on every server you list, through an MCP connector that authenticates with the agent\u0026rsquo;s own managed identity and travels over your VNet. SQL Server sees a single low-privilege gMSA over Kerberos. Nothing is created on SQL beyond a login.\nAudience. Azure and SQL Server engineers who run SRE Agent, or are about to, and want it to see database health. You should be comfortable with Active Directory (gMSA), T-SQL logins, Entra app registrations and az rest.\nHow it fits together # DAB is Microsoft\u0026rsquo;s open-source engine that serves database objects listed in a JSON config as an API. We use only its MCP endpoint (/mcp, Streamable HTTP); REST and GraphQL are off. DAB has no SQL of its own: every tool call becomes a SELECT on a view you configured. ✅\nflowchart LR subgraph AZ[\"Azure VNet (no public endpoint)\"] AG[\"Azure SRE Agentsystem-assigned MI\"] subgraph HOST[\"mcp01 (domain member)\"] DAB[\"DAB 2.1.5 /mcpscheduled task as gMSA\"] end end ENTRA[\"Entra IDapp sql-mcp-api, role MCP.Read\"] SQL1[(\"sql01DMV views\")] SQL2[(\"sql02DMV views\")] AG -- \"1 token for api://appId\" --\u003e ENTRA AG -- \"2 HTTP :5000 + Bearer + X-MS-API-ROLE\" --\u003e DAB DAB -- \"3 signing keys, outbound 443\" --\u003e ENTRA DAB -- \"4 TDS 1433, Kerberos as gmsa-dab$\" --\u003e SQL1 DAB -- \"4\" --\u003e SQL2 The agent asks Entra for a token for api://\u0026lt;appId\u0026gt; with its managed identity. Only identities holding the app role MCP.Read get one: the app requires assignment. ✅ The connector calls http://mcp01.contoso.local:5000/mcp over the VNet, with the token and the header X-MS-API-ROLE: MCP.Read. ✅ DAB checks aud, iss and the signature (keys fetched from Entra over outbound 443) and the role. ✅ DAB connects to SQL Server as the gMSA over Kerberos. It does not pass the caller on. ✅ What the agent gets is three read-only tools: describe_entities (what exists, with column descriptions), read_records (filter, order, select, first) and aggregate_records (count, sum, avg, min, max, group by). There is no \u0026ldquo;run a query\u0026rdquo; tool. ✅\nPrerequisites # Area Requirement SRE Agent Egress mode Azure VNet, private DNS resolution on ✅ Remote MCP server access = off (keeps MCP traffic in your VNet) ✅ System-assigned managed identity (a user-assigned one also works) ✅ / 📄 Active Directory A domain with a KDS root key; Domain Admin for the gMSA ✅ MCP host Windows Server domain member, no public IP. Outbound TCP 443 to Entra for token signing keys. No inbound internet ✅ SQL Server 2022 or later for ##MS_ServerPerformanceStateReader## (we ran 2025). 2016–2019 need VIEW SERVER STATE ✅ 2025 / 📄 older Network Agent subnet → MCP host TCP 5000; the host name resolves from the agent subnet ✅ Entra Someone who can create an app registration and assign an app role ✅ Admin machine Azure CLI, signed in. zsh or PowerShell ✅ zsh / 📄 PowerShell HTTPS is not required: the connector accepts http:// inside the VNet. ✅ On-premises SQL Servers work the same way over VPN or ExpressRoute, as long as the agent subnet can reach the host. 📄\nSteps # Commands for the admin machine come in two tabs. Pick your shell once; every block on the page follows. Commands that run on a Windows server or in SQL appear once.\n1. Create the gMSA (on a domain controller) # # KDS root key: once per forest. Production: -EffectiveImmediately, then wait 10 h. if (-not (Get-KdsRootKey)) { Add-KdsRootKey -EffectiveImmediately } New-ADServiceAccount -Name gmsa-dab -DNSHostName gmsa-dab.contoso.local ` -PrincipalsAllowedToRetrieveManagedPassword \u0026#39;mcp01$\u0026#39; 2. Give the gMSA one login and one role (on each SQL Server, as sysadmin) # CREATE LOGIN [CONTOSO\\gmsa-dab$] FROM WINDOWS WITH DEFAULT_DATABASE = [master]; ALTER SERVER ROLE [##MS_ServerPerformanceStateReader##] ADD MEMBER [CONTOSO\\gmsa-dab$]; SELECT IS_SRVROLEMEMBER(\u0026#39;sysadmin\u0026#39;, N\u0026#39;CONTOSO\\gmsa-dab$\u0026#39;) AS is_sysadmin; -- expect 0 ##MS_ServerPerformanceStateReader## is VIEW SERVER PERFORMANCE STATE: performance DMVs, nothing else. No table data, no security DMVs, no way to change anything. No database user is created. ✅\nOn 2016–2019 use GRANT VIEW SERVER STATE instead. It is broader (it includes security-related state and other sessions\u0026rsquo; query text). 📄\nWithout the grant, two views lie instead of failing. sys.dm_exec_sessions and sys.dm_exec_requests then return only DAB\u0026rsquo;s own session. The counts look valid and are wrong. Check sys.server_permissions, not just \u0026ldquo;it returns rows\u0026rdquo;. 📄 (Learn; we only ran 2025) 3. Install DAB on the MCP host (as local admin) # Install-WindowsFeature RSAT-AD-PowerShell Install-ADServiceAccount gmsa-dab Test-ADServiceAccount gmsa-dab # True Then, on the same host:\nGrant the gMSA Log on as a batch job (a scheduled task needs it). Unpack the self-contained dab_net10.0_win-x64-2.1.5.zip from the DAB GitHub release into C:\\dab\\bin. No .NET install is needed. C:\\dab\\bin\\Microsoft.DataApiBuilder.exe --version prints 2.1.5. Pin the version: 2.1.5 introduced allowed-hosts. Create C:\\dab\\config (gMSA: read) and C:\\dab\\logs (gMSA: modify). Register the scheduled task that is the service (DAB is not a Windows service): $cmd = \u0026#39;/c set ASPNETCORE_URLS=http://0.0.0.0:5000\u0026amp;\u0026amp; C:\\dab\\bin\\Microsoft.DataApiBuilder.exe start --config dab-config.json \u0026gt; C:\\dab\\logs\\dab.log 2\u0026gt;\u0026amp;1\u0026#39; $a = New-ScheduledTaskAction -Execute cmd.exe -Argument $cmd -WorkingDirectory C:\\dab\\config $t = New-ScheduledTaskTrigger -AtStartup $p = New-ScheduledTaskPrincipal -UserId \u0026#39;CONTOSO\\gmsa-dab$\u0026#39; -LogonType Password # no password for a gMSA $s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) Register-ScheduledTask -TaskName \u0026#39;DAB-MCP\u0026#39; -Action $a -Trigger $t -Principal $p -Settings $s -Force Open TCP 5000 only to the agent and VM subnets. Windows Firewall stays on: New-NetFirewallRule -Name dab-mcp-5000 -DisplayName \u0026#39;DAB MCP 5000 (VNet only)\u0026#39; -Direction Inbound ` -Protocol TCP -LocalPort 5000 -RemoteAddress \u0026lt;agent-subnet\u0026gt;,\u0026lt;vm-subnet\u0026gt; -Action Allow -Profile Any 4. Write the DAB configuration # The layout matters more than it looks; the findings explain why.\ndab-config.json, the root: the runtime, a list of per-server files, and a placeholder data source. One file per SQL Server (sql01.json, sql02.json): its connection string and its entities. One entity = one DMV view on one server, named \u0026lt;server\u0026gt;_\u0026lt;view\u0026gt;: sql01_wait_stats, sql02_sessions. The root (C:\\dab\\config\\dab-config.json):\n{ \u0026#34;data-source\u0026#34;: { \u0026#34;database-type\u0026#34;: \u0026#34;mssql\u0026#34;, \u0026#34;connection-string\u0026#34;: \u0026#34;Server=dab-placeholder.invalid;Database=master;Integrated Security=True;Encrypt=True;TrustServerCertificate=True\u0026#34;, \u0026#34;health\u0026#34;: { \u0026#34;enabled\u0026#34;: false, \u0026#34;name\u0026#34;: \u0026#34;placeholder\u0026#34; } }, \u0026#34;data-source-files\u0026#34;: [ \u0026#34;sql01.json\u0026#34;, \u0026#34;sql02.json\u0026#34; ], \u0026#34;entities\u0026#34;: {}, \u0026#34;runtime\u0026#34;: { \u0026#34;rest\u0026#34;: { \u0026#34;enabled\u0026#34;: false }, \u0026#34;graphql\u0026#34;: { \u0026#34;enabled\u0026#34;: false }, \u0026#34;mcp\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;path\u0026#34;: \u0026#34;/mcp\u0026#34;, \u0026#34;allowed-hosts\u0026#34;: [ \u0026#34;mcp01\u0026#34;, \u0026#34;mcp01.contoso.local\u0026#34; ], \u0026#34;dml-tools\u0026#34;: { \u0026#34;describe-entities\u0026#34;: true, \u0026#34;read-records\u0026#34;: true, \u0026#34;aggregate-records\u0026#34;: true, \u0026#34;create-record\u0026#34;: false, \u0026#34;update-record\u0026#34;: false, \u0026#34;delete-record\u0026#34;: false, \u0026#34;execute-entity\u0026#34;: false } }, \u0026#34;host\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;production\u0026#34;, \u0026#34;authentication\u0026#34;: { \u0026#34;provider\u0026#34;: \u0026#34;EntraId\u0026#34;, \u0026#34;jwt\u0026#34;: { \u0026#34;audience\u0026#34;: \u0026#34;\u0026lt;appId\u0026gt;\u0026#34;, \u0026#34;issuer\u0026#34;: \u0026#34;https://login.microsoftonline.com/\u0026lt;tenant-id\u0026gt;/v2.0\u0026#34; } } }, \u0026#34;cache\u0026#34;: { \u0026#34;enabled\u0026#34;: false } } } One server file (sql01.json), shortened to one entity and three of its columns:\n{ \u0026#34;data-source\u0026#34;: { \u0026#34;database-type\u0026#34;: \u0026#34;mssql\u0026#34;, \u0026#34;connection-string\u0026#34;: \u0026#34;Server=sql01.contoso.local;Database=master;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Application Name=dab-mcp\u0026#34;, \u0026#34;health\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;sql01\u0026#34; } }, \u0026#34;entities\u0026#34;: { \u0026#34;sql01_wait_stats\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;sql01: sys.dm_os_wait_stats\u0026#34;, \u0026#34;source\u0026#34;: { \u0026#34;object\u0026#34;: \u0026#34;sys.dm_os_wait_stats\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;view\u0026#34; }, \u0026#34;fields\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;wait_type\u0026#34;, \u0026#34;primary-key\u0026#34;: true }, { \u0026#34;name\u0026#34;: \u0026#34;wait_time_ms\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Total wait time for this wait type in ms, including signal_wait_time_ms.\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;signal_wait_time_ms\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Part of wait_time_ms spent waiting for CPU after the resource was ready (CPU pressure).\u0026#34; } ], \u0026#34;graphql\u0026#34;: { \u0026#34;enabled\u0026#34;: false }, \u0026#34;cache\u0026#34;: { \u0026#34;enabled\u0026#34;: false }, \u0026#34;permissions\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;MCP.Read\u0026#34;, \u0026#34;actions\u0026#34;: [ \u0026#34;read\u0026#34; ] } ] } } } The seven views per server, and the key DAB needs for each:\nEntity DMV view Key wait_stats sys.dm_os_wait_stats wait_type perf_counters sys.dm_os_performance_counters object_name, counter_name, instance_name sys_memory sys.dm_os_sys_memory total_physical_memory_kb memory_clerks sys.dm_os_memory_clerks memory_clerk_address sessions sys.dm_exec_sessions session_id requests sys.dm_exec_requests session_id, request_id query_stats sys.dm_exec_query_stats sql_handle, statement_start_offset, statement_end_offset, plan_handle Rules we learned the hard way:\nList every column in fields, with descriptions on the ones agents filter on. describe_entities shows only what is in fields. In our first run the agent counted 72 \u0026ldquo;user sessions\u0026rdquo; because it did not know about is_user_process; with the column described it answered 2, which was right. ✅ No anonymous role. Every entity is readable only by MCP.Read. ✅ Cache off everywhere: these are live DMVs. ✅ Add entities one at a time and watch the log. One bad entity stops the whole of DAB. ✅ Start it: Start-ScheduledTask DAB-MCP, then Get-NetTCPConnection -LocalPort 5000 -State Listen shows 0.0.0.0.\n5. Create the Entra app and give the agent its role (admin machine) # App registration sql-mcp-api with identifier URI api://\u0026lt;appId\u0026gt;, v2 tokens, one app role MCP.Read for applications, assignment required, and the role assigned to the agent\u0026rsquo;s managed identity. The block is idempotent.\nzsh PowerShell AGENT=/subscriptions/\u0026lt;sub\u0026gt;/resourceGroups/\u0026lt;rg\u0026gt;/providers/Microsoft.App/agents/\u0026lt;agent\u0026gt; AGENT_MI_OID=$(az resource show --ids \u0026#34;$AGENT\u0026#34; --api-version 2026-01-01 --query identity.principalId -o tsv) APPID=$(az ad app list --display-name sql-mcp-api --query \u0026#34;[0].appId\u0026#34; -o tsv) [ -n \u0026#34;$APPID\u0026#34; ] || APPID=$(az ad app create --display-name sql-mcp-api --query appId -o tsv) az ad app update --id \u0026#34;$APPID\u0026#34; --identifier-uris \u0026#34;api://$APPID\u0026#34; az rest -m PATCH --url \u0026#34;https://graph.microsoft.com/v1.0/applications(appId=\u0026#39;$APPID\u0026#39;)\u0026#34; \\ --body \u0026#39;{\u0026#34;api\u0026#34;:{\u0026#34;requestedAccessTokenVersion\u0026#34;:2}}\u0026#39; ROLE=$(az ad app show --id \u0026#34;$APPID\u0026#34; --query \u0026#34;appRoles[?value==\u0026#39;MCP.Read\u0026#39;].id\u0026#34; -o tsv) if [ -z \u0026#34;$ROLE\u0026#34; ]; then ROLE=$(uuidgen | tr \u0026#39;A-Z\u0026#39; \u0026#39;a-z\u0026#39;) az rest -m PATCH --url \u0026#34;https://graph.microsoft.com/v1.0/applications(appId=\u0026#39;$APPID\u0026#39;)\u0026#34; \\ --body \u0026#34;{\\\u0026#34;appRoles\\\u0026#34;:[{\\\u0026#34;allowedMemberTypes\\\u0026#34;:[\\\u0026#34;Application\\\u0026#34;],\\\u0026#34;displayName\\\u0026#34;:\\\u0026#34;MCP.Read\\\u0026#34;,\\\u0026#34;description\\\u0026#34;:\\\u0026#34;Read DMVs through SQL MCP\\\u0026#34;,\\\u0026#34;id\\\u0026#34;:\\\u0026#34;$ROLE\\\u0026#34;,\\\u0026#34;isEnabled\\\u0026#34;:true,\\\u0026#34;value\\\u0026#34;:\\\u0026#34;MCP.Read\\\u0026#34;}]}\u0026#34; fi SPID=$(az ad sp list --filter \u0026#34;appId eq \u0026#39;$APPID\u0026#39;\u0026#34; --query \u0026#34;[0].id\u0026#34; -o tsv) [ -n \u0026#34;$SPID\u0026#34; ] || SPID=$(az ad sp create --id \u0026#34;$APPID\u0026#34; --query id -o tsv) az rest -m PATCH --url \u0026#34;https://graph.microsoft.com/v1.0/servicePrincipals/$SPID\u0026#34; --body \u0026#39;{\u0026#34;appRoleAssignmentRequired\u0026#34;:true}\u0026#39; az rest -m POST --url \u0026#34;https://graph.microsoft.com/v1.0/servicePrincipals/$SPID/appRoleAssignedTo\u0026#34; \\ --body \u0026#34;{\\\u0026#34;principalId\\\u0026#34;:\\\u0026#34;$AGENT_MI_OID\\\u0026#34;,\\\u0026#34;resourceId\\\u0026#34;:\\\u0026#34;$SPID\\\u0026#34;,\\\u0026#34;appRoleId\\\u0026#34;:\\\u0026#34;$ROLE\\\u0026#34;}\u0026#34; $AGENT = \u0026#39;/subscriptions/\u0026lt;sub\u0026gt;/resourceGroups/\u0026lt;rg\u0026gt;/providers/Microsoft.App/agents/\u0026lt;agent\u0026gt;\u0026#39; $AGENT_MI_OID = az resource show --ids $AGENT --api-version 2026-01-01 --query identity.principalId -o tsv $APPID = az ad app list --display-name sql-mcp-api --query \u0026#34;[0].appId\u0026#34; -o tsv if (-not $APPID) { $APPID = az ad app create --display-name sql-mcp-api --query appId -o tsv } az ad app update --id $APPID --identifier-uris \u0026#34;api://$APPID\u0026#34; \u0026#39;{\u0026#34;api\u0026#34;:{\u0026#34;requestedAccessTokenVersion\u0026#34;:2}}\u0026#39; | Out-File -Encoding ascii ver.json az rest -m PATCH --url \u0026#34;https://graph.microsoft.com/v1.0/applications(appId=\u0026#39;$APPID\u0026#39;)\u0026#34; --body \u0026#39;@ver.json\u0026#39; $ROLE = az ad app show --id $APPID --query \u0026#34;appRoles[?value==\u0026#39;MCP.Read\u0026#39;].id\u0026#34; -o tsv if (-not $ROLE) { $ROLE = [guid]::NewGuid().Guid @{ appRoles = @(@{ allowedMemberTypes = @(\u0026#39;Application\u0026#39;); displayName = \u0026#39;MCP.Read\u0026#39;; description = \u0026#39;Read DMVs through SQL MCP\u0026#39;; id = $ROLE; isEnabled = $true; value = \u0026#39;MCP.Read\u0026#39; }) } | ConvertTo-Json -Depth 5 | Out-File -Encoding ascii roles.json az rest -m PATCH --url \u0026#34;https://graph.microsoft.com/v1.0/applications(appId=\u0026#39;$APPID\u0026#39;)\u0026#34; --body \u0026#39;@roles.json\u0026#39; } $SPID = az ad sp list --filter \u0026#34;appId eq \u0026#39;$APPID\u0026#39;\u0026#34; --query \u0026#34;[0].id\u0026#34; -o tsv if (-not $SPID) { $SPID = az ad sp create --id $APPID --query id -o tsv } \u0026#39;{\u0026#34;appRoleAssignmentRequired\u0026#34;:true}\u0026#39; | Out-File -Encoding ascii req.json az rest -m PATCH --url \u0026#34;https://graph.microsoft.com/v1.0/servicePrincipals/$SPID\u0026#34; --body \u0026#39;@req.json\u0026#39; @{ principalId = $AGENT_MI_OID; resourceId = $SPID; appRoleId = $ROLE } | ConvertTo-Json | Out-File -Encoding ascii asg.json az rest -m POST --url \u0026#34;https://graph.microsoft.com/v1.0/servicePrincipals/$SPID/appRoleAssignedTo\u0026#34; --body \u0026#39;@asg.json\u0026#39; Two traps: az ad app update --set api… fails (\u0026ldquo;Couldn\u0026rsquo;t find \u0026lsquo;api\u0026rsquo;\u0026rdquo;), hence the Graph PATCH. And an app role id cannot be replaced once it exists, hence the reuse. ✅ Assign the role before the agent first asks for a token: managed-identity tokens are cached for up to about 24 hours, and an early one has no roles claim. 📄\n6. Keep MCP traffic in the VNet, then add the connector (admin machine) # \u0026ldquo;Remote MCP server access\u0026rdquo; off means MCP traffic goes through your VNet. On, it goes through Microsoft\u0026rsquo;s network to the internet, which cannot reach a private host. Send the whole egress object. ✅\nzsh PowerShell az rest -m PATCH --url \u0026#34;https://management.azure.com$AGENT?api-version=2026-01-01\u0026#34; \\ --body \u0026#39;{\u0026#34;properties\u0026#34;:{\u0026#34;sandboxConfiguration\u0026#34;:{\u0026#34;egress\u0026#34;:{\u0026#34;mode\u0026#34;:\u0026#34;AzureVNet\u0026#34;,\u0026#34;vnetConfiguration\u0026#34;:{\u0026#34;usePrivateDnsResolution\u0026#34;:true},\u0026#34;allowHttpMcpServerNetworkAccess\u0026#34;:false}}}}\u0026#39; az rest -m GET --url \u0026#34;https://management.azure.com$AGENT?api-version=2026-01-01\u0026#34; \\ --query properties.sandboxConfiguration.egress.allowHttpMcpServerNetworkAccess # false \u0026#39;{\u0026#34;properties\u0026#34;:{\u0026#34;sandboxConfiguration\u0026#34;:{\u0026#34;egress\u0026#34;:{\u0026#34;mode\u0026#34;:\u0026#34;AzureVNet\u0026#34;,\u0026#34;vnetConfiguration\u0026#34;:{\u0026#34;usePrivateDnsResolution\u0026#34;:true},\u0026#34;allowHttpMcpServerNetworkAccess\u0026#34;:false}}}}\u0026#39; | Out-File -Encoding ascii egress.json az rest -m PATCH --url \u0026#34;https://management.azure.com${AGENT}?api-version=2026-01-01\u0026#34; --body \u0026#39;@egress.json\u0026#39; az rest -m GET --url \u0026#34;https://management.azure.com${AGENT}?api-version=2026-01-01\u0026#34; ` --query properties.sandboxConfiguration.egress.allowHttpMcpServerNetworkAccess # false Now the connector. In the portal: Builder → Connectors → Add connector → MCP server, Streamable-HTTP, URL http://mcp01.contoso.local:5000/mcp, authentication Managed identity (system-assigned), scope api://\u0026lt;appId\u0026gt;/.default, custom header X-MS-API-ROLE = MCP.Read, then select the three tools. 📄 (the portal path; we ran the ARM call below ✅)\nconnector.json holds no secret. Tool names are prefixed with the connector name:\n{\u0026#34;properties\u0026#34;:{\u0026#34;dataConnectorType\u0026#34;:\u0026#34;Mcp\u0026#34;,\u0026#34;dataSource\u0026#34;:\u0026#34;placeholder\u0026#34;,\u0026#34;identity\u0026#34;:\u0026#34;system\u0026#34;, \u0026#34;extendedProperties\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;http\u0026#34;,\u0026#34;endpoint\u0026#34;:\u0026#34;http://mcp01.contoso.local:5000/mcp\u0026#34;, \u0026#34;authType\u0026#34;:\u0026#34;AzureARM\u0026#34;,\u0026#34;armScope\u0026#34;:\u0026#34;api://\u0026lt;appId\u0026gt;/.default\u0026#34;, \u0026#34;X-MS-API-ROLE\u0026#34;:\u0026#34;MCP.Read\u0026#34;, \u0026#34;selectedTools\u0026#34;:[\u0026#34;dmv_describe_entities\u0026#34;,\u0026#34;dmv_read_records\u0026#34;,\u0026#34;dmv_aggregate_records\u0026#34;], \u0026#34;toolsVisibleToMetaAgent\u0026#34;:[\u0026#34;dmv_describe_entities\u0026#34;,\u0026#34;dmv_read_records\u0026#34;,\u0026#34;dmv_aggregate_records\u0026#34;]}}} zsh PowerShell az rest -m PUT --url \u0026#34;https://management.azure.com$AGENT/connectors/dmv?api-version=2026-01-01\u0026#34; \\ --body @connector.json -o none az rest -m PUT --url \u0026#34;https://management.azure.com${AGENT}/connectors/dmv?api-version=2026-01-01\u0026#34; ` --body \u0026#39;@connector.json\u0026#39; -o none authType: AzureARM is what the portal\u0026rsquo;s \u0026ldquo;Managed identity\u0026rdquo; option writes, and custom headers are flat keys in extendedProperties. A GET afterwards shows endpoint, armScope and the header as null; they are write-only. ✅\nVerification # On SQL, as sysadmin: DAB\u0026rsquo;s session is the gMSA, over Kerberos, and not sysadmin. ✅\nSELECT s.login_name, c.auth_scheme, IS_SRVROLEMEMBER(\u0026#39;sysadmin\u0026#39;, s.login_name) AS is_sysadmin FROM sys.dm_exec_sessions s JOIN sys.dm_exec_connections c ON c.session_id = s.session_id WHERE s.program_name LIKE \u0026#39;dab-mcp%\u0026#39;; -- CONTOSO\\gmsa-dab$ KERBEROS 0 Ask the agent: \u0026ldquo;How many user sessions are there on sql01 right now?\u0026rdquo; and \u0026ldquo;What is the top wait type on sql02?\u0026rdquo;. The thread shows MCP Tool calls. Compare with sqlcmd right after: the same wait types in the same order (values are cumulative), and one more session in sqlcmd (its own). ✅\nOnly the agent can read ✅:\nthe host\u0026rsquo;s own managed identity cannot get a token for api://\u0026lt;appId\u0026gt;: AADSTS501051, not assigned; an ARM token gets HTTP 401 (wrong audience); without the X-MS-API-ROLE header the agent\u0026rsquo;s calls return NoEntitiesConfigured. The path is the VNet. On the MCP host, turn on the Filtering Platform audit for a minute, ask the agent something, and list the sources of inbound connections to port 5000. Every one should be in the agent subnet. ✅\nauditpol /set /subcategory:\u0026#34;Filtering Platform Connection\u0026#34; /success:enable # ...ask the agent a question, then: Get-WinEvent -FilterHashtable @{LogName=\u0026#39;Security\u0026#39;; Id=5156; StartTime=(Get-Date).AddMinutes(-10)} | Where-Object { $_.Message -match \u0026#39;Destination Port:\\s+5000\u0026#39; -and $_.Message -match \u0026#39;Inbound\u0026#39; } | ForEach-Object { [regex]::Match($_.Message,\u0026#39;Source Address:\\s+(\\S+)\u0026#39;).Groups[1].Value } | Group-Object | ForEach-Object { \u0026#34;$($_.Count) x $($_.Name)\u0026#34; } auditpol /set /subcategory:\u0026#34;Filtering Platform Connection\u0026#34; /success:disable # it fills the Security log fast What we found # Finding 1: one unreachable SQL Server stops the whole DAB at startup # DAB reads the schema of every entity when it starts. If any SQL Server is unreachable at that moment, DAB exits. Healthy servers go down with it. ✅\ndab.log says Unable to complete runtime initialization … Cannot obtain Schema for entity sql02_wait_stats … A network-related or instance-specific error. The entity prefix tells you which server. ✅ It does not recover by itself when the server returns. The task\u0026rsquo;s \u0026ldquo;restart on failure\u0026rdquo; never fires: cmd.exe launched fine and DAB exited with -1, which Task Scheduler records as completed (event 201, return code 4294967295, level Information). Only Start-ScheduledTask DAB-MCP brings it back. ✅ The task redirects with \u0026gt;, so every start overwrites dab.log. Read it before you restart. ✅ The Application log gets .NET Runtime 1000 \u0026ldquo;Hosting failed to start\u0026rdquo;, which names no server. ✅ If a server dies while DAB runs, only its own entities fail (the first call after about 15 s, the connect timeout), and they recover on their own when it returns. ✅ Monitor the listener, not the task. After patching a SQL Server or rebooting the MCP host, check that something listens on TCP 5000 (or that an MCP initialize succeeds) from outside. The task state says Ready, which looks harmless. Restart DAB only when all of its SQL Servers are up. Finding 2: the .off workaround # DAB 2.1.5 has no enabled flag for a data source or an entity. We tried the obvious switches on a copy of the config, with one server pointing at a name that does not exist:\nAttempt Result \u0026quot;mcp\u0026quot;: false on every entity of the dead server DAB still fails at startup ✅ plus health.enabled: false on its data source still fails ✅ rename sql02.json to sql02.json.off starts, with sql01\u0026rsquo;s 7 entities ✅ The reason is in DAB\u0026rsquo;s loader: a file listed in data-source-files that does not exist on disk is skipped silently. 📄 (source) ✅ (behaviour). That is why each server gets its own file, including the first one, and why the root holds only a placeholder data source: DAB 2.1.5 refuses a root without one (\u0026ldquo;Invalid connection-string\u0026rdquo;), and with an unresolvable name and zero entities it is never contacted. ✅ Re-test that after every DAB upgrade; a later version might contact it. 📄\nTaking a server out, on the host:\n$s = \u0026#39;sql02\u0026#39; Rename-Item \u0026#34;C:\\dab\\config\\$s.json\u0026#34; \u0026#34;$s.json.off\u0026#34; Stop-ScheduledTask DAB-MCP; Get-Process Microsoft.DataApiBuilder -EA SilentlyContinue | Stop-Process -Force Start-ScheduledTask DAB-MCP; Start-Sleep 15 [bool](Get-NetTCPConnection -LocalPort 5000 -State Listen -EA SilentlyContinue) # True = DAB is up Or from the admin machine, for an Azure VM, through the VM agent (no SSH). 📄 (not run)\nzsh PowerShell az vm run-command invoke -g \u0026lt;rg\u0026gt; -n mcp01 --command-id RunPowerShellScript --query \u0026#39;value[0].message\u0026#39; -o tsv --scripts \\ \u0026#39;Rename-Item C:\\dab\\config\\sql02.json sql02.json.off; Stop-ScheduledTask DAB-MCP; Get-Process Microsoft.DataApiBuilder -EA SilentlyContinue | Stop-Process -Force; Start-ScheduledTask DAB-MCP; Start-Sleep 15; [bool](Get-NetTCPConnection -LocalPort 5000 -State Listen -EA SilentlyContinue)\u0026#39; \u0026#39;Rename-Item C:\\dab\\config\\sql02.json sql02.json.off; Stop-ScheduledTask DAB-MCP; Get-Process Microsoft.DataApiBuilder -EA SilentlyContinue | Stop-Process -Force; Start-ScheduledTask DAB-MCP; Start-Sleep 15; [bool](Get-NetTCPConnection -LocalPort 5000 -State Listen -EA SilentlyContinue)\u0026#39; | Set-Content takeout.ps1 az vm run-command invoke -g \u0026lt;rg\u0026gt; -n mcp01 --command-id RunPowerShellScript --scripts \u0026#39;@takeout.ps1\u0026#39; --query \u0026#39;value[0].message\u0026#39; -o tsv Put it back by swapping the two names and restarting. Because the skip is silent, a typo in data-source-files also drops a server quietly. Count what DAB will load after every change, and alert when the number is below 7 × servers:\n$c = \u0026#39;C:\\dab\\config\u0026#39;; $root = Get-Content \u0026#34;$c\\dab-config.json\u0026#34; -Raw | ConvertFrom-Json $n = 0 foreach ($f in $root.\u0026#39;data-source-files\u0026#39;) { if (Test-Path \u0026#34;$c\\$f\u0026#34;) { $n += @((Get-Content \u0026#34;$c\\$f\u0026#34; -Raw | ConvertFrom-Json).entities.PSObject.Properties).Count } else { \u0026#34;MISSING: $f (DAB skips it silently)\u0026#34; } } \u0026#34;entities DAB will load: $n\u0026#34; Get-ChildItem \u0026#34;$c\\*.json.off\u0026#34; -EA SilentlyContinue | ForEach-Object { \u0026#34;TAKEN OUT: $($_.Name)\u0026#34; } We kept recovery manual on purpose. An automatic retry loop would still fail until the dead server is taken out or comes back, and it would hide the problem.\nFinding 3: why server configuration is not exposed # The natural next question was \u0026ldquo;what is MAXDOP on sql01?\u0026rdquo;. DAB cannot answer it without an object on SQL:\nsys.configurations, sys.database_scoped_configurations and sys.dm_server_registry have sql_variant columns. DAB reads a view\u0026rsquo;s schema with SELECT * and builds its filter model over every column, not just the ones in fields. sql_variant has no mapping, so DAB fails at startup, and leaving the column out of fields does not help. 📄 (DAB source; column types checked ✅) SERVERPROPERTY() and @@VERSION are functions. DAB exposes tables, views and stored procedures only. We tried the DMV functions (dm_exec_sql_text, dm_db_index_physical_stats, dm_io_virtual_file_stats): DAB fails to start with SQL error 216 (\u0026ldquo;parameters were not supplied\u0026rdquo;). ✅ A wrapper view that casts everything to plain types would work, but it is an object on SQL, which this design rules out. So configuration questions stay with sqlcmd and SSMS. Finding 4: scale is not the limit, failure coupling is # We loaded one DAB with up to 50 data sources, seven entities each, and measured. The data sources were aliases of our two real servers. ✅\nServers Listener up / first read Memory (working set) describe_entities full / nameOnly / one entity 2 2.1 s / 4.9 s 132 MB 74 KB / 2.3 KB / 1.5 KB 10 2.4 s / 5.2 s 152 MB 368 KB / 11 KB / 1.5 KB 25 3.5 s / 6.2 s 161 MB 921 KB / 27 KB / 1.5 KB 50 5.2 s / 8.0 s 176 MB 1.8 MB / 54 KB / 1.5 KB At 25 servers the agent found the right entity for \u0026ldquo;top wait on sql17\u0026rdquo; and \u0026ldquo;user sessions on sql22\u0026rdquo; by itself: it called describe_entities with nameOnly first (18 KB), then one entity, and the session count matched sqlcmd. It never pulled the 921 KB full description. ✅\nSo group servers per DAB by failure domain and maintenance window, not by count, and keep each group at 25 or fewer (what we tested). Network latency and connection pools with 25 distinct servers were not measured. 📄\nTroubleshooting # Symptom Cause Fix DAB does not start after a restart; dab.log: Cannot obtain Schema for entity … A SQL Server was unreachable at startup Bring it back or rename its file to .off, then Start-ScheduledTask DAB-MCP DAB stops, all entities down One bad entity (a function, a sql_variant view, a typo) Restore the last good config; add entities one by one HTTP 401, invalid_token Wrong aud/iss, or the host cannot reach Entra for signing keys DAB audience = \u0026lt;appId\u0026gt; (the GUID, not api://…), issuer …/v2.0; outbound 443 to Entra NoEntitiesConfigured or PermissionDenied No X-MS-API-ROLE header, so the role is authenticated Add the header to the connector HTTP 403 The header names a role the token does not carry Assign MCP.Read to the agent\u0026rsquo;s identity Token has no roles Managed-identity token cached from before the assignment Wait (up to ~24 h); assign before first use next time Connector not connected Name not resolvable from the agent subnet, firewall rule missing, DAB not on 0.0.0.0, remote MCP access on, or Host not in allowed-hosts Check each; Get-NetTCPConnection -LocalPort 5000 -State Listen Sessions/requests show one session The login lacks the grant; these views then return only DAB\u0026rsquo;s own session Check sys.server_permissions for the gMSA dab.log is empty Normal in production mode Check the listener and an MCP call instead What we learned # DMV views, yes; DMV functions, no. Without any object on SQL, DAB serves views. Query text, index fragmentation and file I/O need a wrapper, and that is a design decision, not a config flag. A gMSA plus ##MS_ServerPerformanceStateReader## is enough. One login, Kerberos, no database user, not sysadmin. Managed identity end to end, no secret anywhere. The agent\u0026rsquo;s MI gets the token, the app role gates it, DAB validates it. A static bearer token also works, and expires in a day. One unreachable server at startup takes every server down, and nobody restarts it. Watch the listener from outside; Task Scheduler reports success. There is no disable switch, but a missing file is skipped. One file per server and a placeholder root turn that into a clean, silent .off switch. Count the entities to make it loud. Describe your columns. The agent answered \u0026ldquo;72 user sessions\u0026rdquo; until is_user_process had a description. Next in the series: monitoring this setup: alerting on the failure signals above without trusting the task state.\n","date":"4 October 2026","externalUrl":null,"permalink":"/posts/sql-mcp-part-1/","section":"Posts","summary":"","title":"Setting up SQL MCP for Azure SRE Agent","type":"posts"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/series/sql-mcp-for-azure-sre-agent/","section":"Series","summary":"","title":"SQL MCP for Azure SRE Agent","type":"series"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/sql-server/","section":"Tags","summary":"","title":"Sql-Server","type":"tags"},{"content":"Lab-proven guides for Azure SRE Agent, MCP and Data API builder. Every post is built in a real lab first, and every claim is marked: ✅ proven in the lab, 📄 documented only.\nSeries SQL MCP for Azure SRE Agent · 1 Tags #Azure-Sre-Agent #Dab #Entra-Id #Gmsa #Kerberos #Mcp #Sql-Server ","date":"4 October 2026","externalUrl":null,"permalink":"/","section":"SRE Agent Lab","summary":"","title":"SRE Agent Lab","type":"page"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"}]