Revoke-MsIdMcpServerPermission.ps1

function Revoke-MsIdMcpServerPermission {
    <#
    .SYNOPSIS
    Revokes delegated permissions from MCP clients for the Microsoft MCP Server for Enterprise.

    .DESCRIPTION
    This cmdlet revokes OAuth2 delegated permissions from MCP clients (like VS Code or Visual Studio)
    to access the Microsoft MCP Server for Enterprise. You can specify predefined clients or
    provide custom MCP client app IDs.

    .PARAMETER MCPClient
    Specifies the predefined MCP client(s) to revoke permissions from. Valid values are:
    - VisualStudio: Visual Studio
    - VisualStudioCode: Visual Studio Code
    - VisualStudioMSAL: Visual Studio MSAL

    .PARAMETER MCPClientServicePrincipalId
    Specifies custom service principal ID(s) to revoke permissions from. Must be valid GUIDs.

    .PARAMETER Scopes
    Specific scopes to revoke. If not specified, all permissions are revoked.

    .EXAMPLE
    Connect-MgGraph -Scopes DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All
    Revoke-MsIdMcpServerPermission
    Revokes all permissions from Visual Studio Code (default MCP client if none specified).

    .EXAMPLE
    Connect-MgGraph -Scopes DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All
    Revoke-MsIdMcpServerPermission -MCPClient VisualStudioCode -Scopes 'Group.Read.All'
    Revokes specific permissions from Visual Studio Code.

    .EXAMPLE
    Connect-MgGraph -Scopes DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All
    Revoke-MsIdMcpServerPermission -MCPClient 'VisualStudio', 'VisualStudioCode'
    Revokes all permissions from Visual Studio and Visual Studio Code.

    .EXAMPLE
    Connect-MgGraph -Scopes DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All
    Revoke-MsIdMcpServerPermission -MCPClientServicePrincipalId '12345678-1234-1234-1234-123456789012'
    Revokes all permissions from a custom MCP client using its service principal ID.

    .EXAMPLE
    Connect-MgGraph -Scopes DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All
    Revoke-MsIdMcpServerPermission -VisualStudioClient 'VisualStudioMSAL' -Scopes 'User.Read.All'
    Revokes specific permissions from Visual Studio MSAL client.

    .EXAMPLE
    Connect-MgGraph -Scopes DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All
    Revoke-MsIdMcpServerPermission -MCPClientServicePrincipalId '12345678-1234-1234-1234-123456789012' -Scopes 'User.Read.All'
    Revokes specific permissions from a custom MCP client.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'PredefinedClients')]
    param(
        [Parameter(ParameterSetName = 'PredefinedClients', Mandatory = $false)]
        [ValidateSet('VisualStudioCode', 'VisualStudio', 'VisualStudioMSAL', 'ChatGpt', 'ClaudeDesktop')]
        [string[]]$MCPClient = @('VisualStudioCode'),

        [Parameter(ParameterSetName = 'CustomClients', Mandatory = $true)]
        [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')]
        [string[]]$MCPClientServicePrincipalId,

        [Parameter(ParameterSetName = 'PredefinedClients', Mandatory = $false)]
        [Parameter(ParameterSetName = 'CustomClients', Mandatory = $false)]
        [string[]]$Scopes
    )

    begin {
        ## Initialize Critical Dependencies
        $CriticalError = $null
        if (!(Test-MgCommandPrerequisites 'Get-MgServicePrincipal', 'Get-MgOauth2PermissionGrant', 'Remove-MgOauth2PermissionGrant', 'Update-MgOauth2PermissionGrant' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return }

        function Get-ServicePrincipal([string]$appId, [string]$name) {
            $sp = Get-MgServicePrincipal -Filter "appId eq '$appId'" -ErrorAction SilentlyContinue | Select-Object -First 1
            if (-not $sp) {
                throw "Service principal for $name not found. App ID: $appId"
            }
            return $sp
        }

        function Get-Grant {
            param(
                [Parameter(Mandatory)] [string] $ClientSpId,
                [Parameter(Mandatory)] [string] $ResourceSpId
            )
            Get-MgOauth2PermissionGrant `
                -Filter "clientId eq '$ClientSpId' and resourceId eq '$ResourceSpId' and consentType eq 'AllPrincipals'" `
                -Top 1 `
                -Property "id,scope,clientId,resourceId,consentType" `
                -ErrorAction SilentlyContinue |
            Select-Object -First 1
        }

        function Update-GrantScopes([string]$clientSpId, [string]$resourceSpId, [string[]]$targetScopes) {
            $grant = Get-Grant -clientSpId $clientSpId -resourceSpId $resourceSpId | Select-Object -First 1

            if (-not $grant) {
                Write-Verbose "No existing grant found for this client."
                return $null
            }

            if (-not $targetScopes -or $targetScopes.Count -eq 0) {
                Write-Verbose "Removing entire permission grant..."
                Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id -Confirm:$false
                return $null
            }

            $targetString = ($targetScopes | Sort-Object -Unique) -join ' '

            $currentScope = if ($grant.Scope) { $grant.Scope } else { "" }
            if ($currentScope -ceq $targetString) {
                Write-Verbose "Grant already has the correct scopes."
                return $grant
            }

            Write-Verbose "Updating permission grant with remaining scopes..."
            Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id -BodyParameter @{ scope = $targetString }
            return Get-Grant -clientSpId $clientSpId -resourceSpId $resourceSpId
        }

        # Constants
        $resourceAppId = "e8c77dc2-69b3-43f4-bc51-3213c9d915b4"  # Microsoft MCP Server for Enterprise
        $predefinedClients = @{
            "VisualStudioCode" = @{ Name = "Visual Studio Code"; AppId = "aebc6443-996d-45c2-90f0-388ff96faa56" }
            "VisualStudio"     = @{ Name = "Visual Studio"; AppId = "04f0c124-f2bc-4f59-8241-bf6df9866bbd" }
            "VisualStudioMSAL" = @{ Name = "Visual Studio MSAL"; AppId = "62e61498-0c88-438b-a45c-2da0517bebe6" }
            "ChatGpt"          = @{ Name = "ChatGPT"; AppId = "e0476654-c1d5-430b-ab80-70cbd947616a" }
            "ClaudeDesktop"    = @{ Name = "Claude Desktop"; AppId = "08ad6f98-a4f8-4635-bb8d-f1a3044760f0" }
        }

        function Resolve-MCPClient {
            param(
                [string[]]$MCPClients,
                [string[]]$CustomServicePrincipalIds
            )

            $resolvedClients = @()

            # Process MCP clients
            if ($MCPClients) {
                foreach ($client in $MCPClients) {
                    if ($predefinedClients.ContainsKey($client)) {
                        $clientInfo = $predefinedClients[$client]
                        $resolvedClients += @{
                            Name     = $clientInfo.Name
                            AppId    = $clientInfo.AppId
                            IsCustom = $false
                        }
                    }
                }
            }

            # Process custom service principal IDs
            if ($CustomServicePrincipalIds) {
                foreach ($spId in $CustomServicePrincipalIds) {
                    $resolvedClients += @{
                        Name     = "Custom MCP Client"
                        AppId    = $spId
                        IsCustom = $true
                    }
                }
            }

            return $resolvedClients
        }
    }

    process {

        if ($CriticalError) { return }

        # Get resource service principal
        $resourceSp = Get-ServicePrincipal $resourceAppId "Microsoft MCP Server for Enterprise"

        # Resolve MCP clients
        $resolvedClients = Resolve-MCPClient -MCPClients $MCPClient -CustomServicePrincipalIds $MCPClientServicePrincipalId
        Write-Verbose "Resolved $($resolvedClients.Count) MCP client(s): $($resolvedClients.Name -join ', ')"        # Get service principals for the resolved clients
        $clientSps = @()
        foreach ($client in $resolvedClients) {
            try {
                $sp = Get-ServicePrincipal $client.AppId $client.Name
                $clientSps += @{
                    Sp       = $sp
                    Name     = $client.Name
                    IsCustom = $client.IsCustom
                }
                Write-Verbose "Found service principal for: $($client.Name)"
            }
            catch {
                Write-Warning "Could not get service principal for $($client.Name) (App ID: $($client.AppId)): $($_.Exception.Message)"
                continue
            }
        }

        if ($clientSps.Count -eq 0) {
            Write-Warning "No MCP client service principals could be found."
            return
        }

        Write-Host "Operating on $($clientSps.Count) MCP client(s): $($clientSps.Name -join ', ')" -ForegroundColor Cyan        # Process each client service principal
        $results = @()
        $allCurrentScopes = @()

        # First pass: collect all current scopes across all clients
        foreach ($clientSp in $clientSps) {
            $currentGrant = Get-Grant -ClientSpId $clientSp.Sp.Id -ResourceSpId $resourceSp.Id
            if ($currentGrant -and $currentGrant.Scope) {
                $currentScopes = ($currentGrant.Scope -split '\s+' | Where-Object { $_ }) | Sort-Object -Unique
                $allCurrentScopes += $currentScopes
            }
        }

        $allCurrentScopes = $allCurrentScopes | Sort-Object -Unique

        if (-not $allCurrentScopes -and $clientSps.Count -gt 1) {
            Write-Warning "No scopes currently granted to any of the MCP clients."
            return
        }        # Determine operation scope
        if ($Scopes) {
            # Revoke specific scopes
            $scopesToRevoke = $Scopes | Sort-Object -Unique
            $invalidScopes = $scopesToRevoke | Where-Object { $_ -notin $allCurrentScopes }
            if ($invalidScopes) {
                Write-Warning "The following scopes are not currently granted to any client: $($invalidScopes -join ', ')"
            }

            $validScopesToRevoke = $scopesToRevoke | Where-Object { $_ -in $allCurrentScopes }
            if (-not $validScopesToRevoke) {
                Write-Warning "No valid scopes to revoke."
                return
            }

            $actionDescription = "Revoke scopes '$($validScopesToRevoke -join ', ')' from $($clientSps.Count) MCP client(s): $($clientSps.Name -join ', ')"
        }
        else {
            # Revoke all scopes
            $validScopesToRevoke = $allCurrentScopes
            $actionDescription = "Revoke ALL permissions from $($clientSps.Count) MCP client(s): $($clientSps.Name -join ', ')"
        }

        # Confirm action for all clients
        if ($PSCmdlet.ShouldProcess("$($clientSps.Count) MCP client(s)", $actionDescription)) {
            # Second pass: process each client
            foreach ($clientSp in $clientSps) {
                try {
                    $currentGrant = Get-Grant -ClientSpId $clientSp.Sp.Id -ResourceSpId $resourceSp.Id

                    if (-not $currentGrant) {
                        $results += @{
                            Client          = $clientSp.Name
                            Success         = $true
                            Action          = "No existing grant"
                            RemovedScopes   = @()
                            RemainingScopes = @()
                            Error           = $null
                        }
                        continue
                    }

                    # Get current scopes for this specific client
                    $currentClientScopes = if ($currentGrant.Scope) {
                        ($currentGrant.Scope -split '\s+' | Where-Object { $_ }) | Sort-Object -Unique
                    }
                    else {
                        @()
                    }

                    if (-not $currentClientScopes) {
                        $results += @{
                            Client          = $clientSp.Name
                            Success         = $true
                            Action          = "No scopes to revoke"
                            RemovedScopes   = @()
                            RemainingScopes = @()
                            Error           = $null
                        }
                        continue
                    }

                    # Calculate remaining scopes for this client
                    if ($Scopes) {
                        $remainingScopes = $currentClientScopes | Where-Object { $_ -notin $validScopesToRevoke }
                        $actualRemovedScopes = $currentClientScopes | Where-Object { $_ -in $validScopesToRevoke }
                    }
                    else {
                        $remainingScopes = @()
                        $actualRemovedScopes = $currentClientScopes
                    }

                    # Update the grant
                    $result = Update-GrantScopes -clientSpId $clientSp.Sp.Id -resourceSpId $resourceSp.Id -targetScopes $remainingScopes

                    $results += @{
                        Client          = $clientSp.Name
                        Success         = $true
                        Action          = if ($remainingScopes.Count -eq 0) { "All permissions revoked" } else { "Partial revocation" }
                        RemovedScopes   = $actualRemovedScopes
                        RemainingScopes = $remainingScopes
                        Error           = $null
                    }
                }
                catch {
                    $results += @{
                        Client          = $clientSp.Name
                        Success         = $false
                        Action          = "Failed"
                        RemovedScopes   = @()
                        RemainingScopes = @()
                        Error           = $_.Exception.Message
                    }
                }
            }

            # Display results
            # Use measure-object to count successes and failures
            $successCount = ($results | Where-Object Success | Measure-Object).Count
            $errorCount = ($results | Where-Object { -not $_.Success } | Measure-Object).Count

            Write-Host "`nResults Summary:" -ForegroundColor Yellow
            Write-Host "Successfully processed: $successCount client(s)" -ForegroundColor Green
            if ($errorCount -gt 0) {
                Write-Host "Failed to process: $errorCount client(s)" -ForegroundColor Red
            }

            foreach ($result in $results) {
                if ($result.Success) {
                    Write-Host "`n✓ $($result.Client): $($result.Action)" -ForegroundColor Green

                    if ($result.RemovedScopes.Count -gt 0) {
                        Write-Host " Revoked scopes:" -ForegroundColor Yellow
                        $result.RemovedScopes | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
                    }

                    if ($result.RemainingScopes.Count -gt 0) {
                        Write-Host " Remaining scopes:" -ForegroundColor Yellow
                        $result.RemainingScopes | ForEach-Object { Write-Host " - $_" -ForegroundColor Green }
                    }
                }
                else {
                    Write-Host "`n✗ Failed to process $($result.Client)" -ForegroundColor Red
                    Write-Host " Error: $($result.Error)" -ForegroundColor Red
                }
            }
        }
    }
}