psmcp.logger.ps1

function psmcp.writeLog {
    <#
    .SYNOPSIS
        Write a single structured JSON log entry to a file, with optional rotation and level-based filtering.
 
    .DESCRIPTION
        Lightweight structured logger used by the PSMCP server.
        The function accepts a dictionary (preferably an ordered dictionary)
        describing an event and appends a compact JSON line to a log file.
 
    .PARAMETER LogEntry
        Dictionary/ordered-dictionary representing the log payload.
 
    .PARAMETER LogFilePath
        Explicit destination path for the log file. If omitted the
        function falls back to `PWSH_MCP_SERVER_LOG_FILE_PATH` and then a
        default path under the user's profile. The path must be writable by the running process.
 
    .NOTES
 
    Alias: Write-McpLog
    ---
    Write-Information -MessageData $item -InformationAction Continue 6>> $filePath
 
    #>

    [Alias('Write-McpLog')]
    [OutputType([void])]
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'Low'
    )]
    param(
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Dictionary payload for the log entry. Prefer ordered dictionaries.'
        )]
        [System.Collections.IDictionary] $LogEntry,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional explicit file path to write logs.'
        )]
        [string] $LogFilePath
    )

    function getEffectiveLogPath {
        param(
            [Parameter(Mandatory = $false)]
            [string] $LogFilePath
        )

        if (-not [string]::IsNullOrWhiteSpace($LogFilePath)) {
            return $LogFilePath
        }

        if (-not [string]::IsNullOrWhiteSpace($env:PWSH_MCP_SERVER_LOG_FILE_PATH)) {
            return $env:PWSH_MCP_SERVER_LOG_FILE_PATH
        }

        $homeFolder = $HOME ?? [Environment]::GetFolderPath('UserProfile')

        return [System.IO.Path]::Combine($homeFolder, '.cache', 'mcp', 'pwsh_mcp_server.log')
    }

    # Resolve effective log path via fallback chain
    $effectiveLogPath = getEffectiveLogPath -LogFilePath $LogFilePath
    # Ensure directory exists
    $dir = [System.IO.Path]::GetDirectoryName($effectiveLogPath)
    if (-not [string]::IsNullOrWhiteSpace($dir) -and -not (Test-Path -LiteralPath $dir)) {
        New-Item -ItemType Directory -Path $dir -Force -ea SilentlyContinue | Out-Null
    }

    function rotateLogFile {
        # Rotate log file if it exceeds max size
        param([string]$effectiveLogPath)
        $maxSizeKB = [int]($env:PWSH_MCP_SERVER_LOG_MAX_SIZE_KB ?? 10)
        $maxMinutes = [int]($env:PWSH_MCP_SERVER_LOG_ROTATION_MINUTES ?? 15)

        if (Test-Path $effectiveLogPath) {
            $fileInfo = Get-Item $effectiveLogPath
            $minutesSinceLastWrite = (New-TimeSpan -Start $fileInfo.LastWriteTime -End (Get-Date)).TotalMinutes

            if ($fileInfo.Length -gt ($maxSizeKB * 1KB) -or $minutesSinceLastWrite -gt $maxMinutes) {
                $newName = [string]::Format("{0}.{1}.log",
                    ($fileInfo.BaseName),
                    (Get-Date -Format "yyyyMMddHHmmss")
                )
                if ($PSCmdlet.ShouldProcess($effectiveLogPath, "Rotate log file to $newName")) {
                    Rename-Item -Path $effectiveLogPath -NewName $newName -ea SilentlyContinue
                }
            }
        }
    }

    # Build logObject log object with additional metadata
    $logObject = [ordered]@{
        WHEN        = (Get-Date).ToString("o")
        WHAT        = $LogEntry.what ?? "MCP_DEBUG_LOG_ENTRY"
        PSCallStack = Get-PSCallStack | Select-Object -ExpandProperty Command -Skip 1
        log         = $LogEntry
    }

    rotateLogFile -effectiveLogPath $effectiveLogPath

    $addContentSplat = @{
        Path        = $effectiveLogPath
        Value       = ConvertTo-Json -InputObject $logObject -Depth 10 -Compress
        Encoding    = [System.Text.Encoding]::UTF8
        ErrorAction = [System.Management.Automation.ActionPreference]::SilentlyContinue
    }
    if ($PSCmdlet.ShouldProcess($effectiveLogPath, "Write log entry")) {
        Add-Content @addContentSplat
    }

}

function psmcp.writeConsoleLog {
    <#
    .SYNOPSIS
        Build a structured debug notification json payload.
 
    .NOTES
        Snippet for testing:
        (ConvertTo-Json -InputObject (psmcp.writeConsoleLog -text "123") -Compress)
 
    #>

    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string] $text = "TEXT"
    )
    return [ordered]@{
        jsonrpc = "2.0"
        method  = "notifications"
        params  = [ordered]@{
            message    = $text
            caller     = Get-PSCallStack | Select-Object -Property Command -Skip 1 -First 1
            modulePath = $MyInvocation.MyCommand.Module.Path

        }
    }
}