Skip to main content
Glama

Azure Query MCP

npm CI CodeQL License: MIT

A read-only Model Context Protocol (MCP) server that lets an AI assistant query Azure Log Analytics, Microsoft Sentinel and Azure Resource Graph (ARG). Beyond raw KQL, it has purpose-built SOC tools for incident triage, entity timelines, ingestion health and threat hunting.

How it works

  • Runs on your machine. The server is the npm package @akapetaniou/azure-query-mcp, started by your MCP client with npx over stdio. There is nothing to clone, build or deploy into Azure.

  • Uses your own Azure identity. It signs in through DefaultAzureCredential (for example your az login session), so it can read exactly what your Azure RBAC allows, and nothing is shared between users.

  • Talks only to Azure. It connects to Microsoft Entra ID, Azure Resource Manager (including Resource Graph) and the Log Analytics query API of the selected cloud, plus the local managed identity endpoint when DefaultAzureCredential tries it. npx downloads the package from the npm registry on first use. There is no telemetry, and query results are not stored.

  • Covers two data planes. Log Analytics holds logs, events and security data; Resource Graph holds resource inventory and configuration.

Related MCP server: azure-compliance-mcp

Choose the right tool

Need

MCP tool

Data source

Logs, events, telemetry, metrics, incidents, or time-series analysis

query_workspace

Log Analytics workspace

Azure resource inventory, configuration, tags, policy, health, or cross-subscription discovery

query_azure_resources

Azure Resource Graph

Find a workspace or inspect its tables and schemas

list_workspaces, search_tables, describe_table

Azure Resource Manager

Find and triage Microsoft Sentinel incidents

list_incidents, get_incident

SecurityIncident, SecurityAlert

Everything one account, IP or host did across log sources

entity_timeline

Sign-in, audit, Office, endpoint, firewall and alert tables

Is security data still arriving? Stale tables, volume drops, latency

check_ingestion_health

Usage plus a one-hour latency sample

Run curated threat hunts

list_hunting_queries, run_hunting_query

Built-in library plus your own JSON hunts

ARG describes Azure control-plane resources and can be slightly delayed. It does not contain workspace log records. Log Analytics tools take the workspace ID (GUID) or its full ARM resource ID, and a timespan: an ISO 8601 duration back from now (P7D) or an absolute interval (2026-10-01T00:00:00Z/2026-10-02T00:00:00Z).

Security properties

  • All tools are marked read-only, non-destructive, and idempotent.

  • ARG calls the selected cloud's fixed Resource Manager endpoint with API version 2024-04-01; users cannot control the URL.

  • ARG requires explicit subscription scope and caps each page at 1,000 rows and 5 MiB.

  • Log Analytics requires bounded KQL and caps output at 1,000 rows and 2,000 characters per cell.

  • The read-only query policy ignores string literals and comments, so hunting filters such as where OperationName has "delete" are allowed while real management commands, set statements and externaldata are rejected. Malformed (unterminated) strings fail closed.

  • Each Log Analytics query reads only the workspace it is sent to. The cross-resource functions workspace(), app(), resource(), adx(), arg(), cluster() and database() are rejected unless AZURE_QUERY_MCP_ALLOW_CROSS_RESOURCE=true. Azure RBAC still applies either way; this keeps a query from reaching data the user did not point it at.

  • evaluate is limited to plugins that only reshape returned rows: autocluster, bag_unpack, basket, diffpatterns, ipv4_lookup, ipv6_lookup, narrow, pivot and preview. Plugins that reach outside the workspace (http_request, sql_request, python, r, ...) are blocked.

  • Releases are published from GitHub Actions with npm provenance, so each package version is traceable to the commit and workflow that built it.

  • Requests time out, a cancelled tool call aborts its Azure requests, and access tokens are never logged.

  • When Azure rejects a query (HTTP 400), the tool returns the service's explanation, such as SEM0100: Failed to resolve column, so the client can fix the query. Every other Azure failure returns only the HTTP status, the Azure error code and a fix hint, never the upstream message, which can contain tenant IDs, object IDs and scopes.

  • Authentication and authorization are delegated to Microsoft Entra ID and Azure RBAC.

See SECURITY.md before production deployment.

Prerequisites

  • Node.js 22 or later (provides npx)

  • Azure CLI, for signing in interactively with az login, or another identity that DefaultAzureCredential supports, such as a managed identity

  • Azure RBAC on the subscriptions and workspaces to be queried, and nothing more

Recommended least-privilege roles:

Capability

Suggested role and scope

Query ARG and discover workspaces

Reader on the required subscription or narrower resource scope

Read Log Analytics schemas and data, and use the SOC tools

Log Analytics Reader on each required workspace

Custom roles can be narrower. The effective permissions must include the relevant Azure Resource Manager read operations and Log Analytics query access.

Quick start

  1. Sign in to the tenant that contains the target subscriptions and workspaces.

    az login --tenant <tenant-id>
    az account list --output table
  2. Add the server to your MCP client.

    Claude Code (recommended): install the plugin, which adds the MCP server and the agent skill in one step. In Claude Code v2.1.275 or later:

    /plugin install azure-query --marketplace kapetanios55/azure-query-mcp

    On earlier versions, add the marketplace first, then install from it:

    /plugin marketplace add kapetanios55/azure-query-mcp
    /plugin install azure-query@azure-query-mcp

    Claude Code asks for optional settings: the Entra tenant ID (leave empty to use your az login tenant), a hunting-query folder, the Azure cloud, and whether to allow cross-resource queries (see Configuration). Change them later with /plugin configure azure-query@azure-query-mcp. Each plugin version runs the exact npm release it was tested with; get new releases with /plugin update azure-query@azure-query-mcp.

    To add only the MCP server, without the skill:

    claude mcp add azure-query --env AZURE_TENANT_ID=<tenant-id> -- npx -y @akapetaniou/azure-query-mcp@0.6.1

    VS Code (.vscode/mcp.json):

    {
      "inputs": [
        {
          "id": "azureTenantId",
          "type": "promptString",
          "description": "Microsoft Entra tenant ID"
        }
      ],
      "servers": {
        "azure-query": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@akapetaniou/azure-query-mcp@0.6.1"],
          "env": {
            "AZURE_TENANT_ID": "${input:azureTenantId}"
          }
        }
      }
    }

    Claude Desktop (claude_desktop_config.json) and Cursor (~/.cursor/mcp.json):

    {
      "mcpServers": {
        "azure-query": {
          "command": "npx",
          "args": ["-y", "@akapetaniou/azure-query-mcp@0.6.1"],
          "env": {
            "AZURE_TENANT_ID": "<tenant-id>"
          }
        }
      }
    }

    These examples pin the version so every machine runs the same release. Change @0.6.1 to the latest version when you upgrade. Without a version, npx keeps using whichever release it cached first.

  3. Verify both paths with prompts such as:

    List the Log Analytics workspaces in subscription <subscription-id>.
    Show the schema of the Heartbeat table in workspace <workspace-resource-id>.
    Use Azure Resource Graph to count resources by type in subscription <subscription-id>.

For production, use managed identity or workload identity. Avoid client secrets when the hosting platform supports federation.

Never place AZURE_CLIENT_SECRET directly in a committed MCP configuration. Use the host's secret store or managed identity.

Configuration

All settings are environment variables on the MCP server process. The Claude Code plugin sets the first four from its settings.

Variable

Default

Effect

AZURE_TENANT_ID

Tenant of the signed-in identity

Microsoft Entra tenant to authenticate against.

AZURE_QUERY_MCP_HUNTING_DIR

None

Folder of your own hunting queries; see Your own hunting queries.

AZURE_QUERY_MCP_CLOUD

AzurePublicCloud

AzurePublicCloud, AzureUSGovernment or AzureChinaCloud (the az cloud list names also work). An unknown value stops the server instead of falling back to the public cloud.

AZURE_QUERY_MCP_ALLOW_CROSS_RESOURCE

false

true lets queries use workspace(), app(), resource(), adx(), arg(), cluster() and database().

AZURE_TOKEN_CREDENTIALS

All credentials

prod excludes developer credentials from DefaultAzureCredential; see SECURITY.md.

Azure Government and Azure China

Set AZURE_QUERY_MCP_CLOUD and point Azure CLI at the same cloud before signing in, because the CLI credential uses the CLI's own cloud setting:

az cloud set --name AzureUSGovernment
az login --tenant <tenant-id>

Resource Manager, Resource Graph, Log Analytics and the token authority then all use that cloud's endpoints.

Agent skill

plugin/skills/azure-query is an Agent Skills skill that teaches an agent when and how to use this server. It covers which tool fits which question, the two workspace IDs, the KQL rules the server enforces, what each error means, SOC workflows, and safety rules: treat log content as untrusted, stay in the requested scope, and pull only the data needed. Its reference files contain tested example queries for Log Analytics and Resource Graph.

The skill does not replace the MCP server; install both. In Claude Code the plugin installs both together. To add the skill by hand instead, copy the folder into your personal or project skills directory:

git clone --depth 1 https://github.com/kapetanios55/azure-query-mcp.git
cp -r azure-query-mcp/plugin/skills/azure-query ~/.claude/skills/   # or <project>/.claude/skills/

Other agents that support Agent Skills load the same folder from their own skills directory. The test suite validates the skill's frontmatter, checks that it documents every tool and prompt, and runs each example query through the read-only policy (and, for Log Analytics, Microsoft's Kusto analyzer against the official table schemas).

SOC tools

These tools generate their own KQL, so the client does not need to know table schemas. Every generated query goes through the same read-only policy as query_workspace, results are capped at 1,000 rows, and all of them need only Log Analytics Reader. Incidents are read from the SecurityIncident and SecurityAlert tables, so Microsoft Sentinel must be enabled on the workspace for list_incidents and get_incident.

Tool

What it returns

list_incidents

Latest state of each incident, newest first, filterable by status and severity.

get_incident

One incident with its alerts and the account, IP and host entities involved (pivotEntities), ready to pass to entity_timeline.

entity_timeline

A cross-source timeline for a UPN, IP or host, plus complete per-source event counts. Tables that are not ingested are skipped with union isfuzzy=true.

check_ingestion_health

Per-table last-seen time, last-24-hour volume versus the previous daily average, Stale / VolumeDrop / VolumeSpike status, and P50/P95 ingestion latency over the last hour.

list_hunting_queries / run_hunting_query

Built-in hunts (password spray, MFA fatigue, privileged role assignment, inbox forwarding rules, Office apps spawning shells) mapped to MITRE ATT&CK.

Entity values are validated against strict UPN, IP and hostname formats before they are placed in a query, because the Log Analytics API has no parameter binding. Values containing quotes, backslashes or whitespace are rejected.

Your own hunting queries

Set AZURE_QUERY_MCP_HUNTING_DIR to a folder of JSON hunting queries in the SentinelCBContent format (id, name, description, tactics, techniques, query). Each file must pass the read-only policy; files that do not are reported by list_hunting_queries and not loaded. An unreadable directory is reported the same way and the built-in hunts still load.

"env": {
  "AZURE_TENANT_ID": "<tenant-id>",
  "AZURE_QUERY_MCP_HUNTING_DIR": "/path/to/SentinelCBContent/Hunting"
}

Prompts

Prompt

Workflow

triage_incident

get_incident, then entity_timeline for the key entities, then a verdict with evidence and next steps.

ingestion_health_review

check_ingestion_health, likely causes for each problem, and which detections would go blind.

Query correctness

The test suite parses and semantically analyses every built-in query with Microsoft's Kusto language service against a snapshot of the official Azure Monitor table schemas (test/fixtures/table-schemas.json, refreshed with node scripts/update-table-schemas.mjs). An unknown table, column or function fails CI.

Tools

query_azure_resources

Runs read-only ARG KQL against one or more explicitly supplied subscription IDs. The query must begin with an ARG table such as Resources, ResourceContainers, ResourceChanges or AuthorizationResources.

subscriptionIds: ["00000000-0000-4000-8000-000000000000"]
query: Resources | where type =~ 'microsoft.compute/virtualmachines' | project id, name, resourceGroup, location | order by name asc | limit 50
maxResults: 50

Use the returned skipToken with the identical query and subscription scope to request another page.

query_workspace

Runs bounded read-only KQL against one Log Analytics workspace. A result-limiting operator and timespan are mandatory.

workspaceId: 00000000-0000-4000-8000-000000000000
query: Heartbeat | project TimeGenerated, Computer | take 50
timespan: PT1H

workspaceId also accepts the workspace's full ARM resource ID, which the server resolves to its workspace ID once. timespan also accepts an absolute interval, for example 2026-10-01T08:00:00Z/PT6H or 2026-10-01T00:00:00Z/2026-10-02T00:00:00Z; times need Z or an offset.

list_workspaces returns both IDs for each workspace in a subscription. search_tables and describe_table list a workspace's tables and their columns; use them before writing KQL against an unfamiliar table.

Log Analytics examples

The examples below use query_workspace. Supply the workspace and an explicit timespan with every request. Table availability depends on the workspace configuration.

Agent heartbeat recency

Prompt:

For the last 24 hours, show the 25 computers with the most recent heartbeat.

KQL:

Heartbeat
| summarize LastHeartbeat=max(TimeGenerated) by Computer
| top 25 by LastHeartbeat desc

Timespan: P1D

Most common failed sign-ins

Prompt:

For the last seven days, show the 20 users with the most failed sign-ins.

KQL:

SigninLogs
| where ResultType != "0"
| summarize FailedSignIns=count() by UserPrincipalName
| top 20 by FailedSignIns desc

Timespan: P7D

Recent Sentinel incidents

Prompt:

Show the 20 most recently updated Microsoft Sentinel incidents from the last seven days.

KQL:

SecurityIncident
| summarize arg_max(TimeGenerated, *) by IncidentNumber
| project TimeGenerated, IncidentNumber, Title, Severity, Status, Owner
| top 20 by TimeGenerated desc

Timespan: P7D

Use search_tables when the table name is unknown, then use describe_table to retrieve the canonical column names and types before writing KQL.

Azure Resource Graph examples

The examples below use query_azure_resources and one or more explicit subscription IDs. ARG reports Azure control-plane state and can be slightly delayed.

Count resources by type

Resources
| summarize ResourceCount=count() by type
| top 25 by ResourceCount desc

List virtual machines

Resources
| where type =~ 'microsoft.compute/virtualmachines'
| project id, name, resourceGroup, subscriptionId, location
| order by name asc
| limit 100

Find resources missing an owner tag

Resources
| where isempty(tags.owner)
| project id, name, type, resourceGroup, subscriptionId
| order by type asc, name asc
| limit 100

Count NSGs with an inbound port allowed from the internet

Set targetPort to the port to investigate. This query handles a single destination port, destination port arrays, ranges such as 5000-5100, and the * wildcard. It also handles single and array source prefixes.

Resources
| where type =~ 'microsoft.network/networksecuritygroups'
| mv-expand rule = properties.securityRules
| where tostring(rule.properties.direction) =~ 'Inbound'
    and tostring(rule.properties.access) =~ 'Allow'
| extend sourcePrefixes = iif(
    array_length(rule.properties.sourceAddressPrefixes) > 0,
    rule.properties.sourceAddressPrefixes,
    pack_array(rule.properties.sourceAddressPrefix)
  ), destinationPorts = iif(
    array_length(rule.properties.destinationPortRanges) > 0,
    rule.properties.destinationPortRanges,
    pack_array(rule.properties.destinationPortRange)
  )
| mv-expand sourcePrefix = sourcePrefixes
| where tostring(sourcePrefix) in~ ('*', 'Internet', '0.0.0.0/0', '::/0')
| mv-expand destinationPort = destinationPorts
| extend portText=tostring(destinationPort), targetPort=22
| extend rangeStart=toint(split(portText, '-')[0]),
         rangeEnd=toint(split(portText, '-')[1])
| where portText == '*'
    or targetPort == toint(portText)
    or (rangeStart <= targetPort and rangeEnd >= targetPort)
| summarize MatchingRules=count(),
            NsgCount=dcount(id),
            Nsgs=make_set(name, 100)

This query shape was tested against Azure Resource Graph with both positive and zero-result ports.

Important: this identifies candidate exposure in configured custom NSG rules. It does not calculate effective packet reachability, rule priority conflicts, subnet and NIC associations, public IP presence, routes, Azure Firewall behavior, or application listeners. Treat the result as an investigation starting point, not proof that a service is reachable from the internet.

Troubleshooting

Symptom

Action

Wrong-token-issuer or tenant error

Run az login --tenant <tenant-id> and make sure AZURE_TENANT_ID uses the same tenant.

AuthorizationFailed from ARG

Verify Reader access at the requested subscription or resource scope.

Workspace metadata works but queries fail

Verify Log Analytics Reader access to the workspace, and pass the workspace ID or resource ID from list_workspaces.

Table not found

Run search_tables, then query the exact returned table name.

list_hunting_queries reports that AZURE_QUERY_MCP_HUNTING_DIR could not be read

Check that the path exists and is readable by the user running the MCP client, then restart the server.

Cross-resource query functions ... are disabled

Query one workspace per call, or set AZURE_QUERY_MCP_ALLOW_CROSS_RESOURCE=true if reading other workspaces or clusters is intended.

Timespan '...' is not valid

Use a duration (P7D) or an interval with explicit zones (2026-10-01T00:00:00Z/P1D). check_ingestion_health accepts durations only.

Sign-in or 401 errors in Azure Government or Azure China

Check that AZURE_QUERY_MCP_CLOUD and az cloud show name the same cloud, then sign in again.

Query rejected as unbounded

Add take, limit, top, summarize, or count as appropriate.

Server is not visible in VS Code

Check that npx is on the PATH VS Code uses (or, from source, run npm run build and verify the absolute dist/index.js path), then restart the MCP server or reload VS Code.

evaluate plugin rejected

Only row-reshaping plugins are allowed; see Security properties for the list.

Development

Run from source

To work on the server itself (needs Git), clone and build it, then point your MCP client at dist/index.js instead of npx:

git clone https://github.com/kapetanios55/azure-query-mcp.git
cd azure-query-mcp
npm ci
npm run check
"azure-query": {
  "type": "stdio",
  "command": "node",
  "args": ["/absolute/path/to/azure-query-mcp/dist/index.js"]
}

Before a pull request, run the same checks as CI:

npm run check
npm audit --omit=dev

Live smoke test

With az login done and the server built, exercise every tool against a real workspace. Output contains only counts, statuses and timings.

node scripts/live-smoke-test.mjs . <subscription-id> <workspace-resource-id> <workspace-customer-id>

Releasing

  1. Bump version in package.json (for example npm version minor --no-git-tag-version) and update CHANGELOG.md. Set the same version in plugin/.claude-plugin/plugin.json, both its version and the @<version> pin in the MCP server's npx arguments, and in the install examples of this README and plugin/README.md; the test suite fails until they all match. Plugin users receive the new server version only when the plugin version changes.

  2. Merge to main, then start the release in one of two ways:

    • Run it by hand: Actions > Release > Run workflow on main, and type the version (for example 0.6.0). The run fails unless it matches package.json on main. After publishing, it creates the v0.6.0 tag on that commit.

    • Push a tag: git tag -a v0.6.0 <merge-commit> -m "Release 0.6.0" && git push origin v0.6.0.

  3. The Release workflow checks the version against package.json, runs the full check and audit, and publishes to npm with provenance using trusted publishing (no stored npm token). A version already on npm is skipped, and a manual run refuses to publish if the tag already exists on a different commit. Publish promptly after merging: until the new version is on npm, the plugin on main pins a version npm doesn't have yet.

npm's trusted publisher for this package is set to the workflow file release.yml and the npm environment. Renaming either breaks publishing until the npm setting is updated to match.

References

The Resource Graph client follows the Microsoft Learn Azure Resource Graph REST API and Resources API reference.

License

MIT

Available Tools

11 tools
check_ingestion_healthA
Read-onlyIdempotent

Check whether log data is still arriving: per-table last-seen time, 24h volume versus the previous daily average, stale tables, volume drops or spikes, and ingestion latency over the last hour.

Inputs: workspaceId (required), timespan, staleAfterHours.

describe_tableA
Read-onlyIdempotent

Get the canonical schema and retention metadata for a Log Analytics table.

Inputs: tableName (required), workspaceResourceId (required).

entity_timelineA
Read-onlyIdempotent

Build a cross-source activity timeline for one account (UPN), IP address or host: sign-ins, audit events, Office activity, endpoint telemetry, firewall logs and alerts. Tables that are not ingested are skipped automatically.

Inputs: entity (required), entityType (required), workspaceId (required), timespan, +1 more.

get_incidentA
Read-onlyIdempotent

Get one Microsoft Sentinel incident with its alerts and the account, IP and host entities involved. Returned pivotEntities can be passed directly to entity_timeline; unresolvedEntities lists entities recorded without a usable identifier (for example only a display name or SID).

Inputs: workspaceId (required), incidentNumber (required), timespan.

list_hunting_queriesA
Read-onlyIdempotent

List the available threat-hunting queries (built-in plus any loaded from AZURE_QUERY_MCP_HUNTING_DIR) with their MITRE ATT&CK tactics and techniques.

Inputs: no named parameters.

list_incidentsA
Read-onlyIdempotent

List Microsoft Sentinel incidents (latest state of each) from the SecurityIncident table, newest first. Use to find incidents to triage; then call get_incident for details.

Inputs: workspaceId (required), statuses, timespan, maxResults, +1 more.

list_workspacesA
Read-onlyIdempotent

List Azure Log Analytics workspaces accessible in a subscription.

Inputs: subscriptionId (required).

query_azure_resourcesA
Read-onlyIdempotent

Use for Azure resource inventory, configuration, tags, policy, health, and cross-subscription discovery through Azure Resource Graph. Do not use for telemetry, logs, events, or time-series analysis; use query_workspace for those.

Inputs: query (required), subscriptionIds (required), skipToken, maxResults.

query_workspaceA
Read-onlyIdempotent

Use for telemetry, logs, events, metrics, and time-series data stored in a Log Analytics workspace. Run bounded, read-only KQL against one workspace. Do not use for Azure resource inventory or configuration; use query_azure_resources for those.

Inputs: query (required), timespan (required), workspaceId (required).

run_hunting_queryA
Read-onlyIdempotent

Run one hunting query from list_hunting_queries against a workspace. Results are capped at 1,000 rows.

Inputs: queryId (required), workspaceId (required), timespan.

search_tablesA
Read-onlyIdempotent

List or search tables in a Log Analytics workspace using ARM metadata.

Inputs: workspaceResourceId (required), search.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4/5.0

    Scored across 11 tools

    Disambiguation5/5

    Tools are cleanly partitioned by intent, and the two most conflict-prone tools (query_workspace vs query_azure_resources) explicitly exclude each other's use cases in their descriptions. Incident tools (list vs get) and table tools (search_tables vs describe_table) are clearly separated, and entity_timeline is framed as a cross-source pivot rather than a generic query.

    Naming Consistency4/5

    Nearly all tools follow a verb_noun pattern (list_incidents, get_incident, run_hunting_query, check_ingestion_health, describe_table, search_tables). The lone deviation, entity_timeline, is a noun-only name but remains readable and unambiguous in context.

    Tool Count5/5

    Eleven tools is well within the ideal range for a security/telemetry server, and each maps to a distinct stage of the workflow (discovery, querying, schema introspection, incident triage, hunting, health checks). No tool appears redundant or filler.

    Completeness4/5

    The read-only surface covers workspace/table discovery, KQL querying, resource graph inventory, ingestion health, incident retrieval, entity timelines, and hunting queries with good lifecycle coherence. The main gap is incident write-back — the description says list_incidents is for finding incidents to triage, but there is no update_incident to assign, comment, or close one.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables read-only access to SentinelOne's platform through MCP, allowing security investigations, threat hunting, and asset inventory queries via natural language.
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables read-only Kubernetes incident investigation through MCP tools for listing pods, describing resources, fetching logs, and searching runbooks.
      1
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables read-only SQL querying and schema inspection across MSSQL, PostgreSQL, and MySQL databases via MCP tools.
      ISC