Use this skill when asked to monitor, audit, or analyze MCP (Model Context Protocol) server usage in the environment. Triggers on keywords like "MCP usage", "MCP server monitoring", "MCP activity", "Graph MCP", "Sentinel MCP", "Azure MCP", "MCP audit", "tool usage monitoring", "MCP breakdown", "who is using MCP", or when investigating MCP user activity, Graph API calls from MCP servers, or workspace query governance. This skill provides comprehensive MCP server telemetry analysis across Graph MCP, Sentinel MCP, and Azure MCP servers including usage trends, endpoint access patterns, user attribution, cross-server user analysis, sensitive API detection, workspace query governance, and security risk assessment with inline and markdown file reporting.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use this skill when asked to monitor, audit, or analyze MCP (Model Context Protocol) server usage in the environment. Triggers on keywords like "MCP usage", "MCP server monitoring", "MCP activity", "Graph MCP", "Sentinel MCP", "Azure MCP", "MCP audit", "tool usage monitoring", "MCP breakdown", "who is using MCP", or when investigating MCP user activity, Graph API calls from MCP servers, or workspace query governance. This skill provides comprehensive MCP server telemetry analysis across Graph MCP, Sentinel MCP, and Azure MCP servers including usage trends, endpoint access patterns, user attribution, cross-server user analysis, sensitive API detection, workspace query governance, and security risk assessment with inline and markdown file reporting.
threat_pulse_domains
["admin"]
drill_down_prompt
Run MCP usage monitoring report โ Graph/Sentinel/Azure MCP activity, user attribution
MCP Server Usage Monitoring โ Instructions
Purpose
This skill monitors and audits Model Context Protocol (MCP) server usage across your Microsoft Sentinel and Defender XDR environment. MCP servers are AI-powered tools that enable language models to interact with Microsoft security services โ and like any privileged access channel, they require monitoring.
What this skill tracks:
MCP Server
Telemetry Source
Key Identifier
Microsoft Graph MCP Server
MicrosoftGraphActivityLogs
AppId = e8c77dc2-69b3-43f4-bc51-3213c9d915b4
Sentinel Data Lake MCP
CloudAppEvents
RecordType 403, Interface = IMcpToolTemplate
Sentinel Triage MCP
MicrosoftGraphActivityLogs + SigninLogs
AppId = 7b7b3966-1961-47b5-b080-43ca5482e21c ("Microsoft Defender Mcp") โ dedicated AppId with full user attribution via delegated cert auth
Azure MCP Server
AzureActivity
No dedicated AppId โ uses DefaultAzureCredential
Sentinel Data Lake โ Direct KQL
CloudAppEvents
RecordType 379, Operation = KQLQueryCompleted
Workspace Query Sources (Analytics Tier)
LAQueryLogs
All clients querying Log Analytics workspace
What this skill detects:
Graph API call volume, trends, and endpoint diversity via MCP
Sentinel Data Lake MCP tool usage โ tool call breakdown (query_lake, list_sentinel_workspaces, search_tables, etc.), success/failure rates, execution duration, tables accessed via CloudAppEvents (Purview unified audit)
MCP-driven vs Direct KQL delineation โ distinguishes Data Lake queries initiated via MCP tools (RecordType 403, Interface IMcpToolTemplate) from direct KQL queries (RecordType 379) and Analytics tier queries (LAQueryLogs)
Anomalous access patterns: new users, new endpoints, volume spikes, error surges
MCP server usage as a proportion of total workspace activity
Extended landscape awareness: Beyond these four actively monitored MCP servers, Microsoft's MCP ecosystem includes 30+ additional servers (Copilot Studio built-in catalog, Power BI, Fabric RTI, Playwright, Security Copilot Agent Creation, and more). See Extended Microsoft MCP Server Landscape for the full catalog, telemetry surfaces, and monitoring expansion priorities.
โ ๏ธ CRITICAL WORKFLOW RULES - READ FIRST โ ๏ธ
Before starting ANY MCP usage monitoring analysis:
ALWAYS enforce Sentinel workspace selection (see Workspace Selection section below)
ALWAYS ask the user for output mode if not specified: inline chat summary or markdown file report (or both)
ALWAYS ask the user for time range if not specified: default to 30 days, configurable
ALWAYS query all MCP telemetry surfaces โ do not skip any MCP server type
ALWAYS include non-MCP workspace context (Sentinel Engine, Logic Apps) for governance proportion analysis
ALWAYS run independent queries in parallel for performance
ALWAYS attribute activity to specific users โ never present anonymous aggregates
NEVER conflate non-MCP platform activity with MCP activity โ clearly label categories
ALWAYS execute pre-authored queries from Sample KQL Queries EXACTLY as written โ substitute only the time range parameter (e.g., ago(30d) โ ago(90d)). These queries encode mitigations for schema pitfalls documented in Known Pitfalls. Writing equivalent queries from scratch is โ PROHIBITED
Microsoft first-party AppId, same across all tenants. Dedicated AppId โ visible in MicrosoftGraphActivityLogs (API calls to /security/* endpoints) and SigninLogs/AADNonInteractiveUserSignInLogs (AppDisplayName = "Microsoft Defender Mcp"). Delegated auth with certificate (ClientAuthMethod=2), full user attribution. Scopes: SecurityAlert.Read.All, SecurityIncident.Read.All, ThreatHunting.Read.All. Target resources: Microsoft Graph, WindowsDefenderATP. No local SPN โ display name only visible in SigninLogs. ๐ด Confirmed Feb 2026: Empirical telemetry investigation identified 7b7b3966 as the Triage MCP AppId via MicrosoftGraphActivityLogs + SigninLogs correlation.
253895df-6bd8-4eaf-b101-1381ec4306eb
Sentinel Platform Services App Reg
SigninLogs
Sentinel-hosted MCP platform
04b07795-8ddb-461a-bbee-02f9e1bf7b46
Azure MCP Server (local stdio via DefaultAzureCredential โ Azure CLI)
Shared AppId with Azure CLI. In LAQueryLogs, RequestClientApp is empty (not a unique fingerprint). Azure MCP appends \n| limit N to query text โ the only query-level differentiator. Read-only ARM ops don't appear in AzureActivity. ๐ Updated Feb 2026: Previously documented as AppId 1950a258 (AzurePowerShellCredential) with csharpsdk,LogAnalyticsPSClient โ that fingerprint is obsolete; only 1 occurrence found in 30-day lookback.
(none โ uses DefaultAzureCredential)
Azure MCP Server (local stdio)
AzureActivity
ARM write operations only; read ops not logged. Claims.appid = 04b07795. Inherits cred from Azure CLI/VS Code
Primary Azure MCP Server credential path (field-tested Feb 2026). RequestClientApp is empty in LAQueryLogs. Azure MCP appends \n| limit N to query text. Shared AppId with manual az CLI โ disambiguate via query text pattern or session correlation. ๐ Previously documented as 1950a258 (AzurePowerShellCredential) โ that path is obsolete
Portal & Platform Applications (Non-MCP โ for context)
AppId
Service
Telemetry Table
Notes
80ccca67-54bd-44ab-8625-4b79c4dc7775
M365 Security & Compliance Center (Sentinel Portal)
LAQueryLogs
ASI_Portal, ASI_Portal_Connectors โ Sentinel Portal backend, NOT an MCP server
95a5d94c-a1a0-40eb-ac6d-48c5bdee96d5
Azure Portal โ AppInsightsPortalExtension
LAQueryLogs
Azure Portal blade for Log Analytics Usage dashboards/workbooks. RequestClientApp = AppInsightsPortalExtension. Executes billing/usage queries (e.g., Usage | where IsBillable). NOT MCP, NOT VS Code โ runs when user opens Workspace Usage Dashboard in browser. No SPN or app registration in tenant (platform-level first-party app). Not in merill/microsoft-info known apps list.
Built-in scheduled query engine (NOT MCP). Also serves as the execution backend for Advanced Hunting โ RequestClientApp = "M365D_AdvancedHunting" indicates AH queries from Triage MCP, Defender portal, or Security Copilot that hit connected LA tables (see Query 7). Separate from analytics rules (RequestClientApp empty or other values).
Extended Microsoft MCP Server Landscape (Reference)
Beyond the four MCP servers actively monitored by this skill, Microsoft's MCP ecosystem includes many additional servers. This section catalogs them for awareness, threat modeling, and future monitoring expansion.
Sentinel MCP Collections (Microsoft-Hosted)
Microsoft Sentinel exposes three official MCP collections, each at a distinct endpoint:
Create Microsoft Security Copilot agents for complex workflows
โ Not yet monitored
Sentinel Custom MCP Tools: Organizations can create their own MCP tools by exposing saved KQL queries from Advanced Hunting as MCP tools. These execute through the same Sentinel MCP infrastructure and are audited in CloudAppEvents (RecordType 403) alongside built-in tools. See Create custom Sentinel MCP tools.
๐ต Monitoring note: Custom MCP tools appear in CloudAppEvents with the same RecordType 403 and IMcpToolTemplate interface as built-in tools. The ToolName field will show the custom tool name, making them visible in Query 13 without modification.
Power BI MCP Servers
Server
Type
Endpoint / Repo
Purpose
Telemetry Surface
Power BI Remote MCP
Microsoft-hosted
https://api.fabric.microsoft.com/v1/mcp/powerbi
Query Power BI datasets, reports, and workspaces remotely via SSE transport
๐ก PowerBIActivity table (if ingested into Sentinel), Fabric audit logs
Local Power BI model operations (DAX queries, schema exploration)
โ Local only โ no Azure telemetry
โ ๏ธ Data exfiltration risk: Power BI Remote MCP provides API-based access to organizational datasets. If an AI agent connects to this endpoint, it can query sensitive business data. Monitor PowerBIActivity for unusual access patterns if this table is available in your Sentinel workspace.
Query Azure Data Explorer clusters and Fabric Real-Time Intelligence Eventhouses via KQL
๐ก ADX audit logs, Fabric audit events
Azure MCP Server โ Kusto namespace
Local (stdio)
Part of Azure MCP Server (azmcp --namespace kusto)
Manage ADX clusters, databases, tables, and queries via ARM
โ Already covered (Azure ARM operations โ Phase 4)
Kusto Query MCP
Copilot Studio built-in
Copilot Studio catalog
KQL query execution from Copilot Studio agents
๐ก CloudAppEvents (Copilot Studio workload)
๐ต Note: The Fabric RTI MCP Server is open-source and runs locally. It authenticates to ADX/Eventhouse using the user's credentials. If your org uses ADX, queries from this MCP would appear in ADX audit logs (.show queries / diagnostic logs), NOT in Sentinel LAQueryLogs.
GitHub repo operations (issues, PRs, code search) via PAT
โ GitHub audit logs only, not in Sentinel
Microsoft Learn Docs MCP
Cloud-hosted
Certified Copilot Studio connector
Search and fetch official Microsoft Learn documentation
โ Public docs, no security data
Copilot Studio Built-in MCP Servers (19+ servers)
Microsoft Copilot Studio provides a catalog of built-in MCP servers for agent development. These are Microsoft-managed, cloud-hosted servers that agents can connect to.
๐ Medium โ manages other MCP servers via Dataverse/Graph
โ ๏ธ Telemetry gap: Copilot Studio built-in MCP servers are NOT directly visible in LAQueryLogs or MicrosoftGraphActivityLogs. Their activity may appear in:
CloudAppEvents โ under Copilot Studio workload (if Purview unified audit is configured)
M365 unified audit log โ as Copilot Studio agent actions
AuditLogs โ service principal lifecycle events (creation, modification)
AADServicePrincipalSignInLogs โ SPN sign-ins to Bot Framework from Azure internal IPs (fd00:*)
To monitor Copilot Studio agent activity, use the ai-agent-posture skill for comprehensive agent security auditing.
Azure MCP Server โ Full Tool Surface
The Azure MCP Server (already tracked in Phase 4) has a much broader tool surface than just ARM operations. The complete namespace catalog:
๐ต Key Vault access via MCP is particularly security-sensitive. The Azure MCP Server implements elicitation (user confirmation prompts) before returning secrets. However, this can be bypassed with the --insecure-disable-user-confirmation flag. Monitor AzureActivity for Key Vault operations correlated with MCP usage patterns.
Monitoring Expansion Priorities
If expanding this skill's coverage, prioritize based on data access risk:
Priority
Server
Why
How to Monitor
๐ด P1
Copilot Studio built-in M365 MCPs
Email, Teams, admin center access
ai-agent-posture skill + CloudAppEvents
๐ด P1
Security Copilot Agent Creation
Creates autonomous security agents
CloudAppEvents for agent creation events
๐ P2
Power BI Remote MCP
Dataset query access via API
PowerBIActivity table if available
๐ P2
Sentinel Custom MCP Tools
User-defined tools, same audit surface
Already visible in Phase 3 CloudAppEvents
๐ก P3
Fabric RTI MCP
ADX/Eventhouse data access
ADX diagnostic logs
๐ก P3
Kusto Query MCP (Copilot Studio)
KQL from Copilot Studio agents
CloudAppEvents (Copilot Studio workload)
โช P4
Playwright, GitHub, Learn Docs MCPs
Local/public, minimal telemetry
Not monitorable from Sentinel
Note: This catalog reflects the Microsoft MCP ecosystem as of February 2026. The Copilot Studio MCP catalog notes: "This list isn't exhaustive. New MCP connectors are added regularly."
โ MANDATORY: Sentinel Workspace Selection
This skill requires a Sentinel workspace to execute queries. Follow these rules STRICTLY:
When invoked from another skill (e.g., incident-investigation):
Inherit the workspace selection from the parent investigation context
If no workspace was selected in parent context: STOP and ask user to select
When invoked standalone (direct user request):
ALWAYS call list_sentinel_workspaces MCP tool FIRST
If 1 workspace exists: Auto-select, display to user, proceed
If multiple workspaces exist:
Display all workspaces with Name and ID
ASK: "Which Sentinel workspace should I use for this analysis?"
โ STOP AND WAIT for user response
โ DO NOT proceed until user explicitly selects
If a query fails on the selected workspace:
โ DO NOT automatically try another workspace
STOP and report the error, display available workspaces, ASK user to select
๐ด PROHIBITED ACTIONS:
โ Selecting a workspace without user consent when multiple exist
โ Switching to another workspace after a failure without asking
โ Proceeding with analysis if workspace selection is ambiguous
Output Modes
This skill supports two output modes. ASK the user which they prefer if not explicitly specified. Both may be selected.
Mode 1: Inline Chat Summary (Default)
Render the full MCP usage analysis directly in the chat response
Includes ASCII tables, trend charts, endpoint breakdowns, and security assessment
Best for quick review and interactive follow-up questions
Mode 2: Markdown File Report
Save a comprehensive report to reports/mcp-usage/MCP_Usage_Report_<timestamp>.md
All ASCII visualizations render correctly inside markdown code fences (```)
Includes all data from inline mode plus additional detail sections
Use create_file tool โ NEVER use terminal commands for file output
โ Standard markdown tables (| col |) render as formatted tables
Tip: Wrap all ASCII art in triple-backtick code fences for consistent rendering
Scalability & Token Management
This skill was developed in a small lab environment (1โ2 users, single workspace). In larger tenants with many users, MCP servers, and higher query volumes, the query complexity is not a concern โ all queries use summarize, dcount, make_set(..., N), and take operators, so result sets remain bounded regardless of raw table size. Execution time will increase but output shape stays the same.
The primary risk in large environments is LLM token exhaustion during report generation. All query results accumulate in conversation context before the report is written, and this skill file itself consumes significant context. In a large tenant, richer result sets (more users, endpoints, error categories, AppIds) can push past token limits before the report is complete.
Guardrails for Large Environments
1. Tighten result set limits in queries:
Parameter
Small Env (default)
Large Env
make_set(..., N) for users
10
5
make_set(..., N) for endpoints
20โ30
10
make_set(..., N) for errors
5
3
take on governance tables
25
15
take on endpoint rankings
25
15
take on error analysis
50
20
2. Incremental file writes (markdown mode):
Instead of composing the entire report in memory and writing it in one create_file call:
Write the report header and executive summary first with create_file
Append each section (Graph MCP, Sentinel Triage, Data Lake, etc.) using replace_string_in_file to insert content at the end of the file
This allows earlier query results to fall out of active context after being written
3. Two-pass approach for very large tenants:
Pass 1 (Summary): Run all queries with aggressive limits (take 10, make_set(..., 3)). Generate a summary report with top-level numbers only.
Pass 2 (Drill-down): If the user wants detail on a specific section (e.g., "show me the full Data Lake error breakdown"), run targeted queries for that section only.
4. Parallel query batching:
Phases 1โ5 contain independent queries โ always run them in parallel. But avoid running all ~16 queries simultaneously; batch them into 2โ3 groups of 5โ6 queries. This balances throughput against context accumulation.
5. Omit raw query appendix for large reports:
The "Appendix: Query Details" section listing every KQL query used can be omitted in large environments to save tokens. The queries are documented in this skill file and don't need to be repeated in the report.
Indicators You're Hitting Token Limits
Report generation starts but cuts off mid-section
The agent switches to a new conversation turn unexpectedly during report writing
Sections become progressively less detailed toward the end of the report
The agent summarizes findings in chat instead of writing the full markdown file
If any of these occur, ask the agent to: "Continue writing the report from where you left off" โ the incremental file write approach ensures partial progress is saved.
Quick Start (TL;DR)
When a user requests MCP usage monitoring:
Select Workspace โ list_sentinel_workspaces, auto-select or ask
Determine Output Mode โ Ask if not specified: inline, markdown file, or both
Determine Time Range โ Ask if not specified; default 30 days
Run Phase 1 (Graph MCP) โ Daily usage summary, top endpoints, sensitive API access
Run Phase 2 (Sentinel Triage MCP) โ API calls via AppId 7b7b3966, auth events, AH downstream queries
Run Phase 3 (Sentinel Data Lake MCP) โ CloudAppEvents tool usage, error analysis, MCP vs Direct KQL
Run Phase 4 (Azure MCP & ARM) โ ARM operations, resource provider breakdown
Run Phase 5 (Workspace Governance) โ All query sources (Analytics + Data Lake tiers), MCP proportion
Run Phase 6 (Cross-Server User Analysis) โ Top MCP users by server breadth, power user identification
Parallel execution: Phases 1-5 contain independent queries โ run all of them in parallel for performance. Phases 6-7 depend on results from 1-5.
MCP Usage Score Formula
The MCP Usage Score is a composite health and risk indicator that summarizes MCP server activity. Unlike the Drift Score (which is a ratio), this is an absolute assessment based on multiple dimensions.
Business hours: 08:00โ18:00 local time (derive from user's primary sign-in timezone, or use UTC if unknown). Weekends count as off-hours for all 24 hours.
Execution Workflow
Phase 1: Graph MCP Server Analysis
Data source:MicrosoftGraphActivityLogs Filter:AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
Collect:
Execute Query 1 (Unified Daily MCP Activity Trend) via RunAdvancedHuntingQuery โ returns daily Server | Day | Calls | Errors | ErrorRate for ALL 4 MCP servers in one pass. Run this ONCE here; do NOT re-run in Phases 2โ4. Feeds the SVG dashboard Row 5 line chart and volume anomaly detection.
Execute Query 2 (Endpoint & Activity Summary) via RunAdvancedHuntingQuery โ returns per-endpoint rows with call counts, sensitivity flag, off-hours metrics, error rates, and user sets. Replaces former Q2 + Q3 + Q11. Derive: top endpoints (order by CallCount), sensitive APIs (where IsSensitive), off-hours % (sum(OffHoursCalls)/sum(CallCount)).
The Sentinel Triage MCP has a dedicated AppId (7b7b3966-1961-47b5-b080-43ca5482e21c) that appears in both MicrosoftGraphActivityLogs and SigninLogs/AADNonInteractiveUserSignInLogs. This enables definitive attribution of Triage MCP calls โ no heuristics or shared-surface estimation needed.
Key characteristics:
AppDisplayName: "Microsoft Defender Mcp" (visible in SigninLogs)
Auth type: Delegated + certificate (ClientAuthMethod=2) โ user identity always available
Target resources: Microsoft Graph, WindowsDefenderATP
API endpoints: POST /v1.0/security/runHuntingQuery/, GET /security/incidents/, GET /security/alerts_v2/
No local SPN: Microsoft first-party app โ display name only visible in SigninLogs, not in Graph API SPN lookup
๐ต MicrosoftGraphActivityLogs retention varies by environment (depends on Log Analytics workspace configuration and diagnostic settings). Do not assume a fixed retention period โ check with a baseline row count query first.
Collect:
Execute Query 3 to get authentication events by client app (VS Code, Copilot Studio, browser) with user, IP, OS, country
Execute Query 4 to get client app usage breakdown with distinct user counts and last-seen timestamps
Execute Query 5 to get Triage MCP API usage from MicrosoftGraphActivityLogs โ filter by AppId 7b7b3966 for exact Triage MCP calls with endpoint/method/user breakdown
Execute Query 6 to get Triage MCP authentication events from SigninLogs/AADNonInteractiveUserSignInLogs โ sign-in frequency, user attribution, IP, OS, country
Execute Query 7 to get LAQueryLogs for Advanced Hunting downstream queries via fc780465 / M365D_AdvancedHunting. Captures queries from any RunAdvancedHuntingQuery consumer (Triage MCP, Defender portal, Security Copilot) that hit connected LA tables. XDR-native tables (DeviceEvents, EmailEvents) don't appear here.
Phase 3: Sentinel Data Lake MCP Analysis
Data source:CloudAppEvents (Purview unified audit log) Execution tool:RunAdvancedHuntingQuery preferred (30-day lookback, free for Analytics-tier tables). CloudAppEvents uses Timestamp in AH (not TimeGenerated). Fall back to mcp_sentinel-data_query_lake (uses TimeGenerated, 90d retention) only if lookback > 30 days or AH returns errors. Filter:ActionType contains "Sentinel" or ActionType contains "KQL". RecordType is inside RawEventData (not a top-level column) โ extract with parse_json(tostring(RawEventData)).RecordType. RecordType 403 = MCP tools, 379 = Direct KQL.
โ ๏ธ MANDATORY: Execute Query 10 against query_lake before reporting any gap. If the query returns 0 results or table-not-found, THEN report the gap. Do NOT skip this phase based on assumptions about E5 licensing or Purview configuration โ the table may be populated even without explicit Purview setup.
Audit Path: Sentinel Data Lake MCP tools are NOT audited via LAQueryLogs โ they are tracked through Purview unified audit log, surfaced in the CloudAppEvents table. RecordType 403 (inside RawEventData) = Sentinel AI Tool activities, RecordType 379 = KQL activities.
KQL queries executed directly via Sentinel Graph / Data Lake Explorer (no MCP intermediary)
โ ๏ธ Known Limitation (Discovered Mar 2026): RecordType 403 (SentinelAIToolRunCompleted / IMcpToolTemplate) may not be emitted by the Data Lake MCP server. In verified testing, all Data Lake MCP tool calls (query_lake, search_tables) appeared as RecordType 379 with Interface = "InterfaceNotProvided" โ NOT as RecordType 403. When RecordType 403 returns 0 results:
Do NOT report "0 MCP activity" โ the audit pipeline has a gap, not the usage.
Fallback: Use Interface breakdown within RecordType 379. InterfaceNotProvided contains MCP-driven queries. Cross-reference users in InterfaceNotProvided with known Sentinel MCP users from Q4/Q6 (SigninLogs). Known portal interfaces: msglakeexplorer@msec-msg (Portal Data Lake Explorer), msgjobmanagement@msec-msg (scheduled jobs), ipykernel_launcher.py (Jupyter), PowerBIConnector (Power BI), Microsoft.Medeina.Server (Security Copilot).
Report as "Probable MCP" โ clearly note the attribution is based on proxy signal (user overlap), not definitive RecordType 403 classification.
Execute Query 13 to get Azure MCP Server authentication events from SigninLogs/AADNonInteractiveUserSignInLogs โ filter by AppId 04b07795 (Azure CLI credential, field-tested Feb 2026). ๐ Previously documented as AppId 1950a258 (AzurePowerShellCredential) โ that path is obsolete.
Execute Query 14 to get Azure MCP Server workspace queries from LAQueryLogs โ filter by AADClientId 04b07795. RequestClientApp is empty (not a unique fingerprint). Azure MCP appends \n| limit N to query text โ use query text pattern as differentiator.
Detection Method (๐ Updated Feb 2026):
The Azure MCP Server runs as a local .NET process (stdio mode) and authenticates via DefaultAzureCredential. Field-tested Feb 2026: The credential chain now resolves to Azure CLI credential (04b07795-8ddb-461a-bbee-02f9e1bf7b46), NOT AzurePowerShellCredential (1950a258) as previously documented.
Previous fingerprint (OBSOLETE): AppId 1950a258 + RequestClientApp = csharpsdk,LogAnalyticsPSClient. Only 1 occurrence found in 30-day lookback. The Azure MCP Server SDK path has changed.
Current fingerprint (field-tested Feb 2026):
Signal
Azure MCP Server (Current)
Azure CLI (Manual)
Notes
AppId (SigninLogs)
04b07795
04b07795
Shared โ not a unique differentiator
AADClientId (LAQueryLogs)
04b07795
04b07795
Shared
RequestClientApp (LAQueryLogs)
Empty ("")
Empty ("")
Shared โ not a unique differentiator. Empty RequestClientApp is also used by 4+ other AADClientIds
Query text pattern (LAQueryLogs)
Appends \n| limit N to all queries
No standard suffix
โ Best differentiator โ Azure MCP monitor_workspace_log_query always appends a limit operator
AzureActivity (Claims.appid)
04b07795 (write ops only)
04b07795
Shared; read ops not logged. Use Q14 HasLimitSuffix for query-level differentiation
๐จ Key change from previous documentation:
โ RequestClientApp = "csharpsdk,LogAnalyticsPSClient" โ OBSOLETE, no longer produced by Azure MCP Server
โ AppId 04b07795 (Azure CLI) โ current credential path
โ RequestClientApp is empty โ shared with Azure CLI and other tools
โ Query text containing \n| limit โ most reliable query-level differentiator
Disambiguation challenges:
Azure MCP Server queries are difficult to isolate from manual Azure CLI queries in LAQueryLogs because both share the same AppId AND empty RequestClientApp
The \n| limit N suffix appended by monitor_workspace_log_query is the best heuristic but is not guaranteed to be unique
In SigninLogs, UserAgent containing azsdk-net-Identity with OS Microsoft Windows may still help if the credential chain includes Azure Identity SDK components
Consider correlating query timing with known MCP session activity for attribution
Authentication Sequence Observed (Current):
Azure MCP Server acquires token via Azure CLI cached credential
Token is reused for subsequent operations within its lifetime
If MFA claim is missing โ interactive browser prompt (rare with CLI credential)
Subsequent calls reuse the cached token until expiry
๐ด Token Caching Behavior (Field-Tested Feb 2026):
Sign-in events appear at token acquisition time, NOT at each individual API call time
Once a token is cached, subsequent Azure MCP calls (list resources, get configs, etc.) do NOT generate new sign-in events
You will see 1-3 sign-in events per token lifecycle, not one per API call
To count actual API calls, correlate with AzureActivity (write ops) or LAQueryLogs (monitor_workspace_log_query calls)
The ~1hr token lifetime means at most ~24 sign-in event clusters per day of continuous use
AzureActivity visibility: Only ARM write/action/delete operations appear in AzureActivity (Administrative category). Azure MCP Server read-only operations (list subscriptions, list resource groups, list clusters) do NOT appear. Claims.appid = 04b07795 when write operations do occur.
Note: Azure MCP Server is difficult to isolate from manual Azure CLI usage because they share the same AppId and both produce empty RequestClientApp. The \n| limit N query text suffix is the best heuristic for LAQueryLogs. In SigninLogs, the shared AppId means Azure MCP authenticated as Azure CLI โ there is no unique sign-in fingerprint. Present findings as "Azure MCP Server / Azure CLI (shared AppId 04b07795)" in reports.
Phase 5: Workspace Query Governance
Data source:LAQueryLogs (Analytics tier), CloudAppEvents (Data Lake tier) Filter: All AADClientIds (LAQueryLogs), All Sentinel operations (CloudAppEvents)
Collect:
Execute Query 8 to get all clients querying the Analytics tier workspace with query counts, user counts, CPU usage
Data Lake tier query volume from Phase 3 results (Queries 10-12)
MCP proportion calculation: combined MCP query volume (Analytics + Data Lake tiers) / total query volume
Phase 6: Cross-Server User Analysis
Data sources:MicrosoftGraphActivityLogs, CloudAppEvents, SigninLogs, AADNonInteractiveUserSignInLogs
Collect:
Execute Query 9 to get Graph MCP caller attribution โ User vs SPN breakdown
Execute Query 15 to get top MCP users ranked by cross-server breadth โ identifies which users span the most MCP servers and their total call volume
Note: Query 15 joins user activity across all 4 MCP channels (Graph MCP, Triage MCP, Data Lake MCP, Azure CLI/MCP) and resolves UserIds to UPNs via SigninLogs. Data Lake MCP attribution uses InterfaceNotProvided proxy signal when RecordType 403 is unavailable.
Phase 7: Score Computation & Report Generation
Compute per-dimension scores from Phase 1-6 data:
User Diversity: Count distinct users across all MCP channels (use Query 15 cross-server results)
Endpoint Sensitivity: % of Graph MCP calls to sensitive patterns (Phase 1 Query 2 IsSensitive column)
Error Rate: % of non-2xx responses across all MCP channels
Volume Anomaly: Compare most recent day vs rolling average (Phase 1 Query 1 daily data)
Sum dimension scores for composite MCP Usage Score
Include Top MCP Users table in report (Phase 6 โ Query 15 cross-server results)
Generate security assessment with emoji-coded findings
Render output in the user's selected mode
Validate report completeness โ after composing the report, run the Report Completeness Checklist below. Cross-check every required section against the template before saving/presenting. Fix any missing sections before finalizing.
Sample KQL Queries
๐ด MANDATORY: Execute these queries EXACTLY as written. Substitute only the time range parameter (e.g., ago(30d) โ ago(90d)) and entity-specific values where indicated. These queries are schema-verified and encode mitigations for pitfalls documented in Known Pitfalls. Rewriting, paraphrasing, or constructing "equivalent" queries from scratch risks hitting the exact schema issues these queries were designed to avoid.
Action
Status
Rewriting a pre-authored query from scratch
โ PROHIBITED
Removing parse_json() / tostring() wrappers from queries
โ PROHIBITED
Substituting column names without schema verification
โ PROHIBITED
Using has instead of contains for CamelCase fields
โ PROHIBITED
Executing a query not from this section without completing the Pre-Flight Checklist
โ PROHIBITED
Query 1: Unified Daily MCP Activity Trend
Note: Consolidates former Q1 (Graph MCP daily), Q7d (Triage MCP daily), Q23 (Data Lake MCP daily), Q25a (Azure MCP daily) into a single union query.
Feeds: SVG dashboard Row 5 line chart (daily_mcp_trend) โ all 4 series in one query. Tool:mcp_sentinel-data_query_lake (union of SigninLogs + AADNonInteractiveUserSignInLogs fails in AH when AADNonInteractiveUserSignInLogs is on Data Lake tier โ common in customer environments). โ ๏ธ Timestamp: All tables use TimeGenerated in Data Lake (unlike AH where CloudAppEvents uses Timestamp).
// Unified Daily MCP Activity Trend โ all 4 MCP servers in one pass
// Configurable: replace 30d with desired lookback (max 30d for AH)
let lookback = 30d;
// --- Graph MCP (AppId e8c77dc2) ---
let graph_mcp = MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(lookback)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| summarize Calls = count(),
Errors = countif(ResponseStatusCode >= 400)
by Day = bin(TimeGenerated, 1d)
| extend Server = "Graph MCP";
// --- Triage MCP (AppId 7b7b3966) ---
let triage_mcp = MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(lookback)
| where AppId == "7b7b3966-1961-47b5-b080-43ca5482e21c"
| summarize Calls = count(),
Errors = countif(ResponseStatusCode >= 400)
by Day = bin(TimeGenerated, 1d)
| extend Server = "Triage MCP";
// --- Data Lake MCP (CloudAppEvents RecordType 379 + InterfaceNotProvided) ---
let data_lake_mcp = CloudAppEvents
| where TimeGenerated >= ago(lookback)
| where ActionType contains "Sentinel" or ActionType contains "KQL"
| extend RawData = parse_json(tostring(RawEventData))
| extend RecordType = toint(RawData.RecordType),
Interface = tostring(RawData.Interface),
FailureReason = tostring(RawData.FailureReason)
| where RecordType == 379 and (Interface == "InterfaceNotProvided" or isempty(Interface))
| summarize Calls = count(),
Errors = countif(isnotempty(FailureReason) and FailureReason != "")
by Day = bin(TimeGenerated, 1d)
| extend Server = "Data Lake MCP";
// --- Azure MCP/CLI (AppId 04b07795 โ shared with Azure CLI) ---
let azure_interactive = SigninLogs
| where TimeGenerated >= ago(lookback)
| where AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| project TimeGenerated, ResultType;
let azure_noninteractive = AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(lookback)
| where AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| project TimeGenerated, ResultType;
let azure_mcp = union azure_interactive, azure_noninteractive
| summarize Calls = count(),
Errors = countif(ResultType != "0" and ResultType != "")
by Day = bin(TimeGenerated, 1d)
| extend Server = "Azure MCP/CLI";
// --- Union all servers ---
union graph_mcp, triage_mcp, data_lake_mcp, azure_mcp
| extend ErrorRate = iff(Calls > 0, round(100.0 * Errors / Calls, 1), 0.0)
| project Server, Day, Calls, Errors, ErrorRate
| order by Day asc, Server asc
Replaces: former Q2 (Top Endpoints), Q3 (Sensitive API Access), Q11 (Off-Hours Activity). Tool:RunAdvancedHuntingQuery Report derivation: Top endpoints = all rows by CallCount desc. Sensitive endpoints = where IsSensitive. Off-hours % = sum(OffHoursCalls) / sum(CallCount) across all rows.
Tool:RunAdvancedHuntingQuery (30-day lookback, free for Analytics-tier tables). Fall back to mcp_sentinel-data_query_lake only if lookback > 30 days. โ ๏ธ Pitfall-aware: Uses parse_json(Status) and parse_json(DeviceDetail) wrappers โ required for Data Lake (string columns) and safe in AH. Uses = syntax (not as) in project โ see project as Keyword Fails in Advanced Hunting.
// Who is authenticating to Sentinel MCP (via VS Code, Copilot Studio, browser)
SigninLogs
| where TimeGenerated >= ago(30d)
| where ResourceDisplayName =~ "Sentinel Platform Services"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId,
ResourceDisplayName, IPAddress,
ErrorCode = tostring(parse_json(Status).errorCode),
ConditionalAccessStatus, AuthenticationRequirement, ClientAppUsed,
OS = tostring(parse_json(DeviceDetail).operatingSystem),
Country = tostring(parse_json(LocationDetails).countryOrRegion)
| order by TimeGenerated desc
Query 4: Sentinel MCP โ Client App Breakdown
Tool:RunAdvancedHuntingQuery (30-day lookback, free for Analytics-tier tables).
// Which client apps (VS Code, Copilot Studio, browser) are accessing Sentinel MCP
SigninLogs
| where TimeGenerated >= ago(30d)
| where ResourceDisplayName =~ "Sentinel Platform Services"
| summarize
SignInCount = count(),
DistinctUsers = dcount(UserPrincipalName),
Users = make_set(UserPrincipalName, 10),
LastSeen = max(TimeGenerated)
by AppDisplayName, AppId, ClientAppUsed
| order by SignInCount desc
Tool:mcp_sentinel-data_query_lake (union of SigninLogs + AADNonInteractiveUserSignInLogs fails in AH when AADNonInteractiveUserSignInLogs is on Data Lake tier โ common in customer environments). โ ๏ธ Pitfall-aware: Uses parse_json() wrappers on DeviceDetail/LocationDetails โ required for Data Lake (string columns). Uses = syntax (not as) in project.
// Triage MCP authentication events from SigninLogs + AADNonInteractiveUserSignInLogs.
// AppId 7b7b3966 = "Microsoft Defender Mcp" โ delegated auth with certificate.
// Uses parse_json() wrappers for DeviceDetail/LocationDetails (safe in both AH and Data Lake).
let triage_mcp_appid = "7b7b3966-1961-47b5-b080-43ca5482e21c";
let signinlogs_interactive = SigninLogs
| where TimeGenerated >= ago(30d)
| where AppId == triage_mcp_appid
| extend SignInType = "Interactive"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId,
ResourceDisplayName, IPAddress,
ResultType = tostring(ResultType),
ResultDescription = tostring(ResultDescription),
SignInType,
OS = tostring(parse_json(DeviceDetail).operatingSystem),
Browser = tostring(parse_json(DeviceDetail).browser),
Country = tostring(parse_json(LocationDetails).countryOrRegion),
City = tostring(parse_json(LocationDetails).city);
let signinlogs_noninteractive = AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(30d)
| where AppId == triage_mcp_appid
| extend SignInType = "NonInteractive"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId,
ResourceDisplayName, IPAddress,
ResultType = tostring(ResultType),
ResultDescription = tostring(ResultDescription),
SignInType,
OS = tostring(parse_json(DeviceDetail).operatingSystem),
Browser = tostring(parse_json(DeviceDetail).browser),
Country = tostring(parse_json(LocationDetails).countryOrRegion),
City = tostring(parse_json(LocationDetails).city);
union signinlogs_interactive, signinlogs_noninteractive
| summarize
SignIns = count(),
DistinctUsers = dcount(UserPrincipalName),
Users = make_set(UserPrincipalName, 10),
IPs = make_set(IPAddress, 10),
Countries = make_set(Country, 10),
LastSeen = max(TimeGenerated)
by AppDisplayName, SignInType, ResourceDisplayName
| order by SignIns desc
AgenticApp โ confirms this is an agent application
AIAgentBuilder โ agent was created by an AI agent builder platform
AgentCreatedBy:CopilotStudio โ specifically created by Copilot Studio
AgenticInstance โ runtime instance of an agent
power-virtual-agents-* โ Copilot Studio internal tracking tag
Fallback: Check servicePrincipalType โ if it equals "Agent", it is a registered Agent Identity. Note: as of Feb 2026, Copilot Studio agents still show "Application" here despite being true agents.
Name-based filtering is UNRELIABLE โ SPNs with "Agent" in display name may be standard app registrations (e.g., "Contoso Agent Tools" = GitCreatedApp).
Use microsoft_graph_suggest_queries โ microsoft_graph_get for the Graph API calls. Query multiple SPNs in one call: /beta/servicePrincipals?$count=true&$filter=id in ('id1','id2')&$select=id,appId,displayName,servicePrincipalType,tags.
Query 10: Data Lake MCP โ Access Pattern Summary
Note: Consolidates former Q20 (Tool Usage Summary) + Q24 (MCP vs Direct KQL Delineation) into a single query.
Tool:RunAdvancedHuntingQuery (uses Timestamp for CloudAppEvents). โ ๏ธ Pitfall-aware: Uses contains (not has) for ActionType/Operation โ see CloudAppEvents CamelCase Matching. Uses parse_json(tostring(RawEventData)) โ see CloudAppEvents RawEventData Parsing. Filters on SentinelAIToolRunCompleted only โ see CloudAppEvents Double-Counting Prevention.
// Data Lake MCP โ single-pass access pattern delineation + tool/table/workspace inventory
// Combines former Q20 (summary) and Q24 (delineation) into one query
CloudAppEvents
| where Timestamp >= ago(30d)
| where ActionType contains "Sentinel" or ActionType contains "KQL"
| extend RawData = parse_json(tostring(RawEventData))
| extend
Operation = tostring(RawData.Operation),
RecordType = toint(RawData.RecordType),
ToolName = tostring(RawData.ToolName),
Interface = tostring(RawData.Interface),
ExecutionDuration = todouble(RawData.ExecutionDuration),
FailureReason = tostring(RawData.FailureReason),
TablesRead = tostring(RawData.TablesRead),
DatabasesRead = tostring(RawData.DatabasesRead),
TotalRows = toint(RawData.TotalRows),
UserId_raw = tostring(RawData.UserId),
InputParams = tostring(RawData.InputParameters)
| extend
AccessPattern = case(
RecordType == 403 and Interface == "IMcpToolTemplate", "MCP Server-Driven",
RecordType == 379 and (Interface == "InterfaceNotProvided" or isempty(Interface)), "MCP-Driven (Probable)",
RecordType == 379 and Interface has "msglakeexplorer", "Portal (Data Lake Explorer)",
RecordType == 379 and Interface has "msgjobmanagement", "Scheduled Jobs",
RecordType == 379, "Other Direct KQL",
"Other"),
IsSuccess = isempty(FailureReason) or FailureReason == "",
HasKQLQuery = InputParams has "query"
| where Operation contains "Completed" or RecordType == 379 // 'contains' not 'has' โ CamelCase
| summarize
TotalCalls = count(),
SuccessCount = countif(IsSuccess),
FailureCount = countif(not(IsSuccess)),
DistinctTools = dcount(ToolName),
Tools = make_set(ToolName, 20),
DistinctTables = dcount(TablesRead),
Tables = make_set(TablesRead, 30),
Workspaces = make_set(DatabasesRead, 5),
AvgDurationSec = round(avg(ExecutionDuration), 2),
TotalRowsReturned = sum(TotalRows),
DistinctUsers = dcount(UserId_raw),
Users = make_set(UserId_raw, 10),
KQLQueryCount = countif(HasKQLQuery),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by AccessPattern
| extend ErrorRate = round(100.0 * FailureCount / TotalCalls, 1)
| order by TotalCalls desc
Post-processing for Query 10:
If MCP Server-Driven (RecordType 403) has results โ use it directly as the definitive MCP count.
If MCP Server-Driven returns 0 rows but MCP-Driven (Probable) has results โ report the probable count with the audit gap caveat. Cross-reference users with Q4/Q6 SigninLogs to validate.
Portal (Data Lake Explorer) = msglakeexplorer@msec-msg interface, Scheduled Jobs = msgjobmanagement@msec-msg.
Combine with Query 8 (Analytics tier LAQueryLogs โ all workspace sources) for a complete two-tier governance view:
Tier
Data Source
MCP Sources
Non-MCP Sources
Analytics Tier
LAQueryLogs
AH backend fc780465 / M365D_AdvancedHunting(captures AH queries from Triage MCP, Defender portal, Security Copilot that hit connected LA tables; shared surface, see Query 7)
Azure MCP Server (04b07795, empty RequestClientApp, query text `\n
limit N` suffix)
Query 11: Data Lake MCP โ Interface Breakdown
Tool:RunAdvancedHuntingQuery (uses Timestamp for CloudAppEvents). โ ๏ธ Pitfall-aware: Uses contains/parse_json(tostring()) pattern โ see Query 10 pitfall notes. Uses todouble(ExecutionDuration) โ see Data Lake MCP ExecutionDuration Format. When RecordType 403 is present, groups by ToolName; when absent, falls back to Interface field.
// Breakdown of Data Lake access by Interface โ identifies MCP vs Portal vs Jobs
// PRIMARY: Uses RecordType 403 / ToolName when available (MCP audit events)
// FALLBACK: When RecordType 403 absent, groups by Interface field from RecordType 379
// - InterfaceNotProvided = probable MCP-driven (cross-ref with Q4/Q6 SigninLogs)
// - msglakeexplorer@msec-msg = Sentinel Portal Data Lake Explorer
// - msgjobmanagement@msec-msg = Scheduled/job-based queries
// - ipykernel_launcher.py = Jupyter Notebook
// - PowerBIConnector = Power BI
// - Microsoft.Medeina.Server = Security Copilot
CloudAppEvents
| where Timestamp >= ago(30d)
| where ActionType contains "Sentinel" or ActionType contains "KQL"
| extend RawData = parse_json(tostring(RawEventData))
| extend
Operation = tostring(RawData.Operation),
RecordType = toint(RawData.RecordType),
ToolName = tostring(RawData.ToolName),
Interface = tostring(RawData.Interface),
ExecutionDuration = todouble(RawData.ExecutionDuration),
FailureReason = tostring(RawData.FailureReason),
TablesRead = tostring(RawData.TablesRead),
UserId_raw = tostring(RawData.UserId)
| where Operation contains "Completed" or RecordType == 379
| extend
// When RecordType 403 exists, ToolName is the grouping key; otherwise use Interface
GroupKey = iff(RecordType == 403, coalesce(ToolName, "unknown_tool"), coalesce(Interface, "InterfaceNotProvided")),
IsSuccess = isempty(FailureReason) or FailureReason == "",
Source = iff(RecordType == 403, "MCP Tool (RecordType 403)", "Interface (RecordType 379)")
| summarize
CallCount = count(),
SuccessCount = countif(IsSuccess),
FailureCount = countif(not(IsSuccess)),
AvgDurationSec = round(avg(ExecutionDuration), 2),
MaxDurationSec = round(max(ExecutionDuration), 2),
TablesAccessed = make_set(TablesRead, 20),
DistinctUsers = dcount(UserId_raw),
Users = make_set(UserId_raw, 10),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by GroupKey, Source
| extend ErrorRate = round(100.0 * FailureCount / CallCount, 1)
| order by CallCount desc
Query 12: Data Lake MCP โ Error Analysis
Tool:RunAdvancedHuntingQuery (uses Timestamp for CloudAppEvents). โ ๏ธ Pitfall-aware: Uses contains/parse_json(tostring()) pattern โ see Query 10 pitfall notes. Now groups errors by both AccessPattern (MCP vs Portal vs Jobs) and ErrorCategory for richer diagnostics.
// Analyze failed Data Lake queries โ identify schema errors, permission issues, etc.
// PRIMARY: Filters on ActionType contains "SentinelAITool" (RecordType 403) when available
// FALLBACK: When RecordType 403 absent, analyzes all failed RecordType 379 events grouped by Interface
CloudAppEvents
| where Timestamp >= ago(30d)
| where ActionType contains "Sentinel" or ActionType contains "KQL"
| extend RawData = parse_json(tostring(RawEventData))
| extend
Operation = tostring(RawData.Operation),
RecordType = toint(RawData.RecordType),
ToolName = tostring(RawData.ToolName),
Interface = tostring(RawData.Interface),
FailureReason = tostring(RawData.FailureReason),
TablesRead = tostring(RawData.TablesRead),
UserId_raw = tostring(RawData.UserId)
| where Operation contains "Completed" or RecordType == 379
| where isnotempty(FailureReason) and FailureReason != ""
| extend
AccessPattern = case(
RecordType == 403 and Interface == "IMcpToolTemplate", "MCP Server-Driven",
RecordType == 379 and (Interface == "InterfaceNotProvided" or isempty(Interface)), "MCP-Driven (Probable)",
RecordType == 379 and Interface has "msglakeexplorer", "Portal (Data Lake Explorer)",
RecordType == 379 and Interface has "msgjobmanagement", "Scheduled Jobs",
RecordType == 379, "Other Direct KQL",
"Other"),
ErrorCategory = case(
FailureReason has "SemanticError", "Schema/Semantic Error",
FailureReason has "SyntaxError", "KQL Syntax Error",
FailureReason has "Unauthorized" or FailureReason has "403", "Permission Denied",
FailureReason has "Timeout", "Query Timeout",
FailureReason has "NotFound", "Table/Resource Not Found",
"Other Error")
| summarize
ErrorCount = count(),
Tools = make_set(ToolName, 10),
Tables = make_set(TablesRead, 10),
Users = make_set(UserId_raw, 10),
SampleErrors = make_set(substring(FailureReason, 0, 150), 5),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by AccessPattern, ErrorCategory
| order by AccessPattern asc, ErrorCount desc
Query 13: Azure MCP Server โ Authentication Events (SigninLogs)
// Detect Azure MCP Server authentication events via Azure CLI AppId.
//
// ๐ UPDATED Feb 2026: Azure MCP Server now uses Azure CLI credential (04b07795),
// NOT AzurePowerShellCredential (1950a258) as previously documented.
// The old AppId 1950a258 + UserAgent 'azsdk-net-Identity' fingerprint is OBSOLETE.
//
// โ ๏ธ SHARED APPID: 04b07795 is the Azure CLI AppId โ shared with manual 'az' CLI usage.
// There is NO unique sign-in fingerprint for Azure MCP Server vs manual Azure CLI.
// This query returns ALL Azure CLI sign-ins. Correlate with LAQueryLogs (Query 14)
// for query-level attribution via the '\n| limit N' text pattern.
//
// NOTE: Sign-in events represent TOKEN ACQUISITIONS, not individual API calls.
// A cached token serves many Azure MCP calls with no additional sign-in events.
// FIX (Feb 2026): Explicit tostring() casts on ResultType, ResultDescription,
// ConditionalAccessStatus, AuthenticationRequirement to prevent union type mismatches
// between SigninLogs and AADNonInteractiveUserSignInLogs. Removed ResourceId (inconsistent
// across tables). Use parse_json() wrapper on DeviceDetail and LocationDetails โ these
// columns may be stored as string (not dynamic) in Data Lake workspaces, causing
// SemanticError on dot-notation access without parse_json().
let azure_mcp_appid = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
let signinlogs_interactive = SigninLogs
| where TimeGenerated >= ago(90d)
| where AppId == azure_mcp_appid
| extend SignInType = "Interactive"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId,
ResourceDisplayName, IPAddress,
ResultType = tostring(ResultType),
ResultDescription = tostring(ResultDescription),
UserAgent, SignInType,
ConditionalAccessStatus = tostring(ConditionalAccessStatus),
AuthenticationRequirement = tostring(AuthenticationRequirement),
OS = tostring(parse_json(DeviceDetail).operatingSystem),
Country = tostring(parse_json(LocationDetails).countryOrRegion);
let signinlogs_noninteractive = AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(90d)
| where AppId == azure_mcp_appid
| extend SignInType = "Non-Interactive"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId,
ResourceDisplayName, IPAddress,
ResultType = tostring(ResultType),
ResultDescription = tostring(ResultDescription),
UserAgent, SignInType,
ConditionalAccessStatus = tostring(ConditionalAccessStatus),
AuthenticationRequirement = tostring(AuthenticationRequirement),
OS = tostring(parse_json(DeviceDetail).operatingSystem),
Country = tostring(parse_json(LocationDetails).countryOrRegion);
union signinlogs_interactive, signinlogs_noninteractive
| order by TimeGenerated desc
Query 14: Azure MCP Server โ Workspace Queries (LAQueryLogs)
// Detect Azure MCP Server workspace queries via LAQueryLogs.
//
// ๐ UPDATED Feb 2026: Azure MCP Server now uses Azure CLI credential (04b07795).
// RequestClientApp is EMPTY (not 'csharpsdk,LogAnalyticsPSClient' as previously documented).
//
// โ ๏ธ SHARED FINGERPRINT: Empty RequestClientApp + AppId 04b07795 is shared with manual
// Azure CLI and 4+ other AADClientIds. This query returns ALL queries from AppId 04b07795
// with empty RequestClientApp. To isolate Azure MCP Server queries, look for the
// '\n| limit N' suffix that monitor_workspace_log_query always appends to query text.
//
// 30-day pattern analysis (Feb 2026) showed 11 distinct RequestClientApp values:
// - Empty ("") = 417 queries across 5 AADClientIds (Azure MCP, Sentinel DL MCP, Portal, etc.)
// - "csharpsdk,LogAnalyticsPSClient" = only 1 query ever (obsolete fingerprint)
// - "M365D_AdvancedHunting" = Advanced Hunting backend
// - "ASI_Portal" / "ASI_Portal_Connectors" = Sentinel Portal
// - Others: AppInsightsPortalExtension, LogicApps, PSClient, etc.
let azure_cli_appid = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
LAQueryLogs
| where TimeGenerated >= ago(90d)
| where AADClientId == azure_cli_appid
| extend HasLimitSuffix = QueryText has "\n| limit" or QueryText has "\r\n| limit"
| project TimeGenerated, AADEmail, AADClientId,
RequestClientApp,
QueryTextTruncated = substring(QueryText, 0, 300),
ResponseCode, ResponseRowCount,
StatsCPUTimeMs,
RequestTarget,
HasLimitSuffix
| order by TimeGenerated desc
Post-processing: Rows with HasLimitSuffix = true are highly likely Azure MCP Server queries (the monitor_workspace_log_query command always appends | limit N). Rows without the suffix may be manual Azure CLI or other tools using the same credential.
Query 15: Top MCP Users โ Cross-Server Breadth
Tool:RunAdvancedHuntingQuery (7-day lookback default, all tables on Analytics tier).
Purpose: Identifies users with the broadest MCP footprint โ ranking by how many distinct MCP server types they use and their total call volume across all channels. Feeds the Top MCP Users report section and SVG dashboard widget.
let lookback = 7d;
let graph_mcp = MicrosoftGraphActivityLogs
| where TimeGenerated > ago(lookback)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| where isnotempty(UserId)
| summarize Calls = count() by UserId
| project UserId, Server = "Graph MCP", Calls;
let triage_mcp = MicrosoftGraphActivityLogs
| where TimeGenerated > ago(lookback)
| where AppId == "7b7b3966-1961-47b5-b080-43ca5482e21c"
| where isnotempty(UserId)
| summarize Calls = count() by UserId
| project UserId, Server = "Triage MCP", Calls;
let datalake_mcp = CloudAppEvents
| where Timestamp > ago(lookback)
| where ActionType contains "Sentinel" or ActionType contains "KQL"
| extend RawData = parse_json(tostring(RawEventData))
| where tostring(RawData.Interface) == "InterfaceNotProvided" or isempty(tostring(RawData.Interface))
| where isnotempty(AccountObjectId)
| summarize Calls = count() by UserId = AccountObjectId
| project UserId, Server = "Data Lake MCP", Calls;
let azure_mcp = union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(lookback)
| where AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| where isnotempty(UserId)
| summarize Calls = count() by UserId
| project UserId, Server = "Azure CLI/MCP", Calls;
let upn_map = union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(lookback)
| where isnotempty(UserPrincipalName)
| summarize arg_max(TimeGenerated, UserPrincipalName) by UserId
| project UserId, UPN = UserPrincipalName;
union graph_mcp, triage_mcp, datalake_mcp, azure_mcp
| summarize Servers = make_set(Server), ServerCount = dcount(Server), TotalCalls = sum(Calls) by UserId
| join kind=leftouter upn_map on UserId
| project UPN = coalesce(UPN, UserId), ServerCount, Servers, TotalCalls
| sort by ServerCount desc, TotalCalls desc
| take 25
โ ๏ธ Pitfall-aware:
Data Lake MCP leg: Uses ActionType contains (not has) per the CamelCase pitfall. Parses RawEventData once and filters on Interface field for the InterfaceNotProvided proxy signal when RecordType 403 is unavailable (see Phase 3 Known Limitation).
Azure CLI/MCP leg: Uses shared AppId 04b07795 โ includes both Azure MCP Server and manual az CLI sign-ins. Cannot distinguish at this level.
UPN resolution: Joins with SigninLogs to resolve UserId GUIDs to human-readable UPNs. Users with no recent sign-ins will show their GUID instead.
CloudAppEvents timestamp: Uses Timestamp (not TimeGenerated) since this runs via Advanced Hunting.
AADNonInteractiveUserSignInLogs tier: If this table is on Data Lake/Basic tier, the union SigninLogs, AADNonInteractiveUserSignInLogs legs may fail in AH. Fall back to mcp_sentinel-data_query_lake if needed (switch Timestamp โ TimeGenerated for the CloudAppEvents leg).
Post-processing:
Render as a ranked table in the report: | Rank | User (UPN) | Servers Used | MCP Servers | Total Calls |
Server Landscape table โ one row per MCP server with: Server, API Calls, Auth Events, Distinct Users, Error Rate, Status. This table feeds the SVG server_landscape widget directly.
Consolidated KPI block โ aggregate totals across all servers:
Total MCP API Calls: <sum of API calls across Graph + Triage + Data Lake + Azure>
Total Auth Events: <sum of auth events across Triage + Azure + Platform Services>
Distinct MCP Users: <deduplicated count or max across channels>
Active MCP Servers: <count of server types with >0 activity>
Combined MCP Query Share: <MCP queries / total workspace queries %>
Sensitive API Rate: <sensitive / total Graph MCP calls %>
These values are derived from Phase 1-5 query results and MUST be rendered as a single block for SVG extraction. Do not scatter them across per-server sections only.
Graph MCP Server Analysis
Daily usage trend (ASCII bar chart showing requests/day โ from Query 1 unified trend, Graph MCP series)
Top endpoints table (endpoint, call count, % of total, last used)
Sensitive API access summary with user attribution
Caller attribution (User vs SPN vs Agent โ from Query 9)
Sentinel Triage MCP Analysis
Triage MCP API calls from MicrosoftGraphActivityLogs โ filtered by dedicated AppId 7b7b3966 ("Microsoft Defender Mcp")
Daily usage trend (ASCII bar chart showing calls/day โ from Query 1 unified trend, Triage MCP series)
Triage MCP authentication events from SigninLogs/AADNonInteractiveUserSignInLogs โ sign-in frequency, user attribution, IP, country
User attribution table with sign-in type breakdown