McpServers.ps1

function Assert-CopilotMcpConfigNotSymbolicLink {
    [CmdletBinding()]
    param()

    $configDirectory = Join-Path (Get-CopilotHome) '.copilot'
    $configPath = Join-Path $configDirectory 'mcp-config.json'
    try {
        # Inspect the link itself, including dangling links, without resolving its target.
        $config = Get-Item -LiteralPath $configPath -Force -ErrorAction Stop
    } catch [System.Management.Automation.ItemNotFoundException] {
        return
    }

    if ($config.LinkType -eq 'SymbolicLink') {
        throw "Cannot modify Copilot MCP configuration '$configPath': it is a symbolic link to '$($config.LinkTarget)'. Manage the target file directly or use the tool that manages the link. Relative targets are based at '$configDirectory'. Native 'copilot mcp add/remove' would replace the link and was not run."
    }
}

function Get-CopilotMcpServer {
    <#
    .SYNOPSIS
        List configured Copilot CLI MCP servers.
    .DESCRIPTION
        Parses the JSON output of 'copilot mcp list' into typed objects with
        Name, Type, Source, and connection details (Command/Args or URL).
    .PARAMETER Name
        Filter by server name. Supports wildcards.
    .PARAMETER Source
        Filter by source: user, workspace, plugin, or builtin.
    .EXAMPLE
        Get-CopilotMcpServer
        Lists all configured MCP servers.
    .EXAMPLE
        Get-CopilotMcpServer -Source user
        Lists only user-configured servers.
    .EXAMPLE
        Get-CopilotMcpServer Azure*
        Lists servers matching the filter.
    #>

    [OutputType('CopilotMcpServer')]
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Name = '*',

        [ValidateSet('user', 'workspace', 'plugin', 'builtin')]
        [string]$Source
    )
    $copilotExe = (Get-Command copilot -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
    $json = & $copilotExe mcp list --json 2>&1 | Out-String
    $data = $json | ConvertFrom-Json
    foreach ($prop in $data.mcpServers.PSObject.Properties) {
        $server = $prop.Value
        if ($prop.Name -notlike $Name) { continue }
        if ($Source -and $server.source -ne $Source) { continue }
        [PSCustomObject]@{
            PSTypeName = 'CopilotMcpServer'
            Name       = $prop.Name
            Type       = $server.type
            Command    = if ($server.command) { $server.command } else { '' }
            Args       = if ($server.args) { $server.args -join ' ' } else { '' }
            Url        = if ($server.url) { $server.url } else { '' }
            Source     = $server.source
        }
    }
}

function Register-CopilotMcpServer {
    <#
    .SYNOPSIS
        Add an MCP server to the Copilot CLI user configuration.
    .DESCRIPTION
        Wraps 'copilot mcp add' with typed parameters for stdio and HTTP/SSE servers.
        Refuses to invoke the native command when ~/.copilot/mcp-config.json is a
        symbolic link, including relative, chained, and dangling links, because
        the native command would replace the link. Manage the target file directly
        or use the tool that manages the link. Regular files retain native CLI
        behavior and validation. WhatIf only previews the operation.
    .PARAMETER Name
        The server name.
    .PARAMETER Transport
        The transport type: stdio, http, or sse. Defaults to stdio.
    .PARAMETER Command
        The command to run for stdio servers.
    .PARAMETER ArgumentList
        Arguments for the stdio command.
    .PARAMETER Url
        The URL for http/sse servers.
    .PARAMETER Env
        Environment variables as KEY=VALUE strings.
    .PARAMETER Header
        HTTP headers for remote servers.
    .EXAMPLE
        Register-CopilotMcpServer -Name context7 -Transport http -Url https://mcp.context7.com/mcp
        Adds a remote HTTP MCP server.
    .EXAMPLE
        Register-CopilotMcpServer -Name myserver -Command npx -ArgumentList '-y', '@my/mcp-server'
        Adds a local stdio MCP server.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Position = 0, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [ValidateSet('stdio', 'http', 'sse')]
        [string]$Transport = 'stdio',

        [string]$Command,

        [string[]]$ArgumentList,

        [string]$Url,

        [string[]]$Env,

        [string[]]$Header
    )
    Assert-CopilotShimArgument -Value $Name -ParameterName 'Name' -Pattern '^[A-Za-z0-9][A-Za-z0-9._-]*$'
    if ($Command) { Assert-CopilotShimTextArgument -Value $Command -ParameterName 'Command' }
    if ($ArgumentList) {
        foreach ($argument in $ArgumentList) {
            Assert-CopilotShimTextArgument -Value $argument -ParameterName 'ArgumentList'
        }
    }
    if ($Url) { Assert-CopilotShimTextArgument -Value $Url -ParameterName 'Url' }
    if ($Env) {
        foreach ($entry in $Env) {
            Assert-CopilotShimTextArgument -Value $entry -ParameterName 'Env'
        }
    }
    if ($Header) {
        foreach ($entry in $Header) {
            Assert-CopilotShimTextArgument -Value $entry -ParameterName 'Header'
        }
    }

    $copilotExe = (Get-Command copilot -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
    if ($PSCmdlet.ShouldProcess($Name, 'copilot mcp add')) {
        $addArgs = @('mcp', 'add', '--transport', $Transport)
        if ($Env) { foreach ($e in $Env) { $addArgs += '--env', $e } }
        if ($Header) { foreach ($h in $Header) { $addArgs += '--header', $h } }
        $addArgs += $Name
        if ($Transport -eq 'stdio') {
            $addArgs += '--'
            if ($Command) { $addArgs += $Command }
            if ($ArgumentList) { $addArgs += $ArgumentList }
        } else {
            if ($Url) { $addArgs += $Url }
        }
        Assert-CopilotMcpConfigNotSymbolicLink
        & $copilotExe @addArgs 2>&1
        if ($LASTEXITCODE -ne 0) {
            Write-Error "Failed to add MCP server: $Name"
        }
    }
}

function Unregister-CopilotMcpServer {
    <#
    .SYNOPSIS
        Remove an MCP server from the Copilot CLI configuration.
    .DESCRIPTION
        Removes a server by name. Accepts pipeline input from Get-CopilotMcpServer.
        Refuses to invoke the native command when ~/.copilot/mcp-config.json is a
        symbolic link, including relative, chained, and dangling links, because
        the native command would replace the link. Manage the target file directly
        or use the tool that manages the link. The check applies to each pipeline
        item. Regular files retain native CLI behavior and validation. WhatIf only
        previews the operation.
    .PARAMETER InputObject
        A CopilotMcpServer object from Get-CopilotMcpServer.
    .PARAMETER Name
        The server name to remove.
    .EXAMPLE
        Unregister-CopilotMcpServer -Name old-server
        Removes the specified MCP server.
    .EXAMPLE
        Get-CopilotMcpServer old* | Unregister-CopilotMcpServer
        Removes servers matching the filter.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByObject')]
    param(
        [Parameter(ParameterSetName = 'ByObject', ValueFromPipeline, Mandatory)]
        [PSObject]$InputObject,

        [Parameter(ParameterSetName = 'ByName', Position = 0, Mandatory)]
        [string]$Name
    )
    process {
        $removeName = if ($PSCmdlet.ParameterSetName -eq 'ByName') { $Name } else { $InputObject.Name }
        Assert-CopilotShimArgument -Value $removeName -ParameterName 'Name' -Pattern '^[A-Za-z0-9][A-Za-z0-9._-]*$'

        $copilotExe = (Get-Command copilot -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
        if ($PSCmdlet.ShouldProcess($removeName, 'copilot mcp remove')) {
            Assert-CopilotMcpConfigNotSymbolicLink
            & $copilotExe mcp remove $removeName 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Error "Failed to remove MCP server: $removeName"
            }
        }
    }
}