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.
This commit is contained in:
Harry
2026-08-18 14:25:41 +01:00
parent ea582f30d5
commit 79d74db3d3
13 changed files with 1212 additions and 217 deletions
+3
View File
@@ -39,6 +39,9 @@ issues-index.txt
skills/tool-index.md skills/tool-index.md
skills/tool-index.json skills/tool-index.json
# 本机就绪报告(含用户路径 / 客户端配置,不入库)
LOCAL-READINESS.md
# 编译产物 # 编译产物
*.exe *.exe
*.dll *.dll
+7
View File
@@ -7,6 +7,13 @@ Versioning follows [Semantic Versioning](https://semver.org/).
## [Unreleased] ## [Unreleased]
### Changed
- **IDA MCP keep-alive** — `start.ps1` reuses a healthy HTTP server, launches `idalib_supervisor` via windowless Python, never `taskkill`s `ida.exe` (no `/T`). A listening 13337 with `tools/list` timeout is treated as busy, not dead, so the 1-minute watchdog cannot kill a supervisor mid-`idb_open`. `open.ps1` talks ida-pro-mcp 2.x `idb_open`/`idb_list`.
- **IDA discovery** — `ToolDiscovery.ps1` now catalogs `idalib-mcp`, `ida-pro-mcp`, and `ida` with Program Files + per-user Python fallbacks.
### Added
- `ida-reverse` watchdog / scheduled-task installer / GUI launcher / supervisor wrapper (`watchdog.ps1`, `install-autostart.ps1`, `start-gui.ps1`, `run-supervisor.py`) plus portable `LOCAL-SETUP.md`.
## [1.0.1] — 2026-08-08 ## [1.0.1] — 2026-08-08
### Added ### Added
- **Routing single source of truth** — `skills/config/routing.json` (R0–R39 keyword rules with `must` / `mustAll` / `exclude` semantics). `master-route.ps1` now reads this file; hardcoded routing tables removed from scripts. Routing knowledge lives in one place. - **Routing single source of truth** — `skills/config/routing.json` (R0–R39 keyword rules with `must` / `mustAll` / `exclude` semantics). `master-route.ps1` now reads this file; hardcoded routing tables removed from scripts. Routing knowledge lives in one place.
+66
View File
@@ -0,0 +1,66 @@
# IDA ↔ reverse-skill 对接(可移植)
本页是通用步骤,不含某台机器的绝对路径。本机就绪报告留在仓库根目录的 `LOCAL-READINESS.md`(已 gitignore)。
## 目标形态
| 项 | 约定 |
|----|------|
| IDA 安装目录 | 环境变量 `IDADIR`(目录内有 `ida.exe` 或 `ida.dll`) |
| HTTP MCP | `http://127.0.0.1:13337/mcp` |
| 客户端服务器名 | 只留 **`idapro`**(不要同时注册 `ida-pro-mcp`) |
| 启动 | `scripts/start.ps1`(`--unsafe`,无 `?ext=dbg`) |
| 开库 | 大文件优先 `scripts/open.ps1`,不要经部分客户端直调 `idb_open` |
两个 MCP 名字指向同一 13337 会把工具注册两遍,并和 idalib worker 抢端口。
## 安装
```powershell
setx IDADIR "<你的 IDA 安装目录>"
# 必须用 mrexodia/ida-pro-mcp,不要装 PyPI 的 ida-mcp
python -m pip install "git+https://github.com/mrexodia/ida-pro-mcp.git"
# 激活 idalib(路径按本机 IDA 调整)
python "<IDADIR>\idalib\python\py-activate-idalib.py" -d "<IDADIR>"
# 装插件 + 客户端配置
python -m ida_pro_mcp --install --transport streamable-http --scope global
```
## 启动与保活
`type: http` 的 MCP 条目不会代为拉起进程。13337 没监听时,客户端全部报 error。
| 脚本 | 作用 |
|------|------|
| `scripts/start.ps1` | 健康则 `OK:<n>:reuse`;端口在听但 RPC 超时视为忙,不杀;只在无人监听或缺 `py_eval` 时替换 managed supervisor;永不杀 `ida.exe` |
| `scripts/watchdog.ps1` | 每分钟巡检;忙/健康则 reuse;只有 down/stale 才调 `start.ps1` |
| `scripts/install-autostart.ps1` | 注册计划任务 `reverse-skill-ida-mcp`(登录 + 每分钟) |
| `scripts/start-gui.ps1` | idalib license 失败时开 GUI 插件 |
| `scripts/open.ps1` | HTTP 直调 `idb_open`,绕过部分客户端 schema 校验 |
日志:`%LOCALAPPDATA%\reverse-skill\ida-mcp\supervisor.log` 与 `watchdog.log`。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File "skills\ida-reverse\scripts\start.ps1"
powershell -NoProfile -ExecutionPolicy Bypass -File "skills\ida-reverse\scripts\open.ps1" -Path "C:\path\to\target.exe" -TimeoutSeconds 600
powershell -NoProfile -ExecutionPolicy Bypass -File "skills\ida-reverse\scripts\install-autostart.ps1"
```
GUI 占用 13337 但一时没回包时,`start.ps1` 输出 `WARN:gui_busy` 并退出,避免把正在分析的 IDA 干掉。
## 客户端
全部指向 Streamable HTTP:`http://127.0.0.1:13337/mcp`,服务器名 `idapro`。
改配置后必须新开会话。Cursor 在启动时若端口未监听,事后把服务拉起来也**不会自动重连**,需要在 MCP 面板手动刷新。
## 已知注意点
1. System32 文件:`open.ps1` 会复制到临时路径(输出带 `(temp copy)`)
2. `idb_open` 勿经部分客户端 MCP 直调
3. `start.ps1` 优先 `python -m ida_pro_mcp.idalib_supervisor`,比 `.cmd` 包装更稳
4. 正式安装与桌面便携包并存时,以 `IDADIR` 为准
5. 不要加 `?ext=dbg`(默认不暴露调试器工具)
+56 -22
View File
@@ -22,11 +22,12 @@ description: |
### 踩过的坑 ### 踩过的坑
1. **`idalib_open` 不能通过 部分代码 AI 客户端 MCP 直接调用** 1. **`idb_open`(旧名 `idalib_open`)不要直接靠部分 AI 客户端 MCP 调用**
- 部分代码 AI 客户端 的 MCP 客户端对 `idalib_open` 的 output schema 校验有 BUG - 部分代码 AI 客户端 的 MCP 客户端对 open 类工具的 output schema 校验有 BUG
- 报错:`Structured content does not match the tool's output schema` - 报错:`Structured content does not match the tool's output schema`
- **解决办法**:使用 `scripts/open.ps1` 脚本通过 HTTP API 直调,绕过 MCP 校验层 - **解决办法**:使用 `scripts/open.ps1` 脚本通过 HTTP API 直调,绕过 MCP 校验层
- 文件打开后,数据库绑定到共享上下文,其他所有 `idapro_*` 工具可直接使用 - 当前 ida-pro-mcp 2.x 工具名为 `idb_open` / `idb_list` / `idb_save`(不再是 `idalib_*`)
- 文件打开后返回 `session_id`(database),后续工具调用需带该 session
2. **`C:\Windows\System32\` 文件无权限打开** 2. **`C:\Windows\System32\` 文件无权限打开**
- idalib 无法直接读取 System32 目录下的文件 - idalib 无法直接读取 System32 目录下的文件
@@ -52,10 +53,11 @@ description: |
- 已安装最新 `main` 分支版本 - 已安装最新 `main` 分支版本
7. **idalib 超时留下孤儿 worker 进程锁文件** 7. **idalib 超时留下孤儿 worker 进程锁文件**
- 第一次 `open.ps1` 超时后,idalib 的 python worker 子进程变成孤儿进程,咬着 `.id0`/`.id1`/`.nam` 不放 - 第一次 `open.ps1` 超时后,idalib 的 python worker 子进程可能变成孤儿,咬着 `.id0`/`.id1`/`.nam` 不放
- 后续任何工具或手动拖入 IDA GUI 都会报"权限不足" - 后续任何工具或手动拖入 IDA GUI 都会报"权限不足"
- **解决办法**:`start.ps1` 改用 `taskkill /F /T` 杀进程树,不再留孤儿 - **禁止** `taskkill /F /T` 杀进程树——`/T` 会把 GUI `ida.exe` 子进程一起干掉
- **兜底**:`open.ps1` 加了自动降级,检测到旧库被锁自动复制到 Temp 并加 GUID 前缀 - **解决办法**:`start.ps1` 只在端口无人监听、或 `tools/list` 快速返回但缺 `py_eval`(旧 supervisor)时替换 managed supervisor;RPC 超时且 13337 仍在听视为忙,不杀
- **兜底**:`open.ps1` 检测到旧库被锁自动复制到 Temp 并加 GUID 前缀
8. **带自动分析打开看起来像卡死** 8. **带自动分析打开看起来像卡死**
- `idalib_open(run_auto_analysis=true)` 可能长时间不回包,但后端实际上仍在继续打开和分析 - `idalib_open(run_auto_analysis=true)` 可能长时间不回包,但后端实际上仍在继续打开和分析
@@ -63,13 +65,19 @@ description: |
- **当前解决办法**:`open.ps1` 新增 `-TimeoutSeconds`,并改为后台请求 + 前台轮询 + 定时进度输出 - **当前解决办法**:`open.ps1` 新增 `-TimeoutSeconds`,并改为后台请求 + 前台轮询 + 定时进度输出
- 轮询到会话已就绪时会提前返回 `OK:文件名:session_id`,超时则返回 `ERR:open_timeout_xxs` - 轮询到会话已就绪时会提前返回 `OK:文件名:session_id`,超时则返回 `ERR:open_timeout_xxs`
9. **HTTP MCP 会在登录后静默退出**
- Cursor/Claude 的 `type: http` 不会代为拉起进程;旧计划任务只在登录时跑一次
- `pythonw` 无控制台,崩溃时 Application 日志也是空的
- **解决办法**:`start.ps1` 默认健康则复用;`watchdog.ps1` 每分钟巡检;日志在 `%LOCALAPPDATA%\reverse-skill\ida-mcp\`
- 安装:`scripts/install-autostart.ps1`。Cursor 若启动时端口还没起来,仍需在 MCP 面板手动刷新一次
### 工作流程原则 ### 工作流程原则
| 步骤 | 做什么 | 用什么 | | 步骤 | 做什么 | 用什么 |
|------|--------|--------| |------|--------|--------|
| 1 | 确保 HTTP 服务器在运行 | `scripts/start.ps1`(无参数) | | 1 | 确保 HTTP 服务器在运行 | `scripts/start.ps1`(无参数) |
| 2 | 打开目标二进制文件 | `scripts/open.ps1 -Path "xxx.exe"` | | 2 | 打开目标二进制文件 | `scripts/open.ps1 -Path "xxx.exe"` |
| 3 | 使用所有 72 个 MCP 工具 | 直接调用 `idapro_*` 工具 | | 3 | 使用 MCP 分析工具 | 直接调用 `idapro_*` / HTTP tools(约 65 个,视版本而定) |
| 4 | 分析完毕 | 工具自动可用 | | 4 | 分析完毕 | 工具自动可用 |
## 脚本资源 ## 脚本资源
@@ -78,8 +86,14 @@ description: |
路径:`scripts/start.ps1` 路径:`scripts/start.ps1`
- 用 `taskkill /F /T` 杀旧进程树(连 worker 子进程一起清理)→ 后台启动 `idalib-mcp` → 等待就绪(最多 15 秒) - 自动解析 `IDADIR`(环境变量 / 便携版桌面路径 / 常见安装路径)
- 成功输出 `OK:72`,失败输出 `ERR:timeout` - 优先用 IDA 自带 `Python314\python.exe -m ida_pro_mcp.idalib_supervisor`
- 默认先探测 `http://127.0.0.1:13337/mcp`,健康则输出 `OK:<n>:reuse` 并退出
- 13337 在听但 `tools/list` 超时 → `WARN:busy` / `OK:busy:reuse`,**不杀**(supervisor 单线程,开库时无法回包)
- 仅在端口无人监听、或快速返回且缺 `py_eval` 时替换 managed supervisor;**永不杀 `ida.exe`,不用 `taskkill /T`**
- GUI 占用 13337 时输出 `WARN:gui_busy` 并退出,不另起 supervisor
- 成功输出 `OK:<工具数>`(当前约 66),失败输出 `ERR:timeout`
- supervisor 日志:`%LOCALAPPDATA%\reverse-skill\ida-mcp\supervisor.log`
- 服务器在后台运行,不阻塞对话 - 服务器在后台运行,不阻塞对话
**调用方式**: **调用方式**:
@@ -87,11 +101,17 @@ description: |
powershell -File "<skill-root>\ida-reverse\scripts\start.ps1" powershell -File "<skill-root>\ida-reverse\scripts\start.ps1"
``` ```
### watchdog.ps1 / install-autostart.ps1 — 保活
- `watchdog.ps1`:探测 13337,健康则 `OK:<n>:reuse`,挂了才调用 `start.ps1`
- `install-autostart.ps1`:注册计划任务 `reverse-skill-ida-mcp`(登录 + 每分钟)
- 日志:`%LOCALAPPDATA%\reverse-skill\ida-mcp\watchdog.log`
### open.ps1 — 打开二进制文件 ### open.ps1 — 打开二进制文件
路径:`scripts/open.ps1` 路径:`scripts/open.ps1`
- 通过 HTTP API 直调 `idalib_open`,绕过 MCP schema 校验 - 通过 HTTP API 直调 `idb_open`,绕过 MCP schema 校验
- 自动检测 System32 路径并复制到临时目录 - 自动检测 System32 路径并复制到临时目录
- 自动清理同名旧数据库文件(`.id0`/`.id1`/`.nam`/`.til`/`.i64`) - 自动清理同名旧数据库文件(`.id0`/`.id1`/`.nam`/`.til`/`.i64`)
- 旧库被锁时自动降级:复制到 Temp 加 GUID 前缀后打开,不报错 - 旧库被锁时自动降级:复制到 Temp 加 GUID 前缀后打开,不报错
@@ -203,14 +223,11 @@ ERR:open_timeout_600s
- `idapro_open_file(file_path)` — 在 GUI IDA 实例中打开文件 - `idapro_open_file(file_path)` — 在 GUI IDA 实例中打开文件
- 调试器工具默认隐藏,可通过 URL 参数 `?ext=dbg` 启用 - 调试器工具默认隐藏,可通过 URL 参数 `?ext=dbg` 启用
### 会话管理 ### 会话管理(ida-pro-mcp 2.x)
- `idapro_idalib_open(input_path)` — ⚠️ 有 schema 校验 BUG,改用 `open.ps1` 脚本 - `idapro_idb_open` / HTTP `idb_open` — ⚠️ 建议用 `open.ps1` 打开
- `idapro_idalib_list()` — 列出所有 session - `idapro_idb_list` / HTTP `idb_list` — 列出所有 session
- `idapro_idalib_current()` — 当前上下文绑定的 session - `idapro_idb_save` / HTTP `idb_save` — 保存数据库
- `idapro_idalib_switch(session_id)` — 切换到其他 session - 多数分析工具需要 `database=<session_id>` 参数(open.ps1 输出的 session)
- `idapro_idalib_close(session_id)` — 关闭 session
- `idapro_idalib_save(path)` — 保存数据库
- `idapro_idalib_health(session_id)` — 检查 worker 健康状态
### 其他 ### 其他
- `idapro_int_convert(inputs)` — 进制转换(**必须用这个,不要自己算进制!**) - `idapro_int_convert(inputs)` — 进制转换(**必须用这个,不要自己算进制!**)
@@ -222,18 +239,31 @@ ERR:open_timeout_600s
## 逆向分析完整工作流 ## 逆向分析完整工作流
### Step 1: 启动服务器 ### Step 1: 启动服务器
确保 HTTP 服务在后台运行。
**路径 A — Headless idalib(需要有效 license)**
``` ```
powershell -File "scripts/start.ps1" powershell -File "scripts/start.ps1"
``` ```
输出 `OK:72` 表示就绪。 输出 `OK:<工具数>`(当前约 65)表示就绪。
**路径 B — GUI + 插件(idalib license 失败或需要交互分析时)**
```
powershell -File "scripts/start-gui.ps1" -Path "C:\目标.exe"
```
或双击便携版 `Launch-IDA-Pro.cmd`,在 IDA 中打开样本。
确认 Output 窗口出现 `[MCP] ... port=13337` 后,MCP 工具即可用。
通用对接步骤见 `LOCAL-SETUP.md`。
### Step 2: 打开文件 ### Step 2: 打开文件
Headless:
``` ```
powershell -File "scripts/open.ps1" -Path "C:\目标.exe" -TimeoutSeconds 600 powershell -File "scripts/open.ps1" -Path "C:\目标.exe" -TimeoutSeconds 600
``` ```
输出 `OK:文件名:session_id` 表示成功(后带 `(temp copy)` 表示自动降级到临时副本)。 输出 `OK:文件名:session_id` 表示成功(后带 `(temp copy)` 表示自动降级到临时副本)。
若分析时间较长,会周期性输出 `INFO:opening:...`;若达到超时则输出 `ERR:open_timeout_xxs`。 若出现 `ERR:idalib_license:...`,改用路径 B(GUI 模式),不要反复重试 open.ps1。
GUI 模式:在 IDA 里直接 Open 样本即可,无需 open.ps1。
### Step 3: 全局概览(含导入表硬门) ### Step 3: 全局概览(含导入表硬门)
``` ```
@@ -346,7 +376,11 @@ ida-pro-mcp --config
### 前置条件 ### 前置条件
- IDA Pro 已安装且 `IDADIR` 环境变量已设置(或脚本内默认路径正确) - IDA Pro 已安装且 `IDADIR` 环境变量已设置(或脚本内默认路径正确)
- Python 已安装(idalib-mcp 依赖 Python) - 推荐使用 IDA 自带 Python314 中的 `ida-pro-mcp`(便携版已内置)
- 常见本机配置:
- User env `IDADIR` → IDA 安装目录(含 `ida.exe`)
- 可选 `~\Tools\bin\idalib-mcp.cmd` / `ida-pro-mcp.cmd` 包装器
- 客户端 MCP 服务器名只留 `idapro` → `http://127.0.0.1:13337/mcp`
## 任务完成自检(声称完成前 MUST 通过) ## 任务完成自检(声称完成前 MUST 通过)
@@ -1,7 +1,7 @@
# IDA Pro MCP 工具速查 # IDA Pro MCP 工具速查
> 72 个 MCP 工具按功能分类,附常用参数和典型用法。 > ida-pro-mcp 2.x 工具按功能分类,附常用参数和典型用法。
> 服务器名:`idapro`,工具前缀:`idapro_*`,HTTP 模式运行。 > 服务器名:`idapro`,工具前缀:`idapro_*`,HTTP 模式运行。工具数随版本变化(约 66,含 `py_eval`)。
--- ---
@@ -10,9 +10,9 @@
### 服务器启动 ### 服务器启动
```powershell ```powershell
# 启动 MCP HTTP 服务器(后台静默) # 启动 MCP HTTP 服务器(后台静默;健康则 OK:<n>:reuse)
powershell -File "scripts/start.ps1" powershell -File "scripts/start.ps1"
# 输出 OK:72 表示就绪 # 输出 OK:<工具数> 表示就绪(约 66,含 py_eval)
# 打开目标文件(绕过 schema 校验) # 打开目标文件(绕过 schema 校验)
powershell -File "scripts/open.ps1" -Path "C:\target.exe" powershell -File "scripts/open.ps1" -Path "C:\target.exe"
@@ -29,12 +29,12 @@ powershell -File "scripts/open.ps1" -Path "C:\huge.sys" -NoAutoAnalysis
| 工具 | 用途 | 示例 | | 工具 | 用途 | 示例 |
|------|------|------| |------|------|------|
| `idapro_idalib_list()` | 列出所有 session | — | | `idapro_idb_list()` / HTTP `idb_list` | 列出所有 session | — |
| `idapro_idalib_current()` | 当前绑定的 session | — | | `idapro_idb_open()` / HTTP `idb_open` | 打开数据库(优先用 `open.ps1`) | 大文件走脚本 |
| `idapro_idalib_switch(session_id)` | 切换 session | 多文件对比时 | | `idapro_idb_save(path)` / HTTP `idb_save` | 保存数据库 | 保存分析进度 |
| `idapro_idalib_close(session_id)` | 关闭 session | 释放资源 | | `idapro_idb_current()` | 当前绑定的 session(若版本提供) | — |
| `idapro_idalib_save(path)` | 保存数据库 | 保存分析进度 | | `idapro_idb_switch(session_id)` | 切换 session | 多文件对比时 |
| `idapro_idalib_health(session_id)` | 检查 worker 状态 | 排查卡死 | | `idapro_idb_close(session_id)` | 关闭 session | 释放资源 |
| `idapro_server_health()` | 服务器健康检查 | — | | `idapro_server_health()` | 服务器健康检查 | — |
| `idapro_server_warmup()` | 预热子系统 | 首次使用前 | | `idapro_server_warmup()` | 预热子系统 | 首次使用前 |
@@ -508,7 +508,7 @@ idapro_py_eval(code="import ida_funcs; f=ida_funcs.get_func(0x401000); print(f.s
|------|------|------| |------|------|------|
| "No database bound" | 没有打开文件 | 执行 `open.ps1` | | "No database bound" | 没有打开文件 | 执行 `open.ps1` |
| "Failed to open database" | 旧库被锁 | `open.ps1` 自动降级到 Temp | | "Failed to open database" | 旧库被锁 | `open.ps1` 自动降级到 Temp |
| schema 校验失败 | MCP 客户端 BUG | 用 `open.ps1` 代替 `idalib_open` | | schema 校验失败 | MCP 客户端 BUG | 用 `open.ps1` 代替 `idb_open` |
| 工具超时 | 大文件分析中 | 加 `-TimeoutSeconds 600` | | 工具超时 | 大文件分析中 | 加 `-TimeoutSeconds 600` |
| "ERR:timeout" (start.ps1) | 服务器启动失败 | 检查 Python/idalib-mcp 安装 | | "ERR:timeout" (start.ps1) | 服务器启动失败 | 检查 Python/idalib-mcp 安装 |
| 进制转换错误 | 手动计算出错 | 用 `idapro_int_convert` | | 进制转换错误 | 手动计算出错 | 用 `idapro_int_convert` |
@@ -0,0 +1,77 @@
<#
.SYNOPSIS
Register a keep-alive scheduled task for IDA Pro MCP HTTP.
.DESCRIPTION
Replaces the old logon-only reverse-skill-ida-mcp task with:
- At logon + 30s delay
- Once-from-now trigger repeating every 1 minute for 3650 days
(this host cannot write -Daily.Repetition)
- Restart the task up to 3 times if the script itself fails
- Action runs watchdog.ps1 (reuse if healthy, start only if down;
never kill ida.exe when the GUI plugin owns 13337)
Usage:
powershell -File install-autostart.ps1
powershell -File install-autostart.ps1 -Unregister
#>
param(
[switch]$Unregister
)
$ErrorActionPreference = 'Stop'
$taskName = 'reverse-skill-ida-mcp'
$watchdog = Join-Path $PSScriptRoot 'watchdog.ps1'
if (-not (Test-Path -LiteralPath $watchdog)) {
Write-Output "ERR:missing $watchdog"
exit 1
}
if ($Unregister) {
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($existing) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
Write-Output 'OK:unregistered'
exit 0
}
$arg = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$watchdog`""
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arg -WorkingDirectory $PSScriptRoot
$logon = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
$logon.Delay = 'PT30S'
# -Daily.Repetition is not writable on this host; -Once + long duration is.
$repeat = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Minutes 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
-MultipleInstances IgnoreNew `
-RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 1)
$settings.Hidden = $true
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Trigger @($logon, $repeat) `
-Settings $settings `
-Principal $principal `
-Force `
-Description 'Keep IDA Pro MCP HTTP (127.0.0.1:13337) alive. Reuses a healthy server; starts idalib_supervisor only when down.' `
| Out-Null
Start-ScheduledTask -TaskName $taskName
Write-Output "OK:registered:$taskName"
Write-Output 'INFO:triggers=AtLogOn+30s, every 1 min'
Write-Output ("INFO:watchdog={0}" -f $watchdog)
+214 -67
View File
@@ -1,14 +1,17 @@
<# <#
.SYNOPSIS .SYNOPSIS
Open binary file via IDA HTTP API (bypass MCP schema issue) Open binary file via IDA HTTP API (bypass MCP schema issue)
.PARAMETER Path .PARAMETER Path
Binary file path (required) Binary file path (required)
.PARAMETER SessionId .PARAMETER SessionId
Session ID (optional, auto-generated) Preferred session ID (optional, auto-generated)
.PARAMETER NoAutoAnalysis .PARAMETER NoAutoAnalysis
Skip automatic analysis (faster open for large files) Skip automatic analysis (faster open for large files)
.PARAMETER TimeoutSeconds .PARAMETER TimeoutSeconds
Open timeout in seconds, returns timeout instead of blocking forever Open timeout in seconds, returns timeout instead of blocking forever
.PARAMETER Port
MCP HTTP port (default 13337)
#> #>
param( param(
@@ -16,39 +19,89 @@ param(
[string]$Path, [string]$Path,
[string]$SessionId = "", [string]$SessionId = "",
[switch]$NoAutoAnalysis = $false, [switch]$NoAutoAnalysis = $false,
[int]$TimeoutSeconds = 120 [int]$TimeoutSeconds = 120,
[int]$Port = 13337
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
if (-not [string]::IsNullOrWhiteSpace($env:IDADIR)) { function Test-IdaInstallDir {
$env:IDADIR = $env:IDADIR param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
if (-not (Test-Path -LiteralPath $Path)) { return $false }
return (Test-Path -LiteralPath (Join-Path $Path 'ida.exe')) -or (Test-Path -LiteralPath (Join-Path $Path 'ida.dll'))
}
if (-not [string]::IsNullOrWhiteSpace($env:IDADIR) -and (Test-IdaInstallDir -Path $env:IDADIR)) {
$env:IDADIR = [System.IO.Path]::GetFullPath($env:IDADIR)
} }
else { else {
# Fallback: check common IDA installation paths $persisted = [Environment]::GetEnvironmentVariable('IDADIR', 'User')
if ([string]::IsNullOrWhiteSpace($persisted)) {
$persisted = [Environment]::GetEnvironmentVariable('IDADIR', 'Machine')
}
if (-not [string]::IsNullOrWhiteSpace($persisted) -and (Test-IdaInstallDir -Path $persisted)) {
$env:IDADIR = [System.IO.Path]::GetFullPath($persisted)
}
else {
$desktop = [Environment]::GetFolderPath('Desktop')
$idaCandidates = @( $idaCandidates = @(
(Join-Path $desktop 'IDA Pro 9.4\App\IDA Pro'),
(Join-Path $env:USERPROFILE 'Desktop\IDA Pro 9.4\App\IDA Pro'),
(Join-Path $env:USERPROFILE 'Tools\IDA Pro 9.4\App\IDA Pro'),
(Join-Path $env: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 Pro',
'C:\IDA Pro', 'C:\IDA Pro',
'D:\IDA', 'D:\IDA',
(Join-Path $env:USERPROFILE 'Tools\IDA') 'D:\Tools\IDA Pro 9.4\App\IDA Pro'
) )
$foundIda = $idaCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 $foundIda = $idaCandidates | Where-Object { Test-IdaInstallDir -Path $_ } | Select-Object -First 1
if ($foundIda) { if ($foundIda) {
$env:IDADIR = $foundIda $env:IDADIR = [System.IO.Path]::GetFullPath($foundIda)
} else { } else {
Write-Error "ERR:IDADIR not set and IDA Pro not found at common paths. Set IDADIR environment variable to your IDA installation directory." Write-Error "ERR:IDADIR not set and IDA Pro not found at common paths. Set IDADIR environment variable to your IDA installation directory."
exit 1 exit 1
} }
}
} }
$Port = 13337
$TempDir = Join-Path $env:TEMP 'reverse-skill' # Prefer a short temp root to avoid MAX_PATH issues on Windows.
$TempDir = 'C:\rs-ida'
if (-not (Test-Path -LiteralPath $TempDir)) { if (-not (Test-Path -LiteralPath $TempDir)) {
try {
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
} catch {
$TempDir = Join-Path $env:TEMP 'rs-ida'
if (-not (Test-Path -LiteralPath $TempDir)) {
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
}
}
} }
$PollIntervalMs = 2000 $PollIntervalMs = 2000
$ProgressIntervalSeconds = 10 $ProgressIntervalSeconds = 10
function Invoke-IdaMcp {
param(
[string]$Method,
[hashtable]$Params = @{},
[int]$RequestPort,
[int]$TimeoutSec = 30
)
$payload = @{
jsonrpc = '2.0'
id = 1
method = $Method
params = $Params
} | ConvertTo-Json -Depth 12 -Compress
return Invoke-RestMethod "http://127.0.0.1:$RequestPort/mcp" -Method Post -Body $payload `
-ContentType 'application/json' -TimeoutSec $TimeoutSec -ErrorAction Stop
}
function Get-OpenReadySession { function Get-OpenReadySession {
param( param(
[string]$ExpectedSessionId, [string]$ExpectedSessionId,
@@ -56,20 +109,46 @@ function Get-OpenReadySession {
[int]$RequestPort [int]$RequestPort
) )
$listBody = '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"idalib_list","arguments":{}}}' $listResult = Invoke-IdaMcp -Method 'tools/call' -Params @{
$listResult = Invoke-RestMethod "http://127.0.0.1:$RequestPort/mcp" -Method Post -Body $listBody ` name = 'idb_list'
-ContentType "application/json" -TimeoutSec 10 -ErrorAction Stop arguments = @{}
} -RequestPort $RequestPort -TimeoutSec 10
$sessions = @($listResult.result.structuredContent.sessions) $content = $listResult.result.structuredContent
foreach ($candidate in $sessions) { if (-not $content) {
if (-not $candidate) { # Some MCP servers put content in result.content[0].text JSON
continue $text = $listResult.result.content | Where-Object { $_.type -eq 'text' } | Select-Object -First 1 -ExpandProperty text
if ($text) {
try { $content = $text | ConvertFrom-Json } catch { $content = $null }
}
} }
$sessions = @()
if ($content -and $content.sessions) {
$sessions = @($content.sessions)
}
foreach ($candidate in $sessions) {
if (-not $candidate) { continue }
$sameSession = $candidate.session_id -eq $ExpectedSessionId $sameSession = $candidate.session_id -eq $ExpectedSessionId
$samePath = $candidate.input_path -eq $ExpectedPath $samePath = $false
$sameFile = [System.IO.Path]::GetFileName($candidate.input_path) -eq [System.IO.Path]::GetFileName($ExpectedPath) if ($candidate.input_path) {
if (($sameSession -or $samePath -or $sameFile) -and $candidate.is_analyzing -eq $false) { try {
$samePath = [System.IO.Path]::GetFullPath([string]$candidate.input_path) -eq [System.IO.Path]::GetFullPath($ExpectedPath)
} catch {
$samePath = [string]$candidate.input_path -eq $ExpectedPath
}
}
$isAnalyzing = $false
if ($null -ne $candidate.PSObject.Properties['is_analyzing']) {
$isAnalyzing = [bool]$candidate.is_analyzing
}
$sid = [string]$candidate.session_id
if ([string]::IsNullOrWhiteSpace($sid)) { continue }
if (($sameSession -or $samePath) -and -not $isAnalyzing) {
return $candidate return $candidate
} }
} }
@@ -77,77 +156,92 @@ function Get-OpenReadySession {
return $null return $null
} }
if (-not (Test-Path $Path)) { if (-not (Test-Path -LiteralPath $Path)) {
Write-Output "ERR:file_not_found" Write-Output "ERR:file_not_found"
exit 1 exit 1
} }
# Normalize to absolute path
$Path = [System.IO.Path]::GetFullPath($Path)
if ($TimeoutSeconds -le 0) { if ($TimeoutSeconds -le 0) {
Write-Output "ERR:invalid_timeout" Write-Output "ERR:invalid_timeout"
exit 1 exit 1
} }
# 判断是否用了临时副本(避免递归复制) # Probe server first
try {
$null = Invoke-IdaMcp -Method 'tools/list' -Params @{} -RequestPort $Port -TimeoutSec 5
} catch {
Write-Output "ERR:server_not_ready:$($_.Exception.Message)"
Write-Output "HINT: run scripts/start.ps1 first"
exit 1
}
# System32 / locked DB -> temp copy
$isTempCopy = $Path.StartsWith($TempDir, [StringComparison]::OrdinalIgnoreCase) $isTempCopy = $Path.StartsWith($TempDir, [StringComparison]::OrdinalIgnoreCase)
# System32 文件自动复制到 Temp
if (-not $isTempCopy -and $Path -match "C:\\Windows\\System32") { if (-not $isTempCopy -and $Path -match "C:\\Windows\\System32") {
$Filename = [System.IO.Path]::GetFileName($Path) $Filename = [System.IO.Path]::GetFileName($Path)
$TempPath = "$TempDir\$Filename" $TempPath = Join-Path $TempDir $Filename
Copy-Item $Path $TempPath -Force -ErrorAction SilentlyContinue Copy-Item -LiteralPath $Path -Destination $TempPath -Force -ErrorAction SilentlyContinue
if ($?) { if ($?) {
$Path = $TempPath $Path = $TempPath
$isTempCopy = $true $isTempCopy = $true
} }
} }
# 清理同名旧数据库文件(只在非 Temp 副本时尝试)
if (-not $isTempCopy) { if (-not $isTempCopy) {
$dir = [System.IO.Path]::GetDirectoryName($Path) $dir = [System.IO.Path]::GetDirectoryName($Path)
$base = [System.IO.Path]::GetFileNameWithoutExtension($Path) $base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
$oldExts = @(".id0", ".id1", ".id2", ".nam", ".til", ".i64") $oldExts = @('.id0', '.id1', '.id2', '.nam', '.til', '.i64', '.idb')
$hasLocked = $false $hasLocked = $false
foreach ($ext in $oldExts) { foreach ($ext in $oldExts) {
$f = Join-Path $dir "$base$ext" $f = Join-Path $dir "$base$ext"
if (Test-Path $f) { if (Test-Path -LiteralPath $f) {
Remove-Item $f -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue
if (Test-Path $f) { $hasLocked = $true } if (Test-Path -LiteralPath $f) { $hasLocked = $true }
} }
} }
# 旧数据库文件被锁,自动用 Temp 副本
if ($hasLocked) { if ($hasLocked) {
$guid = [System.Guid]::NewGuid().ToString("N").Substring(0, 8) $guid = [System.Guid]::NewGuid().ToString('N').Substring(0, 8)
$newName = "$guid-$([System.IO.Path]::GetFileName($Path))" $newName = "$guid-$([System.IO.Path]::GetFileName($Path))"
$TempPath = "$TempDir\$newName" $TempPath = Join-Path $TempDir $newName
Copy-Item $Path $TempPath -Force Copy-Item -LiteralPath $Path -Destination $TempPath -Force
$Path = $TempPath $Path = $TempPath
$isTempCopy = $true $isTempCopy = $true
} }
} }
$autoAnalysis = if ($NoAutoAnalysis) { "false" } else { "true" } $autoAnalysis = -not $NoAutoAnalysis
$escapedPath = $Path -replace '\\', '\\'
# 始终使用明确的会话 ID,便于超时时轮询会话状态判断是否已成功打开
if (-not $SessionId) { if (-not $SessionId) {
$SessionId = [System.Guid]::NewGuid().ToString("N").Substring(0, 8) $SessionId = [System.Guid]::NewGuid().ToString('N').Substring(0, 8)
} }
$body = @" $arguments = @{
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"idalib_open","arguments":{"input_path":"$escapedPath","run_auto_analysis":$autoAnalysis,"session_id":null}}} input_path = $Path
"@ run_auto_analysis = $autoAnalysis
if ($SessionId) { preferred_session_id = $SessionId
$body = @" mode = 'prefer_headless'
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"idalib_open","arguments":{"input_path":"$escapedPath","run_auto_analysis":$autoAnalysis,"session_id":"$SessionId"}}}
"@
} }
$argJson = $arguments | ConvertTo-Json -Compress
# Keep boolean literals as JSON bools for background job
$bodyObj = @{
jsonrpc = '2.0'
id = 1
method = 'tools/call'
params = @{
name = 'idb_open'
arguments = $arguments
}
}
$body = $bodyObj | ConvertTo-Json -Depth 12 -Compress
# 将打开请求放到后台,避免 HTTP 长时间不回包时阻塞整个脚本
$openJob = Start-Job -ScriptBlock { $openJob = Start-Job -ScriptBlock {
param($RequestBody, $RequestPort) param($RequestBody, $RequestPort)
try { try {
Invoke-RestMethod "http://127.0.0.1:$RequestPort/mcp" -Method Post -Body $RequestBody ` Invoke-RestMethod "http://127.0.0.1:$RequestPort/mcp" -Method Post -Body $RequestBody `
-ContentType "application/json" -ErrorAction Stop -ContentType 'application/json' -TimeoutSec 0 -ErrorAction Stop
} catch { } catch {
$_.Exception.Message $_.Exception.Message
} }
@@ -168,10 +262,12 @@ try {
try { try {
$session = Get-OpenReadySession -ExpectedSessionId $SessionId -ExpectedPath $Path -RequestPort $Port $session = Get-OpenReadySession -ExpectedSessionId $SessionId -ExpectedPath $Path -RequestPort $Port
if ($session) { if ($session) {
$tag = if ($isTempCopy) { " (temp copy)" } else { "" } $tag = if ($isTempCopy) { ' (temp copy)' } else { '' }
Stop-Job -Job $openJob -ErrorAction SilentlyContinue Stop-Job -Job $openJob -ErrorAction SilentlyContinue
Remove-Job -Job $openJob -Force -ErrorAction SilentlyContinue Remove-Job -Job $openJob -Force -ErrorAction SilentlyContinue
Write-Output "OK:$($session.filename):$($session.session_id)$tag" $name = if ($session.filename) { $session.filename } else { [System.IO.Path]::GetFileName($Path) }
$sid = if ($session.session_id) { $session.session_id } else { $SessionId }
Write-Output "OK:${name}:${sid}${tag}"
exit 0 exit 0
} }
} catch {} } catch {}
@@ -193,8 +289,10 @@ try {
try { try {
$session = Get-OpenReadySession -ExpectedSessionId $SessionId -ExpectedPath $Path -RequestPort $Port $session = Get-OpenReadySession -ExpectedSessionId $SessionId -ExpectedPath $Path -RequestPort $Port
if ($session) { if ($session) {
$tag = if ($isTempCopy) { " (temp copy)" } else { "" } $tag = if ($isTempCopy) { ' (temp copy)' } else { '' }
Write-Output "OK:$($session.filename):$($session.session_id)$tag" $name = if ($session.filename) { $session.filename } else { [System.IO.Path]::GetFileName($Path) }
$sid = if ($session.session_id) { $session.session_id } else { $SessionId }
Write-Output "OK:${name}:${sid}${tag}"
exit 0 exit 0
} }
} catch {} } catch {}
@@ -211,22 +309,71 @@ try {
exit 1 exit 1
} }
if ($jobResult.result.structuredContent.success -eq $true) { $structured = $jobResult.result.structuredContent
$session = $jobResult.result.structuredContent.session if (-not $structured) {
$tag = if ($isTempCopy) { " (temp copy)" } else { "" } $text = $jobResult.result.content | Where-Object { $_.type -eq 'text' } | Select-Object -First 1 -ExpandProperty text
Write-Output "OK:$($session.filename):$($session.session_id)$tag" if ($text) {
} else { try { $structured = $text | ConvertFrom-Json } catch { $structured = $null }
# 自动降级:非 Temp 副本失败时,复制到 Temp 重试
if (-not $isTempCopy) {
$guid = [System.Guid]::NewGuid().ToString("N").Substring(0, 8)
$newName = "$guid-$([System.IO.Path]::GetFileName($Path))"
$TempPath = "$TempDir\$newName"
Copy-Item $Path $TempPath -Force
& $PSCommandPath -Path $TempPath -SessionId $SessionId -NoAutoAnalysis:$NoAutoAnalysis -TimeoutSeconds $TimeoutSeconds
} else {
Write-Output "ERR:$($jobResult.result.structuredContent.error)"
} }
} }
$success = $false
$session = $null
$errMsg = $null
if ($structured) {
if ($null -ne $structured.PSObject.Properties['success'] -and $structured.success -eq $true) {
$success = $true
$session = $structured.session
}
if ($null -ne $structured.PSObject.Properties['error'] -and $structured.error) {
$errMsg = [string]$structured.error
}
if ($null -ne $structured.PSObject.Properties['session'] -and $structured.session -and -not $success) {
# Some builds return session without success flag
$success = $true
$session = $structured.session
}
}
if ($success -and $session) {
$tag = if ($isTempCopy) { ' (temp copy)' } else { '' }
$name = if ($session.filename) { $session.filename } else { [System.IO.Path]::GetFileName($Path) }
$sid = if ($session.session_id) { $session.session_id } else { $SessionId }
Write-Output "OK:${name}:${sid}${tag}"
exit 0
}
# Auto-fallback: temp copy retry (skip if license / idalib hard failure)
$hardFail = $false
if ($errMsg -and ($errMsg -match 'license|EULA|Cannot continue without a valid license|batch mode')) {
$hardFail = $true
}
if (-not $isTempCopy -and -not $hardFail) {
try {
$guid = [System.Guid]::NewGuid().ToString('N').Substring(0, 8)
$baseName = [System.IO.Path]::GetFileName($Path)
if ($baseName.Length -gt 40) { $baseName = $baseName.Substring(0, 40) }
$newName = "$guid-$baseName"
$TempPath = Join-Path $TempDir $newName
Copy-Item -LiteralPath $Path -Destination $TempPath -Force
& $PSCommandPath -Path $TempPath -SessionId $SessionId -NoAutoAnalysis:$NoAutoAnalysis -TimeoutSeconds $TimeoutSeconds -Port $Port
exit $LASTEXITCODE
} catch {
Write-Output "ERR:temp_copy_failed:$($_.Exception.Message)"
if ($errMsg) { Write-Output "ERR:open:$errMsg" }
exit 1
}
}
if ($hardFail) {
Write-Output "ERR:idalib_license:$errMsg"
Write-Output "HINT: This machine's hexlic cannot open databases in headless idalib mode. Use GUI mode: Launch-IDA-Pro.cmd, open the binary, then use idapro MCP tools against the GUI plugin on port 13337. See LOCAL-SETUP.md."
exit 1
}
$err = if ($errMsg) { $errMsg } elseif ($structured) { ($structured | ConvertTo-Json -Compress) } else { 'open_failed' }
Write-Output "ERR:$err"
exit 1
} finally { } finally {
if ($openJob) { if ($openJob) {
Stop-Job -Job $openJob -ErrorAction SilentlyContinue Stop-Job -Job $openJob -ErrorAction SilentlyContinue
@@ -0,0 +1,46 @@
"""Hidden idalib supervisor launcher with file logging.
Started via pythonw so no console window appears. stdout/stderr and the
logging module both go to %LOCALAPPDATA%\\reverse-skill\\ida-mcp\\supervisor.log.
"""
from __future__ import annotations
import os
import sys
from datetime import datetime
from pathlib import Path
def _log_path() -> Path:
root = Path(os.environ.get("LOCALAPPDATA") or ".") / "reverse-skill" / "ida-mcp"
root.mkdir(parents=True, exist_ok=True)
return root / "supervisor.log"
def _rotate(path: Path, max_bytes: int = 5 * 1024 * 1024) -> None:
if path.exists() and path.stat().st_size > max_bytes:
bak = path.with_name(path.name + ".1")
if bak.exists():
bak.unlink()
path.replace(bak)
def main() -> None:
log_path = _log_path()
_rotate(log_path)
log_fp = open(log_path, "a", encoding="utf-8", buffering=1, errors="replace")
sys.stdout = log_fp
sys.stderr = log_fp
print(
f"==== start {datetime.now():%Y-%m-%d %H:%M:%S} pid={os.getpid()} ====",
flush=True,
)
from ida_pro_mcp.idalib_supervisor import main as supervisor_main
supervisor_main()
if __name__ == "__main__":
main()
+105
View File
@@ -0,0 +1,105 @@
<#
.SYNOPSIS
Launch portable IDA Pro GUI (with MCP plugin). Prefer this when headless idalib license fails.
.DESCRIPTION
1. Resolve IDADIR
2. Optionally open a binary path
3. Start IDA GUI so the ida_mcp plugin can autostart HTTP on 127.0.0.1:13337
Usage:
powershell -File start-gui.ps1
powershell -File start-gui.ps1 -Path C:\target.exe
#>
param(
[string]$Path,
[string]$IdaDir,
[switch]$UsePortableLauncher
)
$ErrorActionPreference = 'Stop'
function Test-IdaInstallDir {
param([string]$Candidate)
if ([string]::IsNullOrWhiteSpace($Candidate)) { return $false }
if (-not (Test-Path -LiteralPath $Candidate)) { return $false }
$idaExe = Join-Path $Candidate 'ida.exe'
$idaDll = Join-Path $Candidate 'ida.dll'
return (Test-Path -LiteralPath $idaExe) -or (Test-Path -LiteralPath $idaDll)
}
if ([string]::IsNullOrWhiteSpace($IdaDir)) {
if (-not [string]::IsNullOrWhiteSpace($env:IDADIR) -and (Test-IdaInstallDir $env:IDADIR)) {
$IdaDir = $env:IDADIR
} else {
$persisted = [Environment]::GetEnvironmentVariable('IDADIR', 'User')
if (Test-IdaInstallDir $persisted) {
$IdaDir = $persisted
} else {
$candidates = @(
'C:\Program Files\IDA Professional 9.4',
'C:\Program Files\IDA Pro 9.4',
'C:\Program Files\IDA Pro',
(Join-Path $env:USERPROFILE 'Tools\IDAPro94'),
(Join-Path $env:USERPROFILE 'Tools\IDA Pro 9.4\App\IDA Pro'),
(Join-Path ([Environment]::GetFolderPath('Desktop')) 'IDA Pro 9.4\App\IDA Pro'),
(Join-Path $env:USERPROFILE 'Desktop\IDA Pro 9.4\App\IDA Pro')
)
$IdaDir = $candidates | Where-Object { Test-IdaInstallDir $_ } | Select-Object -First 1
}
}
}
if (-not (Test-IdaInstallDir $IdaDir)) {
Write-Output 'ERR:IDADIR_not_found'
exit 1
}
$env:IDADIR = [System.IO.Path]::GetFullPath($IdaDir)
$portableRoot = Split-Path (Split-Path $env:IDADIR -Parent) -Parent
# Desktop\IDA Pro 9.4\App\IDA Pro -> Desktop\IDA Pro 9.4
if ((Split-Path $env:IDADIR -Leaf) -eq 'IDA Pro') {
$maybe = Split-Path (Split-Path $env:IDADIR -Parent) -Parent
if (Test-Path (Join-Path $maybe 'Launch-IDA-Pro.cmd')) {
$portableRoot = $maybe
}
}
Write-Output "INFO:IDADIR=$env:IDADIR"
if ($UsePortableLauncher -or (Test-Path (Join-Path $portableRoot 'Launch-IDA-Pro.cmd'))) {
$launcher = Join-Path $portableRoot 'Launch-IDA-Pro.cmd'
if (Test-Path -LiteralPath $launcher) {
Write-Output "INFO:launcher=$launcher"
if (-not [string]::IsNullOrWhiteSpace($Path)) {
if (-not (Test-Path -LiteralPath $Path)) {
Write-Output 'ERR:file_not_found'
exit 1
}
# Portable launcher starts IDA; then user/file can be passed to ida.exe directly instead
$target = [System.IO.Path]::GetFullPath($Path)
Start-Process -FilePath (Join-Path $env:IDADIR 'ida.exe') -ArgumentList @('"' + $target + '"') -WorkingDirectory $env:IDADIR
} else {
Start-Process -FilePath $launcher -WorkingDirectory $portableRoot
}
Write-Output 'OK:gui_started'
Write-Output 'HINT: Open a binary in IDA, confirm Output shows [MCP] port=13337, then use idapro MCP tools.'
exit 0
}
}
$idaExe = Join-Path $env:IDADIR 'ida.exe'
if (-not [string]::IsNullOrWhiteSpace($Path)) {
if (-not (Test-Path -LiteralPath $Path)) {
Write-Output 'ERR:file_not_found'
exit 1
}
$target = [System.IO.Path]::GetFullPath($Path)
Start-Process -FilePath $idaExe -ArgumentList @('"' + $target + '"') -WorkingDirectory $env:IDADIR
} else {
Start-Process -FilePath $idaExe -WorkingDirectory $env:IDADIR
}
Write-Output 'OK:gui_started'
Write-Output 'HINT: Open a binary in IDA, confirm Output shows [MCP] port=13337, then use idapro MCP tools.'
+423 -87
View File
@@ -3,10 +3,12 @@
Start IDA Pro MCP HTTP server (background, non-blocking) Start IDA Pro MCP HTTP server (background, non-blocking)
.DESCRIPTION .DESCRIPTION
1. Kill old process 1. Resolve IDADIR (env / portable IDA / registry / common paths)
2. Start idalib-mcp HTTP server in hidden window mode 2. Resolve idalib-mcp (PATH, IDA Python314 Scripts, or python -m)
3. Wait for service ready (max 15 seconds) 3. If HTTP already healthy, reuse it (unless -Force)
4. Output result 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 Usage: run without parameters
#> #>
@@ -14,9 +16,164 @@ Usage: run without parameters
param( param(
[string]$IdaDir, [string]$IdaDir,
[int]$Port = 13337, [int]$Port = 13337,
[string]$ServerPath [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 { function Get-InstalledIdaDir {
$registryPaths = @( $registryPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
@@ -31,7 +188,7 @@ function Get-InstalledIdaDir {
Where-Object { Where-Object {
($_.DisplayName -match 'IDA|Hex-Rays') -and ($_.DisplayName -match 'IDA|Hex-Rays') -and
-not [string]::IsNullOrWhiteSpace($_.InstallLocation) -and -not [string]::IsNullOrWhiteSpace($_.InstallLocation) -and
(Test-Path -LiteralPath $_.InstallLocation) (Test-IdaInstallDir -Path $_.InstallLocation)
} | } |
Select-Object -ExpandProperty InstallLocation -First 1 Select-Object -ExpandProperty InstallLocation -First 1
@@ -39,54 +196,129 @@ function Get-InstalledIdaDir {
return $fromRegistry return $fromRegistry
} }
$idaCandidates = @( return Get-PortableIdaCandidates | Where-Object { Test-IdaInstallDir -Path $_ } | Select-Object -First 1
'C:\Program Files\IDA Pro',
'C:\Program Files\IDA',
'C:\IDA Pro',
'C:\IDA',
'D:\IDA',
'E:\Program Files\IDA',
(Join-Path $env:USERPROFILE 'Tools\IDA')
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
return $idaCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
} }
if ([string]::IsNullOrWhiteSpace($IdaDir)) { function Resolve-IdaDir {
if (-not [string]::IsNullOrWhiteSpace($env:IDADIR)) { param([string]$Preferred)
$IdaDir = $env:IDADIR
} else { if (-not [string]::IsNullOrWhiteSpace($Preferred) -and (Test-IdaInstallDir -Path $Preferred)) {
$persistedIdaDir = [Environment]::GetEnvironmentVariable('IDADIR', 'User') return [System.IO.Path]::GetFullPath($Preferred)
if ([string]::IsNullOrWhiteSpace($persistedIdaDir)) {
$persistedIdaDir = [Environment]::GetEnvironmentVariable('IDADIR', 'Machine')
} }
if (-not [string]::IsNullOrWhiteSpace($persistedIdaDir) -and (Test-Path -LiteralPath $persistedIdaDir)) { if (-not [string]::IsNullOrWhiteSpace($env:IDADIR) -and (Test-IdaInstallDir -Path $env:IDADIR)) {
$IdaDir = $persistedIdaDir return [System.IO.Path]::GetFullPath($env:IDADIR)
} else { }
# Search registry install records and common fallback paths
$foundIda = Get-InstalledIdaDir foreach ($scope in @('User', 'Machine')) {
if ($foundIda) { $persisted = [Environment]::GetEnvironmentVariable('IDADIR', $scope)
$IdaDir = $foundIda if (-not [string]::IsNullOrWhiteSpace($persisted) -and (Test-IdaInstallDir -Path $persisted)) {
} else { return [System.IO.Path]::GetFullPath($persisted)
Write-Output "ERR:IDADIR not set and IDA Pro not found. Set IDADIR environment variable."
exit 1
} }
} }
$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
} }
} }
$env:IDADIR = $IdaDir
if ([string]::IsNullOrWhiteSpace($ServerPath)) { function Get-WindowlessPythonPath {
# Try both possible executable names (idalib-mcp is the HTTP server, ida-pro-mcp is the installer CLI) param([string]$PythonExe)
$resolved = Get-Command idalib-mcp -ErrorAction SilentlyContinue if ([string]::IsNullOrWhiteSpace($PythonExe)) { return $PythonExe }
if (-not $resolved) { if ($PythonExe -match '(?i)pythonw\.exe$') { return $PythonExe }
$resolved = Get-Command ida-pro-mcp -ErrorAction SilentlyContinue 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) { if ($resolved) {
$ServerPath = $resolved.Source return @{ Mode = 'exe'; Path = $resolved.Source }
} }
else { }
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' $roamingPython = Join-Path $env:APPDATA 'Python'
if (Test-Path -LiteralPath $roamingPython) { if (Test-Path -LiteralPath $roamingPython) {
$candidate = Get-ChildItem -LiteralPath $roamingPython -Directory -ErrorAction SilentlyContinue | $candidate = Get-ChildItem -LiteralPath $roamingPython -Directory -ErrorAction SilentlyContinue |
@@ -97,75 +329,179 @@ if ([string]::IsNullOrWhiteSpace($ServerPath)) {
Where-Object { Test-Path -LiteralPath $_ } | Where-Object { Test-Path -LiteralPath $_ } |
Select-Object -First 1 Select-Object -First 1
if ($candidate) { if ($candidate) {
$ServerPath = $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
} }
} }
# Auto-bootstrap if still not found $resolvedIdaDir = Resolve-IdaDir -Preferred $IdaDir
if ([string]::IsNullOrWhiteSpace($ServerPath)) { 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' $bootstrapScript = Join-Path $PSScriptRoot '..\..\scripts\bootstrap-reverse.ps1'
if (Test-Path -LiteralPath $bootstrapScript) { if (Test-Path -LiteralPath $bootstrapScript) {
Write-Output "INFO: ida-pro-mcp not found, attempting auto-bootstrap (installing mrexodia/ida-pro-mcp)..." 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 & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrapScript -Capability @('idalib-mcp') -SkipRefresh
$resolved = Get-Command ida-pro-mcp -ErrorAction SilentlyContinue $server = Find-IdalibServer -IdaDirPath $IdaDir -PreferredServerPath $ServerPath
if (-not $resolved) {
$resolved = Get-Command idalib-mcp -ErrorAction SilentlyContinue
}
if ($resolved) {
$ServerPath = $resolved.Source
}
else {
$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'
@('ida-pro-mcp.exe', 'idalib-mcp.exe') | ForEach-Object { Join-Path $scripts $_ }
} |
Where-Object { Test-Path -LiteralPath $_ } |
Select-Object -First 1
if ($candidate) {
$ServerPath = $candidate
}
}
}
} }
} }
if ([string]::IsNullOrWhiteSpace($ServerPath)) { if (-not $server) {
throw 'Missing required CLI tool: ida-pro-mcp — auto-bootstrap failed. Install manually: pip install git+https://github.com/mrexodia/ida-pro-mcp.git && ida-pro-mcp --install' 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
} }
# 清理旧进程(杀进程树,包括 worker 子进程) # Replace only when down or stale (no py_eval). Busy/gui already exited above
$old = Get-Process -Name "ida-pro-mcp" -ErrorAction SilentlyContinue # unless -Force. Never taskkill ida.exe; never use /T.
if (-not $old) { $old = Get-Process -Name "idalib-mcp" -ErrorAction SilentlyContinue } foreach ($procName in @('ida-pro-mcp', 'idalib-mcp', 'idalib_supervisor')) {
if ($old) { $old = Get-Process -Name $procName -ErrorAction SilentlyContinue
if ($old) {
foreach ($process in @($old)) { foreach ($process in @($old)) {
& taskkill.exe /F /T /PID $process.Id 2>$null | Out-Null Stop-IdaMcpManagedProcess -ProcessId $process.Id
} }
Start-Sleep 2 }
}
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
Start-Process -WindowStyle Hidden -FilePath $ServerPath -ArgumentList "--host 127.0.0.1 --port $Port" $logFile = Join-Path $logDir 'supervisor.log'
Rotate-IdaMcpLog -Path $logFile
# 等待就绪 Write-Output "INFO:IDADIR=$IdaDir"
$ready = $false Write-Output "INFO:server=$filePath $($argList -join ' ')"
for ($i = 0; $i -lt 15; $i++) { Write-Output "INFO:log=$logFile"
Start-Sleep -Seconds 1 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 { try {
$r = Invoke-RestMethod "http://127.0.0.1:$Port/mcp" -Method Post ` $psi = New-Object System.Diagnostics.ProcessStartInfo
-Body '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' ` if ($useCmd) {
-ContentType "application/json" -ErrorAction Stop $psi.FileName = 'cmd.exe'
if ($r.result.tools.Count -gt 0) { $psi.Arguments = '/d /c "' + $filePath + '" ' + ($quotedArgs -join ' ')
Write-Output "OK:$($r.result.tools.Count)" } 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 $ready = $true
break break
} }
} catch {}
} }
if (-not $ready) { if (-not $ready) {
Write-Output "ERR:timeout" Write-Output "ERR:timeout"
Write-Output "HINT:check $logFile"
exit 1
} }
+96
View File
@@ -0,0 +1,96 @@
<#
.SYNOPSIS
Ensure IDA Pro MCP HTTP is healthy; start it only when down.
.DESCRIPTION
One-shot health check used by the scheduled task. Safe to run every minute:
a live server is reused, a dead listener is replaced via start.ps1.
Usage:
powershell -File watchdog.ps1
#>
param(
[int]$Port = 13337
)
$ErrorActionPreference = 'Stop'
$logDir = Join-Path $env:LOCALAPPDATA 'reverse-skill\ida-mcp'
if (-not (Test-Path -LiteralPath $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
$logFile = Join-Path $logDir 'watchdog.log'
if ((Test-Path -LiteralPath $logFile) -and ((Get-Item -LiteralPath $logFile).Length -gt 2MB)) {
$bak = "$logFile.1"
if (Test-Path -LiteralPath $bak) { Remove-Item -LiteralPath $bak -Force }
Move-Item -LiteralPath $logFile -Destination $bak -Force
}
function Write-WatchLog {
param([string]$Message)
$line = '{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
Write-Output $Message
}
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 Probe-IdaMcp {
param([int]$Port)
$owners = @(Get-IdaMcpPortOwners -Port $Port)
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 })
if ($count -gt 0 -and ($names -contains 'py_eval')) {
return @{ Status = 'healthy'; Count = $count }
}
if ($count -gt 0) {
return @{ Status = 'stale'; Count = $count }
}
} catch {}
# Listen + RPC timeout = busy (single-threaded supervisor during idb_open).
# Only "nothing listening" or a quick stale (no py_eval) list is down.
if ($owners.Count -gt 0) {
return @{ Status = 'busy'; Count = 0 }
}
return @{ Status = 'down'; Count = 0 }
}
$probe = Probe-IdaMcp -Port $Port
if ($probe.Status -eq 'healthy') {
Write-WatchLog "OK:$($probe.Count)`:reuse"
exit 0
}
if ($probe.Status -eq 'busy') {
Write-WatchLog 'OK:busy:reuse'
exit 0
}
Write-WatchLog ("INFO:{0}, calling start.ps1" -f $probe.Status)
$startScript = Join-Path $PSScriptRoot 'start.ps1'
$startOutput = & $startScript -Port $Port
foreach ($line in @($startOutput)) {
Write-WatchLog "START:$line"
}
$last = (@($startOutput) | Where-Object { $_ -match '^(OK|ERR|WARN):' } | Select-Object -Last 1)
if ($last -match '^(OK:|WARN:gui_busy|WARN:busy)') {
exit 0
}
exit 1
+49
View File
@@ -105,6 +105,55 @@ function Get-ReverseToolCatalog {
[pscustomobject]@{ Type = 'command'; Value = 'zipalign' } [pscustomobject]@{ Type = 'command'; Value = 'zipalign' }
) )
} }
[pscustomobject]@{
Name = 'idalib-mcp'
Skill = 'ida-reverse'
Purpose = 'IDA Pro idalib MCP HTTP/stdio 服务器'
FixedVersion = 'v0.5.0'
VersionArgs = @('--help')
Fallbacks = @(
[pscustomobject]@{ Type = 'command'; Value = 'idalib-mcp' },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\bin\idalib-mcp.cmd') },
[pscustomobject]@{ Type = 'path'; Value = (Join-ReverseOptionalPath -Path $localAppData -ChildPath 'Python\pythoncore-3.14-64\Scripts\idalib-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-ReverseOptionalPath -Path $localAppData -ChildPath 'Programs\Python\Python314\Scripts\idalib-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-ReverseOptionalPath -Path $localAppData -ChildPath 'Programs\Python\Python312\Scripts\idalib-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Desktop\IDA Pro 9.4\App\IDA Pro\Python314\Scripts\idalib-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path ([Environment]::GetFolderPath('Desktop')) 'IDA Pro 9.4\App\IDA Pro\Python314\Scripts\idalib-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\IDA Pro 9.4\App\IDA Pro\Python314\Scripts\idalib-mcp.exe') }
)
}
[pscustomobject]@{
Name = 'ida-pro-mcp'
Skill = 'ida-reverse'
Purpose = 'IDA Pro MCP CLI / 插件安装器'
FixedVersion = 'v0.5.0'
VersionArgs = @('--help')
Fallbacks = @(
[pscustomobject]@{ Type = 'command'; Value = 'ida-pro-mcp' },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\bin\ida-pro-mcp.cmd') },
[pscustomobject]@{ Type = 'path'; Value = (Join-ReverseOptionalPath -Path $localAppData -ChildPath 'Python\pythoncore-3.14-64\Scripts\ida-pro-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-ReverseOptionalPath -Path $localAppData -ChildPath 'Programs\Python\Python314\Scripts\ida-pro-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-ReverseOptionalPath -Path $localAppData -ChildPath 'Programs\Python\Python312\Scripts\ida-pro-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Desktop\IDA Pro 9.4\App\IDA Pro\Python314\Scripts\ida-pro-mcp.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path ([Environment]::GetFolderPath('Desktop')) 'IDA Pro 9.4\App\IDA Pro\Python314\Scripts\ida-pro-mcp.exe') }
)
}
[pscustomobject]@{
Name = 'ida'
Skill = 'ida-reverse'
Purpose = 'IDA Pro 主程序'
FixedVersion = 'v0.5.0'
VersionArgs = @()
Fallbacks = @(
[pscustomobject]@{ Type = 'command'; Value = 'ida' },
[pscustomobject]@{ Type = 'path'; Value = 'C:\Program Files\IDA Professional 9.4\ida.exe' },
[pscustomobject]@{ Type = 'path'; Value = 'C:\Program Files\IDA Pro 9.4\ida.exe' },
[pscustomobject]@{ Type = 'path'; Value = 'C:\Program Files\IDA Pro\ida.exe' },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Desktop\IDA Pro 9.4\App\IDA Pro\ida.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path ([Environment]::GetFolderPath('Desktop')) 'IDA Pro 9.4\App\IDA Pro\ida.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\IDA Pro 9.4\App\IDA Pro\ida.exe') }
)
}
[pscustomobject]@{ [pscustomobject]@{
Name = 'frida' Name = 'frida'
Skill = 'apk-reverse' Skill = 'apk-reverse'
+29
View File
@@ -99,6 +99,35 @@ foreach ($name in $scripts) {
[void]$parseLog.Add("OK $name") [void]$parseLog.Add("OK $name")
} }
} }
$idaScripts = @(
'ida-reverse\scripts\start.ps1',
'ida-reverse\scripts\open.ps1',
'ida-reverse\scripts\watchdog.ps1',
'ida-reverse\scripts\install-autostart.ps1',
'ida-reverse\scripts\start-gui.ps1'
)
foreach ($rel in $idaScripts) {
$p = Join-Path $skillsRoot $rel
$name = $rel
if (-not (Test-Path -LiteralPath $p)) {
Bad ("script missing: {0}" -f $name)
$parseFail++
[void]$parseLog.Add("MISSING $name")
continue
}
$errs = $null
$tokens = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile($p, [ref]$tokens, [ref]$errs)
if ($errs -and $errs.Count -gt 0) {
Bad ("parse fail {0}: {1}" -f $name, $errs[0].Message)
$parseFail++
[void]$parseLog.Add("FAIL $name $($errs[0].Message)")
} else {
Ok ("parse {0}" -f $name)
$parseOk++
[void]$parseLog.Add("OK $name")
}
}
$parseLog -join [Environment]::NewLine | Set-Content (Join-Path $LogDir '02-parse.txt') -Encoding UTF8 $parseLog -join [Environment]::NewLine | Set-Content (Join-Path $LogDir '02-parse.txt') -Encoding UTF8
# --- 3) master-route sample matrix --- # --- 3) master-route sample matrix ---