mcp/Start-SqlCertAuditMcp.ps1
|
# ============================================================================= # Script : Start-SqlCertAuditMcp.ps1 # Author : Keith Ramsey # Created : 2026-06-30 # ============================================================================= # MVP Model Context Protocol (MCP) server for the SqlCertForge.Audit (read-only) # module. Zero-dependency: implements MCP stdio transport (newline-delimited # JSON-RPC 2.0) by hand -- initialize / tools/list / tools/call / ping. # # Exposes ONLY the 4 read-only audit tools. No state-changing cmdlets. This is # the free "funnel" tier; the paid/state-changing surface is the future C#-hosted # MCP with a compiled license (see BusinessCreation MCP plan). # # Protocol rules honored: # - stdout carries ONLY MCP JSON messages, one per line, no embedded newlines. # - stderr is used for any diagnostics. # ============================================================================= [CmdletBinding()] param() $ErrorActionPreference = 'Stop' [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false) # --- load the read-only module (keep stdout clean: route any noise to stderr) --- try { Import-Module (Join-Path $PSScriptRoot '..\SqlCertForge.Audit.psd1') -Force -ErrorAction Stop 3>$null 4>$null 5>$null 6>$null } catch { [Console]::Error.WriteLine("FATAL: could not load SqlCertForge.Audit: $($_.Exception.Message)") exit 1 } # --- tool catalogue: MCP tool name -> PowerShell function + input schema ---- $script:Tools = @( @{ name = 'get_rs_http_config' fn = 'Get-RsHttpConfig' description = 'Inventory a Reporting Services instance HTTPS surface (read-only): reserved URLs, SSL certificate bindings, and the actual registered/browseable URLs. Makes no changes.' inputSchema = @{ type = 'object' properties = @{ RsInstance = @{ type = 'string'; description = "RS instance: 'SSRS', 'PBIRS', or the SQL instance name."; default = 'MSSQLSERVER' } ComputerName = @{ type = 'string'; description = 'Target computer. Defaults to the local machine.' } } } }, @{ name = 'test_rs_https_endpoint' fn = 'Test-RsHttpsEndpoint' description = 'Confirm a Reporting Services endpoint actually SERVES over HTTPS (read-only): discovers the URL, issues a GET (200/401 = healthy), and validates the served certificate SAN/thumbprint. For wildcard reservations, pass VanityHost.' inputSchema = @{ type = 'object' properties = @{ RsInstance = @{ type = 'string'; default = 'MSSQLSERVER' } Application = @{ type = 'string'; enum = @('ReportServerWebService', 'ReportServerWebApp'); default = 'ReportServerWebService' } VanityHost = @{ type = 'string'; description = 'The host name users actually type (required when the reservation is a strong wildcard).' } Url = @{ type = 'string'; description = 'Explicit full URL to test (overrides discovery).' } ExpectedThumbprint = @{ type = 'string'; description = '40-hex thumbprint the served cert must match.' } ComputerName = @{ type = 'string' } } } }, @{ name = 'test_sql_cert_binding' fn = 'Test-SqlCertBinding' description = 'Report the TLS certificate binding for one or more SQL Server instances from the registry (read-only): thumbprint, ForceEncryption, and expiry. Supports remote -Node.' inputSchema = @{ type = 'object' properties = @{ SqlInstance = @{ type = 'array'; items = @{ type = 'string' }; description = "Instance(s): 'MSSQLSERVER' or 'HOST\\INSTANCE'." } Node = @{ type = 'array'; items = @{ type = 'string' }; description = 'Computer(s) to read. Defaults to local.' } } required = @('SqlInstance') } }, @{ name = 'test_rs_cert_binding' fn = 'Test-RsCertBinding' description = 'Report the TLS certificate bound to a Reporting Services endpoint via WMI (read-only): thumbprint and expiry for the requested application.' inputSchema = @{ type = 'object' properties = @{ RsInstance = @{ type = 'string'; default = 'MSSQLSERVER' } Application = @{ type = 'string'; enum = @('ReportServerWebService', 'ReportServerWebApp'); default = 'ReportServerWebService' } ComputerName = @{ type = 'string' } } } } ) function Write-Rpc($obj) { # Compact, single-line JSON (string values keep newlines as escaped \n). [Console]::Out.WriteLine(($obj | ConvertTo-Json -Depth 25 -Compress)) } [Console]::Error.WriteLine("sqlcertforge-audit MCP server ready ($(($script:Tools).Count) tools).") while ($null -ne ($line = [Console]::In.ReadLine())) { if ([string]::IsNullOrWhiteSpace($line)) { continue } try { $msg = $line | ConvertFrom-Json -ErrorAction Stop } catch { [Console]::Error.WriteLine("bad JSON: $($_.Exception.Message)"); continue } $id = if ($msg.PSObject.Properties['id']) { $msg.id } else { $null } $method = [string]$msg.method switch ($method) { 'initialize' { Write-Rpc @{ jsonrpc = '2.0'; id = $id; result = @{ protocolVersion = '2024-11-05' capabilities = @{ tools = @{} } serverInfo = @{ name = 'sqlcertforge-audit'; version = '0.1.0' } } } } 'notifications/initialized' { } # notification: no response 'ping' { Write-Rpc @{ jsonrpc = '2.0'; id = $id; result = @{} } } 'tools/list' { $list = $script:Tools | ForEach-Object { @{ name = $_.name; description = $_.description; inputSchema = $_.inputSchema } } Write-Rpc @{ jsonrpc = '2.0'; id = $id; result = @{ tools = @($list) } } } 'tools/call' { $name = [string]$msg.params.name $tool = $script:Tools | Where-Object { $_.name -eq $name } | Select-Object -First 1 if (-not $tool) { Write-Rpc @{ jsonrpc = '2.0'; id = $id; error = @{ code = -32602; message = "Unknown tool: $name" } } } else { try { $params = @{} if ($msg.params.PSObject.Properties['arguments'] -and $null -ne $msg.params.arguments) { $msg.params.arguments.PSObject.Properties | ForEach-Object { $params[$_.Name] = $_.Value } } $res = & $tool.fn @params $text = $res | ConvertTo-Json -Depth 25 Write-Rpc @{ jsonrpc = '2.0'; id = $id; result = @{ content = @(@{ type = 'text'; text = $text }); isError = $false } } } catch { Write-Rpc @{ jsonrpc = '2.0'; id = $id; result = @{ content = @(@{ type = 'text'; text = "Error: $($_.Exception.Message)" }); isError = $true } } } } } default { if ($null -ne $id) { Write-Rpc @{ jsonrpc = '2.0'; id = $id; error = @{ code = -32601; message = "Method not found: $method" } } } } } } |