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. Does not write to stdout/stderr to keep stdio channels clean for MCP transport. Behavior and configuration: - Log level filtering uses `PWSH_MCP_SERVER_LOG_LEVEL` (default: INFO). - Log file path can be provided via `-LogFilePath`, or via `PWSH_MCP_SERVER_LOG_FILE_PATH`; if neither is set a default path under the user's profile (~/.cache/mcp/pwsh_mcp_server.log) is used. - Log rotation thresholds are configurable via: `PWSH_MCP_SERVER_LOG_MAX_SIZE_KB` `PWSH_MCP_SERVER_LOG_ROTATION_MINUTES` .PARAMETER LogEntry Dictionary/ordered-dictionary representing the log payload. .PARAMETER Level Message severity. The parameter is case-insensitive. Valid values: TRACE, DEBUG, INFO, WARN, ERROR. Default is taken from the `PWSH_MCP_SERVER_LOG_LEVEL` environment variable or `INFO`. .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 = 'Severity for this entry: TRACE, DEBUG, INFO, WARN, ERROR' )] [ValidateSet('TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR')] $Level = ($env:PWSH_MCP_SERVER_LOG_LEVEL ?? 'INFO'), [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 getLogLevelValue { param( [Parameter(Mandatory = $true)] [string] $RequestedLevel ) $levelMap = @{ TRACE = 10; DEBUG = 20; INFO = 30; WARN = 40; ERROR = 50 } # Normalize requested message level $msgLevel = ($RequestedLevel ?? 'INFO').ToUpper() if (-not $levelMap.ContainsKey($msgLevel)) { $msgLevel = 'INFO' } # Determine minimal configured level from environment (falls back to INFO) $minLevel = ($env:PWSH_MCP_SERVER_LOG_LEVEL ?? 'INFO').ToUpper() if (-not $levelMap.ContainsKey($minLevel)) { $minLevel = 'INFO' } # Skip logging if message level is below configured minimum if ($levelMap[$msgLevel] -lt $levelMap[$minLevel]) { return $null } return $msgLevel } 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 } } } } $msgLevel = getLogLevelValue -RequestedLevel $Level if (-not $msgLevel) { return } # Build logObject log object with additional metadata $logObject = [ordered]@{ WHEN = (Get-Date).ToString("o") WHAT = $LogEntry.what ?? "MCP_DEBUG_LOG_ENTRY" LEVEL = $msgLevel PSCallStack = Get-PSCallStack | Select-Object -ExpandProperty Command -Skip 1 log = $LogEntry } rotateLogFile -effectiveLogPath $effectiveLogPath $addContentSplat = @{ Path = $effectiveLogPath Value = ConvertTo-Json -InputObject $logObject -Depth 15 -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 payload for the PSMCP console. .DESCRIPTION Constructs and returns an ordered dictionary representing a lightweight JSON-RPC-style notification. The returned object is intended for debug/console sinks and includes message text, caller information and module metadata to aid diagnostics without writing to stdout/stderr. .PARAMETER text The message text to include in the notification. Defaults to 'notification from PSMCP Server'. .PARAMETER Level The severity level of the notification (e.g., info, warn, error). Defaults to 'info'. .OUTPUTS System.Collections.Specialized.OrderedDictionary - JSON-serializable structure with keys: jsonrpc and params (containing level, msg, caller and data). #> [OutputType([System.Collections.Specialized.OrderedDictionary])] [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string] $text = "notification from PSMCP Server", [Parameter(Mandatory = $false)] [ValidateSet('info', 'warn', 'error', 'debug')] [string] $Level = "info" ) return [ordered]@{ jsonrpc = "2.0" # method = "notifications" params = @{ level = $Level msg = $text caller = Get-PSCallStack | Select-Object -Property Command -Skip 1 -First 1 data = [ordered]@{ message = "[MCP:$($MyInvocation.MyCommand.Module.Name)]" modulePath = $MyInvocation.MyCommand.Module.Path } } } } |