Private/Send-McpRequest.ps1
|
function Select-McpProtocolVersion { [CmdletBinding()] param([string[]]$Supported) $Supported = @($Supported | Where-Object { $_ }) if ($Supported -contains $script:ModernProtocolVersion) { return $script:ModernProtocolVersion } # Versions are ISO dates, so ordinal string comparison orders them correctly $newer = @($Supported | Where-Object { $_ -gt $script:ModernProtocolVersion } | Sort-Object) if ($newer.Count -gt 0) { return $newer[0] } $legacy = @($Supported | Where-Object { $_ -le $script:LegacyProtocolVersion }) if ($legacy.Count -gt 0) { return $null } throw (New-McpError -ErrorId 'McpNoCompatibleVersion' -Category NotImplemented -TargetObject $Supported ` -Message "Server supports protocol versions [$($Supported -join ', ')]; PSMcpClient supports $($script:ModernProtocolVersion) (modern) or $($script:LegacyProtocolVersion) and earlier (legacy).") } function Add-IgnoredMcpRequestId { param( [Parameter(Mandatory)]$Session, [Parameter(Mandatory)][string]$Id ) $maxIgnoredIds = 256 if ($Session.IgnoredIds.Count -ge $maxIgnoredIds) { $Session.IgnoredIds.RemoveAt(0) } $Session.IgnoredIds.Add($Id) } function Send-McpRequest { [CmdletBinding()] param( [Parameter(Mandatory)]$Session, [Parameter(Mandatory)][string]$Method, [System.Collections.IDictionary]$Params, [int]$TimeoutSec, [switch]$Raw, [switch]$NoVersionRetry ) if ($TimeoutSec -le 0) { $TimeoutSec = $Session.RequestTimeoutSec } $name = $Session.Name $Session.NextId++ $id = $Session.NextId # Copy so the caller's dictionary is never mutated (variable names are case-insensitive: not $params) $requestParams = [ordered]@{} if ($null -ne $Params) { foreach ($key in $Params.Keys) { $requestParams[$key] = $Params[$key] } } if ($Session.Era -eq 'Modern') { $requestParams['_meta'] = [ordered]@{ 'io.modelcontextprotocol/protocolVersion' = $Session.NegotiatedVersion 'io.modelcontextprotocol/clientInfo' = $script:ClientInfo 'io.modelcontextprotocol/clientCapabilities' = @{} } } $message = [ordered]@{ jsonrpc = '2.0'; id = $id; method = $Method; params = $requestParams } Write-McpMessage -Session $Session -Message $message $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $timeoutMs = [long]$TimeoutSec * 1000 while ($true) { $remaining = $timeoutMs - $stopwatch.ElapsedMilliseconds $read = if ($remaining -le 0) { [pscustomobject]@{ State = 'Timeout'; Line = $null } } else { Read-McpLine -Session $Session -TimeoutMs ([int]$remaining) } if ($read.State -eq 'Timeout') { Add-IgnoredMcpRequestId -Session $Session -Id "$id" $elapsed = [int]$stopwatch.Elapsed.TotalSeconds throw (New-McpError -ErrorId 'McpRequestTimeout' -Category OperationTimeout -TargetObject $Method ` -Exception ([System.TimeoutException]::new("MCP request '$Method' to server '$name' timed out after ${elapsed}s.")) ` -Message "MCP request '$Method' to server '$name' timed out after ${elapsed}s.") } if ($read.State -eq 'Eof') { throw (New-McpProcessExitedError -Session $Session -Context "while waiting for a response to '$Method'") } $line = $read.Line if ([string]::IsNullOrWhiteSpace($line)) { continue } Write-Verbose "[$name] <- $line" try { $response = ConvertFrom-Json -InputObject $line -AsHashtable -Depth 64 } catch { Write-Warning "[$name] non-JSON line on stdout ignored: $line" continue } if ($response -isnot [System.Collections.IDictionary]) { Write-Warning "[$name] unsupported JSON-RPC payload (batch or scalar) ignored" continue } $hasId = $response.ContainsKey('id') -and $null -ne $response['id'] if (-not $hasId) { if ($response.ContainsKey('method')) { Receive-McpNotification -Session $Session -Message $response } else { Write-Verbose "[$name] malformed message without id or method ignored" } continue } if ($response.ContainsKey('method')) { # Legacy servers may send sampling/elicitation/roots requests; decline so they don't hang Write-Verbose "[$name] declining server-initiated request '$($response.method)' (id $($response.id))" Write-McpMessage -Session $Session -Message ([ordered]@{ jsonrpc = '2.0' id = $response.id error = @{ code = -32601; message = "Method not found: $($response.method) (PSMcpClient does not support server-initiated requests)" } }) continue } $responseId = "$($response['id'])" if ($responseId -ne "$id") { if ($Session.IgnoredIds.Contains($responseId)) { $null = $Session.IgnoredIds.Remove($responseId) Write-Verbose "[$name] late response for timed-out request id $responseId discarded" } else { Write-Warning "[$name] response for unexpected request id $responseId (awaiting $id) discarded" } continue } if ($response.ContainsKey('error')) { $rpcError = $response['error'] $supported = if ($rpcError.data -is [System.Collections.IDictionary]) { $rpcError.data['supported'] } else { $null } if ($rpcError.code -eq -32022 -and $Session.Era -eq 'Modern' -and -not $NoVersionRetry -and $supported) { $version = Select-McpProtocolVersion -Supported @($supported) if ($version) { Write-Verbose "[$name] server rejected protocol version $($Session.NegotiatedVersion); retrying '$Method' with $version" $Session.NegotiatedVersion = $version return Send-McpRequest -Session $Session -Method $Method -Params $Params -TimeoutSec $TimeoutSec -Raw:$Raw -NoVersionRetry } } if ($Raw) { return $response } throw (New-McpError -ErrorId 'McpProtocolError' -Category InvalidOperation -TargetObject $rpcError ` -Message "MCP error $($rpcError.code): $($rpcError.message)") } if ($Raw) { return $response } $result = $response['result'] if ($result -is [System.Collections.IDictionary] -and $result.ContainsKey('resultType') -and $result['resultType'] -ne 'complete') { throw (New-McpError -ErrorId 'McpUnsupportedResult' -Category NotImplemented -TargetObject $result ` -Message "Server returned resultType '$($result['resultType'])' for '$Method'; PSMcpClient supports only 'complete' results (no multi round-trip requests).") } return $result } } |