Un outil déclaré bloqué dans Microsoft Agent 365 peut-il encore atteindre directement son serveur Model Context Protocol (MCP) ? Pour répondre sérieusement, il faut tester le chemin d’exécution, le résultat métier et les routes de contournement — pas seulement capturer une page d’administration.
L’inventaire Agent 365 ne prouve pas le blocage
Dans le centre d’administration Microsoft 365, le chemin Agents → Tools expose les serveurs MCP et les opérations qu’ils publient. Une entrée comme Work IQ peut ainsi présenter des actions distinctes telles que ask, fetch, create, update et delete.

Cette visibilité améliore l’inventaire, mais elle ne constitue pas une preuve d’autorisation. Trois états doivent être distingués : l’outil est visible, une décision de gouvernance est configurée, puis cette décision est effectivement appliquée pendant l’appel.
Le chemin documenté pour connecter une passerelle est Agents → Settings → Gateways. Si cette entrée n’apparaît pas, l’intégration ne doit pas être considérée comme opérationnelle.

Au 25 octobre 2026, Microsoft précise que l’expérience visible peut varier selon le tenant, la région, le déploiement, les licences, l’approbation administrative et les politiques de l’organisation. L’absence du menu ne permet donc pas, à elle seule, d’identifier la cause.
La passerelle doit se trouver dans le chemin réel
L’architecture attendue est simple : l’agent appelle Azure API Management (APIM), qui évalue les politiques avant de transmettre la requête au serveur MCP. Mais une seconde route doit être recherchée : un appel direct de l’agent, ou de son identité, vers le backend.

La documentation Microsoft Learn sur l’intégration confirme que Microsoft Agent 365 ne redirige pas automatiquement les agents vers une passerelle hébergée par le client. Le développeur doit configurer le point de terminaison MCP de la passerelle dans l’agent.
Une politique APIM ne protège pas une route qu’elle ne voit pas
Lors d’un blocage, Microsoft Agent 365 utilise le plan de contrôle APIM pour insérer une politique gérée. Pour un serveur entier, une politique return-response est placée au début de la section inbound de l’API. Sa présence constitue une preuve utile, mais elle doit encore être corrélée avec un appel rejeté et l’absence de modification du backend.
Pour approfondir cette frontière de sécurité, consultez notre guide sur la protection des outils MCP à la source.
Les prérequis dépendent du niveau APIM
L’intégration annoncée le 9 octobre 2026 reste en préversion publique. Le niveau APIM détermine directement la granularité disponible.
| Niveau APIM | Capacités Agent 365 MCP | Limites à intégrer au pilote |
|---|---|---|
| APIM v1 | Non pris en charge | Aucun inventaire ni blocage via cette intégration |
| APIM v2 | Inventaire et blocage du serveur | Pas de blocage individuel des outils |
| AI Gateway en préversion | Blocage du serveur et contrôle par outil | Liste statique des outils requise pour le contrôle fin |
Le blocage par outil n’est disponible que sur le niveau AI Gateway en préversion. Si un serveur MCP ne publie pas de liste statique d’outils autorisés, Microsoft Agent 365 ne peut pas récupérer cette liste ; le blocage doit alors être réalisé depuis le portail AI Gateway.
Selon l’annonce officielle du niveau AI Gateway, la préversion est sans frais et limitée à East US 2 et Sweden Central. Ces contraintes doivent être vérifiées avant d’interpréter l’absence d’une configuration comme une erreur de droits.
L’activation s’effectue dans Agents → Settings → Gateways en activant Azure API Management service and AI Gateway. Un administrateur client habilité accorde ensuite, une seule fois au niveau du tenant, le rôle Azure A365 MCP Gateway Policy Operator. Les opérations de gouvernance utilisent alors l’identité applicative de Microsoft Agent 365.
Agent 365 est inclus dans Microsoft 365 E7
Un pilote doit séparer lecture et écriture
Commencez avec un service à faible risque et des données synthétiques. Une illustration efficace consiste à exposer get_ticket, qui lit un ticket de test, et close_ticket, qui change uniquement son état.
Ces noms représentent un contrat de test, pas des opérations Work IQ existantes. L’objectif est de vérifier trois propriétés : la lecture reste disponible, l’écriture est bloquée pour l’identité restreinte et aucune route directe ne permet la même mutation.
Points forts
- Résultat métier facile à vérifier
- Séparation nette entre lecture et mutation
- Preuves corrélables entre agent, passerelle et backend
Limites
- Un outil générique execute réduit la valeur du contrôle fin
- Un backend surautorisé peut contourner la sémantique du nom de l’outil
Un contrôle post-invocation ne remplace pas l’autorisation préalable. Filtrer la réponse d’un outil après l’appel ne peut pas annuler un ticket fermé, un message envoyé ou une permission modifiée. Pour une écriture irréversible, la décision doit intervenir avant l’exécution.
Mise en œuvre
Aucun module PowerShell externe n’est requis pour le harnais ci-dessous : il utilise les classes .NET disponibles dans PowerShell. Le compte de test doit seulement posséder les autorisations métier nécessaires à l’appel du serveur MCP ; le script ne requiert aucune permission Microsoft Graph ni le rôle A365 MCP Gateway Policy Operator.
Aucun délai de propagation chiffré n’est indiqué dans les documents vérifiés. Ne lancez les tests qu’après l’apparition de la politique gérée dans APIM ; tant qu’elle n’est pas visible, un résultat d’exécution reste non concluant.
Activer et consentir la passerelle
Dans Agents → Settings → Gateways, activez Azure API Management service and AI Gateway, puis faites approuver le rôle Azure A365 MCP Gateway Policy Operator au niveau du tenant.
Faire pointer l’agent vers APIM
Remplacez le point de terminaison MCP direct par celui de la passerelle. Vérifiez ensuite que le serveur backend n’accepte pas la même identité en dehors d’APIM.
Capturer les requêtes du scénario synthétique
Enregistrez le JSON-RPC exact de la lecture et de l’écriture dans read-request.json et write-request.json. N’insérez aucun jeton, secret ou contenu métier réel dans ces fichiers.
Validez d’abord que les deux fichiers contiennent du JSON exploitable :
1$files = './read-request.json', './write-request.json'2 3foreach ($file in $files) {4 if (-not (Test-Path -LiteralPath $file)) {5 throw "Fichier introuvable : $file"6 }7 8 Get-Content -LiteralPath $file -Raw |9 ConvertFrom-Json -ErrorAction Stop |10 Out-Null11}12 13Write-Host 'Les deux charges JSON-RPC sont valides.'Un test direct peut déclencher la mutation réelle
Le script suivant teste le blocage via APIM, l’outil frère autorisé, l’état du ticket, puis la route directe. Il ne conserve ni jeton ni corps de réponse : seulement les statuts, indicateurs isError, identifiants de corrélation et empreintes SHA-256.
1[CmdletBinding()]2param(3 [Parameter(Mandatory)]4 [uri]$GatewayUri,5 6 [Parameter(Mandatory)]7 [uri]$DirectUri,8 9 [Parameter(Mandatory)]10 [ValidateSet('Tool', 'Server')]11 [string]$BlockType,12 13 [Parameter(Mandatory)]14 [string]$ReadRequestPath,15 16 [Parameter(Mandatory)]17 [string]$WriteRequestPath,18 19 [Parameter(Mandatory)]20 [string]$VerificationPattern,21 22 [Parameter(Mandatory)]23 [Security.SecureString]$AccessToken,24 25 [string]$EvidenceDirectory = './evidence',26 27 [ValidateRange(5, 300)]28 [int]$TimeoutSeconds = 3029)30 31Set-StrictMode -Version Latest32$ErrorActionPreference = 'Stop'33 34function Get-Sha256 {35 param([Parameter(Mandatory)][string]$Value)36 37 $algorithm = [Security.Cryptography.SHA256]::Create()38 try {39 $bytes = [Text.Encoding]::UTF8.GetBytes($Value)40 $hash = $algorithm.ComputeHash($bytes)41 return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()42 }43 finally {44 $algorithm.Dispose()45 }46}47 48function Invoke-McpRequest {49 param(50 [Parameter(Mandatory)][string]$Name,51 [Parameter(Mandatory)][uri]$Uri,52 [Parameter(Mandatory)][string]$Body,53 [Parameter(Mandatory)][hashtable]$Headers54 )55 56 $correlationId = [guid]::NewGuid().ToString()57 $message = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::Post, $Uri)58 $responseBody = ''59 $statusCode = $null60 $networkError = $null61 62 try {63 foreach ($header in $Headers.GetEnumerator()) {64 [void]$message.Headers.TryAddWithoutValidation($header.Key, $header.Value)65 }66 67 [void]$message.Headers.TryAddWithoutValidation('X-Correlation-ID', $correlationId)68 $message.Content = [Net.Http.StringContent]::new(69 $Body,70 [Text.Encoding]::UTF8,71 'application/json'72 )73 74 $response = $script:HttpClient.SendAsync($message).GetAwaiter().GetResult()75 try {76 $statusCode = [int]$response.StatusCode77 $responseBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()78 }79 finally {80 $response.Dispose()81 }82 }83 catch {84 $networkError = $_.Exception.Message85 }86 finally {87 $message.Dispose()88 }89 90 $isErrorPattern = [char]34 + 'isError' + [char]34 + '[ ]*:[ ]*true'91 92 [pscustomobject]@{93 Name = $Name94 Uri = $Uri.ToString()95 CorrelationId = $correlationId96 StatusCode = $statusCode97 IsError = $responseBody -match $isErrorPattern98 NetworkError = $networkError99 RequestSha256 = Get-Sha256 -Value $Body100 ResponseSha256 = if ($responseBody) { Get-Sha256 -Value $responseBody } else { $null }101 ResponseBody = $responseBody102 }103}104 105$readBody = Get-Content -LiteralPath $ReadRequestPath -Raw106$writeBody = Get-Content -LiteralPath $WriteRequestPath -Raw107 108$script:HttpClient = [Net.Http.HttpClient]::new()109$script:HttpClient.Timeout = [TimeSpan]::FromSeconds($TimeoutSeconds)110 111$tokenPointer = [IntPtr]::Zero112$plainToken = $null113 114try {115 $tokenPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($AccessToken)116 $plainToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPointer)117 $headers = @{ Authorization = 'Bearer ' + $plainToken }118 119 $blocked = Invoke-McpRequest -Name 'BlockedWriteViaGateway' -Uri $GatewayUri -Body $writeBody -Headers $headers120 $allowed = Invoke-McpRequest -Name 'AllowedReadViaGateway' -Uri $GatewayUri -Body $readBody -Headers $headers121 $verifyGateway = Invoke-McpRequest -Name 'VerifyAfterGatewayBlock' -Uri $GatewayUri -Body $readBody -Headers $headers122 $direct = Invoke-McpRequest -Name 'DirectWriteAttempt' -Uri $DirectUri -Body $writeBody -Headers $headers123 $verifyDirect = Invoke-McpRequest -Name 'VerifyAfterDirectAttempt' -Uri $GatewayUri -Body $readBody -Headers $headers124 125 $gatewayBlockPassed = if ($BlockType -eq 'Tool') {126 $blocked.StatusCode -eq 200 -and $blocked.IsError127 }128 else {129 $blocked.StatusCode -eq 403130 }131 132 $checks = [ordered]@{133 GatewayBlockRecognized = $gatewayBlockPassed134 SiblingReadStillAllowed = ($allowed.StatusCode -ge 200 -and $allowed.StatusCode -lt 300 -and -not $allowed.IsError)135 StateUnchangedAfterGateway = ($verifyGateway.ResponseBody -match $VerificationPattern)136 DirectRouteDenied = ($null -ne $direct.NetworkError -or $direct.StatusCode -lt 200 -or $direct.StatusCode -ge 300 -or $direct.IsError)137 StateUnchangedAfterDirect = ($verifyDirect.ResponseBody -match $VerificationPattern)138 }139 140 $publicResults = @(141 $blocked, $allowed, $verifyGateway, $direct, $verifyDirect142 ) | Select-Object Name, Uri, CorrelationId, StatusCode, IsError,143 NetworkError, RequestSha256, ResponseSha256144 145 New-Item -ItemType Directory -Path $EvidenceDirectory -Force | Out-Null146 $timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')147 $evidencePath = Join-Path $EvidenceDirectory "agent365-mcp-$timestamp.json"148 149 [ordered]@{150 GeneratedUtc = (Get-Date).ToUniversalTime().ToString('o')151 BlockType = $BlockType152 Checks = $checks153 Requests = $publicResults154 } | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $evidencePath -Encoding utf8155 156 $checks.GetEnumerator() | Format-Table -AutoSize | Out-Host157 Write-Output $evidencePath158 159 if ($checks.Values -contains $false) {160 exit 2161 }162 163 exit 0164}165finally {166 $script:HttpClient.Dispose()167 $plainToken = $null168 169 if ($tokenPointer -ne [IntPtr]::Zero) {170 [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPointer)171 }172}Exécutez-le avec le jeton de l’identité restreinte et une expression régulière identifiant l’état attendu du ticket :
1$gatewayUri = [uri](Read-Host 'Point de terminaison MCP APIM')2$directUri = [uri](Read-Host 'Point de terminaison MCP direct')3$expectedState = Read-Host 'Expression régulière prouvant que le ticket reste ouvert'4$token = Read-Host 'Jeton OAuth de l’identité de test' -AsSecureString5 6$evidencePath = & ./Test-Agent365McpGovernance.ps1 `7 -GatewayUri $gatewayUri `8 -DirectUri $directUri `9 -BlockType Tool `10 -ReadRequestPath './read-request.json' `11 -WriteRequestPath './write-request.json' `12 -VerificationPattern $expectedState `13 -AccessToken $token14 15if ($LASTEXITCODE -ne 0) {16 throw 'Au moins un contrôle d’acceptation a échoué.'17}Pour un blocage d’outil, la réponse attendue est HTTP 200 avec isError=True dans le JSON-RPC. Pour un serveur bloqué, le résultat attendu est HTTP 403. Examiner uniquement le statut HTTP créerait donc un faux positif pour le contrôle par outil.
Six tests et des preuves corrélées
Le pilote n’est validé que si les contrôles positifs, négatifs et de contournement sont tous documentés.
| Test | Action | Preuve attendue |
|---|---|---|
| Lecture autorisée | Lire le ticket via APIM | Réponse attendue et événement de passerelle corrélé |
| Écriture restreinte | Fermer le ticket via APIM | HTTP 200 avec isError=True pour un outil, ou HTTP 403 pour un serveur |
| Outil frère | Relire après le blocage | Lecture disponible sans panne globale du service |
| Route directe | Tenter la même écriture sur le backend | Refus réseau ou applicatif, puis état métier inchangé |
| Autre identité | Rejouer avec l’opérateur approuvé | Résultat conforme à sa politique propre |
| Retour arrière | Restaurer la configuration approuvée | Capacité rétablie et piste d’audit conservée |

Conservez pour chaque appel l’heure UTC, l’identité effective, l’identifiant de corrélation, la révision de politique, le résultat APIM et l’état final du backend. N’archivez jamais de jeton, de clé d’abonnement ou de charge métier complète.
Le dépannage doit partir du symptôme observé, pas de l’état actuel du bouton d’administration.
| Symptôme | Cause probable | Résolution |
|---|---|---|
| HTTP 200 alors que l’outil est bloqué | Blocage par outil renvoyé dans JSON-RPC | Contrôler isError=True dans le corps de réponse |
| APIM bloque mais le ticket est modifié | Route directe ou autre identité autorisée | Restreindre le backend au trafic de la passerelle et vérifier l’identité effective |
| Tout le serveur devient indisponible | Blocage serveur utilisé faute de granularité | Employer AI Gateway avec une liste statique d’outils ou séparer le contrat MCP |
| Le menu Gateways est absent | Déploiement, région, licence, approbation ou politique du tenant | Vérifier les prérequis sans contourner le processus de consentement |
| Le client échoue sans événement APIM | Requête non routée par la passerelle | Comparer le point de terminaison configuré dans l’agent avec celui d’APIM |
Notre présentation de la gouvernance centralisée avec Agent 365 complète cette validation d’exécution par les responsabilités d’administration et d’inventaire.
Questions fréquentes
Une entrée dans le registre Agent 365 applique-t-elle une autorisation ?
Non. Le registre établit qu’un serveur ou un outil est connu, mais pas que chaque requête traverse un point d’application. Il faut vérifier séparément la configuration de l’agent, la politique APIM et l’état du backend.
Pourquoi un outil bloqué peut-il répondre en HTTP 200 ?
Le blocage par outil renvoie une réponse JSON-RPC avec isError=True et une description afin d’éviter les nouvelles tentatives de l’agent. Le blocage d’un serveur entier renvoie, lui, HTTP 403.
Peut-on gouverner chaque outil avec APIM v2 ?
Non. APIM v2 fournit l’inventaire et le blocage au niveau du serveur. Le contrôle individuel des outils exige le niveau AI Gateway en préversion et une liste statique d’outils autorisés.
Agent 365 force-t-il automatiquement le passage par APIM ?
Non. L’agent doit être configuré pour utiliser le point de terminaison MCP de la passerelle. Le backend doit également refuser ou rendre inaccessible la route directe.
Passer du pilote à une preuve exploitable
Contrôles avant validation de production
- Confirmer le niveau APIM, la région et la prise en charge du serveur MCP
- Documenter le consentement au rôle
A365 MCP Gateway Policy Operator - Vérifier le point de terminaison réellement configuré dans l’agent
- Limiter le backend au trafic provenant de la passerelle
- Attendre l’apparition de la politique gérée avant les tests
- Tester lecture, écriture, identité alternative et route directe
- Corréler le rejet APIM avec l’absence de mutation métier
- Archiver les preuves sans secrets ni charges métier complètes
Si le serveur expose une opération générale comme execute, commencez par redessiner le contrat MCP ou renforcer l’autorisation du backend. Si les opérations de lecture et d’écriture sont déjà séparées, le pilote peut se concentrer sur le blocage de l’écriture et la fermeture de la route directe.
Pour les scénarios où l’autorisation doit être portée jusqu’au service applicatif, notre guide MCP Enterprise Authorization avec Microsoft Entra ID et App Service prolonge cette approche.
Pour aller plus loin
- Govern tools with Microsoft Agent 365 and Azure API Management — prérequis, consentement, politiques gérées et comportement exact des blocages.
- Microsoft Agent 365 and Azure API Management integration for MCP governance — comparaison officielle entre APIM v2 et AI Gateway.
- AI Gateway tier of API Management now in public preview — régions disponibles, modèle de préversion et fonctionnement des politiques.
- Understand MCP server options in Agent 365 — variantes du registre Tools selon le tenant, les licences et les politiques.
