askill
mcp-usage-monitoring

mcp-usage-monitoringSafety 95Repository

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", "AI agent monitoring", "MCP audit", "tool usage monitoring", "MCP breakdown", "who is using MCP", or when investigating AI agent access patterns, 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, sensitive API detection, workspace query governance, and security risk assessment with inline and markdown file reporting.

50 stars
1.2k downloads
Updated 3/5/2026

Package Files

Loading files...
SKILL.md

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 ServerTelemetry SourceKey Identifier
Microsoft Graph MCP ServerMicrosoftGraphActivityLogsAppId = e8c77dc2-69b3-43f4-bc51-3213c9d915b4
Sentinel Data Lake MCPCloudAppEventsRecordType 403, Interface = IMcpToolTemplate
Sentinel Triage MCPMicrosoftGraphActivityLogs + SigninLogsAppId = 7b7b3966-1961-47b5-b080-43ca5482e21c ("Microsoft Defender Mcp") — dedicated AppId with full user attribution via delegated cert auth
Azure MCP ServerAzureActivityNo dedicated AppId — uses DefaultAzureCredential
Sentinel Data Lake — Direct KQLCloudAppEventsRecordType 379, Operation = KQLQueryCompleted
Workspace Query Sources (Analytics Tier)LAQueryLogsAll clients querying Log Analytics workspace

What this skill detects:

  • Graph API call volume, trends, and endpoint diversity via MCP
  • Sensitive/high-risk Graph endpoint access (PIM, credentials, Identity Protection)
  • Sentinel workspace query patterns by client application
  • User vs. Service Principal vs. Agent Identity attribution across all MCP channels
  • Entra Agent ID detection — distinguishes AI agent identities from standard SPNs and human users (see Agent Identity Detection)
  • Azure ARM operations potentially originating from Azure MCP Server
  • Non-MCP platform query sources for governance context (Sentinel Engine, Logic Apps)
  • 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.


📑 TABLE OF CONTENTS

  1. Critical Workflow Rules - Start here!
  2. Agent Identity Detection - Entra Agent ID vs SPN vs User
  3. Extended MCP Server Landscape - Full Microsoft MCP ecosystem catalog
  4. Output Modes - Inline chat vs. Markdown file
  5. Scalability & Token Management - Guidance for large environments
  6. Quick Start - 8-step investigation pattern
  7. MCP Usage Score Formula - Composite health & risk scoring
  8. Execution Workflow - Complete 6-phase process
  9. Sample KQL Queries - Validated query patterns
  10. Report Template - Output format specification
  11. Proactive Alerting — KQL Data Lake Jobs - Scheduled anomaly detection
  12. Known Pitfalls - Edge cases and false positives
  13. Error Handling - Troubleshooting guide

⚠️ CRITICAL WORKFLOW RULES - READ FIRST ⚠️

Before starting ANY MCP usage monitoring analysis:

  1. ALWAYS enforce Sentinel workspace selection (see Workspace Selection section below)
  2. ALWAYS ask the user for output mode if not specified: inline chat summary or markdown file report (or both)
  3. ALWAYS ask the user for time range if not specified: default to 30 days, configurable
  4. ALWAYS query all MCP telemetry surfaces — do not skip any MCP server type
  5. ALWAYS include non-MCP workspace context (Sentinel Engine, Logic Apps) for governance proportion analysis
  6. ALWAYS run independent queries in parallel for performance
  7. ALWAYS attribute activity to specific users — never present anonymous aggregates
  8. NEVER conflate non-MCP platform activity with MCP activity — clearly label categories
  9. ALWAYS check for Agent Identity (Entra Agent ID) callers — distinguish AI agents from human users and standard SPNs (see Agent Identity Detection)
  10. 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

Agent Identity Detection

Microsoft Entra Agent ID (public preview) introduces a new first-class identity type for AI agents, distinct from traditional service principals and user accounts. MCP servers may be called by human users, standard SPNs, or Agent Identities — and the telemetry to distinguish them varies by table.

Reference: What are agent identities | Agent sign-in and audit logs | Agent OAuth protocols

Entra Identity Taxonomy

Identity CategoryTypesMCP Usage Pattern
Human identitiesUser accounts (workforce, external)User signs into VS Code → MCP server acts on behalf of user (delegated)
Workload identitiesApp registrations, service principals, managed identitiesStandard SPN authenticates to MCP (client credentials / app-only)
🆕 Agent identitiesAgent Identity, Agent Identity Blueprint, Agent UserAI agent uses MCP via Agent OBO, Autonomous App, or Agent User flow

Agent ID Object Types

ObjectWhat It IsAnalogy
Agent Identity BlueprintReusable template defining a class of agent (e.g., "SOC Triage Agent")Like an app registration
Agent Identity Blueprint PrincipalSPN created when a blueprint is instantiated in a tenantLike the SPN for an app registration
Agent IdentitySpecial SPN with subtype agent — represents a single agent instanceLike a service principal, but agent-specific
Agent UserOptional user account paired 1:1 with an agent identity (for mailbox, Teams, etc.)Like a user account, but non-human

Architectural detail: Agent identities are modeled as single-tenant service principals with an "agent" subtype. The objectId and appId always have the same value (unlike regular app registrations where they differ).

Agent Authentication Flows

FlowPatternWhen UsedTelemetry Impact
Agent On-Behalf-Of (OBO)Delegated — agent acts on behalf of a signed-in userUser-initiated agents, copilotsUserId = human user, ServicePrincipalId = agent SPN
Autonomous App FlowApp-only — agent acts under its own authority (client_credentials)Background agents, scheduled tasksServicePrincipalId = agent SPN, UserId empty
Agent User FlowAgent uses its paired agent user accountAgents needing mailbox, Teams accessUserId = Agent User (non-human with UPN)

How to Detect Agent Identities in Telemetry

⚠️ Known Limitation (Preview): MicrosoftGraphActivityLogs does NOT yet distinguish Agent IDs from regular SPNs. Requests from agent identities appear as regular applications with the agent identity in the AppId column. Requests from agent users appear as regular users with the agent user ID in UserId.

Detection Strategy:

Telemetry SourceDetection MethodReliabilityStatus
Graph API /beta tagsQuery /beta/servicePrincipals/{id}?$select=tags — check for AgenticApp, AIAgentBuilder, AgentCreatedBy:CopilotStudio tags🟢 Most reliable✅ Available now
AuditLogsAgent lifecycle CRUD operations — filter OperationName has "agent" or InitiatedBy = Power Virtual Agents Service🟢 High✅ Available now
AADServicePrincipalSignInLogsAgent sign-ins to resources like Bot Framework from Azure internal IPs (fd00:*)🟢 High✅ Available now
MicrosoftGraphActivityLogsJoin with sign-in logs to identify agent callers by AppId/ServicePrincipalId🟡 Requires cross-ref⚠️ Workaround needed
SigninLogsNew agentSignIn resource type; filter by agent/agentType🟡 Preview-dependent✅ Available in preview
Graph API servicePrincipalTypeGET /servicePrincipals?$filter=servicePrincipalType eq 'Agent'🔴 Not yet reliable — Copilot Studio agents still show "Application"⚠️ Preview gap

🔵 Field-tested finding: The servicePrincipalType property does NOT yet show "Agent" for Copilot Studio-created agents — they still return "Application" on both /v1.0 and /beta. The tags array on /beta is currently the only reliable programmatic indicator. Key tags: AgenticApp, AIAgentBuilder, AgentCreatedBy:CopilotStudio, AgenticInstance.

Sign-in log filters for agents:

  • Portal: Filter by Agent type = Agent Identity or Agent ID user and Is Agent = Yes
  • Graph API: $filter=signInEventTypes/any(t: t eq 'servicePrincipal') and agent/agentType eq 'AgentIdentity'

MicrosoftGraphActivityLogs — User vs SPN vs Agent Attribution

The MicrosoftGraphActivityLogs table has two identity fields that determine "who" is calling Graph APIs:

FieldPopulated WhenIdentity Type
UserIdA human user (or Agent User) made the request via delegated permissionsUser / Agent User
ServicePrincipalIdA service principal (or Agent Identity) made the request via app-only permissionsSPN / Agent Identity
ClientAuthMethodAlways populated — indicates how the caller authenticated0 = public client (user), 1 = client secret (SPN), 2 = certificate (SPN)
RolesPopulated when app-only (application permissions)App-only flow indicator
ScopesPopulated when delegated (user context)Delegated flow indicator

Decision tree for caller attribution:

IF ServicePrincipalId is populated AND UserId is empty:
   → App-only flow: SPN or Agent Identity is the autonomous caller
   → Check ClientAuthMethod: 1 = secret, 2 = certificate
   → Cross-reference ServicePrincipalId with Entra to determine if Agent Identity

IF UserId is populated AND ServicePrincipalId is populated:
   → Delegated flow: User (or Agent User) signed in, SPN is the client app
   → Agent OBO: Agent acts on behalf of user — both fields populated

IF UserId is populated AND ServicePrincipalId is empty:
   → Direct user-delegated flow (e.g., VS Code → Graph MCP)
   → Check if UserId maps to an Agent User (non-human account with UPN)

Products Already Using Agent ID

ProductAgent ID UsageTelemetry Fingerprint (Field-Tested)
Copilot Studio agentsEach created agent gets an SPN with agentic tags automatically. Initiated by Power Virtual Agents Service. Your user gets added as owner.AuditLogs: InitiatedBy = Power Virtual Agents Service, TargetType = ServicePrincipal. SPN sign-ins to Bot Framework from Azure internal IPv6 (fd00:34f2:*). Tags: AgenticApp, AgentCreatedBy:CopilotStudio, AgenticInstance
Entra CA Optimization AgentMicrosoft first-party agent identityMay appear in tenant sign-in logs
Custom AI agentsDevelopers can register agents via Agent Identity BlueprintWill appear as SPNs with agent subtype

⚠️ Name ambiguity warning: An SPN named with "Agent" in its display name does NOT mean it's an Agent Identity. Example: "Contoso Agent Tools" is a standard GitCreatedApp with servicePrincipalType: Application and no agentic tags. Always verify via /beta tags — never rely on display name alone.

Impact on This Skill

When running MCP usage monitoring:

  1. Phase 1 (Graph MCP): Check BOTH UserId and ServicePrincipalId in Query 1 results — if ServicePrincipalId is populated with a non-empty value, an SPN or Agent Identity is calling Graph MCP autonomously
  2. New Phase (Agent Detection): Run Query 17 to identify Agent Identities in the tenant and cross-reference against MCP callers
  3. Report section: Include a dedicated "Agent vs User Attribution" breakdown showing how many MCP calls originated from humans, standard SPNs, and Agent Identities
  4. Security flag: Agent Identities calling sensitive Graph endpoints autonomously (without user OBO context) should be flagged 🟠 — verify authorization

Known AppIds Reference

MCP Servers & AI Agents

AppIdServiceTelemetry TableNotes
e8c77dc2-69b3-43f4-bc51-3213c9d915b4Microsoft Graph MCP Server for EnterpriseMicrosoftGraphActivityLogsRead-only Graph API proxy
7b7b3966-1961-47b5-b080-43ca5482e21cSentinel Triage MCP ("Microsoft Defender Mcp")MicrosoftGraphActivityLogs, SigninLogs, AADNonInteractiveUserSignInLogsMicrosoft 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-1381ec4306ebSentinel Platform Services App RegSigninLogsSentinel-hosted MCP platform
04b07795-8ddb-461a-bbee-02f9e1bf7b46Azure MCP Server (local stdio via DefaultAzureCredential → Azure CLI)SigninLogs, AADNonInteractiveUserSignInLogs, LAQueryLogsShared 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)AzureActivityARM write operations only; read ops not logged. Claims.appid = 04b07795. Inherits cred from Azure CLI/VS Code
(no AppId — Purview unified audit)Sentinel Data Lake MCPCloudAppEventsRecordType 403; Interface IMcpToolTemplate; tools: query_lake, list_sentinel_workspaces, search_tables

Sentinel MCP Collection Endpoints

Endpoint URLCollectionMonitored
https://sentinel.microsoft.com/mcp/data-explorationData Exploration (Data Lake MCP)✅ Phase 3
https://sentinel.microsoft.com/mcp/triageTriage (Triage MCP)✅ Phase 2
https://sentinel.microsoft.com/mcp/security-copilot-agent-creationSecurity Copilot Agent Creation❌ See Landscape

Client Applications

AppIdServiceTelemetry TableNotes
aebc6443-996d-45c2-90f0-388ff96faa56Visual Studio CodeSigninLogsVS Code as MCP client → Sentinel
9ba5f2e4-6bbf-4df2-b19b-7f1bcb926818PowerPlatform-sentinelmcp-ConnectorSigninLogsCopilot Studio → Sentinel MCP
04b07795-8ddb-461a-bbee-02f9e1bf7b46Azure CLI (DefaultAzureCredential)SigninLogs, AADNonInteractiveUserSignInLogs, LAQueryLogsPrimary 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)

AppIdServiceTelemetry TableNotes
80ccca67-54bd-44ab-8625-4b79c4dc7775M365 Security & Compliance Center (Sentinel Portal)LAQueryLogsASI_Portal, ASI_Portal_Connectors — Sentinel Portal backend, NOT an MCP server
95a5d94c-a1a0-40eb-ac6d-48c5bdee96d5Azure Portal — AppInsightsPortalExtensionLAQueryLogsAzure 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.
de8c33bb-995b-4d4a-9d04-8d8af5d59601PowerPlatform-AzureMonitorLogs-ConnectorAADNonInteractiveUserSignInLogs, LAQueryLogsLogic Apps → Log Analytics (NOT MCP)
fc780465-2017-40d4-a0c5-307022471b92Sentinel Engine (analytics rules, UEBA, Advanced Hunting backend)LAQueryLogsBuilt-in scheduled query engine (NOT MCP). Also serves as the execution backend for Advanced HuntingRequestClientApp = "M365D_AdvancedHunting" indicates AH queries from Triage MCP, Defender portal, or Security Copilot that hit connected LA tables (see Query 7b). 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:

CollectionEndpoint URLPurposeMonitored by This Skill
Data Explorationhttps://sentinel.microsoft.com/mcp/data-explorationquery_lake, search_tables, list_sentinel_workspaces, entity analyzer✅ Phase 3 (CloudAppEvents)
Triagehttps://sentinel.microsoft.com/mcp/triageIncident triage, Advanced Hunting, entity investigation✅ Phase 2 (MicrosoftGraphActivityLogs + SigninLogs — AppId 7b7b3966)
Security Copilot Agent Creationhttps://sentinel.microsoft.com/mcp/security-copilot-agent-creationCreate 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 21 without modification.

Power BI MCP Servers

ServerTypeEndpoint / RepoPurposeTelemetry Surface
Power BI Remote MCPMicrosoft-hostedhttps://api.fabric.microsoft.com/v1/mcp/powerbiQuery Power BI datasets, reports, and workspaces remotely via SSE transport🟡 PowerBIActivity table (if ingested into Sentinel), Fabric audit logs
Power BI Modeling MCPLocal (stdio)microsoft/powerbi-modeling-mcpLocal 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.

Fabric & Azure Data Explorer MCP Servers

ServerTypeEndpoint / RepoPurposeTelemetry Surface
Fabric RTI MCP ServerLocal (stdio)microsoft/fabric-rti-mcpQuery Azure Data Explorer clusters and Fabric Real-Time Intelligence Eventhouses via KQL🟡 ADX audit logs, Fabric audit events
Azure MCP Server — Kusto namespaceLocal (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 MCPCopilot Studio built-inCopilot Studio catalogKQL 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.

Developer & Productivity MCP Servers

ServerTypeRepoPurposeTelemetry Surface
Playwright MCPLocal (stdio)microsoft/playwright-mcp (26.9k ⭐)Browser automation via accessibility tree — enables LLMs to interact with web pages❌ Local only — no Azure telemetry
GitHub MCP ServerLocal (stdio)github/github-mcp-serverGitHub repo operations (issues, PRs, code search) via PAT❌ GitHub audit logs only, not in Sentinel
Microsoft Learn Docs MCPCloud-hostedCertified Copilot Studio connectorSearch 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.

Source: Built-in MCP servers catalog

CategoryMCP ServersSecurity Relevance
Microsoft 365Outlook Mail, Outlook Calendar, 365 User Profile, Teams, Word, 365 Copilot (Search)🔴 High — email, calendar, user profile access
SharePoint & OneDriveSharePoint and OneDrive, SharePoint Lists🟠 Medium — file and data access
Administration365 Admin Center🔴 High — administrative control plane
DataverseDataverse MCP🟠 Medium — business data access
Dynamics 365Sales, Finance, Supply Chain, Service, ERP, Contact Center (6 sub-variants)🟡 Low-Medium — business application data
FabricFabric MCP🟠 Medium — analytics data access
Office 365 OutlookContact Management, Email Management, Meeting Management🔴 High — email and contact data
Meta-ServerMCP Management MCP🟠 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 — agent identity lifecycle events (creation, modification via Power Virtual Agents Service)
  • AADServicePrincipalSignInLogs — agent SPN sign-ins to Bot Framework from Azure internal IPs (fd00:*)

To monitor Copilot Studio agent activity touching these MCP servers, use the Agent Identity Detection workflow (Phase 6) to identify agent SPNs, then trace their sign-in and audit activity.

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:

CategoryNamespacesSecurity-Relevant Tools
AI & MLfoundry, search, speechAI Foundry model access, Search index queries
Identityrole⚠️ RBAC role assignments — view and manage
Securitykeyvault, appconfig, confidentialledger🔴 Key Vault secrets/keys/certs, App Configuration
Databasescosmos, mysql, postgres, redis, sqlDatabase access and management
Storagestorage, fileshares, storagesync, managedlustreBlob, file, and storage account access
Computeappservice, functionapp, aksApp Service, Functions, Kubernetes
Networkingeventhubs, servicebus, eventgrid, communication, signalrMessaging and event services
DevOpsbicepschema, deploy, monitor, workbooks, grafanaInfrastructure deployment, monitoring
Governancepolicy, quota, resourcehealth, cloudarchitectPolicy management, resource health
Othermarketplace, virtualdesktop, loadtesting, acrVDI, container registry, load testing

🔵 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:

PriorityServerWhyHow to Monitor
🔴 P1Copilot Studio built-in M365 MCPsEmail, Teams, admin center accessAgent Identity Detection (Phase 6) + CloudAppEvents
🔴 P1Security Copilot Agent CreationCreates autonomous security agentsCloudAppEvents for agent creation events
🟠 P2Power BI Remote MCPDataset query access via APIPowerBIActivity table if available
🟠 P2Sentinel Custom MCP ToolsUser-defined tools, same audit surfaceAlready visible in Phase 3 CloudAppEvents
🟡 P3Fabric RTI MCPADX/Eventhouse data accessADX diagnostic logs
🟡 P3Kusto Query MCP (Copilot Studio)KQL from Copilot Studio agentsCloudAppEvents (Copilot Studio workload)
P4Playwright, GitHub, Learn Docs MCPsLocal/public, minimal telemetryNot 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):

  1. ALWAYS call list_sentinel_workspaces MCP tool FIRST
  2. If 1 workspace exists: Auto-select, display to user, proceed
  3. 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
  4. 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
  • Filename pattern: reports/mcp-usage/MCP_Usage_Report_YYYYMMDD_HHMMSS.md

Markdown Rendering Notes

  • ✅ ASCII tables, box-drawing characters, and bar charts render perfectly in markdown code blocks
  • ✅ Unicode block characters (▓░█) display correctly in monospaced fonts
  • ✅ Emoji indicators (🔴🟢🟡⚠️✅) render natively in GitHub-flavored markdown
  • ✅ 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:

ParameterSmall Env (default)Large Env
make_set(..., N) for users105
make_set(..., N) for endpoints20–3010
make_set(..., N) for errors53
take on governance tables2515
take on endpoint rankings2515
take on error analysis5020

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:

  1. Select Workspacelist_sentinel_workspaces, auto-select or ask
  2. Determine Output Mode → Ask if not specified: inline, markdown file, or both
  3. Determine Time Range → Ask if not specified; default 30 days
  4. Run Phase 1 (Graph MCP) → Daily usage summary, top endpoints, sensitive API access
  5. Run Phase 2 (Sentinel Triage MCP) → API calls via AppId 7b7b3966, auth events, AH downstream queries
  6. Run Phase 3 (Sentinel Data Lake MCP) → CloudAppEvents tool usage, error analysis, MCP vs Direct KQL
  7. Run Phase 4 (Azure MCP & ARM) → ARM operations, resource provider breakdown
  8. Run Phase 5 (Workspace Governance) → All query sources (Analytics + Data Lake tiers), MCP proportion
  9. Run Phase 6 (Agent Identity) → Entra Agent ID detection, caller attribution
  10. Run Phase 7 (Assessment) → Compute MCP Usage Score, security assessment, render report

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.

Scoring Dimensions

$$ \text{MCPUsageScore} = \sum_{i} \text{DimensionScore}_i $$

Each dimension contributes 0–20 points to a maximum of 100:

DimensionMax PointsGreen (0-5)Yellow (6-12)Red (13-20)
User Diversity201-2 known users3-5 users or 1 unknown>5 users or unknown users
Endpoint Sensitivity200% sensitive endpoints1-30% sensitive>30% calls to sensitive APIs
Error Rate20<1% errors1-5% errors>5% errors
Volume Anomaly20Within ±50% of daily avg50-200% spike>200% spike vs avg
Off-Hours Activity20<5% off-hours5-20% off-hours>20% calls outside business hours

Interpretation Scale

ScoreMeaningAction
0–25Healthy✅ Normal MCP usage, no concerns
26–50Elevated🟡 Review — minor anomalies detected
51–75Concerning🟠 Investigate — multiple risk signals present
76–100Critical🔴 Immediate review — significant security risk

Sensitivity Classification

Sensitive Graph API endpoints — flag any MCP calls to these patterns:

roleManagement, roleAssignments, roleEligibility,
authentication/methods, identityProtection, riskyUsers,
riskDetections, conditionalAccess, servicePrincipals,
appRoleAssignments, oauth2PermissionGrants,
auditLogs, directoryRoles, privilegedAccess,
security/alerts, security/incidents

Off-Hours Definition

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 to get daily usage summary with user count, success/error breakdown, avg duration
  • Execute Query 2 to get top 25 Graph API endpoints accessed with call counts and last-used timestamps
  • Execute Query 3 to get sensitive/high-risk endpoint access with user and scope details
  • Execute Query 4 to discover all distinct AppIds calling Graph API for MCP identification

Phase 2: Sentinel Triage MCP Analysis

Data sources: MicrosoftGraphActivityLogs, SigninLogs, AADNonInteractiveUserSignInLogs
Filter: AppId = 7b7b3966-1961-47b5-b080-43ca5482e21c ("Microsoft Defender Mcp")

Detection Method (Confirmed Feb 2026):

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
  • Scopes: SecurityAlert.Read.All, SecurityIncident.Read.All, ThreatHunting.Read.All
  • 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 5 to get authentication events by client app (VS Code, Copilot Studio, browser) with user, IP, OS, country
  • Execute Query 6 to get client app usage breakdown with distinct user counts and last-seen timestamps
  • Execute Query 7 to get Triage MCP API usage from MicrosoftGraphActivityLogs — filter by AppId 7b7b3966 for exact Triage MCP calls with endpoint/method/user breakdown
  • Execute Query 7a to get Triage MCP authentication events from SigninLogs/AADNonInteractiveUserSignInLogs — sign-in frequency, user attribution, IP, OS, country
  • Execute Query 7b 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.
  • Execute Query 7c to get portal/platform query volume from LAQueryLogs for governance context

Phase 3: Sentinel Data Lake MCP Analysis

Data source: CloudAppEvents (Purview unified audit log)
Execution tool: mcp_sentinel-data_query_lake — queries use TimeGenerated (Data Lake column). CloudAppEvents is available on both Data Lake (90d retention) and Advanced Hunting (30d, uses Timestamp). Always try Data Lake first for full retention coverage.
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 20 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.

MCP vs Direct KQL Delineation:

Access PatternRecordTypeInterfaceOperationWhat It Represents
MCP Server-driven403IMcpToolTemplateSentinelAIToolRunStarted, SentinelAIToolRunCompletedTool calls via Sentinel Data Lake MCP (e.g., query_lake, list_sentinel_workspaces, search_tables)
Direct KQL379Microsoft.SentinelGraph.AIPrimitives.Core.Services.KqsServiceKQLQueryCompletedKQL queries executed directly via Sentinel Graph / Data Lake Explorer (no MCP intermediary)

Key RawEventData Fields:

FieldDescriptionExample
ToolNameMCP tool invokedquery_lake, list_sentinel_workspaces, search_tables, analyze_url_entity
InterfaceExecution interface — distinguishes MCP from directIMcpToolTemplate (MCP) vs KqsService (direct)
ExecutionDurationDuration in seconds (as string)"2.4731712"
FailureReasonError message if failed"SemanticError: 'DeviceDetail' column does not exist"
TablesReadTables accessed by the query"SigninLogs"
DatabasesReadLog Analytics workspace name"la-yourworkspace"
TotalRowsRows returned100
InputParametersFull tool input including KQL query text and workspaceIdJSON string with query and workspaceId keys

Collect:

  • Execute Query 20 to get Data Lake MCP tool usage summary with success/failure breakdown
  • Execute Query 21 to get tool-level breakdown with call counts and avg execution duration
  • Execute Query 22 to get error analysis for failed Data Lake MCP tool calls
  • Execute Query 23 to get daily activity trend across MCP and Direct KQL
  • Execute Query 24 to get MCP vs Direct KQL delineation summary

Phase 4: Azure MCP Server & ARM Operations

Data sources: AzureActivity, SigninLogs, AADNonInteractiveUserSignInLogs, LAQueryLogs
Filter: Caller = user UPN (AzureActivity), AppId = 04b07795-8ddb-461a-bbee-02f9e1bf7b46 (sign-in logs, LAQueryLogs)

Collect:

  • Execute Query 8 to get ARM operations by hour/IP with operation counts and resource provider sets
  • Execute Query 9 to get resource provider breakdown with top operations per provider
  • Execute Query 25 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 26 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):

SignalAzure MCP Server (Current)Azure CLI (Manual)Notes
AppId (SigninLogs)04b0779504b07795Shared — not a unique differentiator
AADClientId (LAQueryLogs)04b0779504b07795Shared
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 queriesNo standard suffixBest differentiator — Azure MCP monitor_workspace_log_query always appends a limit operator
AzureActivity (Claims.appid)04b07795 (write ops only)04b07795Shared; read ops not logged

🚨 Key change from previous documentation:

  • RequestClientApp = "csharpsdk,LogAnalyticsPSClient"OBSOLETE, no longer produced by Azure MCP Server
  • ❌ AppId 1950a258 (AzurePowerShellCredential) — OBSOLETE credential path
  • ✅ 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):

  1. Azure MCP Server acquires token via Azure CLI cached credential
  2. Token is reused for subsequent operations within its lifetime
  3. If MFA claim is missing → interactive browser prompt (rare with CLI credential)
  4. 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. AzureActivity has a ~2-4 hour ingestion lag.

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 10 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 20-24)
  • MCP proportion calculation: combined MCP query volume (Analytics + Data Lake tiers) / total query volume
  • Execute Query 14 to get non-MCP platform context (Sentinel Engine, Logic Apps, Sentinel Portal volumes)

Phase 6: Agent Identity Detection

Data sources: MicrosoftGraphActivityLogs, AADServicePrincipalSignInLogs, AuditLogs, Microsoft Graph API

Collect:

  • Execute Query 17 to get Graph MCP caller attribution — User vs SPN vs Agent breakdown
  • Agent Identity inventory via Graph API — GET /servicePrincipals?$filter=servicePrincipalType eq 'Agent' (via Graph MCP microsoft_graph_suggest_queriesmicrosoft_graph_get)
  • Execute Query 18 to get Agent Identity sign-in events from AADServicePrincipalSignInLogs where applicable
  • Execute Query 19 to get Agent Identity CRUD operations from AuditLogs — creation, modification, deletion of agent identities

Note: This phase depends on Entra Agent ID (preview) being available in the tenant. If no agent identities exist, report: "✅ No Entra Agent Identities detected in tenant — all MCP callers are standard users or service principals." and skip Queries 18-19.

Phase 7: Score Computation & Report Generation

  1. Compute per-dimension scores from Phase 1-6 data:
    • User Diversity: Count distinct users AND distinct Agent Identities across all MCP channels
    • Endpoint Sensitivity: % of Graph MCP calls to sensitive patterns (Phase 1 Query 3 vs Query 1)
    • 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)
    • Off-Hours Activity: % of MCP calls outside 08:00-18:00 (derive from TimeGenerated)
  2. Sum dimension scores for composite MCP Usage Score
  3. Include Agent Identity attribution in report if any agent callers detected (Phase 6)
  4. Generate security assessment with emoji-coded findings
  5. Render output in the user's selected mode

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.

ActionStatus
Rewriting a pre-authored query from scratchPROHIBITED
Removing parse_json() / tostring() wrappers from queriesPROHIBITED
Substituting column names without schema verificationPROHIBITED
Using has instead of contains for CamelCase fieldsPROHIBITED
Executing a query not from this section without completing the Pre-Flight ChecklistPROHIBITED

Query 1: Graph MCP — Daily Usage Summary

// Graph MCP daily usage trend with success/error breakdown
// Configurable: replace ago(30d) with desired lookback
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| summarize 
    Requests = count(),
    DistinctUsers = dcount(UserId),
    SuccessCount = countif(ResponseStatusCode >= 200 and ResponseStatusCode < 300),
    ErrorCount = countif(ResponseStatusCode >= 400),
    AvgDurationMs = avg(DurationMs)
    by bin(TimeGenerated, 1d)
| extend ErrorRate = round(100.0 * ErrorCount / Requests, 1)
| order by TimeGenerated desc

Query 2: Graph MCP — Top Endpoints Accessed

// Most-called Graph API endpoints via MCP, with user and recency info
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| extend Endpoint = tostring(split(RequestUri, "?")[0])
| summarize 
    CallCount = count(),
    DistinctUsers = dcount(UserId),
    LastUsed = max(TimeGenerated),
    Methods = make_set(RequestMethod, 5)
    by Endpoint
| order by CallCount desc
| take 25

Query 3: Graph MCP — Sensitive API Access

// Flag MCP calls to high-risk Graph endpoints (PIM, credentials, Identity Protection)
let sensitive_patterns = dynamic([
    "roleManagement", "roleAssignments", "roleEligibility",
    "authentication/methods", "identityProtection", "riskyUsers",
    "riskDetections", "conditionalAccess", "servicePrincipals",
    "appRoleAssignments", "oauth2PermissionGrants",
    "auditLogs", "directoryRoles", "privilegedAccess",
    "security/alerts", "security/incidents"
]);
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| where RequestUri has_any (sensitive_patterns)
| project TimeGenerated, UserId, RequestMethod, RequestUri, 
    ResponseStatusCode, IPAddress, Scopes
| order by TimeGenerated desc

Query 4: All Graph API AppIds — Discovery

// Discover all apps calling Graph APIs — find unknown MCP servers or AI agents
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| summarize 
    RequestCount = count(),
    DistinctUsers = dcount(UserId),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    SampleEndpoints = make_set(tostring(split(RequestUri, "?")[0]), 3)
    by AppId
| order by RequestCount desc

Query 5: Sentinel MCP — Authentication Events

⚠️ Pitfall-aware: Uses parse_json(Status) and parse_json(DeviceDetail) wrappers — see SigninLogs Status Field Needs parse_json(). Direct dot-notation fails in Data Lake.

// 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, 
    tostring(parse_json(Status).errorCode) as ErrorCode,
    ConditionalAccessStatus, AuthenticationRequirement, ClientAppUsed,
    tostring(parse_json(DeviceDetail).operatingSystem) as OS,
    tostring(parse_json(LocationDetails).countryOrRegion) as Country
| order by TimeGenerated desc

Query 6: Sentinel MCP — Client App Breakdown

// 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

Query 7: Sentinel Triage MCP — API Call Activity (Dedicated AppId)

// Measure Sentinel Triage MCP API calls via its dedicated AppId in MicrosoftGraphActivityLogs.
// AppId 7b7b3966 = "Microsoft Defender Mcp" — the Triage MCP server's own identity.
// This gives DEFINITIVE attribution of Triage MCP calls — no shared-surface estimation needed.
//
// Confirmed Feb 2026: AppId 7b7b3966 appears in MicrosoftGraphActivityLogs with delegated
// auth (certificate), full UserId attribution, and scopes SecurityAlert.Read.All,
// SecurityIncident.Read.All, ThreatHunting.Read.All.
//
// Known API endpoints:
//   - POST /v1.0/security/runHuntingQuery/ (Advanced Hunting)
//   - GET  /security/incidents/ (ListIncidents, GetIncidentById)
//   - GET  /security/alerts_v2/ (ListAlerts, GetAlertById)
let triage_mcp_appid = "7b7b3966-1961-47b5-b080-43ca5482e21c";
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == triage_mcp_appid
| extend Endpoint = extract(@"/v\d\.\d/(.+?)(\?|$)", 1, RequestUri)
| summarize 
    Calls = count(),
    DistinctUsers = dcount(UserId),
    Users = make_set(UserId, 10),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by RequestMethod, Endpoint
| order by Calls desc
| take 25

Query 7a: Sentinel Triage MCP — Authentication Events (SigninLogs)

// Triage MCP authentication events from SigninLogs + AADNonInteractiveUserSignInLogs.
// AppId 7b7b3966 = "Microsoft Defender Mcp" — delegated auth with certificate.
// Uses parse_json() wrappers for DeviceDetail/LocationDetails (Data Lake string columns).
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

Query 7b: LAQueryLogs — Advanced Hunting Downstream Queries (Supplementary Signal)

// SUPPLEMENTARY detection: Advanced Hunting queries (from Triage MCP, Defender portal,
// Security Copilot, or any RunAdvancedHuntingQuery consumer) that hit connected
// Log Analytics workspace tables.
//
// AH downstream queries appear under fc780465 (Sentinel Engine) with
// RequestClientApp "M365D_AdvancedHunting" — full user attribution (AADEmail populated).
//
// This is a DOWNSTREAM signal — it only fires when RunAdvancedHuntingQuery targets
// Sentinel-connected LA tables (SigninLogs, AuditLogs, SecurityAlert, etc.).
// Queries hitting XDR-native tables (DeviceEvents, EmailEvents, etc.) stay in the
// Defender XDR backend and never appear here.
//
// Use alongside Query 7 (MicrosoftGraphActivityLogs) for complete Triage MCP coverage:
//   - Query 7 = PRIMARY: Triage MCP API calls filtered by dedicated AppId 7b7b3966
//   - Query 7b = SUPPLEMENTARY: downstream query execution when AH hits LA tables
//
// ATTRIBUTION LIMITATION: Cannot distinguish Triage MCP AH queries from Defender portal
// AH queries or Security Copilot AH queries — all appear as M365D_AdvancedHunting.
LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId == "fc780465-2017-40d4-a0c5-307022471b92" and RequestClientApp == "M365D_AdvancedHunting"
| summarize 
    QueryCount = count(),
    DistinctUsers = dcount(AADEmail),
    Users = make_set(AADEmail, 10),
    AvgCPUMs = avg(StatsCPUTimeMs),
    TotalRowsReturned = sum(ResponseRowCount),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by AADClientId, RequestClientApp
| order by QueryCount desc

Query 7c: LAQueryLogs — Portal & Platform Query Volume (Non-MCP Governance)

// Query volume for Portal/Platform AppIds in LAQueryLogs (governance context).
// NOTE: These are NOT MCP servers — included for workspace query governance baseline.
// 80ccca67 = M365 Security & Compliance Center (Sentinel Portal backend)
// 95a5d94c = Azure Portal AppInsightsPortalExtension (Usage dashboards)
let portal_appids = dynamic([
    "80ccca67-54bd-44ab-8625-4b79c4dc7775",  // M365 Security & Compliance Center (Sentinel Portal)
    "95a5d94c-a1a0-40eb-ac6d-48c5bdee96d5"   // Azure Portal — AppInsightsPortalExtension (Usage dashboards)
]);
LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId in (portal_appids)
| extend Category = "Portal/Platform"
| summarize 
    QueryCount = count(),
    DistinctUsers = dcount(AADEmail),
    AvgCPUMs = avg(StatsCPUTimeMs),
    AvgRowsPerQuery = avg(ResponseRowCount),
    TotalRowsReturned = sum(ResponseRowCount),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by AADClientId, Category
| order by QueryCount desc

Query 8: Azure MCP — ARM Operations

// Azure ARM operations (may include Azure MCP Server — no dedicated AppId)
// Substitute <UPN> with the user's UPN
AzureActivity
| where TimeGenerated >= ago(30d)
| where Caller =~ '<UPN>'
| where CategoryValue == "Administrative"
| summarize
    OperationCount = count(),
    DistinctOperations = dcount(OperationNameValue),
    ResourceProviders = make_set(ResourceProviderValue, 10)
    by bin(TimeGenerated, 1d), CallerIpAddress
| order by TimeGenerated desc

Query 9: Azure MCP — Resource Provider Breakdown

// Azure resource types being accessed — detect unexpected resource access
// Substitute <UPN> with the user's UPN
AzureActivity
| where TimeGenerated >= ago(30d)
| where Caller =~ '<UPN>'
| where CategoryValue == "Administrative"
| summarize
    CallCount = count(),
    Operations = make_set(OperationNameValue, 5),
    DistinctResources = dcount(ResourceGroup)
    by ResourceProviderValue
| order by CallCount desc

Query 10: All Workspace Query Sources — Complete Governance View

// Every client querying the workspace — MCP and non-MCP combined
LAQueryLogs
| where TimeGenerated >= ago(30d)
| summarize 
    QueryCount = count(),
    DistinctUsers = dcount(AADEmail),
    AvgCPUMs = avg(StatsCPUTimeMs),
    TotalRowsReturned = sum(ResponseRowCount)
    by AADClientId
| order by QueryCount desc

Query 11: Graph MCP — Off-Hours Activity Detection

// Identify MCP calls outside business hours (08:00-18:00 UTC)
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| extend HourOfDay = datetime_part("hour", TimeGenerated)
| extend DayOfWeek = dayofweek(TimeGenerated) / 1d
| extend IsOffHours = HourOfDay < 8 or HourOfDay >= 18 or DayOfWeek >= 5
| summarize
    TotalCalls = count(),
    OffHoursCalls = countif(IsOffHours),
    BusinessHoursCalls = countif(not(IsOffHours)),
    OffHoursUsers = make_set_if(UserId, IsOffHours, 10)
    by bin(TimeGenerated, 1d)
| extend OffHoursPct = round(100.0 * OffHoursCalls / TotalCalls, 1)
| order by TimeGenerated desc

Query 12: Graph MCP — New User Detection (Baseline Comparison)

// Detect users who started using Graph MCP in the recent window but weren't in baseline
let baselineStart = ago(60d);
let baselineEnd = ago(7d);
let recentStart = ago(7d);
let baseline_users = MicrosoftGraphActivityLogs
| where TimeGenerated between (baselineStart .. baselineEnd)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| distinct UserId;
MicrosoftGraphActivityLogs
| where TimeGenerated >= recentStart
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| where UserId !in (baseline_users)
| summarize
    CallCount = count(),
    FirstSeen = min(TimeGenerated),
    Endpoints = make_set(tostring(split(RequestUri, "?")[0]), 10),
    IPs = make_set(IPAddress, 5)
    by UserId
| order by CallCount desc

Query 13: Sentinel MCP — Query Content Audit

// Audit actual KQL queries executed by MCP servers and Portal/Platform apps (sample)
let mcp_server_appids = dynamic([
    "7b7b3966-1961-47b5-b080-43ca5482e21c"   // Sentinel Triage MCP ("Microsoft Defender Mcp")
]);
let portal_appids = dynamic([
    "80ccca67-54bd-44ab-8625-4b79c4dc7775",  // M365 Security & Compliance Center (Sentinel Portal)
    "95a5d94c-a1a0-40eb-ac6d-48c5bdee96d5"   // Azure Portal — AppInsightsPortalExtension (Usage dashboards)
]);
LAQueryLogs
| where TimeGenerated >= ago(7d)
| where AADClientId in (mcp_server_appids) or AADClientId in (portal_appids)
| extend Category = case(
    AADClientId in (mcp_server_appids), "MCP Server",
    "Portal/Platform")
| project TimeGenerated, AADEmail, AADClientId, Category,
    QueryTextTruncated = substring(QueryText, 0, 200),
    ResponseCode, ResponseRowCount, StatsCPUTimeMs
| order by TimeGenerated desc
| take 50

Query 14: Unified MCP + Non-MCP Dashboard

// Combined MCP vs non-MCP view for workspace governance (Analytics tier)
let mcp_graph = MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| summarize Count = count() by Source = "Graph MCP", Category = "MCP", bin(TimeGenerated, 1d);
let mcp_triage = MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "7b7b3966-1961-47b5-b080-43ca5482e21c"
| summarize Count = count() by Source = "Sentinel Triage MCP (Microsoft Defender Mcp)", Category = "MCP", bin(TimeGenerated, 1d);
let sentinel_portal = LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId == "80ccca67-54bd-44ab-8625-4b79c4dc7775"
| summarize Count = count() by Source = "Sentinel Portal", Category = "Portal", bin(TimeGenerated, 1d);
let portal_appinsights = LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId == "95a5d94c-a1a0-40eb-ac6d-48c5bdee96d5"
| summarize Count = count() by Source = "Azure Portal (AppInsightsPortalExtension)", Category = "Portal", bin(TimeGenerated, 1d);
let platform_logic_apps = LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId == "de8c33bb-995b-4d4a-9d04-8d8af5d59601"
| summarize Count = count() by Source = "Logic Apps Connector", Category = "Platform", bin(TimeGenerated, 1d);
let platform_sentinel_engine = LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId == "fc780465-2017-40d4-a0c5-307022471b92"
| summarize Count = count() by Source = "Sentinel Engine", Category = "Platform", bin(TimeGenerated, 1d);
union mcp_graph, mcp_triage, sentinel_portal, portal_appinsights, platform_logic_apps, platform_sentinel_engine
| summarize TotalQueries = sum(Count) by Source, Category, bin(TimeGenerated, 1d)
| order by TimeGenerated desc

Note: This query covers the Analytics tier (LAQueryLogs + MicrosoftGraphActivityLogs). For Data Lake tier query governance, combine with Query 24 results from CloudAppEvents.

Query 15: Sentinel Audit — Configuration Changes

// Sentinel workspace configuration changes (analytics rules, connectors, etc.)
SentinelAudit
| where TimeGenerated >= ago(30d)
| summarize 
    ChangeCount = count(),
    OperationTypes = make_set(OperationName)
    by SentinelResourceType
| order by ChangeCount desc

Query 16: Logic Apps — Non-MCP Query Monitoring

// Track automated queries from Logic Apps (standard connector, NOT MCP)
LAQueryLogs
| where TimeGenerated >= ago(30d)
| where AADClientId == "de8c33bb-995b-4d4a-9d04-8d8af5d59601"
| summarize
    QueryCount = count(),
    DistinctQueries = dcount(QueryText),
    AvgCPUMs = avg(StatsCPUTimeMs),
    TotalRows = sum(ResponseRowCount),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by AADEmail, RequestClientApp
| order by QueryCount desc

Query 17: Graph MCP — Caller Attribution (User vs SPN vs Agent)

// Attribute Graph MCP calls to User, Service Principal, or Agent Identity
// Key: UserId populated = delegated (user), ServicePrincipalId populated = app-only (SPN/Agent)
// ClientAuthMethod: 0 = public client (user), 1 = client secret (SPN), 2 = certificate (SPN)
MicrosoftGraphActivityLogs
| where TimeGenerated >= ago(30d)
| where AppId == "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"
| extend CallerType = case(
    isnotempty(ServicePrincipalId) and isempty(UserId), "ServicePrincipal/Agent (App-Only)",
    isnotempty(UserId) and isnotempty(ServicePrincipalId), "Delegated (User+SPN/Agent OBO)",
    isnotempty(UserId) and isempty(ServicePrincipalId), "User (Delegated)",
    "Unknown")
| extend AuthMethod = case(
    ClientAuthMethod == 0, "Public Client",
    ClientAuthMethod == 1, "Client Secret",
    ClientAuthMethod == 2, "Client Certificate",
    "Unknown")
| summarize
    CallCount = count(),
    DistinctEndpoints = dcount(tostring(split(RequestUri, "?")[0])),
    SuccessRate = round(100.0 * countif(ResponseStatusCode >= 200 and ResponseStatusCode < 300) / count(), 1),
    SampleEndpoints = make_set(tostring(split(RequestUri, "?")[0]), 5),
    IPs = make_set(IPAddress, 5)
    by CallerType, AuthMethod, UserId, ServicePrincipalId
| order by CallCount desc

Post-processing: For any rows where CallerType = "ServicePrincipal/Agent (App-Only)", cross-reference the ServicePrincipalId with Entra via Graph API:

  1. Primary method (most reliable): Query /beta/servicePrincipals/{id}?$select=id,appId,displayName,servicePrincipalType,tags — check tags array for agentic indicators:
    • 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
  2. 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.
  3. 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_queriesmicrosoft_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 18: Agent Identity Sign-In Events

// Agent Identity sign-ins — look for Copilot Studio agent SPNs
// Field-tested: Copilot Studio agents sign in to "Bot Framework" from Azure internal IPv6 (fd00:34f2:*)
// Also check for Graph, Sentinel resources in case agents expand scope
// Cross-reference ServicePrincipalId values from Query 19 AuditLogs results
// Substitute <AGENT_SPN_IDS> with confirmed agent SPN IDs (or remove filter for broad discovery)
AADServicePrincipalSignInLogs
| where TimeGenerated >= ago(30d)
// Uncomment the next line if you have confirmed agent SPN IDs from Query 19 / Graph API:
// | where ServicePrincipalId in (<AGENT_SPN_IDS>)
| where ResourceDisplayName has_any ("Graph", "Sentinel", "Microsoft Graph", "Bot Framework")
| summarize
    SignInCount = count(),
    SuccessCount = countif(ResultType == "0" or ResultType == 0),
    FailCount = countif(ResultType != "0" and ResultType != 0),
    DistinctResources = dcount(ResourceDisplayName),
    Resources = make_set(ResourceDisplayName, 10),
    IPs = make_set(IPAddress, 5),
    Locations = make_set(Location, 5),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by ServicePrincipalName, ServicePrincipalId, AppId
| order by SignInCount desc

Telemetry patterns for Copilot Studio agents (field-tested):

  • Resource: Bot Framework (NOT Microsoft Graph — agents talk to Bot Framework runtime)
  • IP addresses: Azure internal IPv6 (fd00:34f2:*) — these are Azure infrastructure IPs, not user IPs
  • Sign-in volume: Low (single-digit sign-ins typical for idle agents)
  • Correlation: Match ServicePrincipalId here with TargetId from Query 19 AuditLogs to confirm the same agent

Query 19: Agent Identity CRUD Operations in AuditLogs

// Track creation, modification, and deletion of Agent Identities
// These operations indicate agent lifecycle management in the tenant
// Key insight: Copilot Studio agents are created by "Power Virtual Agents Service"
// and have display names like "Agent (Microsoft Copilot Studio)" or "<Name> (Microsoft Copilot Studio)"
AuditLogs
| where TimeGenerated >= ago(30d)
| where OperationName has_any ("agent", "Agent")
    or Category == "AgentIdentity"
    or (OperationName has_any ("service principal") and (
        tostring(TargetResources) has "agent"
        or tostring(TargetResources) has "Copilot Studio"
        or tostring(InitiatedBy) has "Power Virtual Agents"
    ))
| project TimeGenerated, OperationName, Result,
    InitiatedBy = coalesce(
        tostring(parse_json(tostring(InitiatedBy)).user.userPrincipalName),
        tostring(parse_json(tostring(InitiatedBy)).app.displayName)),
    TargetName = tostring(parse_json(tostring(parse_json(tostring(TargetResources))[0])).displayName),
    TargetId = tostring(parse_json(tostring(parse_json(tostring(TargetResources))[0])).id),
    TargetType = tostring(parse_json(tostring(parse_json(tostring(TargetResources))[0])).type)
| extend IsAgenticSPN = iff(
    InitiatedBy == "Power Virtual Agents Service"
    or TargetName has "Copilot Studio"
    or TargetName has "AgenticApp", true, false)
| order by TimeGenerated desc
| take 50

Interpreting results:

  • InitiatedBy = Power Virtual Agents Service → Copilot Studio created/modified this agent SPN automatically
  • InitiatedBy = user@domain.com → Human manually created/modified this SPN (may or may not be an agent)
  • OperationName = Hard delete service principal shortly after Add service principal → Testing/experimentation pattern
  • Multiple operations (Add SPNAdd ownerUpdate SPN) within seconds → Copilot Studio automated provisioning sequence

Query 20: Data Lake MCP — Tool Usage Summary

⚠️ 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.

// Sentinel Data Lake MCP tool usage from CloudAppEvents (Purview unified audit)
// RecordType 403 = Sentinel AI Tool activities (MCP), RecordType 379 = KQL activities (direct)
// IMPORTANT: Use 'contains' (not 'has') for ActionType — CamelCase values have no word boundaries
CloudAppEvents
| where TimeGenerated >= 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)
| extend 
    AccessPattern = case(
        RecordType == 403 and Interface == "IMcpToolTemplate", "MCP Server-Driven",
        RecordType == 379, "Direct KQL",
        "Other"),
    IsSuccess = isempty(FailureReason) or FailureReason == ""
| where Operation contains "Completed" or RecordType == 379  // Focus on completed events; use 'contains' not 'has' — CamelCase has no word boundaries
| summarize
    TotalCalls = count(),
    SuccessCount = countif(IsSuccess),
    FailureCount = countif(not(IsSuccess)),
    DistinctTools = dcount(ToolName),
    DistinctTables = dcount(TablesRead),
    AvgDurationSec = round(avg(ExecutionDuration), 2),
    TotalRowsReturned = sum(TotalRows),
    DistinctUsers = dcount(UserId_raw),
    Users = make_set(UserId_raw, 10)
    by AccessPattern
| extend ErrorRate = round(100.0 * FailureCount / TotalCalls, 1)
| order by TotalCalls desc

Query 21: Data Lake MCP — Tool Breakdown

⚠️ Pitfall-aware: Uses contains/parse_json(tostring()) pattern — see Query 20 pitfall notes. Uses todouble(ExecutionDuration) — see Data Lake MCP ExecutionDuration Format.

// Breakdown of individual MCP tool usage from CloudAppEvents
// Shows which Data Lake MCP tools are being called most frequently
CloudAppEvents
| where TimeGenerated >= ago(30d)
| where ActionType contains "SentinelAITool"
| extend RawData = parse_json(tostring(RawEventData))
| extend 
    Operation = tostring(RawData.Operation),
    ToolName = tostring(RawData.ToolName),
    ExecutionDuration = todouble(RawData.ExecutionDuration),
    FailureReason = tostring(RawData.FailureReason),
    TablesRead = tostring(RawData.TablesRead)
| where Operation == "SentinelAIToolRunCompleted"  // Only count completions to avoid double-counting
| extend IsSuccess = isempty(FailureReason) or FailureReason == ""
| 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),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by ToolName
| extend ErrorRate = round(100.0 * FailureCount / CallCount, 1)
| order by CallCount desc

Query 22: Data Lake MCP — Error Analysis

⚠️ Pitfall-aware: Uses contains/parse_json(tostring()) pattern — see Query 20 pitfall notes.

// Analyze failed Data Lake MCP tool calls — identify schema errors, permission issues, etc.
CloudAppEvents
| where TimeGenerated >= ago(30d)
| where ActionType contains "SentinelAITool"
| extend RawData = parse_json(tostring(RawEventData))
| extend 
    Operation = tostring(RawData.Operation),
    ToolName = tostring(RawData.ToolName),
    FailureReason = tostring(RawData.FailureReason),
    TablesRead = tostring(RawData.TablesRead),
    UserId_raw = tostring(RawData.UserId)
| where Operation == "SentinelAIToolRunCompleted"
| where isnotempty(FailureReason) and FailureReason != ""
| extend 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(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by ErrorCategory
| order by ErrorCount desc

Query 23: Data Lake MCP — Daily Activity Trend

⚠️ Pitfall-aware: Uses contains/parse_json(tostring()) pattern — see Query 20 pitfall notes.

// Daily activity trend for Data Lake MCP tools and Direct KQL
// Use this for volume anomaly detection and trend analysis
CloudAppEvents
| where TimeGenerated >= 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),
    Interface = tostring(RawData.Interface),
    FailureReason = tostring(RawData.FailureReason)
| extend 
    AccessPattern = case(
        RecordType == 403 and Interface == "IMcpToolTemplate", "MCP Server-Driven",
        RecordType == 379, "Direct KQL",
        "Other"),
    IsSuccess = isempty(FailureReason) or FailureReason == ""
| where Operation contains "Completed" or RecordType == 379  // 'contains' not 'has' — CamelCase
| summarize
    Calls = count(),
    SuccessCount = countif(IsSuccess),
    FailureCount = countif(not(IsSuccess))
    by AccessPattern, bin(TimeGenerated, 1d)
| extend ErrorRate = round(100.0 * FailureCount / Calls, 1)
| order by TimeGenerated desc, AccessPattern asc

Query 24: MCP vs Direct KQL — Access Pattern Delineation

// Comprehensive delineation of MCP-driven vs Direct KQL access to Sentinel Data Lake
// Combines RecordType, Interface, and Operation to classify every Data Lake interaction
// This is the key governance query for understanding WHO is querying the Data Lake and HOW
CloudAppEvents
| where TimeGenerated >= 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, "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(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by AccessPattern
| extend ErrorRate = round(100.0 * FailureCount / TotalCalls, 1)
| order by TotalCalls desc

Post-processing for Query 24:

  • MCP Server-Driven rows represent tool calls via the Sentinel Data Lake MCP (GitHub Copilot → MCP Server → Data Lake)
  • Direct KQL rows represent queries executed via Sentinel Graph / Data Lake Explorer (human or service principal → KQL query → Data Lake)
  • Combine this with Query 14 (Analytics tier) for a complete two-tier governance view:
TierData SourceMCP SourcesNon-MCP Sources
Analytics TierLAQueryLogsAH backend fc780465 / M365D_AdvancedHunting (captures AH queries from Triage MCP, Defender portal, Security Copilot that hit connected LA tables; shared surface, see Query 7b)Sentinel Portal (80ccca67), Sentinel Engine analytics (fc780465, non-AH), Logic Apps (de8c33bb)
Data Lake TierCloudAppEventsData Lake MCP (RecordType 403, IMcpToolTemplate)Direct KQL (RecordType 379, KqsService)
Graph APIMicrosoftGraphActivityLogsGraph MCP (e8c77dc2)
Azure MCPSigninLogs, AADNonInteractiveUserSignInLogs, LAQueryLogsAzure MCP Server (04b07795, empty RequestClientApp, query text `\nlimit N` suffix)

Query 25: Azure MCP Server — Authentication Events (SigninLogs)

⚠️ Pitfall-aware: Uses parse_json(Status)/parse_json(DeviceDetail) wrappers — see SigninLogs Status Field Needs parse_json(). Uses extend SignInType to avoid Type pseudo-column — see Type Column Unavailable in Data Lake Union Contexts.

// 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 26)
// 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 26: 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 27: Azure MCP Server — AzureActivity Claims Correlation

⚠️ Pitfall-aware: Uses parse_json(Claims) to extract appid — AzureActivity Claims is a JSON string. Only ARM write/action/delete operations appear (reads not logged). See Azure MCP Server Detection.

// Check AzureActivity for Azure MCP Server write operations (read ops not logged)
// Parse Claims.appid to identify the source application
// 🔄 UPDATED Feb 2026: Now uses Azure CLI AppId 04b07795 (previously 1950a258)
AzureActivity
| where TimeGenerated >= ago(90d)
| where CategoryValue == "Administrative"
| extend ClaimsData = parse_json(Claims)
| extend ClaimsAppId = tostring(ClaimsData.appid)
| where ClaimsAppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| project TimeGenerated, Caller, CallerIpAddress,
    OperationNameValue, ResourceProviderValue,
    ActivityStatusValue, ClaimsAppId,
    ResourceGroup
| order by TimeGenerated desc

Report Template

Inline Chat Report Structure

The inline report MUST include these sections in order:

  1. Header — Workspace, analysis period, data sources checked, MCP servers detected
  2. Executive Summary — 2-3 sentence overview of MCP usage posture
  3. Graph MCP Server Analysis
    • Daily usage trend (ASCII bar chart showing requests/day over the period)
    • Top endpoints table (endpoint, call count, % of total, last used)
    • Sensitive API access summary with user attribution
  4. Sentinel Triage MCP Analysis
    • Triage MCP API calls from MicrosoftGraphActivityLogs — filtered by dedicated AppId 7b7b3966 ("Microsoft Defender Mcp")
    • Triage MCP authentication events from SigninLogs/AADNonInteractiveUserSignInLogs — sign-in frequency, user attribution, IP, country
    • User attribution table with sign-in type breakdown
  5. Sentinel Data Lake MCP Analysis
    • MCP tool usage summary (success/failure, avg duration)
    • Tool breakdown table (query_lake, list_sentinel_workspaces, search_tables, etc.)
    • Error analysis with error categories and sample failure reasons
    • Daily activity trend (ASCII bar chart)
    • MCP vs Direct KQL delineation table
  6. Azure MCP & ARM Analysis
    • Azure MCP Server authentication events (detected via AppId 04b07795 — Azure CLI credential, shared AppId)
    • Azure MCP Server workspace queries from LAQueryLogs (detected via AADClientId 04b07795 + empty RequestClientApp + \n| limit N query text suffix)
    • ARM operation volume and resource providers accessed
    • Source attribution via Claims.appid (Azure Portal, AI Studio, Power Platform connectors, etc.)
  7. Workspace Query Governance (Two-Tier)
    • Analytics Tier (LAQueryLogs): All query sources table with MCP vs Portal vs Platform breakdown
    • Data Lake Tier (CloudAppEvents): MCP-driven vs Direct KQL breakdown
    • Combined MCP proportion across both tiers
    • Pareto analysis of query sources
  8. MCP Usage Score — Per-dimension breakdown with scoring rationale
  9. Security Assessment — Emoji-coded findings table with evidence citations
  10. Recommendations — Prioritized action items based on findings

Report Visualization Patterns

Daily Usage Trend (ASCII)

Graph MCP Usage — Last 30 Days
Day         Calls  Trend
─────────────────────────────────────
2026-02-07  │ 23   ████████████
2026-02-06  │  0   
2026-02-05  │ 45   ██████████████████████
2026-02-04  │ 12   ██████
...
─────────────────────────────────────
Avg: 15.2/day  Peak: 45  Total: 152

Workspace Query Proportion (ASCII)

Analytics Tier Query Sources — Last 30d (LAQueryLogs)
──────────────────────────────────────────
Sentinel Engine    ████████████████████████████████████ 88.4%  (10,354)
Logic Apps         ████                                  7.0%     (821)
Triage MCP          █                                    4.1%     (481)
Sentinel Portal                                          0.4%      (48)
──────────────────────────────────────────
MCP Servers: 4.1% │ Portal: 0.4% │ Platform: 95.4%

Data Lake Tier Query Sources — Last 30d (CloudAppEvents)
──────────────────────────────────────────
Data Lake MCP      ████████████████████████████████████ 97.1%  (1,028)
Direct KQL                                               2.9%      (34)
──────────────────────────────────────────
MCP Server-Driven: 97.1% │ Direct KQL: 2.9%

Endpoint Access Distribution (ASCII)

Top Graph MCP Endpoints — 30d
─────────────────────────────────────────────────────
conditionalAccess/policies    ████████████  27  (17.8%)
users                         ██████████    22  (14.5%)
roleManagement/directory      ████████      18  (11.8%)
servicePrincipals             ██████        14   (9.2%)
groups                        █████         11   (7.2%)
...
─────────────────────────────────────────────────────
🔴 Sensitive: 82/152 (53.9%)  │  ✅ Standard: 70/152 (46.1%)

MCP Usage Score Card (ASCII)

┌──────────────────────────────────────────────────────┐
│               MCP USAGE SCORE: 22/100                │
│                 Rating: ✅ HEALTHY                    │
├──────────────────────────────────────────────────────┤
│ User Diversity     [██░░░░░░░░] 3/20  (1-2 users)   │
│ Endpoint Sensitiv  [████████░░] 14/20 (54% sensitive)│
│ Error Rate         [░░░░░░░░░░] 0/20  (<1% errors)  │
│ Volume Anomaly     [██░░░░░░░░] 3/20  (within norm)  │
│ Off-Hours Activity [█░░░░░░░░░] 2/20  (<5% off-hrs)  │
└──────────────────────────────────────────────────────┘

Markdown File Report Structure

When outputting to markdown file, include everything from the inline format PLUS:

# MCP Server Usage Monitoring Report

**Generated:** YYYY-MM-DD HH:MM UTC
**Workspace:** <workspace_name>
**Analysis Period:** <start> → <end> (<N> days)
**Data Sources:** MicrosoftGraphActivityLogs, SigninLogs, LAQueryLogs, CloudAppEvents, AzureActivity, SentinelAudit

---

## Executive Summary

<2-3 sentence summary: MCP servers detected, total usage volume, risk level, key findings>

---

## Graph MCP Server

### Daily Usage Trend
<ASCII bar chart — requests per day>

### Top Endpoints
| Rank | Endpoint | Calls | % Total | Users | Last Used |
|------|----------|-------|---------|-------|-----------|
| 1 | ... | ... | ... | ... | ... |

### Sensitive API Access
| Endpoint | Calls | Users | Methods | Risk |
|----------|-------|-------|---------|------|
| roleManagement/... | 18 | 1 | GET | 🟠 Read access to PIM |
| ... | ... | ... | ... | ... |

**Summary:** X of Y calls (Z%) targeted sensitive endpoints. <Risk assessment>.

---

## Sentinel Triage MCP

### Triage MCP API Calls (MicrosoftGraphActivityLogs — AppId `7b7b3966`)
| Endpoint | Method | Calls | Users | First Seen | Last Seen |
|----------|--------|-------|-------|------------|----------|
| ... | ... | ... | ... | ... | ... |

### Triage MCP Authentication Events (SigninLogs — "Microsoft Defender Mcp")
| Sign-In Type | Sign-Ins | Users | IPs | Countries | Resource | Last Seen |
|-------------|----------|-------|-----|-----------|----------|----------|
| ... | ... | ... | ... | ... | ... | ... |

---

## Sentinel Data Lake MCP

> **Audit Source:** `CloudAppEvents` (Purview unified audit log)  
> **Classification:** RecordType 403 + Interface `IMcpToolTemplate` = MCP-driven | RecordType 379 = Direct KQL

### MCP vs Direct KQL Delineation
| Access Pattern | Total Calls | Success | Failures | Error Rate | Avg Duration | Users |
|---------------|-------------|---------|----------|------------|-------------|-------|
| 🤖 MCP Server-Driven | ... | ... | ... | ...% | ...s | ... |
| 👤 Direct KQL | ... | ... | ... | ...% | ...s | ... |

### MCP Tool Breakdown
| Tool Name | Calls | Success | Failures | Error Rate | Avg Duration | Last Seen |
|-----------|-------|---------|----------|------------|-------------|-----------|
| `query_lake` | ... | ... | ... | ...% | ...s | ... |
| `list_sentinel_workspaces` | ... | ... | ... | ...% | ...s | ... |
| `search_tables` | ... | ... | ... | ...% | ...s | ... |
| ... | ... | ... | ... | ... | ... | ... |

### Error Analysis
| Error Category | Count | % of Failures | Sample Error | Affected Tools |
|---------------|-------|---------------|--------------|----------------|
| Schema/Semantic Error | ... | ...% | `column 'X' does not exist` | ... |
| ... | ... | ... | ... | ... |

### Daily Activity Trend
<ASCII bar chart — MCP + Direct KQL calls per day>

---

## Azure MCP Server

> **Detection Method:** Azure CLI credential (AppId `04b07795`, shared with manual `az` CLI). `RequestClientApp` is empty in LAQueryLogs. Best differentiator: Azure MCP appends `\\n| limit N` to query text via `monitor_workspace_log_query`. 🔄 Previously documented as AppId `1950a258` + `csharpsdk,LogAnalyticsPSClient` — that fingerprint is obsolete.

### Authentication Timeline
| Timestamp | Resource | Result | Auth Type | UserAgent | Notes |
|-----------|----------|--------|-----------|-----------|-------|\n| ... | ... | ... | ... | ... | ... |

### Workspace Queries (LAQueryLogs)
| Timestamp | Query (truncated) | Response | CPU (ms) | Source App |
|-----------|-------------------|----------|----------|------------|\n| ... | ... | ... | ... | ... |

### AzureActivity Write Operations
| Timestamp | Operation | Resource Provider | Status | Claims.appid |
|-----------|-----------|-------------------|--------|-------------|\n| ... | ... | ... | ... | `04b07795` |

---

## Azure ARM Operations (All Sources)

> **Source Attribution:** ARM operations attributed via `Claims.appid` in AzureActivity.\n> Azure MCP Server read-only operations NOT logged in AzureActivity.

### ARM Source Attribution
| AppId | App Name | Calls | Operations |
|-------|----------|-------|------------|
| ... | ... | ... | ... |

### Operations by Resource Provider
| Resource Provider | Calls | Top Operations | Distinct Resources |
|-------------------|-------|----------------|-------------------|
| ... | ... | ... | ... |

---

## Workspace Query Governance (Two-Tier)

### Analytics Tier (LAQueryLogs)
| Rank | AppId | Source | Category | Queries | % Total | Users |
|------|-------|--------|----------|---------|---------|-------|
| 1 | ... | Sentinel Engine | Platform | ... | ... | ... |
| 2 | ... | Sentinel Triage MCP | MCP Server | ... | ... | ... |
| 3 | ... | Sentinel Portal | Portal | ... | ... | ... |
| ... | ... | ... | ... | ... | ... | ... |

### Data Lake Tier (CloudAppEvents)
| Access Pattern | Calls | % Total | Users | Tables Accessed |
|---------------|-------|---------|-------|-----------------|
| 🤖 MCP Server-Driven | ... | ...% | ... | ... |
| 👤 Direct KQL | ... | ...% | ... | ... |

### Combined MCP Proportion
<ASCII proportion bar — Analytics + Data Lake tiers combined>

MCP queries represent X% of combined query volume:
- Analytics tier: X of Y queries via Sentinel Triage MCP (Z%)
- Data Lake tier: X of Y queries via Data Lake MCP (Z%)
- Graph API: X calls via Graph MCP

---

## Agent Identity Attribution

### Caller Type Breakdown (Graph MCP)
| Caller Type | Distinct Callers | Total Calls | % of Graph MCP |
|-------------|-----------------|-------------|----------------|
| 👤 Human User | ... | ... | ...% |
| 🤖 Service Principal | ... | ... | ...% |
| 🤖🔵 Agent Identity (SPN subtype) | ... | ... | ...% |
| 👤🔵 Agent User | ... | ... | ...% |

### Agent Identity Inventory (from AuditLogs + Graph API `/beta` tags)
| Agent Name | SPN ID | AppId | Tags | Created By | Created | Status | Sign-In Resource |
|------------|--------|-------|------|-----------|---------|--------|------------------|
| ... | ... | ... | `AgenticApp`, `AgentCreatedBy:CopilotStudio` | Power Virtual Agents Service | YYYY-MM-DD | Active/Deleted | Bot Framework |

### Agent Lifecycle Timeline

── ── (by ) └──


> **Detection Method (Field-Tested):**
> 1. `AuditLogs` → filter for `OperationName has "agent"` or `InitiatedBy = "Power Virtual Agents Service"` or `TargetResources has "Copilot Studio"`
> 2. Graph API → `GET /beta/servicePrincipals?$filter=id in (...)&$select=id,appId,displayName,servicePrincipalType,tags`
> 3. **Primary classifier:** `/beta` `tags` array — look for `AgenticApp`, `AIAgentBuilder`, `AgentCreatedBy:CopilotStudio`
> 4. **DO NOT** rely on `servicePrincipalType` (still shows `"Application"` for agents) or display name (unreliable)
> 5. If no Agent Identities exist in tenant: "✅ No Entra Agent Identities detected in this tenant."

---

## MCP Usage Score

<ASCII score card>

### Dimension Breakdown
| Dimension | Score | Evidence |
|-----------|-------|----------|
| User Diversity | X/20 | N distinct users across M MCP channels |
| Endpoint Sensitivity | X/20 | N% of Graph MCP calls to sensitive endpoints |
| Error Rate | X/20 | N% error rate across all channels |
| Volume Anomaly | X/20 | Peak day was N% of rolling average |
| Off-Hours Activity | X/20 | N% of calls outside 08:00-18:00 UTC |

---

## Security Assessment

| Factor | Finding |
|--------|---------|
| 🔴/🟢/🟡/🟠 **Factor** | Evidence-based finding |

---

## Recommendations

1. ⚠️/🟢 <Prioritized action item with evidence>
2. ...

---

## Appendix: Query Details

Render a single markdown table summarizing all queries executed. **Do NOT include full KQL text** — the canonical queries are already documented in this SKILL.md file. The appendix serves as an audit trail only.

| Query | Table(s) | Records Scanned | Results | Execution |
|-------|----------|----------------:|--------:|----------:|
| Q1 — Graph MCP Daily Usage | MicrosoftGraphActivityLogs | X,XXX | N rows | X.XXs |
| Q2 — Top Graph Endpoints | MicrosoftGraphActivityLogs | X,XXX | N rows | X.XXs |
| ... | ... | ... | ... | ... |

*Query definitions: see the Sample KQL Queries section in this SKILL.md file.*

Proactive Alerting — KQL Data Lake Jobs

This skill provides on-demand visibility (Phases 1-7 above). For continuous, scheduled anomaly detection that feeds Sentinel analytics rules, use the companion KQL Data Lake Jobs defined in:

📄 queries/identity/mcp_anomaly_detection_kql_jobs.md

Maturity Model

TierCapabilityImplementation
1. Visibility (current skill)On-demand MCP usage reports via Copilot chatThis SKILL.md — Phases 1-7, Queries 1-27
2. Baselining14-day behavioral baselines per user per MCP serverKQL Jobs 1-8 build baselines automatically
3. AlertingAutomated anomaly detection → Sentinel incidentsKQL Jobs promote to _KQL_CL tables → Analytics Rules fire
4. EnforcementReal-time guardrails, scope limits (future)Not yet available — requires MCP protocol-level controls

KQL Job Inventory

JobAnomaly TypeSource Table(s)Destination TableSchedule
1New sensitive Graph endpointMicrosoftGraphActivityLogsMCPGraphAnomalies_KQL_CLDaily
2Graph MCP volume spike (3x baseline)MicrosoftGraphActivityLogsMCPGraphAnomalies_KQL_CLDaily
3Off-hours Graph MCP activityMicrosoftGraphActivityLogsMCPGraphAnomalies_KQL_CLDaily
4Graph MCP error rate anomalyMicrosoftGraphActivityLogsMCPGraphAnomalies_KQL_CLDaily
5New Azure MCP Server userAADNonInteractiveUserSignInLogsMCPAzureAnomalies_KQL_CLDaily
6New Azure MCP resource targetAADNonInteractiveUserSignInLogsMCPAzureAnomalies_KQL_CLDaily
7Sentinel workspace query anomaliesLAQueryLogsMCPSentinelAnomalies_KQL_CLDaily
8Cross-MCP activity chainsMultiple (join)MCPCrossMCPCorrelation_KQL_CLDaily

Why KQL Jobs (Not Summary Rules)

KQL jobs support multi-table joins — critical for Job 7 (LAQueryLogs + baseline) and Job 8 (Graph + Azure + Sentinel cross-correlation). Summary rules are limited to single-table with lookup() joins to analytics-tier tables only.

Architecture

Data Lake ──[KQL Jobs (daily)]──► _KQL_CL tables (analytics tier) ──[Analytics Rules]──► Incidents

Key design constraints:

  • 15-minute delay: All queries use now() - 15m to account for Data Lake ingestion latency
  • Anomaly-only promotion: Only flagged records are written to analytics tier (cost optimization)
  • Separate timestamp: DetectedTime preserves original event time; TimeGenerated reflects job execution time
  • 3 concurrent job limit: Per tenant — prioritize Jobs 1, 7, 8 for highest-value detections

For full query definitions, deployment checklist, and companion analytics rule templates, see queries/identity/mcp_anomaly_detection_kql_jobs.md.


Known Pitfalls

Agent Identity vs Standard SPN Ambiguity

Problem: MicrosoftGraphActivityLogs does NOT yet distinguish Agent Identities from standard SPNs. Both appear in the ServicePrincipalId field with no subtype indicator. Agent Users appear in UserId indistinguishably from human users. Additionally, servicePrincipalType still returns "Application" even for confirmed Copilot Studio agents — the "Agent" subtype is not yet populated (as of Feb 2026). This is a documented known limitation of the Entra Agent ID preview.
Solution (Field-Tested): Use the /beta tags property as the primary detection method. Query: GET /beta/servicePrincipals/{id}?$select=id,appId,displayName,servicePrincipalType,tags. Confirmed Copilot Studio agents will have tags including AgenticApp, AIAgentBuilder, AgentCreatedBy:CopilotStudio, and AgenticInstance. Standard app registrations (even those with "Agent" in the display name) will NOT have these tags — they may have tags like GitCreatedApp or disableLegacyUserImpersonation* instead. For UserId values, check against the tenant's agent user inventory. If no Agent Identities exist in the tenant, note this and skip agent-specific analysis.

Agent Display Name Unreliable for Classification

Problem: An SPN with "Agent" in its display name is NOT necessarily an Agent Identity. Example from live testing: "Contoso Agent Tools" is a standard app registration (servicePrincipalType: Application, tags: GitCreatedApp) — NOT a Copilot Studio agent despite the name. Conversely, a generic name like "Agent (Microsoft Copilot Studio)" IS a real agent.
Solution: NEVER classify SPNs as agents based on display name alone. Always check the /beta tags array for agentic indicators (AgenticApp, AgentCreatedBy:CopilotStudio). In AuditLogs, check InitiatedBy — agents created by Power Virtual Agents Service are Copilot Studio agents, while those created by a user@domain.com are manual app registrations.

Azure MCP Server Detection (🔄 Updated Feb 2026)

Problem: Azure MCP Server uses DefaultAzureCredential and the credential chain now resolves to Azure CLI (AppId 04b07795-8ddb-461a-bbee-02f9e1bf7b46), NOT AzurePowerShellCredential (1950a258) as previously documented. In LAQueryLogs, RequestClientApp is empty (not csharpsdk,LogAnalyticsPSClient). The previously documented fingerprint (1950a258 + csharpsdk,LogAnalyticsPSClient) appeared only once in 30-day lookback and is obsolete. ARM read operations (the majority of MCP calls) do not appear in AzureActivity.

Previous fingerprint (OBSOLETE):

  • ❌ AppId 1950a258-227b-4e31-a9cf-717495945fc2 (AzurePowerShellCredential)
  • RequestClientApp = "csharpsdk,LogAnalyticsPSClient" in LAQueryLogs
  • ❌ UserAgent azsdk-net-Identity as primary differentiator (shared by many Azure SDK services)

Current fingerprint (field-tested Feb 2026):

  • ✅ AppId 04b07795-8ddb-461a-bbee-02f9e1bf7b46 (Azure CLI)
  • RequestClientApp is empty (shared with Azure CLI and 4+ other AADClientIds — not a unique fingerprint)
  • ✅ Azure MCP monitor_workspace_log_query appends \n| limit N to query text — best query-level differentiator
  • ✅ Token caching: sign-in events represent access sessions, not individual API calls

Solution: Azure MCP Server queries can be identified in LAQueryLogs with moderate confidence by filtering for AADClientId 04b07795 + query text containing \n| limit (the suffix added by monitor_workspace_log_query). In SigninLogs, the shared AppId means Azure MCP is indistinguishable from manual Azure CLI usage — present as "Azure MCP Server / Azure CLI (shared AppId 04b07795)" in reports. The empty RequestClientApp bucket contains queries from 5+ different tools, so this field cannot be used for attribution.

Limitations:

  • ARM read operations produce sign-in events but NOT AzureActivity records
  • If the user also runs az CLI manually, sign-in events from both are indistinguishable
  • The \n| limit N query text suffix is the only reliable query-level differentiator but is heuristic
  • The credential chain may change with Azure MCP Server updates — monitor for AppId shifts
  • AzureActivity has ~2-4h ingestion lag; SigninLogs ~1-2h; LAQueryLogs/AADNonInteractiveUserSignInLogs ~5-15 min

MicrosoftGraphActivityLogs Availability

Problem: Graph activity logs are NOT enabled by default. If the table is empty or doesn't exist, Graph MCP analysis cannot proceed.
Solution: If MicrosoftGraphActivityLogs returns 0 results or table-not-found error, report: "⚠️ Microsoft Graph activity logs are not enabled in this tenant. Enable them at: https://learn.microsoft.com/en-us/graph/microsoft-graph-activity-logs-overview". Skip Graph MCP analysis gracefully and proceed with other MCP channels.

LAQueryLogs Diagnostic Settings

Problem: LAQueryLogs requires diagnostic settings to be configured on the Log Analytics workspace. Without it, workspace query governance analysis is impossible.
Solution: If LAQueryLogs returns empty, report: "⚠️ LAQueryLogs not available — enable Log Analytics workspace diagnostic settings to monitor query activity." Skip workspace governance analysis and note the gap.

AppId Misclassification History (Field-Tested Feb 2026)

80ccca67 — Previously assumed to be a Graph MCP variant. Actually the M365 Security & Compliance Center (Sentinel Portal backend, RequestClientApp = ASI_Portal). Categorize as "Sentinel Portal (Non-MCP)". Graph MCP has only ONE AppId: e8c77dc2.

95a5d94c — Previously assumed to be "VS Code Copilot" (MCP Client). Actually the Azure Portal — AppInsightsPortalExtension blade, executing Usage dashboard/workbook queries in the browser. No SPN or app registration in tenant; not in merill/microsoft-info known apps list. Categorize as "Portal/Platform (Non-MCP)".

📘 Takeaway: When encountering an unknown AppId in LAQueryLogs, check the RequestClientApp field first — it reliably reveals the actual source (e.g., AppInsightsPortalExtension, ASI_Portal). Do not assume an AppId is MCP-related without verifying via Graph API SPN lookup, sign-in logs, and query content analysis.

CloudAppEvents CamelCase Matching (ActionType AND Operation)

Problem: Both ActionType and RawEventData.Operation values in CloudAppEvents for Sentinel operations use CamelCase without word boundaries (e.g., SentinelAIToolRunCompleted, KQLQueryCompleted). The has operator requires word boundaries and will NOT match these values. Field-tested Feb 2026: has "Completed" returns false for ALL Operation values including KQLQueryCompleted — the has operator fails on substrings within CamelCase tokens.
Solution: Always use contains (not has) when filtering ActionType or Operation for Sentinel/KQL operations:

// ✅ CORRECT — 'contains' works with CamelCase
| where ActionType contains "Sentinel" or ActionType contains "KQL"
| where Operation contains "Completed"

// ❌ WRONG — 'has' requires word boundaries, fails on CamelCase
| where ActionType has "Sentinel" or ActionType has "KQL"
| where Operation has "Completed"  // Returns 0 rows — silently drops ALL MCP events!

Impact if missed: Query 20 (MCP vs Direct KQL delineation) will show 0 MCP events and ONLY Direct KQL — because MCP events (RecordType 403) are filtered out by Operation has "Completed", while Direct KQL events (RecordType 379) survive via the OR RecordType == 379 fallback. This creates a false impression that no MCP-driven queries exist.

CloudAppEvents RawEventData Parsing

Problem: RawEventData in CloudAppEvents is a dynamic column but often contains nested JSON that requires double-parsing. Direct property access (e.g., RawEventData.ToolName) may return empty.
Solution: Always parse explicitly with parse_json(tostring(RawEventData)):

| extend RawData = parse_json(tostring(RawEventData))
| extend ToolName = tostring(RawData.ToolName)

Data Lake MCP Has No AppId

Problem: Unlike Graph MCP (e8c77dc2) and Sentinel Triage MCP (7b7b3966), the Sentinel Data Lake MCP has no dedicated AppId in any telemetry table. It is not visible in LAQueryLogs, SigninLogs, or MicrosoftGraphActivityLogs.
Solution: Data Lake MCP activity is audited exclusively via CloudAppEvents (Purview unified audit log). Filter by ActionType contains "SentinelAITool" (preferred — top-level column) or extract RecordType from RawEventData with toint(parse_json(tostring(RawEventData)).RecordType) == 403 and Interface == "IMcpToolTemplate". Note: RecordType is NOT a top-level column in CloudAppEvents — it is nested inside RawEventData and must be extracted via parse_json().

Table availability (field-tested Feb 2026): CloudAppEvents was confirmed available on both Data Lake (TimeGenerated, 90d retention) and Advanced Hunting (Timestamp, 30d retention) in a standard Sentinel workspace without explicit Purview/E5 configuration. Always attempt the query first — only report a gap if the table returns 0 results or a table-not-found error. Do not skip Phase 3 based on licensing assumptions.

CloudAppEvents Double-Counting Prevention

Problem: Each Data Lake MCP tool call generates TWO events: SentinelAIToolRunStarted (RecordType 403) and SentinelAIToolRunCompleted (RecordType 403). Counting both will double the actual call count.
Solution: Always filter on Operation == "SentinelAIToolRunCompleted" for call counts, duration analysis, and error analysis. Use SentinelAIToolRunStarted only when investigating specific timing sequences or queue behavior.

Data Lake MCP ExecutionDuration Format

Problem: The ExecutionDuration field in RawEventData is stored as a string (e.g., "2.4731712"), not a numeric type. Aggregation functions (avg, max) will fail without conversion.
Solution: Use todouble(RawData.ExecutionDuration) to convert before aggregation.

Sentinel Engine False Association

Problem: The Sentinel analytics engine (fc780465-2017-40d4-a0c5-307022471b92) generates the highest query volume in most workspaces but is NOT an MCP server. Including it in MCP totals would massively inflate the numbers.
Solution: ALWAYS label Sentinel Engine and Logic Apps Connector as "Platform (Non-MCP)" in reports. The MCP proportion calculation MUST exclude these from the MCP numerator.

SigninLogs Status Field Needs parse_json() in Data Lake

Problem: The Status column in SigninLogs / AADNonInteractiveUserSignInLogs is a dynamic field containing {errorCode, failureReason, additionalDetails}, but Data Lake workspaces may store it as a string. Using dot-notation (Status.errorCode) without parse_json() causes parser errors (Expected: ;) or SemanticErrors.
Solution: Always use tostring(parse_json(Status).errorCode) — same pattern as DeviceDetail, LocationDetails, and ConditionalAccessPolicies. This works regardless of whether the column is stored as dynamic or string. Query 5 was fixed for this in Feb 2026.

Type Column Unavailable in Data Lake Union Contexts

Problem: The Type pseudo-column (table name) is NOT resolvable in union queries executed via Sentinel Data Lake. Using summarize by Type in a union SigninLogs, AADNonInteractiveUserSignInLogs query fails with SemanticError: Failed to resolve scalar expression named 'Type'.
Solution: When you need to distinguish source tables in a union, add | extend TableName = "SigninLogs" (or "AADNonInteractive") within each union leg before the union operator. Then summarize by TableName. This is already handled in Query 25 via the SignInType field pattern (extend SignInType = "Interactive" / "Non-Interactive"), but ad-hoc summary variants must use the extend approach — never Type.

Non-Interactive Sign-In Noise

Problem: AADNonInteractiveUserSignInLogs may contain Logic Apps connector activity (de8c33bb) that looks like user activity but is automated.
Solution: When reporting Sentinel MCP auth events from SigninLogs, distinguish interactive (user-initiated) from non-interactive (automated) sources. The LogicApps connector is NOT MCP — exclude it from MCP auth counts.

Off-Hours Timezone Uncertainty

Problem: TimeGenerated is always UTC, but "off-hours" has different meaning depending on the user's timezone. A UTC 06:00 call might be 22:00 local or 14:00 local.
Solution: Default to UTC for off-hours calculation. If the user's timezone is known from sign-in data (LocationDetails), adjust. Always state the timezone assumption in the report.

Multi-Tenant Token Confusion

Problem: Azure MCP Server uses DefaultAzureCredential and may authenticate against the wrong tenant if multiple credentials are cached, causing queries to fail or return data from an unexpected tenant.
Solution: Read config.json for the azure_mcp.tenant parameter. When making Azure MCP Server calls, always pass the tenant parameter explicitly. Note this risk in the report.

Agent User UPNs Masquerading as Human Users

Problem: Agent Users are assigned UPNs and appear in UserId fields identically to human users. Without checking the account type, an agent user's MCP activity would be reported as human activity.
Solution: When the MCP Usage Score flags unexpected user diversity or new users (Query 12), cross-reference the UserId with Entra to check if the user object is an Agent User (userType and account metadata). Agent Users created by Copilot Studio or Agent Identity Blueprints will have specific metadata distinguishing them from workforce accounts.

Rate Limiting Not Visible in Logs

Problem: Graph MCP Server is capped at 100 calls/min/user. If throttled, calls may not appear in logs (no log entry = no visibility).
Solution: If daily call counts show sudden drops to 0 after a high-volume period, note possible throttling. Check for 429 Too Many Requests response codes in Query 1 raw data.

SentinelAudit Table Availability

Problem: SentinelAudit requires Sentinel auditing and health monitoring to be enabled. It may not exist in all workspaces.
Solution: If SentinelAudit returns table-not-found, skip Query 15 gracefully. Report: "⚠️ Sentinel auditing not enabled — cannot check configuration changes."


Error Handling

Common Issues

IssueSolution
MicrosoftGraphActivityLogs table not foundGraph activity logs not enabled. Report gap, skip Graph MCP analysis, provide enablement link.
LAQueryLogs table not foundDiagnostic settings not configured on LA workspace. Report gap, skip governance analysis.
SentinelAudit table not foundSentinel health monitoring not enabled. Report gap, skip config change analysis.
AzureActivity returns 0 resultsNo ARM operations in the time range, or no administrative actions by the specified user.
Agent Identity query returns emptyEntra Agent ID not enabled or no agents registered. Report gap as "✅ No Agent Identities detected", skip Queries 18-19.
AADServicePrincipalSignInLogs missing agent subtypeAgent ID preview may not be active. Agent SPNs will appear as regular SPNs. Note limitation.
SPN servicePrincipalType shows Application for known agentsExpected behavior as of Feb 2026. Copilot Studio agents still report as Application. Use /beta tags array instead (AgenticApp, AgentCreatedBy:CopilotStudio).
Agent sign-ins show Azure internal IPv6 (fd00:*)Expected for Copilot Studio agents — they authenticate from Azure infrastructure. Not a concern.
Agent SPN hard-deleted shortly after creationTesting/experimentation pattern. AuditLogs show Add service principalHard delete service principal within minutes. Report duration and flag if unexpected.
SigninLogs returns 0 for Sentinel Platform ServicesNo one authenticated to Sentinel MCP in the time range. Report as "✅ No Sentinel MCP authentication events detected."
CloudAppEvents table not foundPurview unified audit not available (requires E5 license). Report gap: "⚠️ CloudAppEvents not available — cannot monitor Data Lake MCP usage. Requires Microsoft 365 E5 or Purview audit." Skip Phase 3 (Data Lake MCP).
CloudAppEvents returns 0 for Sentinel operationsNo Data Lake MCP or Direct KQL activity in the time range. Report as "✅ No Sentinel Data Lake activity detected in CloudAppEvents."
ActionType has "Sentinel" returns 0 but data existsCamelCase bug — use contains instead of has for ActionType matching. See Known Pitfalls.
Operation has "Completed" drops MCP events silentlySame CamelCase bug — has "Completed" returns false for ALL CamelCase operations (SentinelAIToolRunCompleted, KQLQueryCompleted). MCP events (RecordType 403) are silently dropped; Direct KQL survives only via OR RecordType == 379 fallback. Use contains "Completed". See Known Pitfalls.
RawEventData.ToolName returns emptyDouble-parse required: use parse_json(tostring(RawEventData)) then extract fields. See Known Pitfalls.
Query timeoutReduce lookback from 30d to 7d, or add `
Unknown AppId in LAQueryLogsCross-reference with Entra ID > App Registrations. May be a custom MCP server or third-party tool.
Multiple workspaces availableFollow workspace selection rules — STOP, list all, ASK user, WAIT.
Azure MCP calls indistinguishable from CLIPartially resolved: AppId 04b07795 is shared with Azure CLI. Use `\n

Validation Checklist

Before presenting results, verify:

  • All MCP telemetry surfaces were queried (Graph, Sentinel Triage, Sentinel Data Lake, Azure ARM, LAQueryLogs, CloudAppEvents)
  • Tables that don't exist are reported as gaps, not silent omissions
  • Non-MCP sources (Sentinel Engine, Logic Apps, Sentinel Portal) are clearly labeled as "Platform/Portal (Non-MCP)"
  • 80ccca67 is classified as "M365 Security & Compliance Center (Sentinel Portal)" — NOT as an MCP server
  • 95a5d94c is classified as "Azure Portal — AppInsightsPortalExtension" — NOT as MCP Client or VS Code Copilot. Verify via RequestClientApp field.
  • MCP proportion calculation excludes non-MCP platform sources from the MCP numerator
  • Two-tier governance view included: Analytics tier (LAQueryLogs) + Data Lake tier (CloudAppEvents)
  • Data Lake MCP vs Direct KQL delineation is clearly presented (RecordType 403 vs 379)
  • CloudAppEvents queries use contains (not has) for ActionType matching
  • CloudAppEvents queries use contains (not has) for Operation field matching (same CamelCase issue)
  • CloudAppEvents RawEventData is parsed with parse_json(tostring(RawEventData)) pattern
  • Data Lake MCP tool call counts use SentinelAIToolRunCompleted only (not Started) to avoid double-counting
  • All user attribution is based on actual query results, not assumptions
  • Azure MCP Server detection uses AppId 04b07795 (Azure CLI) with empty RequestClientApp and query text \n| limit N suffix as differentiator. Present as "Azure MCP Server / Azure CLI (shared AppId)" in reports
  • Graph MCP sensitive endpoint percentage is calculated from actual data
  • Off-hours analysis states the timezone assumption (default: UTC)
  • Empty results are explicitly reported with ✅ (not silently omitted)
  • AppId cross-reference table is included for any unknown AppIds discovered
  • The MCP Usage Score calculation is transparent with per-dimension evidence
  • All ASCII visualizations are wrapped in code fences for markdown compatibility
  • Agent Identity detection was attempted (Query 17 for caller attribution, Query 19 for CRUD lifecycle, Graph API /beta tags for classification)
  • Graph API cross-reference used /beta endpoint with tags property — NOT servicePrincipalType alone (which is unreliable)
  • Agent classification based on tags (AgenticApp, AgentCreatedBy:CopilotStudio), NOT display name
  • If Agent Identities found: reported separately from human users and standard SPNs, with lifecycle timeline
  • If no Agent Identities: confirmed with "✅ No Entra Agent Identities detected in tenant"

Prerequisites

For complete MCP server monitoring, ensure these data sources are enabled:

Data SourceEnabling DocumentationRequired For
Microsoft Graph activity logsEnable Graph activity logsGraph MCP analysis (Queries 1-4, 11-12, 17)
CloudAppEvents (Purview unified audit)Requires M365 E5 license; enable Sentinel Data Lake auditingData Lake MCP analysis (Queries 20-24)
Sentinel auditing and health monitoringEnable Sentinel monitoringConfig change detection (Query 15)
LAQueryLogs (diagnostic settings)Configure diagnostic settings on LA workspaceWorkspace governance (Queries 7, 10, 13-14, 16)
AzureActivityEnabled by default for ARM operationsAzure MCP analysis (Queries 8-9)
SigninLogsEntra ID diagnostic settingsSentinel MCP auth events (Queries 5-6)
Purview audit logsIncluded with E5 licenseCloudAppEvents ingestion — required for Data Lake MCP monitoring (Queries 20-24). RecordType 403 (AI Tool) and 379 (KQL)

If any prerequisite is not met, the skill will report the gap and skip the affected analysis sections.


Cross-References

  • KQL Jobs for proactive alerting: queries/identity/mcp_anomaly_detection_kql_jobs.md — Scheduled Data Lake jobs that promote MCP anomalies to analytics tier for automated Sentinel alerting
  • Main skill registry: .github/copilot-instructions.md — Skill detection and global rules
  • Scope drift analysis: .github/skills/scope-drift-detection/SKILL.md — Can be run on MCP-related service principals for behavioral drift detection
  • Sentinel Data Lake auditing: Auditing lake activities — Official docs on RecordType 403/379 audit events in CloudAppEvents
  • Sentinel MCP tool collections: Tool collection overview — Data Exploration, Triage, and Security Copilot Agent Creation collections
  • Sentinel MCP custom tools: Create custom MCP tools — Expose saved KQL queries as MCP tools
  • Copilot Studio MCP catalog: Built-in MCP servers — 19+ Microsoft-managed MCP servers for agent development
  • Azure MCP Server tools: Available tools — Full Azure MCP Server tool catalog (40+ namespaces)
  • Power BI MCP: Remote endpoint at https://api.fabric.microsoft.com/v1/mcp/powerbi, Modeling at microsoft/powerbi-modeling-mcp
  • Fabric RTI MCP: Fabric RTI MCP overview | GitHub
  • Playwright MCP: GitHub — Browser automation MCP (26.9k ⭐, local only)
  • Entra Agent ID docs: What are agent identities | Agent sign-in logs | Agent OAuth protocols | Known issues

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

85/100Analyzed 2/22/2026

Highly detailed and well-structured skill for monitoring MCP server usage in Microsoft Sentinel/Defender XDR environments. Provides comprehensive KQL queries, agent identity detection guidance, and clear workflow rules. Slight completeness issue due to truncated section, but otherwise excellent technical reference. Strong bonus for tags, dedicated skills folder, and structured content. Internal-only signal is moderate since it requires Microsoft security products, but the concepts are transferable.

95
90
72
88
82

Metadata

Licenseunknown
Version-
Updated3/5/2026
PublisherSCStelz

Tags

apici-cddatabasegithubgithub-actionsllmobservabilitypromptingsecurity