PowerShell.MCP.psm1
|
# PowerShell.MCP Module Script # Provides automatic cleanup when Remove-Module is executed # On Linux/macOS, PSReadLine interferes with timer events # Remove it to ensure MCP polling works correctly if (-not $IsWindows) { Remove-Module PSReadLine -ErrorAction SilentlyContinue } # Set OnRemove script block to execute cleanup automatically $ExecutionContext.SessionState.Module.OnRemove = { try { #Write-Host "[PowerShell.MCP] Module removal detected, starting cleanup..." -ForegroundColor Yellow # Load and execute MCPCleanup.ps1 from embedded resources $assembly = [System.Reflection.Assembly]::GetAssembly([PowerShell.MCP.MCPModuleInitializer]) $resourceName = "PowerShell.MCP.Resources.MCPCleanup.ps1" $stream = $assembly.GetManifestResourceStream($resourceName) if ($stream) { $reader = New-Object System.IO.StreamReader($stream) $cleanupScript = $reader.ReadToEnd() $reader.Close() $stream.Close() # Execute cleanup script Invoke-Expression $cleanupScript #Write-Host "[PowerShell.MCP] OnRemove cleanup completed" -ForegroundColor Green } else { #Write-Warning "[PowerShell.MCP] MCPCleanup.ps1 resource not found" } } catch { #Write-Warning "[PowerShell.MCP] Error during module removal cleanup: $($_.Exception.Message)" } } #Write-Host "[PowerShell.MCP] Module loaded with OnRemove cleanup support" -ForegroundColor Green <# .SYNOPSIS Gets the path to the PowerShell.MCP.Proxy executable for the current platform. .DESCRIPTION Returns the full path to the platform-specific PowerShell.MCP.Proxy executable. Use this path in your MCP client configuration. .PARAMETER Escape If specified, escapes backslashes for use in JSON configuration files. .EXAMPLE Get-MCPProxyPath Returns: C:\Program Files\PowerShell\7\Modules\PowerShell.MCP\bin\win-x64\PowerShell.MCP.Proxy.exe .EXAMPLE Get-MCPProxyPath -Escape Returns: C:\\Program Files\\PowerShell\\7\\Modules\\PowerShell.MCP\\bin\\win-x64\\PowerShell.MCP.Proxy.exe .OUTPUTS System.String #> function Get-MCPProxyPath { [CmdletBinding()] [OutputType([string])] param( [switch]$Escape ) $moduleBase = $PSScriptRoot $binFolder = Join-Path $moduleBase 'bin' # Determine RID based on OS and architecture $rid = if ($IsWindows) { switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { 'X64' { 'win-x64' } 'Arm64' { 'win-arm64' } default { 'win-x64' } } } elseif ($IsMacOS) { switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { 'X64' { 'osx-x64' } 'Arm64' { 'osx-arm64' } default { 'osx-x64' } } } elseif ($IsLinux) { switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { 'X64' { 'linux-x64' } 'Arm64' { 'linux-arm64' } 'Arm' { 'linux-arm' } default { 'linux-x64' } } } else { throw "Unsupported operating system" } # Determine executable name $exeName = if ($IsWindows) { 'PowerShell.MCP.Proxy.exe' } else { 'PowerShell.MCP.Proxy' } $proxyPath = Join-Path $binFolder $rid $exeName if (-not (Test-Path $proxyPath)) { throw "PowerShell.MCP.Proxy not found at: $proxyPath. Please ensure the module is properly installed for your platform ($rid)." } if ($Escape) { return $proxyPath.Replace('\', '\\') } return $proxyPath } <# .SYNOPSIS Gets information about the MCP client that owns this console. .DESCRIPTION Returns ownership information for the current PowerShell console, including whether it is owned by an MCP proxy, the proxy's PID, the agent ID, and the client name (e.g., Claude Desktop, Claude Code, VS Code). .EXAMPLE Get-MCPOwner Owned : True ProxyPid : 22208 AgentId : cc19706b ClientName : Claude Desktop .EXAMPLE Get-MCPOwner Owned : False ProxyPid : AgentId : ClientName : .OUTPUTS PSCustomObject with Owned, ProxyPid, AgentId, and ClientName properties #> function Get-MCPOwner { [CmdletBinding()] [OutputType([PSCustomObject])] param() $pipeName = [PowerShell.MCP.MCPModuleInitializer]::GetPipeName() if (-not $pipeName) { return [PSCustomObject]@{ Owned = $false ProxyPid = $null AgentId = $null ClientName = $null } } # Parse pipe name segments # Unowned: PowerShell.MCP.Communication.{pwshPid} (4 segments) # Owned: PowerShell.MCP.Communication.{proxyPid}.{agentId}.{pwshPid} (6 segments) $segments = $pipeName.Split('.') if ($segments.Length -ne 6) { return [PSCustomObject]@{ Owned = $false ProxyPid = $null AgentId = $null ClientName = $null } } $proxyPid = [int]$segments[3] $agentId = $segments[4] # Determine client name by examining process path and parent chain # Uses Get-Process Path and Parent properties (cross-platform, no Win32_Process) $clientName = $null try { $proxyProcess = Get-Process -Id $proxyPid -ErrorAction SilentlyContinue $currentProcess = $proxyProcess for ($i = 0; $currentProcess -and $i -lt 5; $i++) { $processName = $currentProcess.ProcessName.ToLower() $processPath = $currentProcess.Path # Check process name and path for known clients if ($processName -eq 'claude' -or $processPath -match 'AnthropicClaude') { $clientName = 'Claude Desktop' break } elseif ($processName -eq 'node' -or $processPath -match 'claude-code|claude_code') { $clientName = 'Claude Code' break } elseif ($processName -match '^code$|^code - insiders$') { $clientName = 'VS Code' break } elseif ($processName -match 'cursor') { $clientName = 'Cursor' break } # Get parent process (PowerShell 7.4+, cross-platform) $currentProcess = $currentProcess.Parent } # Fallback to proxy process name if (-not $clientName -and $proxyProcess) { $clientName = $proxyProcess.ProcessName } } catch { # Ignore errors in process lookup } return [PSCustomObject]@{ Owned = $true ProxyPid = $proxyPid AgentId = $agentId ClientName = $clientName } } <# .SYNOPSIS Installs PowerShell.MCP skills for Claude Code. .DESCRIPTION Copies skill files from the PowerShell.MCP module to the Claude Code skills directory (~/.claude/skills/). These skills provide slash commands for common PowerShell.MCP operations. .PARAMETER Name Specifies the names of skills to install. If not specified, all available skills are installed. Available skills: ps-analyze, ps-create-procedure, ps-dictation, ps-exec-procedure, ps-html-guidelines, ps-learn, ps-map .PARAMETER Force Overwrites existing skill files without prompting. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER PassThru Returns the installed skill file objects. .EXAMPLE Install-ClaudeSkill Installs all available skills to ~/.claude/skills/ .EXAMPLE Install-ClaudeSkill ps-analyze, ps-learn Installs only the 'ps-analyze' and 'ps-learn' skills. .EXAMPLE Install-ClaudeSkill -WhatIf Shows which skills would be installed without actually installing them. .EXAMPLE Install-ClaudeSkill ps-analyze -Force Installs the 'ps-analyze' skill, overwriting if it exists. .OUTPUTS System.IO.FileInfo (when -PassThru is specified) #> function Install-ClaudeSkill { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.IO.FileInfo])] param( [Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateSet('ps-analyze', 'ps-create-procedure', 'ps-dictation', 'ps-exec-procedure', 'ps-html-guidelines', 'ps-learn', 'ps-map')] [string[]]$Name, [switch]$Force, [switch]$PassThru ) begin { $moduleSkillsPath = Join-Path $PSScriptRoot 'skills' $userSkillsPath = Join-Path $HOME '.claude' 'skills' if (-not (Test-Path $moduleSkillsPath)) { throw "Skills directory not found in module: $moduleSkillsPath" } if (-not (Test-Path $userSkillsPath)) { New-Item -Path $userSkillsPath -ItemType Directory -Force | Out-Null Write-Verbose "Created directory: $userSkillsPath" } $skillsToInstall = @() } process { if ($Name) { $skillsToInstall += $Name } } end { $availableSkills = Get-ChildItem -Path $moduleSkillsPath -Filter '*.md' -File if ($skillsToInstall.Count -eq 0) { $skillsToInstall = $availableSkills | ForEach-Object { $_.BaseName } } $installedCount = 0 $skippedCount = 0 foreach ($skillName in $skillsToInstall | Select-Object -Unique) { $sourceFile = Join-Path $moduleSkillsPath "$skillName.md" $destFile = Join-Path $userSkillsPath "$skillName.md" if (-not (Test-Path $sourceFile)) { Write-Warning "Skill not found: $skillName" continue } $shouldInstall = $true if ((Test-Path $destFile) -and -not $Force) { $sourceHash = (Get-FileHash $sourceFile -Algorithm MD5).Hash $destHash = (Get-FileHash $destFile -Algorithm MD5).Hash if ($sourceHash -eq $destHash) { Write-Verbose "Skill '$skillName' is already up to date" $skippedCount++ $shouldInstall = $false } else { Write-Warning "Skill '$skillName' already exists with different content. Use -Force to overwrite." $skippedCount++ $shouldInstall = $false } } if ($shouldInstall -and $PSCmdlet.ShouldProcess($destFile, "Install skill '$skillName'")) { Copy-Item -Path $sourceFile -Destination $destFile -Force $installedCount++ Write-Host "Installed: $skillName" -ForegroundColor Green if ($PassThru) { Get-Item $destFile } } } if (-not $WhatIfPreference) { Write-Host "`nInstalled: $installedCount skill(s), Skipped: $skippedCount skill(s)" -ForegroundColor Cyan if ($installedCount -gt 0) { Write-Host "Restart Claude Code to use the new skills." -ForegroundColor Yellow } } } } |