Files
reverse-skill/skills/ida-reverse/scripts/start.ps1
T
Harry 79d74db3d3 feat(ida-reverse): keep-alive supervisor and ida-pro-mcp 2.x open
Reuse a healthy HTTP MCP, launch idalib_supervisor via windowless Python, and never taskkill ida.exe. Treat a listening 13337 with tools/list timeout as busy so the 1-minute watchdog cannot kill a supervisor mid-idb_open. open.ps1 uses idb_open/idb_list. Add watchdog, autostart, GUI launcher, and ToolDiscovery catalog entries.
2026-08-18 14:25:41 +01:00

508 lines
18 KiB
PowerShell

<#
.SYNOPSIS
Start IDA Pro MCP HTTP server (background, non-blocking)
.DESCRIPTION
1. Resolve IDADIR (env / portable IDA / registry / common paths)
2. Resolve idalib-mcp (PATH, IDA Python314 Scripts, or python -m)
3. If HTTP already healthy, reuse it (unless -Force)
4. Otherwise kill stale listeners and start a logged supervisor
5. Wait for service ready (max 30 seconds)
6. Output OK:<tool_count>, OK:<tool_count>:reuse, or ERR:...
Usage: run without parameters
#>
param(
[string]$IdaDir,
[int]$Port = 13337,
[string]$ServerPath,
[int]$WaitSeconds = 30,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
function Get-IdaMcpLogDir {
$dir = Join-Path $env:LOCALAPPDATA 'reverse-skill\ida-mcp'
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
return $dir
}
function Rotate-IdaMcpLog {
param(
[string]$Path,
[int]$MaxBytes = 5MB
)
if (-not (Test-Path -LiteralPath $Path)) { return }
if ((Get-Item -LiteralPath $Path).Length -le $MaxBytes) { return }
$bak = "$Path.1"
if (Test-Path -LiteralPath $bak) {
Remove-Item -LiteralPath $bak -Force
}
Move-Item -LiteralPath $Path -Destination $bak -Force
}
function Test-IdaMcpHealth {
param([int]$Port)
$probe = Probe-IdaMcp -Port $Port
if ($probe.Status -eq 'healthy') { return $probe.Count }
return 0
}
function Probe-IdaMcp {
param([int]$Port)
$owners = @(Get-IdaMcpPortOwners -Port $Port)
$guiOwners = @($owners | Where-Object { Test-IdaGuiProcess -ProcessId $_ })
$listening = $owners.Count -gt 0
try {
$r = Invoke-RestMethod "http://127.0.0.1:$Port/mcp" -Method Post `
-Body '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' `
-ContentType 'application/json' -TimeoutSec 3 -ErrorAction Stop
$tools = @($r.result.tools)
$count = $tools.Count
$names = @($tools | ForEach-Object { $_.name })
# Supervisor without --unsafe is "up" but missing py_eval. Treat as stale.
if ($count -gt 0 -and ($names -contains 'py_eval')) {
return @{ Status = 'healthy'; Count = $count; Owners = $owners; GuiOwners = $guiOwners }
}
if ($count -gt 0) {
return @{ Status = 'stale'; Count = $count; Owners = $owners; GuiOwners = $guiOwners }
}
} catch {}
if ($guiOwners.Count -gt 0) {
return @{ Status = 'gui_busy'; Count = 0; Owners = $owners; GuiOwners = $guiOwners }
}
# Single-threaded supervisor cannot answer tools/list during idb_open.
# A listen socket is busy, not dead — never taskkill on RPC timeout.
if ($listening) {
return @{ Status = 'busy'; Count = 0; Owners = $owners; GuiOwners = $guiOwners }
}
return @{ Status = 'down'; Count = 0; Owners = $owners; GuiOwners = $guiOwners }
}
function Get-IdaMcpPortOwners {
param([int]$Port)
try {
return @(
Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty OwningProcess -Unique |
Where-Object { $_ -and $_ -gt 0 }
)
} catch {
return @()
}
}
function Get-IdaMcpProcessInfo {
param([int]$ProcessId)
return Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId" -ErrorAction SilentlyContinue
}
function Test-IdaGuiProcess {
param([int]$ProcessId)
$proc = Get-IdaMcpProcessInfo -ProcessId $ProcessId
if (-not $proc) { return $false }
return [string]$proc.Name -match '(?i)^(ida|ida64|idaq|idaq64)\.exe$'
}
function Test-ManagedSupervisorProcess {
param([int]$ProcessId)
$proc = Get-IdaMcpProcessInfo -ProcessId $ProcessId
if (-not $proc) { return $false }
$name = [string]$proc.Name
$cmd = [string]$proc.CommandLine
if ($name -match '(?i)^(ida|ida64|idaq|idaq64)\.exe$') { return $false }
if ($name -match '(?i)^(python|pythonw|cmd)\.exe$') {
return $cmd -match '(?i)(run-supervisor\.py|idalib_supervisor|idalib-mcp|ida-pro-mcp)'
}
return $name -match '(?i)^(idalib-mcp|ida-pro-mcp|idalib_supervisor)'
}
function Get-ManagedSupervisorProcessIds {
$ids = New-Object System.Collections.Generic.List[int]
foreach ($proc in @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)) {
if (-not $proc.ProcessId) { continue }
$candidateId = [int]$proc.ProcessId
if (Test-ManagedSupervisorProcess -ProcessId $candidateId) {
[void]$ids.Add($candidateId)
}
}
return @($ids | Select-Object -Unique)
}
function Stop-IdaMcpManagedProcess {
param([int]$ProcessId)
if ($ProcessId -le 0) { return }
if (Test-IdaGuiProcess -ProcessId $ProcessId) { return }
# No /T: force_gui may have spawned ida.exe as a child of the supervisor.
& taskkill.exe /F /PID $ProcessId 2>$null | Out-Null
}
function Get-PortableIdaCandidates {
$desktop = [Environment]::GetFolderPath('Desktop')
$userProfile = $env:USERPROFILE
return @(
(Join-Path $desktop 'IDA Pro 9.4\App\IDA Pro'),
(Join-Path $desktop 'IDA Pro\App\IDA Pro'),
(Join-Path $userProfile 'Desktop\IDA Pro 9.4\App\IDA Pro'),
(Join-Path $userProfile 'Tools\IDA Pro 9.4\App\IDA Pro'),
(Join-Path $userProfile 'Tools\IDA Pro\App\IDA Pro'),
(Join-Path $userProfile 'Tools\IDA'),
'C:\Program Files\IDA Professional 9.4',
'C:\Program Files\IDA Pro 9.4',
'C:\Program Files\IDA Pro',
'C:\Program Files\IDA',
'C:\IDA Pro',
'C:\IDA',
'D:\IDA',
'D:\Tools\IDA Pro 9.4\App\IDA Pro',
'E:\Program Files\IDA'
)
}
function Test-IdaInstallDir {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
if (-not (Test-Path -LiteralPath $Path)) { return $false }
$idaExe = Join-Path $Path 'ida.exe'
$idaDll = Join-Path $Path 'ida.dll'
return (Test-Path -LiteralPath $idaExe) -or (Test-Path -LiteralPath $idaDll)
}
function Get-InstalledIdaDir {
$registryPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$fromRegistry = $registryPaths |
ForEach-Object {
Get-ItemProperty $_ -ErrorAction SilentlyContinue
} |
Where-Object {
($_.DisplayName -match 'IDA|Hex-Rays') -and
-not [string]::IsNullOrWhiteSpace($_.InstallLocation) -and
(Test-IdaInstallDir -Path $_.InstallLocation)
} |
Select-Object -ExpandProperty InstallLocation -First 1
if ($fromRegistry) {
return $fromRegistry
}
return Get-PortableIdaCandidates | Where-Object { Test-IdaInstallDir -Path $_ } | Select-Object -First 1
}
function Resolve-IdaDir {
param([string]$Preferred)
if (-not [string]::IsNullOrWhiteSpace($Preferred) -and (Test-IdaInstallDir -Path $Preferred)) {
return [System.IO.Path]::GetFullPath($Preferred)
}
if (-not [string]::IsNullOrWhiteSpace($env:IDADIR) -and (Test-IdaInstallDir -Path $env:IDADIR)) {
return [System.IO.Path]::GetFullPath($env:IDADIR)
}
foreach ($scope in @('User', 'Machine')) {
$persisted = [Environment]::GetEnvironmentVariable('IDADIR', $scope)
if (-not [string]::IsNullOrWhiteSpace($persisted) -and (Test-IdaInstallDir -Path $persisted)) {
return [System.IO.Path]::GetFullPath($persisted)
}
}
$found = Get-InstalledIdaDir
if ($found) {
return [System.IO.Path]::GetFullPath($found)
}
return $null
}
function Test-PythonHasIdaProMcp {
param([string]$PythonExe)
if ([string]::IsNullOrWhiteSpace($PythonExe) -or -not (Test-Path -LiteralPath $PythonExe)) {
return $false
}
# pythonw.exe has no console — probe with sibling python.exe when possible
$probeExe = $PythonExe
if ($PythonExe -match '(?i)pythonw\.exe$') {
$sibling = Join-Path (Split-Path $PythonExe -Parent) 'python.exe'
if (Test-Path -LiteralPath $sibling) { $probeExe = $sibling }
}
try {
$check = & $probeExe -c "import ida_pro_mcp; print('ok')" 2>$null
return ($LASTEXITCODE -eq 0) -or ($check -match 'ok')
} catch {
return $false
}
}
function Get-WindowlessPythonPath {
param([string]$PythonExe)
if ([string]::IsNullOrWhiteSpace($PythonExe)) { return $PythonExe }
if ($PythonExe -match '(?i)pythonw\.exe$') { return $PythonExe }
if ($PythonExe -match '(?i)python\.exe$') {
$pythonw = Join-Path (Split-Path $PythonExe -Parent) 'pythonw.exe'
if (Test-Path -LiteralPath $pythonw) { return $pythonw }
}
return $PythonExe
}
function Find-IdalibServer {
param(
[string]$IdaDirPath,
[string]$PreferredServerPath
)
if (-not [string]::IsNullOrWhiteSpace($PreferredServerPath) -and (Test-Path -LiteralPath $PreferredServerPath)) {
return @{ Mode = 'exe'; Path = $PreferredServerPath }
}
# Prefer direct `pythonw -m ida_pro_mcp.idalib_supervisor` (no console window; more stable than .cmd)
$pythonCandidates = @(
(Join-Path $IdaDirPath 'Python314\pythonw.exe'),
(Join-Path $IdaDirPath 'Python314\python.exe'),
(Join-Path $IdaDirPath 'python\pythonw.exe'),
(Join-Path $IdaDirPath 'python\python.exe'),
(Join-Path $env:LOCALAPPDATA 'Python\pythoncore-3.14-64\pythonw.exe'),
(Join-Path $env:LOCALAPPDATA 'Python\pythoncore-3.14-64\python.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Python\Python314\pythonw.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Python\Python314\python.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Python\Python312\pythonw.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Python\Python312\python.exe'),
(Get-Command pythonw -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -First 1),
(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -First 1)
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
foreach ($py in $pythonCandidates) {
# `py` launcher is not a real interpreter for -m in Start-Process; skip bare py.exe
if ($py -match '\\py\.exe$') { continue }
if (Test-PythonHasIdaProMcp -PythonExe $py) {
$launch = Get-WindowlessPythonPath -PythonExe $py
return @{ Mode = 'module'; Path = $launch; Module = 'ida_pro_mcp.idalib_supervisor' }
}
}
# Prefer native .exe entrypoints over .cmd wrappers
$scriptCandidates = @(
(Join-Path $env:LOCALAPPDATA 'Python\pythoncore-3.14-64\Scripts\idalib-mcp.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Python\Python312\Scripts\idalib-mcp.exe'),
(Join-Path $IdaDirPath 'Python314\Scripts\idalib-mcp.exe'),
(Join-Path $IdaDirPath 'Python314\Scripts\ida-pro-mcp.exe')
)
foreach ($candidate in $scriptCandidates) {
if (Test-Path -LiteralPath $candidate) {
return @{ Mode = 'exe'; Path = $candidate }
}
}
foreach ($name in @('idalib-mcp', 'ida-pro-mcp')) {
$resolved = Get-Command $name -ErrorAction SilentlyContinue
if ($resolved) {
return @{ Mode = 'exe'; Path = $resolved.Source }
}
}
foreach ($candidate in @(
(Join-Path $env:USERPROFILE 'Tools\bin\idalib-mcp.cmd'),
(Join-Path $env:USERPROFILE 'Tools\bin\ida-pro-mcp.cmd')
)) {
if (Test-Path -LiteralPath $candidate) {
return @{ Mode = 'exe'; Path = $candidate }
}
}
$roamingPython = Join-Path $env:APPDATA 'Python'
if (Test-Path -LiteralPath $roamingPython) {
$candidate = Get-ChildItem -LiteralPath $roamingPython -Directory -ErrorAction SilentlyContinue |
ForEach-Object {
$scripts = Join-Path $_.FullName 'Scripts'
@('idalib-mcp.exe', 'ida-pro-mcp.exe') | ForEach-Object { Join-Path $scripts $_ }
} |
Where-Object { Test-Path -LiteralPath $_ } |
Select-Object -First 1
if ($candidate) {
return @{ Mode = 'exe'; Path = $candidate }
}
}
return $null
}
$probe = Probe-IdaMcp -Port $Port
$guiOwners = @($probe.GuiOwners)
if ($guiOwners.Count -gt 0) {
Write-Output ("WARN:gui_busy:port={0} pid={1}" -f $Port, ($guiOwners -join ','))
Write-Output 'HINT: IDA GUI owns 13337; not killing ida.exe. Wait for analysis or close IDA.'
exit 0
}
if (-not $Force) {
if ($probe.Status -eq 'healthy') {
Write-Output "OK:$($probe.Count)`:reuse"
exit 0
}
if ($probe.Status -eq 'busy') {
Write-Output ("WARN:busy:port={0} pid={1}" -f $Port, ($probe.Owners -join ','))
Write-Output 'HINT: 13337 is listening but tools/list timed out (likely idb_open). Not killing supervisor.'
exit 0
}
}
$resolvedIdaDir = Resolve-IdaDir -Preferred $IdaDir
if (-not $resolvedIdaDir) {
Write-Output "ERR:IDADIR not set and IDA Pro not found. Set IDADIR to your IDA install dir (folder containing ida.exe)."
exit 1
}
$IdaDir = $resolvedIdaDir
$env:IDADIR = $IdaDir
# Ensure IDA bins + bundled Python are visible to the child process
$idaPythonDir = Join-Path $IdaDir 'Python314'
if (-not (Test-Path -LiteralPath $idaPythonDir)) {
$idaPythonDir = Join-Path $IdaDir 'python'
}
$env:PATH = "$IdaDir;$idaPythonDir;$(Join-Path $idaPythonDir 'Scripts');$env:PATH"
$server = Find-IdalibServer -IdaDirPath $IdaDir -PreferredServerPath $ServerPath
# Auto-bootstrap only if still missing
if (-not $server) {
$bootstrapScript = Join-Path $PSScriptRoot '..\..\scripts\bootstrap-reverse.ps1'
if (Test-Path -LiteralPath $bootstrapScript) {
Write-Output "INFO: ida-pro-mcp not found, attempting auto-bootstrap (installing mrexodia/ida-pro-mcp)..."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrapScript -Capability @('idalib-mcp') -SkipRefresh
$server = Find-IdalibServer -IdaDirPath $IdaDir -PreferredServerPath $ServerPath
}
}
if (-not $server) {
Write-Output 'ERR:Missing idalib-mcp — install into IDA Python: <IDADIR>\Python314\python.exe -m pip install git+https://github.com/mrexodia/ida-pro-mcp.git'
exit 1
}
# Replace only when down or stale (no py_eval). Busy/gui already exited above
# unless -Force. Never taskkill ida.exe; never use /T.
foreach ($procName in @('ida-pro-mcp', 'idalib-mcp', 'idalib_supervisor')) {
$old = Get-Process -Name $procName -ErrorAction SilentlyContinue
if ($old) {
foreach ($process in @($old)) {
Stop-IdaMcpManagedProcess -ProcessId $process.Id
}
}
}
foreach ($managedPid in @(Get-ManagedSupervisorProcessIds)) {
Stop-IdaMcpManagedProcess -ProcessId $managedPid
}
Start-Sleep -Seconds 1
$wrapper = Join-Path $PSScriptRoot 'run-supervisor.py'
# --unsafe exposes py_eval / py_exec_file. dbg_* stay hidden (need ?ext=dbg; do not add).
$argList = @('--host', '127.0.0.1', '--port', "$Port", '--unsafe')
if (Test-Path -LiteralPath $wrapper) {
$filePath = $server.Path
if ($filePath -match '(?i)python\.exe$') {
$pythonw = Join-Path (Split-Path $filePath -Parent) 'pythonw.exe'
if (Test-Path -LiteralPath $pythonw) { $filePath = $pythonw }
}
if ($filePath -notmatch '(?i)pythonw?\.exe$') {
$filePath = $server.Path
if ($server.Mode -eq 'module') {
$argList = @('-u', '-m', $server.Module) + $argList
}
} else {
$argList = @('-u', $wrapper) + $argList
}
} elseif ($server.Mode -eq 'module') {
$filePath = $server.Path
$argList = @('-u', '-m', $server.Module) + $argList
} else {
$filePath = $server.Path
}
$logDir = Get-IdaMcpLogDir
$logFile = Join-Path $logDir 'supervisor.log'
Rotate-IdaMcpLog -Path $logFile
Write-Output "INFO:IDADIR=$IdaDir"
Write-Output "INFO:server=$filePath $($argList -join ' ')"
Write-Output "INFO:log=$logFile"
Write-Output 'INFO:window=hidden (pythonw / no console)'
# Detached + hidden start:
# pythonw + run-supervisor.py keeps logs without a cmd.exe window.
# Win32_Process.Create so the server survives agent/terminal Job Objects.
$quotedArgs = foreach ($a in $argList) {
if ($a -match '[\s"]') { '"' + ($a -replace '"', '\"') + '"' } else { $a }
}
$useCmd = $filePath -match '(?i)\.cmd$'
if ($useCmd) {
$commandLine = 'cmd.exe /d /c "' + $filePath + '" ' + ($quotedArgs -join ' ')
} else {
$commandLine = '"' + $filePath + '" ' + ($quotedArgs -join ' ')
}
$procId = 0
try {
$create = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
CommandLine = $commandLine
CurrentDirectory = $IdaDir
}
if ($create -and $create.ReturnValue -eq 0 -and $create.ProcessId) {
$procId = [int]$create.ProcessId
}
} catch {}
if ($procId -le 0) {
try {
$psi = New-Object System.Diagnostics.ProcessStartInfo
if ($useCmd) {
$psi.FileName = 'cmd.exe'
$psi.Arguments = '/d /c "' + $filePath + '" ' + ($quotedArgs -join ' ')
} else {
$psi.FileName = $filePath
$psi.Arguments = ($quotedArgs -join ' ')
}
$psi.WorkingDirectory = $IdaDir
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$p = [System.Diagnostics.Process]::Start($psi)
if ($p) { $procId = $p.Id }
} catch {}
}
if ($procId -le 0) {
Write-Output 'ERR:failed_to_start_process'
exit 1
}
Write-Output "INFO:pid=$procId"
# Wait for readiness via MCP tools/list
$ready = $false
$toolCount = 0
for ($i = 0; $i -lt $WaitSeconds; $i++) {
Start-Sleep -Seconds 1
$toolCount = Test-IdaMcpHealth -Port $Port
if ($toolCount -gt 0) {
Write-Output "OK:$toolCount"
$ready = $true
break
}
}
if (-not $ready) {
Write-Output "ERR:timeout"
Write-Output "HINT:check $logFile"
exit 1
}