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:
@@ -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,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.
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
@@ -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)) {
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
|
||||||
|
} catch {
|
||||||
|
$TempDir = Join-Path $env:TEMP 'rs-ida'
|
||||||
if (-not (Test-Path -LiteralPath $TempDir)) {
|
if (-not (Test-Path -LiteralPath $TempDir)) {
|
||||||
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
|
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()
|
||||||
@@ -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.'
|
||||||
@@ -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
|
|
||||||
if ($foundIda) {
|
|
||||||
$IdaDir = $foundIda
|
|
||||||
} else {
|
|
||||||
Write-Output "ERR:IDADIR not set and IDA Pro not found. Set IDADIR environment variable."
|
|
||||||
exit 1
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$env:IDADIR = $IdaDir
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($ServerPath)) {
|
foreach ($scope in @('User', 'Machine')) {
|
||||||
# Try both possible executable names (idalib-mcp is the HTTP server, ida-pro-mcp is the installer CLI)
|
$persisted = [Environment]::GetEnvironmentVariable('IDADIR', $scope)
|
||||||
$resolved = Get-Command idalib-mcp -ErrorAction SilentlyContinue
|
if (-not [string]::IsNullOrWhiteSpace($persisted) -and (Test-IdaInstallDir -Path $persisted)) {
|
||||||
if (-not $resolved) {
|
return [System.IO.Path]::GetFullPath($persisted)
|
||||||
$resolved = Get-Command ida-pro-mcp -ErrorAction SilentlyContinue
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) {
|
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 }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-bootstrap if still not found
|
return $null
|
||||||
if ([string]::IsNullOrWhiteSpace($ServerPath)) {
|
}
|
||||||
|
|
||||||
|
$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'
|
$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')) {
|
||||||
|
$old = Get-Process -Name $procName -ErrorAction SilentlyContinue
|
||||||
if ($old) {
|
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)) {
|
||||||
Start-Process -WindowStyle Hidden -FilePath $ServerPath -ArgumentList "--host 127.0.0.1 --port $Port"
|
Stop-IdaMcpManagedProcess -ProcessId $managedPid
|
||||||
|
}
|
||||||
# 等待就绪
|
|
||||||
$ready = $false
|
|
||||||
for ($i = 0; $i -lt 15; $i++) {
|
|
||||||
Start-Sleep -Seconds 1
|
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 {
|
try {
|
||||||
$r = Invoke-RestMethod "http://127.0.0.1:$Port/mcp" -Method Post `
|
$create = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
|
||||||
-Body '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' `
|
CommandLine = $commandLine
|
||||||
-ContentType "application/json" -ErrorAction Stop
|
CurrentDirectory = $IdaDir
|
||||||
if ($r.result.tools.Count -gt 0) {
|
}
|
||||||
Write-Output "OK:$($r.result.tools.Count)"
|
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
|
$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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|||||||
Reference in New Issue
Block a user