Private/Write-McpMessage.ps1
|
function Get-McpStderrTail { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'stderr is diagnostic only; an unreadable stream must not mask the original error')] [CmdletBinding()] param( [Parameter(Mandatory)]$Session, [int]$Lines = 20 ) $text = $null try { if ($Session.StderrTask.Wait(1000)) { $text = $Session.StderrTask.Result } } catch { } if ([string]::IsNullOrWhiteSpace($text)) { return '' } $all = @($text -split "`r?`n" | Where-Object { $_ }) ($all | Select-Object -Last $Lines) -join "`n" } function New-McpProcessExitedError { [CmdletBinding()] param( [Parameter(Mandatory)]$Session, [string]$Context ) $exitCode = try { $Session.Process.ExitCode } catch { 'unknown' } $message = "MCP server '$($Session.Name)' exited (exit code $exitCode)" if ($Context) { $message += " $Context" } $tail = Get-McpStderrTail -Session $Session if ($tail) { $message += ".`nLast stderr output:`n$tail" } else { $message += '.' } New-McpError -ErrorId 'McpProcessExited' -Category ResourceUnavailable -TargetObject $Session.Name -Message $message } function Write-McpMessage { [CmdletBinding()] param( [Parameter(Mandatory)]$Session, [Parameter(Mandatory)]$Message ) $json = ConvertTo-Json -InputObject $Message -Compress -Depth 20 Write-Verbose "[$($Session.Name)] -> $json" if ($Session.Process.HasExited) { throw (New-McpProcessExitedError -Session $Session -Context 'before the request could be sent') } try { $Session.StdIn.WriteLine($json) } catch [System.IO.IOException] { throw (New-McpProcessExitedError -Session $Session -Context 'while writing to its stdin') } } function Read-McpLine { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Task.Wait throws for faulted tasks; the fault is inspected and rethrown after the loop')] [CmdletBinding()] param( [Parameter(Mandatory)]$Session, [Parameter(Mandatory)][int]$TimeoutMs ) # A ReadLineAsync that times out stays pending on the StreamReader; reuse it rather than start a second one if ($null -eq $Session.PendingRead) { $Session.PendingRead = $Session.StdOut.ReadLineAsync() } $task = $Session.PendingRead $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() while (-not $task.IsCompleted) { $remaining = $TimeoutMs - $stopwatch.ElapsedMilliseconds if ($remaining -le 0) { return [pscustomobject]@{ State = 'Timeout'; Line = $null } } $slice = [int][Math]::Min($remaining, 250) try { $null = $task.Wait($slice) } catch { } if ($task.IsCompleted) { break } if ($Session.Process.HasExited) { try { $null = $task.Wait(500) } catch { } if (-not $task.IsCompleted) { return [pscustomobject]@{ State = 'Eof'; Line = $null } } } } $Session.PendingRead = $null if ($task.IsFaulted) { throw $task.Exception.InnerException } $line = $task.Result if ($null -eq $line) { return [pscustomobject]@{ State = 'Eof'; Line = $null } } [pscustomobject]@{ State = 'Line'; Line = $line } } |