Public/MCP.ps1

<#
.SYNOPSIS
Resolves an MCP request from a client, invoking the appropriate logic based on the method and returning a response.
 
.DESCRIPTION
This function is responsible for handling incoming MCP requests, validating them, and invoking the appropriate logic
based on the requested method. It supports methods for initializing the MCP connection, listing available tools, calling
specific tools, and handling notifications.
 
Responses are returned in JSON-RPC 2.0 format.
 
.PARAMETER Group
The group to which the MCP tools belong. This allows for organizing tools into different groups.
 
.PARAMETER ServerName
The name of the MCP server. (Default: 'Pode MCP Server').
 
.PARAMETER ServerVersion
The version of the MCP server. (Default: Current Pode version).
 
.EXAMPLE
# resolve requests for MCP tools in a default group, using Pode server name and version
Add-PodeMcpGroup -Name 'default'
Resolve-PodeMcpRequest -Group 'default'
 
.EXAMPLE
# resolve requests for MCP tools in a custom group, with custom server name and version
Add-PodeMcpGroup -Name 'custom'
Resolve-PodeMcpRequest -Group 'custom' -ServerName 'Custom MCP Server' -ServerVersion '1.0.0'
#>

function Resolve-PodeMcpRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Group,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $ServerName = 'Pode MCP Server',

        [Parameter()]
        [string]
        $ServerVersion
    )

    # validate the request
    if (!(Test-PodeMcpRequest -Group $Group)) {
        return
    }

    # invoke the appropriate logic per the MCP method
    $content = @{}

    switch ($WebEvent.Data['method'].ToLowerInvariant()) {
        'initialize' {
            # logic to initialize the MCP connection
            $content = Invoke-PodeMcpInitialize -ServerName $ServerName -ServerVersion $ServerVersion
            Write-PodeMcpSuccessResponse -Result $content
        }

        'tools/list' {
            # logic to list available tools
            $content = Invoke-PodeMcpToolList -Group $Group
            Write-PodeMcpSuccessResponse -Result $content
        }

        'tools/call' {
            # logic to call a specific tool
            $content = Invoke-PodeMcpToolCall -Group $Group
            if ($null -ne $content) {
                Write-PodeMcpSuccessResponse -Result $content
            }
        }

        { $_ -ilike 'notifications/*' } {
            # handle something here eventually, for now set send an HTTP 202
            Write-PodeMcpSuccessResponse
        }

        default {
            Write-PodeMcpErrorResponse -Message "Method '$($_)' not found" -Type MethodNotFound
        }
    }
}

<#
.SYNOPSIS
Builds a Textual response object for an MCP tool, which can be returned to the client.
 
.DESCRIPTION
This function creates a hashtable representing a textual response for an MCP tool. The hashtable includes a
'type' key with the value 'text', and a 'text' key containing the provided value. If the provided value is not a string,
it will be converted to a string using Out-String.
 
.PARAMETER Value
The value to be included in the textual response. This can be any object, which will be converted to a string if necessary.
 
.EXAMPLE
# create a simple text response for an MCP tool
New-PodeMcpTextContent -Value 'Hello, world!'
 
.EXAMPLE
# create a text response for an MCP tool from a non-string object
$data = @{ Name = 'Alice'; Age = 30 }
New-PodeMcpTextContent -Value $data
#>

function New-PodeMcpTextContent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [object]
        $Value
    )

    if ($null -eq $Value) {
        $Value = [string]::Empty
    }
    elseif ($Value -isnot [string]) {
        $Value = $Value | Out-String
    }

    return @{
        type = 'text'
        text = $Value
    }
}

<#
.SYNOPSIS
Builds an Image response object for an MCP tool, which can be returned to the client.
 
.DESCRIPTION
This function creates a hashtable representing an image response for an MCP tool. The hashtable includes a
'type' key with the value 'image', a 'data' key containing the base64-encoded image data, and a 'mimeType' key
specifying the MIME type of the image.
 
.PARAMETER Bytes
The byte array representing the image data.
 
.PARAMETER MimeType
The MIME type of the image.
 
.EXAMPLE
# create an image response for an MCP tool
$imageBytes = [System.IO.File]::ReadAllBytes('path\to\image.png')
New-PodeMcpImageContent -Bytes $imageBytes -MimeType 'image/png'
#>

function New-PodeMcpImageContent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter(Mandatory = $true)]
        [string]
        $MimeType
    )

    if ($null -eq $Bytes) {
        $Bytes = @()
    }

    return @{
        type     = 'image'
        data     = [Convert]::ToBase64String($Bytes)
        mimeType = $MimeType
    }
}

<#
.SYNOPSIS
Builds an Audio response object for an MCP tool, which can be returned to the client.
 
.DESCRIPTION
This function creates a hashtable representing an audio response for an MCP tool. The hashtable includes a
'type' key with the value 'audio', a 'data' key containing the base64-encoded audio data, and a 'mimeType' key
specifying the MIME type of the audio.
 
.PARAMETER Bytes
The byte array representing the audio data.
 
.PARAMETER MimeType
The MIME type of the audio.
 
.EXAMPLE
# create an audio response for an MCP tool
$audioBytes = [System.IO.File]::ReadAllBytes('path\to\audio.mp3')
New-PodeMcpAudioContent -Bytes $audioBytes -MimeType 'audio/mpeg'
#>

function New-PodeMcpAudioContent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter(Mandatory = $true)]
        [string]
        $MimeType
    )

    if ($null -eq $Bytes) {
        $Bytes = @()
    }

    return @{
        type     = 'audio'
        data     = [Convert]::ToBase64String($Bytes)
        mimeType = $MimeType
    }
}

<#
.SYNOPSIS
Adds a new MCP tool to the server.
 
.DESCRIPTION
Adds a new MCP tool to the server, optionally inside a custom Group.
 
.PARAMETER Name
The name of the MCP tool.
 
.PARAMETER Description
A brief description of the MCP tool, this is required to help MCP clients understand what the tool does.
 
.PARAMETER ScriptBlock
The ScriptBlock that defines the MCP tool's functionality.
 
.PARAMETER Group
The Group(s) to which the MCP tool belongs.
 
.PARAMETER AutoSchema
If set, the input schema for the tool will attempt to be automatically generated based on the parameters of the provided
ScriptBlock - using parameter attributes for additional context.
 
This only supports basic parameter types: string, int, long, double, float, and bool (including arrays of these types).
 
If supplied you don't need to manually define properties via Add-PodeMcpToolProperty, but you can still do so if you want
to and override the auto-generated schema for specific properties.
 
.PARAMETER PassThru
If set, the function will return the tool after it has been added to enable pipeline chaining.
 
.EXAMPLE
# add a simple MCP tool to a default group
Add-PodeMcpGroup -Name 'default'
Add-PodeMcpTool -Name 'Greet' -Description 'Returns a random greeting' -Group 'default' -ScriptBlock {
    $greetings = @('Hello', 'Hi', 'Hey', 'Greetings', 'Salutations')
    $greeting = Get-Random -InputObject $greetings
    return New-PodeMcpTextContent -Value "$($greeting) from the Pode MCP tool!"
}
 
.EXAMPLE
# add a simple MCP tool to a custom group with auto-generated schema
Add-PodeMcpGroup -Name 'custom'
Add-PodeMcpTool -Name 'GreetPerson' -Description 'Returns a random greeting to a person' -Group 'custom' -AutoSchema -ScriptBlock {
    param(
        [Parameter(Mandatory = $true, HelpMessage = 'The name of the person to greet')]
        [string]$Name
    )
    $greetings = @('Hello', 'Hi', 'Hey', 'Greetings', 'Salutations')
    $greeting = Get-Random -InputObject $greetings
    return New-PodeMcpTextContent -Value "$($greeting), $($Name)! from the Pode MCP tool!"
}
 
.EXAMPLE
# add a simple MCP tool to a custom group, and return the tool for chaining
Add-PodeMcpGroup -Name 'custom'
Add-PodeMcpTool -Name 'GreetPersonInLocation' -Description 'Returns a random greeting to a person' -Group 'custom' -ScriptBlock {
    param(
        [string]$Name
    )
    $greetings = @('Hello', 'Hi', 'Hey', 'Greetings', 'Salutations')
    $greeting = Get-Random -InputObject $greetings
    return New-PodeMcpTextContent -Value "$($greeting), $($Name)! via the Pode MCP tool!"
} -PassThru |
    Add-PodeMcpToolProperty -Name 'Name' -Required -Definition (
        New-PodeJsonSchemaString -Description 'The name of the person to greet'
    )
#>

function Add-PodeMcpTool {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Description,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Group,

        [switch]
        $AutoSchema,

        [switch]
        $PassThru
    )

    # make sure groups are unique
    $Group = $Group | Sort-Object -Unique

    # ensure the tool doesn't already exist, if so throw an error
    if (Test-PodeMcpTool -Name $Name) {
        throw ($PodeLocale.mcpToolAlreadyExistsExceptionMessage -f $Name)
    }

    # ensure the group(s) exist, if not throw an error
    foreach ($g in $Group) {
        if (Test-PodeMcpGroup -Name $g) {
            continue
        }

        throw ($PodeLocale.mcpToolGroupDoesNotExistExceptionMessage -f $g)
    }

    # initialize the tool info
    $toolInfo = @{
        Name        = $Name
        Description = $Description
        ScriptBlock = $ScriptBlock
        Groups      = @($Group)
        Properties  = @()
    }

    # if auto-schema allowed, attempt to generate basic input schema from the scriptblock parameters
    if ($AutoSchema) {
        Add-PodeMcpToolAutoSchema -Tool $toolInfo -ScriptBlock $ScriptBlock
    }

    # add the tool
    $PodeContext.Server.Mcp.Tools[$Name] = $toolInfo

    # add the tool to the specified group(s)
    foreach ($g in $Group) {
        $PodeContext.Server.Mcp.Groups[$g].Tools += $Name
    }

    # if required, return the tool info for chaining
    if ($PassThru) {
        return $toolInfo
    }
}

<#
.SYNOPSIS
Adds a property to an existing MCP tool, which will be included in the tool's input schema.
 
.DESCRIPTION
Adds a property to an existing MCP tool, which will be included in the tool's input schema. This allows you to define
the expected input for the tool, which can be used by MCP clients to understand how to call the tool and provide
better user experiences.
 
.PARAMETER Tool
The Tool to which the property should be added; this will be the output of Add-PodeMcpTool if -PassThru is used.
 
.PARAMETER Name
The Name of the property.
 
.PARAMETER Definition
The Definition of the property, which will be a JSON Schema built using the New-PodeJsonSchema* cmdlets.
 
.PARAMETER Required
Indicates whether the property is required.
 
.PARAMETER PassThru
If set, the function will return the tool after the property has been added to enable pipeline chaining.
 
.LINK
https://modelcontextprotocol.info/docs/concepts/tools/
 
.EXAMPLE
# add a property to an MCP tool using the output from Add-PodeMcpTool
Add-PodeMcpTool -Name 'GreetPersonInLocation' -Description 'Returns a random greeting to a person in a location' -ScriptBlock {
    param(
        [string]$Name,
        [string]$Location
    )
    $greetings = @('Hello', 'Hi', 'Hey', 'Greetings', 'Salutations')
    $greeting = Get-Random -InputObject $greetings
    return New-PodeMcpTextContent -Value "$($greeting), $($Name) from $($Location)! via the Pode MCP tool!"
} -PassThru |
    Add-PodeMcpToolProperty -Name 'Name' -Required -Definition (
        New-PodeJsonSchemaString -Description 'The name of the person to greet'
    ) |
    Add-PodeMcpToolProperty -Name 'Location' -Required -Definition (
        New-PodeJsonSchemaString -Description 'The location of the person to greet'
    )
#>

function Add-PodeMcpToolProperty {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [hashtable]
        $Tool,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $Definition,

        [switch]
        $Required,

        [switch]
        $PassThru
    )

    # build the property definition
    $def = $Definition | New-PodeJsonSchemaProperty -Name $Name -Required:$Required

    # add the property to the tool's properties
    $Tool.Properties += $def

    # return tool if requested
    if ($PassThru) {
        return $Tool
    }
}

<#
.SYNOPSIS
Retrieves an MCP tool(s).
 
.DESCRIPTION
Retrieves an MCP tool or tools from the server based on the provided name and group.
 
.PARAMETER Name
The Name of the MCP tool to retrieve. If not specified, all tools in the specified group will be returned.
 
.PARAMETER Group
The Group(s) from which to retrieve the MCP tool(s).
 
.EXAMPLE
# retrieve a specific MCP tool by name from a default group
Get-PodeMcpTool -Name 'GreetPerson' -Group 'default'
 
.EXAMPLE
# retrieve all MCP tools from a specific group
Get-PodeMcpTool -Group 'custom'
 
.EXAMPLE
# attempt to retrieve a non-existent tool, which will return null
Get-PodeMcpTool -Name 'NonExistentTool'
 
.EXAMPLE
# attempt to retrieve tools from a non-existent group, which will return null
Get-PodeMcpTool -Group 'NonExistentGroup'
#>

function Get-PodeMcpTool {
    [CmdletBinding()]
    [OutputType([hashtable])]
    [OutputType([hashtable[]])]
    param(
        [Parameter()]
        [string[]]
        $Name,

        [Parameter()]
        [string[]]
        $Group
    )

    $hasNames = ![string]::IsNullOrEmpty($Name)
    $hasGroups = ![string]::IsNullOrEmpty($Group)

    # if no group(s) or tool name(s) provided, return all tools
    if (!$hasGroups -and !$hasNames) {
        return $PodeContext.Server.Mcp.Tools.Values
    }

    # if group(s) provided, get tools in group(s)
    $groupTools = @()
    if ($hasGroups) {
        $groupTools = $Group | Foreach-Object {
            if ($PodeContext.Server.Mcp.Groups.ContainsKey($_)) {
                $PodeContext.Server.Mcp.Groups[$_].Tools
            }
        } | Sort-Object -Unique
    }

    # if no tool name(s) provided, use group tools - if group(s) provided we need to filter
    if (!$hasNames) {
        $Name = $groupTools
    }
    elseif ($hasGroups) {
        $Name = $Name | Where-Object { $groupTools -icontains $_ }
    }

    # now return tools if they exist
    return $Name | ForEach-Object {
        if ($PodeContext.Server.Mcp.Tools.ContainsKey($_)) {
            $PodeContext.Server.Mcp.Tools[$_]
        }
    }
}

<#
.SYNOPSIS
Tests the existence of an MCP tool.
 
.DESCRIPTION
Tests whether an MCP tool exists, or if it exists in the specified group.
 
.PARAMETER Name
The Name of the MCP tool, if not specified the function will check if any tools exist in the specified group.
 
.PARAMETER Group
The Group in which to check for the MCP tool(s).
 
.EXAMPLE
# test for the existence of a specific MCP tool by name from a default group
Test-PodeMcpTool -Name 'GreetPerson' -Group 'default'
 
.EXAMPLE
# test for the existence of any MCP tools in a specific group
Test-PodeMcpTool -Group 'custom'
 
.EXAMPLE
# test for the existence of a non-existent tool, which will return false
Test-PodeMcpTool -Name 'NonExistentTool'
#>

function Test-PodeMcpTool {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Group
    )

    $hasName = ![string]::IsNullOrEmpty($Name)
    $hasGroup = ![string]::IsNullOrEmpty($Group)

    # if no name or group, return true if any tools exist
    if (!$hasName -and !$hasGroup) {
        return $PodeContext.Server.Mcp.Tools.Count -gt 0
    }

    # if no group provided, return true if tool exists
    if (!$hasGroup) {
        return $PodeContext.Server.Mcp.Tools.ContainsKey($Name)
    }

    # if no name provided, return true if any tools in group
    if (!$hasName) {
        return $PodeContext.Server.Mcp.Groups.ContainsKey($Group) -and
        ($PodeContext.Server.Mcp.Groups[$Group].Tools.Count -gt 0)
    }

    # otherwise both provided, return true if tool exists in group
    return $PodeContext.Server.Mcp.Groups.ContainsKey($Group) -and
    ($PodeContext.Server.Mcp.Groups[$Group].Tools -icontains $Name) -and
    $PodeContext.Server.Mcp.Tools.ContainsKey($Name)
}

<#
.SYNOPSIS
Removes an MCP tool.
 
.DESCRIPTION
Removes an MCP tool in general, or from the specified group.
 
.PARAMETER Name
The Name of the MCP tool to remove.
 
.PARAMETER Group
The Group from which to remove the MCP tool.
 
.EXAMPLE
# remove a specific MCP tool from all groups, and the tool itself
Remove-PodeMcpTool -Name 'GreetPerson'
 
.EXAMPLE
# remove a specific MCP tool from a specific group only - but keep the tool itself in case it's in other groups
Remove-PodeMcpTool -Name 'GreetPerson' -Group 'custom'
 
.EXAMPLE
# attempt to remove a non-existent tool, which will do nothing
Remove-PodeMcpTool -Name 'NonExistentTool'
#>

function Remove-PodeMcpTool {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Group
    )

    # if the tool doesn't exist, do nothing
    if (!$PodeContext.Server.Mcp.Tools.ContainsKey($Name)) {
        return
    }

    # if no groups specified, get the groups the tool is in
    $hasGroups = ![string]::IsNullOrEmpty($Group)
    if (!$hasGroups) {
        $Group = $PodeContext.Server.Mcp.Tools[$Name].Groups
    }

    # remove the tool from each group
    foreach ($g in $Group) {
        if ($PodeContext.Server.Mcp.Groups.ContainsKey($g)) {
            $null = $PodeContext.Server.Mcp.Groups[$g].Tools.Remove($Name)
        }
    }

    # if no groups were specified then remove the tool entirely
    if (!$hasGroups) {
        $null = $PodeContext.Server.Mcp.Tools.Remove($Name)
    }

    # otherwise, keep the tool but remove the group(s) from its info
    else {
        $tool = $PodeContext.Server.Mcp.Tools[$Name]
        $tool.Groups = $tool.Groups | Where-Object { $_ -inotin $Group }
    }
}

<#
.SYNOPSIS
Updates an existing MCP tool.
 
.DESCRIPTION
Updates the properties of an existing MCP tool, including its description, scriptblock, and/or resetting the properties.
 
.PARAMETER Name
The Name of the MCP tool to update.
 
.PARAMETER Description
An optional new Description for the MCP tool.
 
.PARAMETER ScriptBlock
An optional new ScriptBlock for the MCP tool.
 
.PARAMETER ResetProperties
A switch to reset the Properties of the MCP tool, if a new ScriptBlock has changed the parameters which need to be supplied.
 
.PARAMETER PassThru
A switch parameter to return the updated MCP tool.
 
.EXAMPLE
# update the description of an existing MCP tool
Update-PodeMcpTool -Name 'GreetPerson' -Description 'Returns a random greeting to a person, updated description'
 
.EXAMPLE
# update the scriptblock of an existing MCP tool, and reset its properties
Update-PodeMcpTool -Name 'GreetPerson' -ScriptBlock {
    param(
        [string]$Name,
        [string]$Location
    )
    $greetings = @('Hello', 'Hi', 'Hey', 'Greetings', 'Salutations')
    $greeting = Get-Random -InputObject $greetings
    return New-PodeMcpTextContent -Value "$($greeting), $($Name) from $($Location)! via the updated Pode MCP tool!"
} -ResetProperties
#>

function Update-PodeMcpTool {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [switch]
        $ResetProperties,

        [switch]
        $PassThru
    )

    # error if the tool doesn't exist
    if (!$PodeContext.Server.Mcp.Tools.ContainsKey($Name)) {
        # The MCP tool '$($Name)' does not exist
        throw ($PodeLocale.mcpToolDoesNotExistExceptionMessage -f $Name)
    }

    $tool = $PodeContext.Server.Mcp.Tools[$Name]

    # update tool description
    if ($PSBoundParameters.ContainsKey('Description')) {
        $tool.Description = $Description
    }

    # update tool scriptblock
    if ($PSBoundParameters.ContainsKey('ScriptBlock')) {
        $tool.ScriptBlock = $ScriptBlock
    }

    # reset tool properties if requested
    if ($ResetProperties) {
        $tool.Properties = @()
    }

    # return tool if requested
    if ($PassThru) {
        return $tool
    }
}

<#
.SYNOPSIS
Adds a new MCP tool group to the server.
 
.DESCRIPTION
Adds a new MCP tool group to the server, which can be used to organize tools into different categories.
 
.PARAMETER Name
The Name of the MCP tool group.
 
.PARAMETER Description
An optional brief Description for the MCP tool group.
 
.EXAMPLE
# add a new MCP tool group
Add-PodeMcpGroup -Name 'default' -Description 'The default group for MCP tools'
#>

function Add-PodeMcpGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Description
    )

    if ($PodeContext.Server.Mcp.Groups.ContainsKey($Name)) {
        # The MCP tool group '$($Name)' already exists
        throw ($PodeLocale.mcpToolGroupAlreadyExistsExceptionMessage -f $Name)
    }

    $PodeContext.Server.Mcp.Groups[$Name] = @{
        Name        = $Name
        Description = $Description
        Tools       = @()
    }
}

<#
.SYNOPSIS
Removes an MCP tool group from the server.
 
.DESCRIPTION
Removes an MCP tool group from the server.
 
.PARAMETER Name
The Name of the MCP tool group to remove.
 
.EXAMPLE
# remove an MCP tool group
Remove-PodeMcpGroup -Name 'default'
#>

function Remove-PodeMcpGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # skip if group doesn't exist
    if (!$PodeContext.Server.Mcp.Groups.ContainsKey($Name)) {
        return
    }

    # remove the group from any tools that were in it
    foreach ($toolName in $PodeContext.Server.Mcp.Groups[$Name].Tools) {
        if ($PodeContext.Server.Mcp.Tools.ContainsKey($toolName)) {
            $tool = $PodeContext.Server.Mcp.Tools[$toolName]
            $tool.Groups = $tool.Groups | Where-Object { $_ -ne $Name }
        }
    }

    # remove the group
    $null = $PodeContext.Server.Mcp.Groups.Remove($Name)
}

<#
.SYNOPSIS
Retrieves an MCP tool group or groups.
 
.DESCRIPTION
Retrieves an MCP tool group or groups from the server.
 
.PARAMETER Name
The Name of the MCP tool group to retrieve. If not specified, all groups will be returned.
 
.EXAMPLE
# retrieve a specific MCP tool group by name
Get-PodeMcpGroup -Name 'default'
#>

function Get-PodeMcpGroup {
    [CmdletBinding()]
    [OutputType([hashtable])]
    [OutputType([hashtable[]])]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    # if no name, return all groups
    if ([string]::IsNullOrEmpty($Name)) {
        return $PodeContext.Server.Mcp.Groups.Values
    }

    # otherwise, return specified group(s) if it exists
    $groups = @()
    foreach ($n in $Name) {
        if ($PodeContext.Server.Mcp.Groups.ContainsKey($n)) {
            $groups += $PodeContext.Server.Mcp.Groups[$n]
        }
    }

    return $groups
}

<#
.SYNOPSIS
Tests the existence of an MCP tool group.
 
.DESCRIPTION
Tests the existence of an MCP tool group.
 
.PARAMETER Name
The Name of the MCP tool group to test. If not specified, the function will return true if any groups exist.
 
.EXAMPLE
# test if a specific MCP tool group exists
Test-PodeMcpGroup -Name 'default'
 
.EXAMPLE
# test if any MCP tool groups exist
Test-PodeMcpGroup
#>

function Test-PodeMcpGroup {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter()]
        [string]
        $Name
    )

    # if no name, return true if any groups exist
    if ([string]::IsNullOrEmpty($Name)) {
        return $PodeContext.Server.Mcp.Groups.Count -gt 0
    }

    # otherwise, return true if specified group exists
    return $PodeContext.Server.Mcp.Groups.ContainsKey($Name)
}

<#
.SYNOPSIS
Clears all tools from an MCP tool group.
 
.DESCRIPTION
Clears all tools from an MCP tool group.
 
.PARAMETER Name
The Name of the MCP tool group to clear.
 
.EXAMPLE
# clear all tools from a specific MCP tool group
Clear-PodeMcpGroup -Name 'default'
#>

function Clear-PodeMcpGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    # if group exists, clear its tools
    if (!$PodeContext.Server.Mcp.Groups.ContainsKey($Name)) {
        return
    }

    # remove the group from any tools that were in it
    foreach ($toolName in $PodeContext.Server.Mcp.Groups[$Name].Tools) {
        if ($PodeContext.Server.Mcp.Tools.ContainsKey($toolName)) {
            $tool = $PodeContext.Server.Mcp.Tools[$toolName]
            $tool.Groups = $tool.Groups | Where-Object { $_ -ne $Name }
        }
    }

    # clear the tools from the group
    $PodeContext.Server.Mcp.Groups[$Name].Tools.Clear()
}

<#
.SYNOPSIS
Registers an MCP tool to a group.
 
.DESCRIPTION
Registers an MCP tool to a specified MCP tool group.
 
.PARAMETER Name
The Name of the MCP tool to register.
 
.PARAMETER Group
The Name of the MCP tool group to register the tool to.
 
.EXAMPLE
# register a specific MCP tool to a specific MCP tool group
Register-PodeMcpToolToGroup -Name 'tool1' -Group 'default'
#>

function Register-PodeMcpToolToGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Group
    )

    # error if group or tool doesn't exist
    if (!$PodeContext.Server.Mcp.Tools.ContainsKey($Name)) {
        # The MCP tool '$($Name)' does not exist
        throw ($PodeLocale.mcpToolDoesNotExistExceptionMessage -f $Name)
    }

    if (!$PodeContext.Server.Mcp.Groups.ContainsKey($Group)) {
        # The MCP tool group '$($Group)' does not exist
        throw ($PodeLocale.mcpToolGroupDoesNotExistExceptionMessage -f $Group)
    }

    # add the tool to the group if it's not already in it
    if ($PodeContext.Server.Mcp.Groups[$Group].Tools -inotcontains $Name) {
        $PodeContext.Server.Mcp.Groups[$Group].Tools += $Name
    }

    # add the group to the tool's info if it's not already in it
    $tool = $PodeContext.Server.Mcp.Tools[$Name]
    if ($tool.Groups -inotcontains $Group) {
        $tool.Groups += $Group
    }
}

<#
.SYNOPSIS
Unregisters an MCP tool from a group.
 
.DESCRIPTION
Unregisters an MCP tool from a specified MCP tool group.
 
.PARAMETER Name
The Name of the MCP tool to unregister.
 
.PARAMETER Group
The Name of the MCP tool group to unregister the tool from.
 
.EXAMPLE
# unregister a specific MCP tool from a specific MCP tool group
Unregister-PodeMcpToolFromGroup -Name 'tool1' -Group 'default'
#>

function Unregister-PodeMcpToolFromGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Group
    )

    # error if group or tool doesn't exist
    if (!$PodeContext.Server.Mcp.Tools.ContainsKey($Name)) {
        # The MCP tool '$($Name)' does not exist
        throw ($PodeLocale.mcpToolDoesNotExistExceptionMessage -f $Name)
    }

    if (!$PodeContext.Server.Mcp.Groups.ContainsKey($Group)) {
        # The MCP tool group '$($Group)' does not exist
        throw ($PodeLocale.mcpToolGroupDoesNotExistExceptionMessage -f $Group)
    }

    # remove the tool from the group if it's in it
    if ($PodeContext.Server.Mcp.Groups[$Group].Tools -icontains $Name) {
        $null = $PodeContext.Server.Mcp.Groups[$Group].Tools.Remove($Name)
    }

    # remove the group from the tool's info if it's in it
    $tool = $PodeContext.Server.Mcp.Tools[$Name]
    if ($tool.Groups -icontains $Group) {
        $null = $tool.Groups.Remove($Group)
    }
}