Private/MCP.ps1
|
function Test-PodeMcpRequest { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string] $Group ) # check if the group exists, if not return an error if (!(Test-PodeMcpGroup -Name $Group)) { Write-PodeMcpErrorResponse -Message "Group '$($Group)' not found" -Type InternalError return $false } # ensure request has valid jsonrpc version if ($WebEvent.Data['jsonrpc'] -ne '2.0') { Write-PodeMcpErrorResponse -Message 'Invalid JSON-RPC version' -Type InvalidRequest return $false } # do we have a MCP method, error if not $method = $WebEvent.Data['method'] if ([string]::IsNullOrEmpty($method)) { Write-PodeMcpErrorResponse -Message 'Missing method' -Type InvalidRequest return $false } # for non-notification methods, ensure we have an ID if ([string]::IsNullOrEmpty($WebEvent.Data['id']) -and ($method -inotlike 'notifications/*')) { Write-PodeMcpErrorResponse -Message 'Missing ID for non-notification method' -Type InvalidRequest return $false } return $true } # https://www.jsonrpc.org/specification function Write-PodeMcpErrorResponse { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Message, [Parameter(Mandatory = $true)] [ValidateSet('ParseError', 'InvalidRequest', 'MethodNotFound', 'InvalidParams', 'InternalError')] [string] $Type, [Parameter()] [int] $StatusCode = 400 ) # determine the error code $errCode = switch ($Type.ToLowerInvariant()) { 'parseerror' { -32700 } 'invalidrequest' { -32600 } 'methodnotfound' { -32601 } 'invalidparams' { -32602 } 'internalerror' { -32603 } } # write the response $response = @{ jsonrpc = '2.0' id = $WebEvent.Data['id'] error = @{ code = $errCode message = $Message } } Write-PodeJsonResponse -Value $response -StatusCode $StatusCode } # https://www.jsonrpc.org/specification function Write-PodeMcpSuccessResponse { [CmdletBinding()] param( [Parameter()] [hashtable] $Result = $null ) # if the result is null, return a 202 Accepted with no content if ($null -eq $Result) { Set-PodeResponseStatus -Code 202 return } # otherwise, return a 200 OK with the result $response = @{ jsonrpc = '2.0' id = $WebEvent.Data['id'] result = $Result } Write-PodeJsonResponse -Value $response } function Invoke-PodeMcpInitialize { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] $ServerName, [Parameter()] [string] $ServerVersion ) if ([string]::IsNullOrEmpty($ServerVersion)) { $ServerVersion = $PodeContext.Server.Version } return @{ protocolVersion = '2025-11-25' capabilities = @{ tools = @{ listChanged = $false } } serverInfo = @{ name = $ServerName version = $ServerVersion } } } function Invoke-PodeMcpToolList { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] $Group ) # build the tools content $content = @{ tools = @() } # loop through the tools in the group and add them to the content foreach ($toolName in $PodeContext.Server.Mcp.Groups[$Group].Tools) { $tool = $PodeContext.Server.Mcp.Tools[$toolName] $content.tools += @{ name = $tool.Name description = $tool.Description inputSchema = New-PodeJsonSchemaObject -Property $tool.Properties } } return $content } function Invoke-PodeMcpToolCall { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] $Group ) # get tool name and arguments $toolName = $WebEvent.Data['params']['name'] $toolArgs = $WebEvent.Data['params']['arguments'] # check if the tool exists, if not return an error if (!(Test-PodeMcpTool -Group $Group -Name $toolName)) { Write-PodeMcpErrorResponse -Message "Tool '$($toolName)' not found in group '$($Group)'" -Type InvalidParams return $null } $tool = $PodeContext.Server.Mcp.Tools[$toolName] # attempt to invoke the scriptblock, passing in the arguments try { [hashtable[]]$result = Invoke-PodeScriptBlock -ScriptBlock $tool.ScriptBlock -Arguments $toolArgs -Scoped -Splat -Return } catch { Write-PodeMcpErrorResponse -Message "Error invoking tool '$($toolName)' in group '$($Group)': $($_.Exception.Message)" -Type InternalError -StatusCode 500 $_ | Write-PodeErrorLog return $null } return @{ content = $result isError = $false } } function Add-PodeMcpToolAutoSchema { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable] $Tool, [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock ) # do nothing if we don't have any parameters if (($null -eq $ScriptBlock.Ast.ParamBlock) -or ($ScriptBlock.Ast.ParamBlock.Parameters.Count -eq 0)) { return } foreach ($param in $ScriptBlock.Ast.ParamBlock.Parameters) { # get values from param attributes, such as Mandatory, HelpMessage, and also any Validate attributes $isMandatory = $false $helpMessage = $null $dontShow = $false $validRangeMin = $null $validRangeMax = $null $validSet = $null $validLengthMin = $null $validLengthMax = $null $validCountMin = $null $validCountMax = $null foreach ($attr in $param.Attributes) { switch ($attr.TypeName.FullName.ToLowerInvariant()) { 'parameter' { foreach ($arg in $attr.NamedArguments) { switch ($arg.ArgumentName.ToLowerInvariant()) { 'mandatory' { $isMandatory = $arg.Argument.Extent.Text -eq '$true' } 'helpmessage' { $helpMessage = $arg.Argument.Value.ToString() } 'dontshow' { $dontShow = $arg.Argument.Extent.Text -eq '$true' } } } } 'validaterange' { $validRangeMin = $attr.PositionalArguments[0].Value $validRangeMax = $attr.PositionalArguments[1].Value } 'validateset' { $validSet = $attr.PositionalArguments.Value } 'validatelength' { $validLengthMin = $attr.PositionalArguments[0].Value $validLengthMax = $attr.PositionalArguments[1].Value } 'validatecount' { $validCountMin = $attr.PositionalArguments[0].Value $validCountMax = $attr.PositionalArguments[1].Value } } } # if don't show is set, skip this parameter if ($dontShow) { continue } # build the property definition for parameter, based on the parameter type $paramName = $param.Name.Extent.Text.TrimStart('$') $definition = $null # build params for base types $_params = @{} if (($null -ne $validRangeMin) -or ($null -ne $validRangeMax)) { $_params.Minimum = $validRangeMin $_params.Maximum = $validRangeMax } if (($null -ne $validLengthMin) -or ($null -ne $validLengthMax)) { $_params.MinLength = $validLengthMin $_params.MaxLength = $validLengthMax } if ($null -ne $validSet) { $_params.Enum = $validSet } # what is the base type $type = $param.StaticType if ($param.StaticType.IsArray) { $type = $param.StaticType.GetElementType() } else { $_params.Description = $helpMessage } switch ($type) { 'string' { $definition = New-PodeJsonSchemaString @_params } { $_ -iin 'int', 'long' } { $definition = New-PodeJsonSchemaInteger @_params } { $_ -iin 'double', 'float' } { $definition = New-PodeJsonSchemaNumber @_params } 'bool' { $definition = New-PodeJsonSchemaBoolean @_params } default { # Unsupported parameter type '$($type)' for parameter '$($paramName)' in tool '$($Tool.Name)'. Auto schema generation only supports string, int, long, double, float, and bool types. throw ($PodeLocale.mcpToolAutoSchemaUnsupportedParameterTypeExceptionMessage -f $type, $paramName, $Tool.Name) } } # if it's an array, we need to wrap the definition in an array definition if ($param.StaticType.IsArray) { $_params = @{ Item = $definition Description = $helpMessage } if (($null -ne $validCountMin) -or ($null -ne $validCountMax)) { $_params.MinItems = $validCountMin $_params.MaxItems = $validCountMax } $definition = New-PodeJsonSchemaArray @_params } # add the property to the tool's properties Add-PodeMcpToolProperty -Tool $Tool -Name $paramName -Required:$isMandatory -Definition $definition } } |