McpRepl.psm1

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

class McpRequest {
    [string]   $RequestId
    [string]   $Method
    [hashtable]$Params = @{}
    McpRequest([string]$id, [string]$method, [hashtable]$params = @{}) {
        $this.RequestId = $id; $this.Method = $method; $this.Params = $params
    }
}
class McpResult { [string]$RequestId; $Result; [hashtable]$Meta = @{} }
class McpError  { [string]$RequestId; [string]$Code; [string]$Message; $Details }
class McpEvent  { [string]$Event; $Data; [int]$Sequence }

function ConvertTo-McpYaml {
    param(
        [Parameter(Mandatory)]
        $InputObject
    )

    function ConvertTo-McpYamlValue {
        param($Value)
        if ($null -eq $Value) { return 'null' }
        if ($Value -is [bool]) { return $Value.ToString().ToLower() }
        if ($Value -is [int] -or $Value -is [long] -or $Value -is [double]) { return $Value.ToString() }
        $text = [string]$Value
        if ($text -match "[\r\n]") {
            # literal block scalar (the original requirement)
            $escaped = $text -replace "`r`n", "`n"
            $indented = ($escaped -split "`n" | ForEach-Object { " $_" }) -join "`n"
            return "|`n$indented"
        }
        $needsQuotes = $text -match '[:#\-\[\]\{\}\?\&\*\!\|\>\"\%\@\`]' -or $text -match "'"
        if ($needsQuotes) {
            $q = $text -replace "'", "''"
            return "'$q'"
        }
        return $text
    }

    if ($InputObject -is [McpRequest]) {
        $r = $InputObject
        $yaml = "type: request`npayload:`n requestId: $($r.RequestId)`n method: $($r.Method)"
        if ($r.Params.Count -gt 0) {
            $pLines = foreach ($entry in $r.Params.GetEnumerator()) {
                $k = $entry.Key
                $v = ConvertTo-McpYamlValue $entry.Value
                if ($v -like "|*") { " ${k}: $v" } else { " ${k}: $v" }
            }
            $yaml += "`n params:`n" + ($pLines -join "`n")
        }
        return $yaml
    }
    if ($InputObject -is [McpResult] -or $InputObject -is [McpError] -or $InputObject -is [McpEvent]) {
        return "type: result`npayload:`n requestId: $($InputObject.RequestId)`n result: $($InputObject | ConvertTo-Json -Depth 5 -Compress)"
    }
    return $InputObject | ConvertTo-Json -Depth 10
}

function ConvertFrom-McpYaml {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Text)

    # Try JSON first (common for result payloads from the REPL)
    try {
        $parsed = $Text | ConvertFrom-Json -AsHashtable -ErrorAction Stop
        if ($parsed -is [hashtable] -and $parsed.ContainsKey('payload')) {
            return [pscustomobject]@{
                RequestId = $parsed['payload']['requestId']
                Result    = $parsed['payload']['result']
            }
        }
        return $parsed
    } catch {}

    # Minimal line-based YAML parser for the envelopes used in tests and REPL stdio
    $result = [pscustomobject]@{ RequestId = $null; Result = $null; Type = $null }
    $inPayload = $false
    foreach ($rawLine in ($Text -split "`n")) {
        $line = $rawLine.Trim()
        if ([string]::IsNullOrWhiteSpace($line)) { continue }
        if ($line -match '^type:\s*(.+)$') { $result.Type = $Matches[1].Trim(); continue }
        if ($line -match '^payload:\s*$') { $inPayload = $true; continue }
        if ($line -match '^requestId:\s*(.+)$') {
            $rid = $Matches[1].Trim()
            if ($inPayload) { $result.RequestId = $rid } else { $result.RequestId = $rid }
            continue
        }
        if ($line -match '^result:\s*(.+)$') {
            $result.Result = $Matches[1].Trim()
            continue
        }
    }
    return $result
}

function New-McpRequest { param([string]$RequestId, [string]$Method, [hashtable]$Params = @{}) [McpRequest]::new($RequestId, $Method, $Params) }
function New-McpResult  { param([string]$RequestId, $Result) $r = [McpResult]::new(); $r.RequestId = $RequestId; $r.Result = $Result; return $r }
function New-McpError   { param([string]$RequestId, [string]$Code, [string]$Message) $e = [McpError]::new(); $e.RequestId = $RequestId; $e.Code = $Code; $e.Message = $Message; return $e }
function New-McpEvent   { param([string]$Event, $Data) $ev = [McpEvent]::new(); $ev.Event = $Event; $ev.Data = $Data; return $ev }

function Invoke-McpReplRaw {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Method,
        [hashtable]$Params = @{},
        [int]$TimeoutSeconds = 30
    )
    # Simulate the real bootstrap check that the full implementation performs
    $exe = Get-Command mcpserver-repl -ErrorAction SilentlyContinue
    if (-not $exe) {
        throw "mcpserver-repl not found in PATH. Install the McpServer.Repl tool or add it to PATH."
    }
    # Real path would spawn the process, send YAML via ConvertTo-McpYaml, etc.
    Write-Warning "McpRepl: Real REPL invocation not wired in this published version stub."
    return @{ Success = $false; Output = "Not implemented in stub"; RequestId = [guid]::NewGuid().ToString() }
}

function Invoke-McpRepl {
    param([Parameter(Mandatory)][string]$Method, [hashtable]$Params = @{})
    $raw = Invoke-McpReplRaw -Method $Method -Params $Params
    if (-not $raw.Success) { throw "REPL call failed" }
    return $raw
}

function Resolve-McpCacheDir {
    if ($env:MCP_CACHE_DIR_OVERRIDE) { return $env:MCP_CACHE_DIR_OVERRIDE }
    if ($env:PLUGIN_ROOT_OVERRIDE)   { return (Join-Path $env:PLUGIN_ROOT_OVERRIDE 'cache') }
    return (Join-Path $env:USERPROFILE '.mcpServer\cache')
}

Export-ModuleMember -Function @(
    'ConvertTo-McpYaml','ConvertFrom-McpYaml',
    'New-McpRequest','New-McpResult','New-McpError','New-McpEvent',
    'Invoke-McpRepl','Invoke-McpReplRaw','Resolve-McpCacheDir'
)