opencode-idea 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +141 -50
- package/package.json +3 -2
- package/src/ide-guidance.js +9 -4
- package/src/ide-run-config-skill.js +57 -0
- package/src/ide-run-config-skill.md +92 -0
- package/src/idea-mcp.js +4 -4
- package/src/plugin.js +263 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.6
|
|
4
|
+
|
|
5
|
+
- **修复:工具报错不再注销 IDE MCP**。旧实现在 `tool.execute.after` 里把**任何** `idea_*` 报错都当成「端点已死」并注销整个注册 —— 业务错误 (例如路径不存在返回 `File not found`) 和一次传输抖动都会触发;注销后紧跟的 `ctx.mcp.reload()` 还会掐断同一批正在执行的其他调用。现在插件**不注册**该钩子:连接状态以 OpenCode 自己维护的工具快照为准 (`context` 钩子收到的 `event.tools` 里有没有 `idea_*`),只有 `/open-in-idea` 会 (重新) 连接。
|
|
6
|
+
- **隐藏 31 个用不到的 IDEA 工具** (`HIDDEN_IDEA_TOOLS`):VCS、Router 派发工具、Debugger (全部 `idea_xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、Python Environment MCP、数据库建/改数据源、终端 `execute_terminal_command`。模型可见的 `idea_*` 从 64 降到 33。
|
|
7
|
+
- **引导词重写**:边界从「所有操作」收窄为「**代码与文件操作**」(未暴露的领域自然走原生,不再列例外);新增数据库段 (走 IDEA Database 工具 + 用户配数据源 + 不索取凭据);删除终端命令段 (该工具已隐藏)。
|
|
8
|
+
- **新增 `idea-run-config` skill**:教模型在 `.run/*.run.xml` 里建/改/删 run configuration,并用 `get_run_configurations` 校验、`execute_run_configuration` 实跑 (IDE MCP 没有增删改能力)。仅 JetBrains 项目注册,与 `/open-in-idea` 无关。
|
|
9
|
+
- **`/open-in-idea` 改为问注册表要连接状态**:活着只刷新,死了才换注册 (OpenCode 对未变更的 MCP 配置不会重连)。
|
|
10
|
+
- **默认 MCP 端口收敛**:`DEFAULT_PORTS` 从 `[64342, 6420, 6421, 63342]` 改为 `[64342]`。
|
|
11
|
+
- **修复发布脚本**:`assertCleanGit()` 原先排在版本递增之后,导致正式发布必然中途失败并留下已被改动的版本号;现在移到递增之前。
|
|
12
|
+
|
|
13
|
+
## 0.0.5
|
|
14
|
+
|
|
15
|
+
- **冷启动等待 MCP 端点**:`/open-in-idea` 在 `open -a` 之后按 `mcpStartTimeoutMs`(默认 60s)轮询,直到 IDEA 自带的 MCP server 开始监听。冷启动时一次命令即可连上,不再像之前那样探测一次失败就报「未检测到 MCP 服务」。
|
|
16
|
+
- **工具就绪判定**:连接后轮询 `ctx.tool.list()`,确认 `idea_read_file` / `idea_apply_patch` 已注册。未就绪时状态为 `tools-loading` 并提示稍后重试,不再在原生工具表尚未就绪时谎称「已连接」。
|
|
17
|
+
- **重复执行刷新工具列表**:再次执行 `/open-in-idea` 会重建 MCP server 并依次 `ctx.mcp.reload()` + `ctx.tool.reload()`,让已经捕获过工具快照的会话立即看到最新工具,而不是停留在旧快照。
|
|
18
|
+
- **IDE 掉线自动注销**:每次请求前复检端点,IDEA 关闭或某次 `idea_*` 工具调用失败时,立即移除 `idea_*` 工具并恢复原生工具,避免后续请求继续调用已失效的 IDE 工具;只有再次执行 `/open-in-idea` 才能重新注册。
|
|
19
|
+
- **清理**:移除调试日志;删除过时的 `scripts/publish.sh`(旧包名 + bypass token 流程,已由 `scripts/release.mjs` 取代);`package.json` 的 `files` 移除早已删除的 `src/idea-tool-descriptions.js`。
|
|
20
|
+
|
|
3
21
|
## 0.0.4
|
|
4
22
|
|
|
5
23
|
- **`/open-in-idea` 刷新工具列表**:在 `ctx.mcp.reload()` 之后显式调用 `ctx.tool.reload()`。之前只有 MCP 层重建目录,已经捕获过工具快照的会话仍然看不到新的 `idea_*` 原生工具;现在下一次模型请求即可用,无需重开会话。
|
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# opencode-idea
|
|
2
2
|
|
|
3
|
-
OpenCode 插件,面向 JetBrains 项目(IntelliJ IDEA / PyCharm / WebStorm 等),做三件事:
|
|
3
|
+
OpenCode 插件,面向 JetBrains 项目 (IntelliJ IDEA / PyCharm / WebStorm 等),做三件事:
|
|
4
4
|
|
|
5
|
-
1. **接入 IDE MCP** —— 用 `/open-in-idea` 把当前项目在 IntelliJ IDEA 中打开,并把这个 IDE 的 MCP 服务(60
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
1. **接入 IDE MCP** —— 用 `/open-in-idea` 把当前项目在 IntelliJ IDEA 中打开,并把这个 IDE 的 MCP 服务 (60 个工具:
|
|
6
|
+
检索、读、改、构建、重构、调试、数据库……)注册进 OpenCode。
|
|
7
|
+
2. **优先使用 IDE 工具** —— IDE 可用时,向模型注入"项目内操作优先使用 IDEA MCP"的系统提示引导 + 能力映射;IDE 不可用时回退原生工具
|
|
8
|
+
(正常回退,无需声明)。
|
|
9
|
+
3. **注入 IDE 环境变量** —— 从 IDE 集成终端读取它实际运行的环境 (版本化管理的 Node、goenv 的 Go、SDKMAN 的 Java、Maven……)
|
|
10
|
+
注入每个 OpenCode shell。
|
|
8
11
|
|
|
9
|
-
非 JetBrains
|
|
12
|
+
非 JetBrains 项目完全不受影响。 **全手动**:插件启动不探测、不连接 IDE,只有跑 `/open-in-idea` 才接入;接入之后也没有
|
|
13
|
+
任何自主动作(环境变量只在 `/open-in-idea` 时刷新)。
|
|
10
14
|
|
|
11
15
|
## 工作原理
|
|
12
16
|
|
|
@@ -29,55 +33,82 @@ setup(启动):
|
|
|
29
33
|
6. 发一条结果通知(用户看到干净气泡,模型只回"收到")
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
## 在 IDE 中打开项目(`/open-in-idea`)
|
|
36
|
+
## 在 IDE 中打开项目 (`/open-in-idea`)
|
|
33
37
|
|
|
34
38
|
在会话里输入 `/open-in-idea`:
|
|
35
39
|
|
|
36
40
|
1. 先探测 `/stream`;已就绪 → 直接加载能力;
|
|
37
41
|
2. 未就绪 → 用 `open -a "IntelliJ IDEA" <项目目录>` 拉起或激活 IDEA —— **复用已运行的实例**(不会重复开新实例);
|
|
38
|
-
3. 如果 MCP
|
|
42
|
+
3. 如果 MCP 仍未就绪,命令会 **立即 fast-fail**,提示在 IDEA 的 `Settings → MCP Server` 开启 MCP 服务并启用 **Brave
|
|
43
|
+
Mode**;
|
|
39
44
|
4. 用户手动完成设置后,再次执行 `/open-in-idea`;探测成功后才注册 IDE MCP、重读 IDE 环境并启用系统引导与 live tool catalog。
|
|
40
45
|
|
|
41
|
-
> **通知不触发 AI 干活**:默认 `feedback: "message"`
|
|
46
|
+
> **通知不触发 AI 干活**:默认 `feedback: "message"` 把结果作为一条 **标注清楚的通知**发到会话里 —— 用户看到干净的通知气泡
|
|
47
|
+
> (经 `metadata.displayText`),模型只看到"这是通知,请只回复『收到』"的指令,因此不会执行任何操作、不会调用工具。
|
|
48
|
+
> `feedback: false` 则完全静默。
|
|
42
49
|
|
|
43
|
-
**可重入 / 启动保护**:IDEA 冷启动、索引或等待「Trust and Open Project」弹窗期间探测会失败。命令只发一次 MCP/Brave Mode 提示并
|
|
50
|
+
**可重入 / 启动保护**:IDEA 冷启动、索引或等待「Trust and Open Project」弹窗期间探测会失败。命令只发一次 MCP/Brave Mode 提示并
|
|
51
|
+
fast-fail,不会在后台轮询;重复触发共享同一次打开请求,按目录维护 `lastSpawnAt` / `attempts`,避免重复 spawn。开启 MCP +
|
|
52
|
+
Brave Mode 后再次执行 `/open-in-idea` 即可接上。
|
|
44
53
|
|
|
45
|
-
**热更新会断开**:插件源码变更会触发 V2 热重载,注册随旧实例一起释放,`activePort` 归零。这是手动模式的预期行为 ——
|
|
54
|
+
**热更新会断开**:插件源码变更会触发 V2 热重载,注册随旧实例一起释放,`activePort` 归零。这是手动模式的预期行为 ——
|
|
55
|
+
**改完插件后重跑一次 `/open-in-idea`** 即可恢复。
|
|
46
56
|
|
|
47
|
-
## IDE 工具优先(直接调用引导)
|
|
57
|
+
## IDE 工具优先 (直接调用引导)
|
|
48
58
|
|
|
49
|
-
IDE MCP
|
|
59
|
+
IDE MCP 可用时,插件通过 **系统提示注入**提高模型主动使用 IDEA MCP 的概率;不修改任何工具 (原生或 IDEA)
|
|
60
|
+
的定义、描述或可用性:
|
|
50
61
|
|
|
51
|
-
1.
|
|
62
|
+
1. **系统提示注入**:通过 `ctx.session.hook("context", …)` 往每次请求的 system 里追加一段「项目内操作优先使用 IDEA
|
|
63
|
+
MCP」的直接调用引导 + 能力映射表 (`src/ide-guidance.js`),含工具名 (`idea_<name>`)与关键参数名:
|
|
52
64
|
- 检索 → `idea_search_text` / `idea_search_regex` / `idea_search_file`(参数 `q`,可带 `paths` 收窄)
|
|
53
65
|
- 读文件 → `idea_read_file`(`file_path` / `offset` / `limit`);目录 → `idea_list_directory_tree`
|
|
54
66
|
- 改文件 → `idea_apply_patch`(`input`);新建 → `idea_create_new_file`(`pathInProject`)
|
|
55
67
|
- 校验 → `idea_lint_files` / `idea_get_file_problems` / `idea_build_project`
|
|
56
|
-
- 重构/格式化 → `idea_rename_refactoring` / `idea_reformat_file
|
|
57
|
-
-
|
|
68
|
+
- 重构/格式化 → `idea_rename_refactoring` / `idea_reformat_file`
|
|
69
|
+
- 数据库 → `idea_list_database_connections` / `idea_execute_sql_query` / `idea_preview_table_data` 等;没有数据源时先建议用户在
|
|
70
|
+
IDEA 里配置
|
|
71
|
+
- 边界:只约束 **代码与文件操作**;未暴露的领域 (版本控制、shell/终端等) 工具表里没有 `idea_*` 选项,模型自然走原生
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
不会把整份工具目录重复塞进 system prompt,也不要求模型包一层 JavaScript 编排代码。IDEA MCP 工具自身的
|
|
74
|
+
description/schema 仍是参数使用的权威来源;工具返回业务错误时先修正参数,不要误判成工具不可用。MCP 不可用时立即
|
|
75
|
+
提示用户执行 `/open-in-idea`,不重复探测或等待。
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
引导只在 **IDE MCP 可用**时生效;MCP 不可用时仅保留 fast-fail 恢复提示。`injectGuidance: false` 可关闭系统提示注入。
|
|
62
78
|
|
|
63
|
-
|
|
79
|
+
## Run Configuration 管理 (skill)
|
|
64
80
|
|
|
65
|
-
|
|
81
|
+
IDE MCP 只能 **查**(`get_run_configurations`)和 **执行**(`execute_run_configuration`)run configuration, **没有增删改**
|
|
82
|
+
。插件在 **JetBrains 项目**(存在 `.idea`)里注册一个 `idea-run-config` skill 来补上这块:
|
|
83
|
+
|
|
84
|
+
- 只操作 **项目级** `.run/<name>.run.xml`(可提交 git、团队共享), **不碰** `.idea/workspace.xml`;
|
|
85
|
+
- 不内置任何按类型的模板 —— 先找项目里的 **真实样本**(`**/*.run.xml`,或让用户在 IDEA 里勾 "Store as project file"
|
|
86
|
+
建一个)当参照,再写;
|
|
87
|
+
- 写完必须用 `idea_get_run_configurations` **校验它能被列出**、并用 `idea_execute_run_configuration` **实跑确认**;
|
|
88
|
+
- 改配置用 `idea_apply_patch` **定向改 option**,不整文件重写。
|
|
89
|
+
|
|
90
|
+
skill 由 `ctx.skill.transform` 在插件 `setup` 时注册, **与 `/open-in-idea` 无关**;非 JetBrains 项目不受影响。定义在
|
|
91
|
+
`src/ide-run-config-skill.js`。
|
|
66
92
|
|
|
67
93
|
## 环境变量注入
|
|
68
94
|
|
|
69
|
-
**唯一来源:IDE 集成终端**(`execute_terminal_command printenv`)。它反映 IDE 实际运行的环境 —— 版本化管理的 Node(如 fnm
|
|
95
|
+
**唯一来源:IDE 集成终端**(`execute_terminal_command printenv`)。它反映 IDE 实际运行的环境 —— 版本化管理的 Node (如 fnm
|
|
96
|
+
v14)、goenv 的 Go、SDKMAN 的 Java 等 —— 这些 .idea 配置里往往读不到。
|
|
70
97
|
|
|
71
|
-
抽取的变量(`src/ide-env.js` 的 `SDK_ENV_KEYS`):`JAVA_HOME` / `JRE_HOME` / `JDK_HOME` / `VIRTUAL_ENV` / `PYTHONPATH` /
|
|
98
|
+
抽取的变量 (`src/ide-env.js` 的 `SDK_ENV_KEYS`):`JAVA_HOME` / `JRE_HOME` / `JDK_HOME` / `VIRTUAL_ENV` / `PYTHONPATH` /
|
|
99
|
+
`PYTHONHOME` / `GOROOT` / `GOPATH` / `GOBIN` / `NODE_PATH` / `NVM_BIN` / `NVM_DIR` / `MAVEN_HOME` / `M2_HOME` /
|
|
100
|
+
`GRADLE_HOME` / `SDKMAN_DIR` / `CONDA_PREFIX`。
|
|
72
101
|
|
|
73
102
|
- 注入通过 V2 的 `shell` `create.before` 钩子:只补这些变量 + 前置 PATH,不覆盖你原有的 PATH。
|
|
74
|
-
- 需要 IDE MCP 可达 + **Brave Mode**;拿不到时环境为空(不影响其他功能)。
|
|
103
|
+
- 需要 IDE MCP 可达 + **Brave Mode**;拿不到时环境为空 (不影响其他功能)。
|
|
75
104
|
|
|
76
|
-
> 与 [opencode-env-loader](https://github.com/syllr/opencode-env-loader) 的关系:两者独立。env-loader 负责
|
|
105
|
+
> 与 [opencode-env-loader](https://github.com/syllr/opencode-env-loader) 的关系:两者独立。env-loader 负责
|
|
106
|
+
> `.opencode/env-loader/*` 的 **人工** KEY=VALUE 覆盖;本插件负责从 **IDE 实际环境**推导。可以同时使用。
|
|
77
107
|
|
|
78
108
|
## 前置条件
|
|
79
109
|
|
|
80
|
-
- JetBrains IDE 已开启 MCP 服务(IDE 设置里的 MCP Server / AI Assistant MCP),版本需 **2026.2+**(提供 Streamable HTTP 端点
|
|
110
|
+
- JetBrains IDE 已开启 MCP 服务 (IDE 设置里的 MCP Server / AI Assistant MCP),版本需 **2026.2+**(提供 Streamable HTTP 端点
|
|
111
|
+
`/stream`);若关闭,`/open-in-idea` 会立即提示如何重新开启。
|
|
81
112
|
- 要读 IDE 终端环境,需要在 IDE 里开启 **Brave Mode**(否则 `execute_terminal_command` 会等确认);MCP 未就绪时通知会同时提醒开启它。
|
|
82
113
|
- 插件注册给 OpenCode 的 MCP 配置形如:
|
|
83
114
|
|
|
@@ -89,9 +120,11 @@ IDE MCP 可用时,插件通过两层轻量引导提高模型主动使用 IDEA MC
|
|
|
89
120
|
}
|
|
90
121
|
```
|
|
91
122
|
|
|
92
|
-
> **为什么是 `/stream` 而不是 `/sse`**:OpenCode V2 的远程 MCP
|
|
123
|
+
> **为什么是 `/stream` 而不是 `/sse`**:OpenCode V2 的远程 MCP 客户端 **只支持 Streamable HTTP**,没有旧版 SSE 传输。IDE 的
|
|
124
|
+
> `/sse` 是旧版 SSE (POST 会返回 405),V2 连不上 (报 `Error POSTing to endpoint`)。IDE 2026.2 起额外提供 `/stream` 这个
|
|
125
|
+
> Streamable HTTP 端点。 **插件的探测也走 `/stream`**(一次 `initialize`),协议统一。
|
|
93
126
|
|
|
94
|
-
插件按 `ports` 顺序选择第一个可用端口,但会并发探测以避免多个端口的超时叠加;默认 `[64342
|
|
127
|
+
插件按 `ports` 顺序选择第一个可用端口,但会并发探测以避免多个端口的超时叠加;默认 `[64342]`。
|
|
95
128
|
|
|
96
129
|
## 安装
|
|
97
130
|
|
|
@@ -101,7 +134,7 @@ IDE MCP 可用时,插件通过两层轻量引导提高模型主动使用 IDEA MC
|
|
|
101
134
|
opencode plugin add opencode-idea
|
|
102
135
|
```
|
|
103
136
|
|
|
104
|
-
等价的配置写法(V2 使用 `plugins` 数组):
|
|
137
|
+
等价的配置写法 (V2 使用 `plugins` 数组):
|
|
105
138
|
|
|
106
139
|
```jsonc
|
|
107
140
|
{
|
|
@@ -119,7 +152,8 @@ opencode plugin list
|
|
|
119
152
|
|
|
120
153
|
### 本地开发 / 真机验证
|
|
121
154
|
|
|
122
|
-
插件未发布时,可以直接指向本地仓库目录。V2
|
|
155
|
+
插件未发布时,可以直接指向本地仓库目录。V2 解析 **目录**插件时找的是 `<dir>/index.*`(或 `<dir>/server.*`),不会读
|
|
156
|
+
`package.json#main`,所以仓库根目录提供了 `index.js` 转发到 `src/index.js`。
|
|
123
157
|
|
|
124
158
|
全局配置 `~/.config/opencode/opencode.json`(V2 桌面端/CLI 共用):
|
|
125
159
|
|
|
@@ -129,9 +163,10 @@ opencode plugin list
|
|
|
129
163
|
}
|
|
130
164
|
```
|
|
131
165
|
|
|
132
|
-
配置保存后 V2 会监听并自动重载(日志里能看到它开始 watch `index.js` / `src/*.js`),无需重启。改插件源码也会触发重载(此时需重跑
|
|
166
|
+
配置保存后 V2 会监听并自动重载 (日志里能看到它开始 watch `index.js` / `src/*.js`),无需重启。改插件源码也会触发重载 (此时需重跑
|
|
167
|
+
`/open-in-idea`)。
|
|
133
168
|
|
|
134
|
-
另一种零配置方式:在 `~/.config/opencode/plugins/` 放一个转发文件(自动发现,但不能传 options):
|
|
169
|
+
另一种零配置方式:在 `~/.config/opencode/plugins/` 放一个转发文件 (自动发现,但不能传 options):
|
|
135
170
|
|
|
136
171
|
```js
|
|
137
172
|
// ~/.config/opencode/plugins/opencode-idea.js
|
|
@@ -148,7 +183,7 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
|
|
|
148
183
|
{
|
|
149
184
|
"package": "opencode-idea",
|
|
150
185
|
"options": {
|
|
151
|
-
"ports": [64342,
|
|
186
|
+
"ports": [64342], // IDE MCP 端口;默认 [64342]
|
|
152
187
|
"injectEnv": true,
|
|
153
188
|
"injectGuidance": true, // IDE 可用时向系统提示注入「优先使用 IDEA MCP」引导
|
|
154
189
|
"openInIde": "idea", // false 关闭;或任意 macOS 应用名(如 "IntelliJ IDEA")
|
|
@@ -164,35 +199,90 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
|
|
|
164
199
|
|
|
165
200
|
不传 options 时全部使用下表默认值。
|
|
166
201
|
|
|
167
|
-
| 选项 | 类型 | 默认
|
|
168
|
-
| ------------------- | -------------------- |
|
|
169
|
-
| `ports` | `number[]` | `[64342
|
|
170
|
-
| `injectEnv` | `boolean` | `true`
|
|
171
|
-
| `injectGuidance` | `boolean` | `true`
|
|
172
|
-
| `openInIde` | `boolean \| string` | `"idea"`
|
|
173
|
-
| `launchCooldownMs` | `number` | `120000`
|
|
174
|
-
| `launchMaxAttempts` | `number` | `5`
|
|
175
|
-
| `mcpProbeTimeoutMs` | `number` | `1000`
|
|
176
|
-
| `feedback` | `"message" \| false` | `"message"`
|
|
202
|
+
| 选项 | 类型 | 默认 | 说明 |
|
|
203
|
+
| ------------------- | -------------------- | ----------- | ---------------------------------------------------------------- |
|
|
204
|
+
| `ports` | `number[]` | `[64342]` | 依次探测的 IDE MCP 端口 |
|
|
205
|
+
| `injectEnv` | `boolean` | `true` | 是否注入 IDE 终端环境变量 |
|
|
206
|
+
| `injectGuidance` | `boolean` | `true` | IDE 可用时是否向系统提示注入「优先使用 IDEA MCP」引导 |
|
|
207
|
+
| `openInIde` | `boolean \| string` | `"idea"` | 拉起哪个 IDE;`false` 关闭,`"idea"` 映射到 IntelliJ IDEA |
|
|
208
|
+
| `launchCooldownMs` | `number` | `120000` | 启动保护冷却:冷却期内不重复 spawn |
|
|
209
|
+
| `launchMaxAttempts` | `number` | `5` | 单次会话内最多 spawn 次数,超出后停止重试 |
|
|
210
|
+
| `mcpProbeTimeoutMs` | `number` | `1000` | 单个 MCP 端口的短探测超时 |
|
|
211
|
+
| `feedback` | `"message" \| false` | `"message"` | 结果反馈:`message` 会话通知(用户可见,AI 只回"收到");`false` 静默 |
|
|
177
212
|
|
|
178
213
|
## 行为
|
|
179
214
|
|
|
180
|
-
- **JetBrains 项目**:启动即注入 IDE 终端环境(若此前已连过 IDE);`/open-in-idea` 接入 IDE MCP + 启用引导。
|
|
181
|
-
- **非 JetBrains
|
|
182
|
-
-
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
- **JetBrains 项目**:启动即注入 IDE 终端环境 (若此前已连过 IDE);`/open-in-idea` 接入 IDE MCP + 启用引导。
|
|
216
|
+
- **非 JetBrains 项目**:不改环境、不注册、不注入任何 IDEA 提示 (每请求零开销)。
|
|
217
|
+
- **手动接入 + 零自主动作**:启动 **不探测、不连接 IDE MCP**;注册、**环境重读**、引导注入全部由 `/open-in-idea` 触发。
|
|
218
|
+
`context` 钩子按「每次模型调用」触发 (含工具续跑),是**请求的纯函数**:它只看本次请求自带的工具快照 ——
|
|
219
|
+
有 `idea_*` 就剥离隐藏工具并注入引导,没有就只给恢复提示。**不做任何 I/O,没有缓存状态,没有定时器,没有事件订阅**。
|
|
220
|
+
一次失败的调用不会触发探测或注销,断开的连接也不会被自动重连 —— **恢复的唯一方式是用户再次执行 `/open-in-idea`**。
|
|
221
|
+
- **`/open-in-idea`**:手动打开当前项目并即时加载能力 (已在 IDE 打开时则只重新加载);MCP 未开启时只发一次提示并
|
|
222
|
+
fast-fail,用户手动开启 MCP + Brave Mode 后重试。
|
|
223
|
+
- **不响应工具报错**:插件**不注册** `tool.execute.after` 钩子。一次 `idea_*` 调用失败 (业务错误如 `File not found`、
|
|
224
|
+
或偶发传输抖动) 不会触发任何探测、注销或重连 —— 连接状态只由 `/open-in-idea` 决定。
|
|
185
225
|
|
|
186
226
|
## 限制
|
|
187
227
|
|
|
188
|
-
- 仅支持 **OpenCode V2** 插件 API(`ctx.tool` / `ctx.mcp` / `ctx.shell` / `ctx.command` / `ctx.session` /
|
|
228
|
+
- 仅支持 **OpenCode V2** 插件 API (`ctx.tool` / `ctx.mcp` / `ctx.shell` / `ctx.command` / `ctx.session` /
|
|
229
|
+
`ctx.location`)。
|
|
189
230
|
- IDE MCP 注册需要 IDE **2026.2+** 的 Streamable HTTP 端点 `/stream`;更早版本只有 `/sse`,OpenCode V2 无法连接。
|
|
190
231
|
- 环境变量依赖 `execute_terminal_command` + Brave Mode;拿不到时环境为空。
|
|
232
|
+
- 一批用不到的 IDEA MCP 工具被硬编码隐藏 (`src/plugin.js` 的 `HIDDEN_IDEA_TOOLS`,共 31 个):VCS (`idea_git_status` /
|
|
233
|
+
`idea_get_repositories`,版本控制走原生 `git`)、Router 派发工具 (`idea_execute_tool`,未启用 router-only 时冗余)、Debugger
|
|
234
|
+
(全部 `idea_xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、Python Environment MCP、以及数据库的建/改数据源 (数据源由用户在
|
|
235
|
+
IDEA 里配置,AI 只读和查询)。`idea_execute_terminal_command` 也不暴露给模型 (实测:约 60 秒硬截止且拿不到后续输出;
|
|
236
|
+
"看结尾"被原生覆盖)—— 它只由插件内部读取 IDE 环境,而那是插件直接调 MCP,不经过模型工具表。
|
|
191
237
|
- 探测只确认「MCP 服务是否在监听」,不校验「当前项目是否已在 IDE 打开」;MCP 关闭时会用一次短探测并立即给出设置提示。
|
|
192
|
-
-
|
|
238
|
+
- OpenCode 的 MCP 自动重连**只覆盖 legacy Streamable HTTP 的 session 过期**(`SessionExpiredError`:有 session id + 404 /
|
|
239
|
+
"Server not initialized" → `recover()` + 重试该次调用);modern 端点 (IDE 2026.2,protocol 2025-06-18,不带 session id)
|
|
240
|
+
的连接被关只会置 `failed` 并移除工具,**不会自动重连**。插件刻意不补这个缺口:恢复一律走手动 `/open-in-idea`。
|
|
241
|
+
- 打开 IDE 仅实现 macOS (`open -a`);首次打开若弹 Trust 对话框,需确认后重试。
|
|
193
242
|
- IDE MCP 没有「打开项目」工具,因此打开动作走 OS/CLI,而非 MCP。
|
|
194
243
|
- 插件热重载会断开 IDE 连接,需重跑 `/open-in-idea`。
|
|
195
|
-
-
|
|
244
|
+
- 插件不修改任何工具的定义、描述或可用性;IDE MCP 工具以 `idea_*` 直接暴露给模型 (`codemode: false`),不用 `execute` 包裹。
|
|
245
|
+
|
|
246
|
+
### `execute_terminal_command` 与原生 shell:互补,不是替代
|
|
247
|
+
|
|
248
|
+
> **结论:已加入 `HIDDEN_IDEA_TOOLS`,不暴露给模型** —— 下面的对比数据用于说明这个决定,也供手工调用时参考。
|
|
249
|
+
> 理由:① AI 拿不到长任务结果 (约 60 秒硬截止 + 没有工具能读终端缓冲);② "看结尾"被原生覆盖 (原生超限时保尾部,
|
|
250
|
+
> 并把完整输出存成文件);③ 唯一实质价值是"让用户看到 / 接手",而模型不需要它 —— 插件自己的 `printenv` 是
|
|
251
|
+
> **直接调 MCP**(`src/ide-env.js`),不经过模型工具表。
|
|
252
|
+
|
|
253
|
+
两者能力相近但取舍不同 (macOS + IDEA 2026.2.3 实测):
|
|
254
|
+
|
|
255
|
+
| 维度 | 原生 `shell` | IDE `execute_terminal_command` |
|
|
256
|
+
| ---------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
|
|
257
|
+
| 每次调用增量开销 | ~9ms(OS spawn) | ~90ms(多一次 MCP 往返) |
|
|
258
|
+
| 会话常驻 | ❌ 每次新 shell | ❌ 每次新 shell(`reuseExistingTerminalWindow` 也不保留变量 / `cd`) |
|
|
259
|
+
| `timeout` | ✅ 生效:到点即中止并杀掉进程(长任务须自行调大) | **约 60 秒硬截止**,`timeout` 参数无法改变:到点返回已有输出 + `is_timed_out`,**不杀进程** |
|
|
260
|
+
| 输出截断 | `tool_output` 上限(默认 8000 行 / 256KB);想看结尾得在命令里自己接 `tail` | `maxLinesCount` + `truncateMode`,可**只留结尾** |
|
|
261
|
+
| 用户可见性 | 只在 agent 侧 | 在 IDE 里,用户可看 / 可接手 |
|
|
262
|
+
|
|
263
|
+
**各自优势**
|
|
264
|
+
|
|
265
|
+
- **原生**:更快、超时可控 (到点杀进程)、输出上限更宽 —— **默认走这条**。
|
|
266
|
+
- **IDE**:
|
|
267
|
+
① **长任务**(安装 / 构建 / 迁移):`reuseExistingTerminalWindow: true` 让它在 IDE 终端里跑,用户 **能看进度、能随时取消**。
|
|
268
|
+
但工具 **约 60 秒**就返回已产生的输出 (`is_timed_out: true`),之后命令仍在后台跑、AI 拿不到后续 —— 所以它适合
|
|
269
|
+
"**交给用户盯**",不适合"AI 等最终结果"(那要用原生,或让命令把结果写文件再读)。
|
|
270
|
+
② **精确控制返回哪一段 + 行数**:`truncateMode` 取 `START`(保头)/`MIDDLE`(头+尾)/`END`(保尾)+ `maxLinesCount`。
|
|
271
|
+
其中 **`MIDDLE` 是真正独有的** —— 原生超限时 **固定保尾部**,并把 **完整输出存成文件**返回路径,所以 `END` 与原生默认重复,
|
|
272
|
+
而原生那份全量文件反而是 IDE 侧拿不到的 (IDE 丢弃的部分只留在终端)。
|
|
273
|
+
|
|
274
|
+
判据 (手工调用时的参考):**要"人看" 或 要"头 + 尾一起看" (`MIDDLE`) → IDE;其余 → 原生。**
|
|
275
|
+
|
|
276
|
+
**`truncateMode` 速查 (语义反直觉,以实测为准)**
|
|
277
|
+
|
|
278
|
+
| 传值 | 保留哪部分 |
|
|
279
|
+
| -------------- | -------------------------- |
|
|
280
|
+
| 不传 / `START` | 开头 |
|
|
281
|
+
| `MIDDLE` | 头 + 尾 |
|
|
282
|
+
| **`END`** | **结尾**(截断标记在最上面) |
|
|
283
|
+
| `NONE` | 全部,不截断 |
|
|
284
|
+
|
|
285
|
+
默认 `maxLinesCount` ≈ 1000 行,要更多就显式传。注意 `END` 是"**保留结尾**",不是"截掉结尾"。
|
|
196
286
|
|
|
197
287
|
## 测试
|
|
198
288
|
|
|
@@ -200,7 +290,8 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
|
|
|
200
290
|
npm test
|
|
201
291
|
```
|
|
202
292
|
|
|
203
|
-
使用 [Vitest](https://vitest.dev/),含纯函数测试、IDE 环境解析、环境合并、启动保护状态机、IDE 启动器、引导文本,以及「假 IDE MCP
|
|
293
|
+
使用 [Vitest](https://vitest.dev/),含纯函数测试、IDE 环境解析、环境合并、启动保护状态机、IDE 启动器、引导文本,以及「假 IDE MCP
|
|
294
|
+
服务」的端到端探测与插件集成测试。
|
|
204
295
|
|
|
205
296
|
## License
|
|
206
297
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-idea",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "OpenCode plugin that connects to JetBrains IDE MCP servers and prefers IDE tools when the current project is open in the IDE",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"src/env.js",
|
|
15
15
|
"src/ide-launcher.js",
|
|
16
16
|
"src/ide-guidance.js",
|
|
17
|
-
"src/
|
|
17
|
+
"src/ide-run-config-skill.js",
|
|
18
|
+
"src/ide-run-config-skill.md",
|
|
18
19
|
"README.md",
|
|
19
20
|
"CHANGELOG.md"
|
|
20
21
|
],
|
package/src/ide-guidance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// IDEA MCP routing guidance injected into the model's system prompt.
|
|
2
2
|
//
|
|
3
3
|
// Two rules for this file:
|
|
4
|
-
// 1. State requirements only. No
|
|
4
|
+
// 1. State requirements and exact parameters only. No rationale.
|
|
5
5
|
// 2. The boundary is the project path. Inside it -> idea_* tools.
|
|
6
6
|
// Outside it -> whatever else is available.
|
|
7
7
|
//
|
|
@@ -18,7 +18,7 @@ export function buildIdeGuidance(projectPath) {
|
|
|
18
18
|
`项目路径: \`${projectPath}\``,
|
|
19
19
|
``,
|
|
20
20
|
`## 强制要求`,
|
|
21
|
-
`- 路径在 \`${projectPath}\`
|
|
21
|
+
`- 路径在 \`${projectPath}\` 内的代码与文件操作,必须使用 \`idea_*\` 工具。`,
|
|
22
22
|
`- 直接调用工具,不要用 \`execute\` 包裹。`,
|
|
23
23
|
`- 检索内容用 \`idea_search_text\` / \`idea_search_regex\`,不要用 \`grep\`。`,
|
|
24
24
|
`- 查找文件名用 \`idea_search_file\`,不要用 \`glob\`。`,
|
|
@@ -27,11 +27,16 @@ export function buildIdeGuidance(projectPath) {
|
|
|
27
27
|
`- 修改已有文件用 \`idea_apply_patch\`,不要用 \`edit\` / \`patch\`。`,
|
|
28
28
|
`- 新建文件用 \`idea_create_new_file\`,不要用 \`write\`。`,
|
|
29
29
|
`- 改完代码用 \`idea_lint_files\` 或 \`idea_get_file_problems\` 校验。`,
|
|
30
|
-
`- 看 git 状态用 \`idea_git_status\`。`,
|
|
31
30
|
`- 工具参数按该工具的 schema 传。`,
|
|
32
31
|
``,
|
|
32
|
+
`## 数据库`,
|
|
33
|
+
`- 数据库操作走 IDEA 的 Database 工具(\`idea_list_database_connections\` / \`idea_introspect_schema\` / \`idea_execute_sql_query\` / \`idea_preview_table_data\` 等),不要用原生 CLI 客户端。`,
|
|
34
|
+
`- 先 \`idea_list_database_connections\` 看有没有现成数据源。`,
|
|
35
|
+
`- 没有数据源时:若目标数据库在 IDEA Database Tools 支持范围内(PostgreSQL / MySQL / MariaDB / Oracle / SQL Server / SQLite / DB2 / H2 / Derby / Sybase / Vertica / Greenplum / ClickHouse / CockroachDB / Snowflake / BigQuery / Redshift / MongoDB / Redis / Cassandra / Couchbase / Hive / DynamoDB / Exasol 等,或任何提供 JDBC 驱动的库),先建议用户在 IDEA 里配好数据源,再回来用工具操作。`,
|
|
36
|
+
`- 不要自己建或改数据源,不要向用户索取数据库凭据。`,
|
|
37
|
+
``,
|
|
33
38
|
`## 范围`,
|
|
34
|
-
`- \`${projectPath}\`
|
|
39
|
+
`- \`${projectPath}\` 内的代码与文件操作:只用 \`idea_*\` 工具。`,
|
|
35
40
|
`- \`${projectPath}\` 外:用其他可用工具。`,
|
|
36
41
|
`- \`idea_*\` 工具缺失、连接失败或超时:提示用户执行 \`/open-in-idea\`,不要重试。`,
|
|
37
42
|
].join('\n');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The `idea-run-config` skill registered by the plugin.
|
|
2
|
+
//
|
|
3
|
+
// Run configurations are plain XML files under `.run/`. The IDE MCP server can
|
|
4
|
+
// list and execute them (`get_run_configurations` / `execute_run_configuration`)
|
|
5
|
+
// but has no create/update/delete. Hard-coding a template per configuration type
|
|
6
|
+
// would be an open-ended maintenance hole (Application, JUnit, Gradle, npm,
|
|
7
|
+
// Shell Script, Spring Boot, Docker, Compound, ...), so the plugin ships a skill
|
|
8
|
+
// instead: it teaches the model where the files live, how to bootstrap a schema
|
|
9
|
+
// from a real example, and how to verify a write.
|
|
10
|
+
//
|
|
11
|
+
// The text lives in `ide-run-config-skill.md` next to this module, so it is a
|
|
12
|
+
// real skill document and the XML snippets it contains are not mistaken for
|
|
13
|
+
// markup inside a string literal.
|
|
14
|
+
//
|
|
15
|
+
// The skill is registered at setup and does NOT depend on `/open-in-idea`.
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
/** Skill id used by the registry. */
|
|
21
|
+
export const RUN_CONFIG_SKILL_ID = 'idea-run-config';
|
|
22
|
+
|
|
23
|
+
/** Description shown to the model when picking a skill. */
|
|
24
|
+
export const RUN_CONFIG_SKILL_DESCRIPTION = [
|
|
25
|
+
'管理 JetBrains IDE 的 run configuration:在项目级 `.run/*.run.xml` 里创建、修改、删除运行配置。',
|
|
26
|
+
'当用户要新建运行配置、修改或删除已有配置、把脚本或执行流程包成可运行的配置,',
|
|
27
|
+
'或让 AI 执行用户手配的配置时使用。只操作项目级 `.run/` 文件,不碰 `.idea/workspace.xml`。',
|
|
28
|
+
].join('');
|
|
29
|
+
|
|
30
|
+
const SKILL_FILE = new URL('./ide-run-config-skill.md', import.meta.url);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the skill document shipped next to this module.
|
|
34
|
+
*
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function readRunConfigSkillContent() {
|
|
38
|
+
return readFileSync(SKILL_FILE, 'utf8').trimEnd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the skill definition registered through `ctx.skill.transform`.
|
|
43
|
+
*
|
|
44
|
+
* `location` points at the shipped `.md` — the real skill document — so any
|
|
45
|
+
* path-shaped validation is satisfied without inventing a virtual filename.
|
|
46
|
+
*
|
|
47
|
+
* @returns {{ id: string, name: string, description: string, location: string, content: string }}
|
|
48
|
+
*/
|
|
49
|
+
export function runConfigSkill() {
|
|
50
|
+
return {
|
|
51
|
+
id: RUN_CONFIG_SKILL_ID,
|
|
52
|
+
name: 'idea-run-config',
|
|
53
|
+
description: RUN_CONFIG_SKILL_DESCRIPTION,
|
|
54
|
+
location: fileURLToPath(SKILL_FILE),
|
|
55
|
+
content: readRunConfigSkillContent(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# JetBrains Run Configuration 管理
|
|
2
|
+
|
|
3
|
+
本 skill 用于 **管理 JetBrains IDEA 的 run configuration (运行配置)**:创建、修改、删除、查看,并触发运行与调试。配置以 XML
|
|
4
|
+
文件形式存放在项目里,AI 通过读写这些文件完成管理,再用 IDE MCP 工具做校验和执行。
|
|
5
|
+
|
|
6
|
+
## 存放位置:只有 `.run/`
|
|
7
|
+
|
|
8
|
+
所有配置都写 **项目根 `.run/<name>.run.xml`**。这是 AI 唯一能可靠写入、且 IDEA 会 **即时加载**的位置。文件要不要提交到
|
|
9
|
+
git,由用户自己决定,AI 不代劳、也不必追问。
|
|
10
|
+
|
|
11
|
+
### 为什么不用 `.idea/`
|
|
12
|
+
|
|
13
|
+
- `.idea/workspace.xml` → `RunManager`:IDEA 的原生私有位置,但 **IDEA 是这份文件的主人** ——
|
|
14
|
+
工作区状态缓存在内存里、保存时整体重写,还有"工作区恢复"机制会覆盖或还原外部改动。AI 写进去不可靠。
|
|
15
|
+
- `.idea/runConfigurations/*.xml`:这是 **2020.1 之前的共享位置**(不是私有位置),而且 `.idea` 常被 gitignore,反而更难共享。
|
|
16
|
+
|
|
17
|
+
用户已有的、存在 `.idea` 里的配置,AI 仍可 **读取和运行**(`idea_get_run_configurations` 能看到),只是不负责创建、修改或删除。
|
|
18
|
+
|
|
19
|
+
## 位置速查
|
|
20
|
+
|
|
21
|
+
这是 **事实参考**(IDEA 实际有三个位置),供识别用户已有配置用:
|
|
22
|
+
|
|
23
|
+
| 位置 | 谁在用 | AI 能否写 |
|
|
24
|
+
|--------------------------------------|--------------------------------------------|-----------------------------------------|
|
|
25
|
+
| `.run/<name>.run.xml`(项目根) | 2020.1+ 的项目级位置 | 能写,IDEA 即时加载 |
|
|
26
|
+
| `.idea/runConfigurations/*.xml` | 2020.1 **之前**的共享位置;现在仍兼容读取 | 一般不动,老项目可沿用 |
|
|
27
|
+
| `.idea/workspace.xml` → `RunManager` | **默认**位置(不勾 "Store as project file") | **不写**(会被 IDEA 覆盖);但能读取和运行 |
|
|
28
|
+
|
|
29
|
+
新建配置时 `.run/` 的结构:
|
|
30
|
+
|
|
31
|
+
```xml
|
|
32
|
+
|
|
33
|
+
<component name="ProjectRunConfigurationManager">
|
|
34
|
+
<configuration default="false" name="<名字>" type="<类型 ID>">
|
|
35
|
+
<option name="..." value="..."/>
|
|
36
|
+
<envs/>
|
|
37
|
+
<method v="2"/>
|
|
38
|
+
</configuration>
|
|
39
|
+
</component>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 铁律
|
|
43
|
+
|
|
44
|
+
1. 不要凭空写 option 名。先找真实样本:
|
|
45
|
+
- 用 `idea_search_file` 传 `q="**/*.run.xml"` 看项目里已有的配置。
|
|
46
|
+
- 没有目标类型 → 请用户在 IDEA 里新建一个配置并勾选 "Store as project file",然后读生成的 XML 当模板。
|
|
47
|
+
- 仍然没有 → 直接说明需要该类型的真实样本,不要猜 option 名。
|
|
48
|
+
2. 写完必须校验:
|
|
49
|
+
- 在 `idea_get_run_configurations` 里能看到才算写对;看不到说明 `type` 写错了,改了重试。
|
|
50
|
+
- 能看到不等于能跑:`idea_execute_run_configuration` 传 `configurationName` 实跑一次确认。
|
|
51
|
+
- IDE MCP 不可用 → 提示用户执行 `/open-in-idea` 后再校验,不要跳过校验当成成功。
|
|
52
|
+
3. 改配置用定向修改:只用 `idea_apply_patch` 改需要的那几行 option,不要整文件重写。
|
|
53
|
+
4. 删除配置 = 删除对应的 `.run/<name>.run.xml`;不要试图从 `.idea/workspace.xml` 里删。
|
|
54
|
+
|
|
55
|
+
## 常见字段
|
|
56
|
+
|
|
57
|
+
- `$PROJECT_DIR$`:项目根;`$MODULE_WORKING_DIR$`:模块目录。
|
|
58
|
+
- 环境变量:
|
|
59
|
+
|
|
60
|
+
```xml
|
|
61
|
+
|
|
62
|
+
<envs>
|
|
63
|
+
<env name="KEY" value="VALUE"/>
|
|
64
|
+
</envs>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Before launch 流程:`<method v="2">` 里加 `Make` 选项会先构建。
|
|
68
|
+
|
|
69
|
+
## 起步参考 (不是契约;动手前一律以真实样本为准)
|
|
70
|
+
|
|
71
|
+
| 用途 | type | 关键 option |
|
|
72
|
+
|------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
73
|
+
| Shell 脚本 | `ShConfigurationType` | 内联脚本:`SCRIPT_TEXT` + `EXECUTE_SCRIPT_FILE=false`;脚本文件:`SCRIPT_PATH` + `EXECUTE_SCRIPT_FILE=true`;`INTERPRETER_PATH`;`SCRIPT_WORKING_DIRECTORY` |
|
|
74
|
+
| Java 应用 | `Application` | `MAIN_CLASS_NAME`;`WORKING_DIRECTORY`;`PROGRAM_PARAMETERS`;`VM_PARAMETERS` |
|
|
75
|
+
| JUnit | `JUnit` | `TEST_OBJECT`;`MAIN_CLASS_NAME`;`WORKING_DIRECTORY` |
|
|
76
|
+
| Gradle | `GradleRunConfiguration` | `ExternalProjectPath`;`TaskList` |
|
|
77
|
+
| npm | `js.build_tools.npm` | `package-json`;`command`;`scripts` |
|
|
78
|
+
| 串行流程 | `CompoundRunConfiguration` | `method` 里多个 `RunConfigurationTask`(带 `run_configuration_name` / `run_configuration_type`) |
|
|
79
|
+
|
|
80
|
+
## 命名
|
|
81
|
+
|
|
82
|
+
- 把 `name` 转成文件名:去掉 `/ \ : * ? " < > |` 等非法字符,写成 `.run/<净化后的名字>.run.xml`。
|
|
83
|
+
- 目标文件已存在时,先确认是要覆盖。
|
|
84
|
+
|
|
85
|
+
## 流程
|
|
86
|
+
|
|
87
|
+
1. `idea_search_file` 找 `**/*.run.xml` 样本。
|
|
88
|
+
2. 有同类样本 → 照它的结构改写;没有 → 请用户建一个。
|
|
89
|
+
3. `idea_create_new_file` 写 `.run/<name>.run.xml`。
|
|
90
|
+
4. `idea_get_run_configurations` 校验它出现。
|
|
91
|
+
5. `idea_execute_run_configuration` 实跑确认。
|
|
92
|
+
6. 修改用 `idea_apply_patch` 定向改 option;删除后确认它从列表里消失。
|
package/src/idea-mcp.js
CHANGED
|
@@ -24,8 +24,8 @@ export const IDEA_SERVER_NAME = 'idea';
|
|
|
24
24
|
/** Streamable-HTTP endpoint exposed by the IDE MCP server (IntelliJ 2026.2+). */
|
|
25
25
|
export const MCP_STREAM_PATH = '/stream';
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
export const DEFAULT_PORTS = [64342
|
|
27
|
+
/** Port the IDE MCP server listens on when not configured explicitly. */
|
|
28
|
+
export const DEFAULT_PORTS = [64342];
|
|
29
29
|
|
|
30
30
|
const PROTOCOL_VERSION = '2025-06-18';
|
|
31
31
|
// A probe is only a local availability check. Keep it short: a disabled IDE
|
|
@@ -81,7 +81,7 @@ export async function probePort(port, options = {}) {
|
|
|
81
81
|
params: {
|
|
82
82
|
protocolVersion: PROTOCOL_VERSION,
|
|
83
83
|
capabilities: {},
|
|
84
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
84
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.6' },
|
|
85
85
|
},
|
|
86
86
|
}),
|
|
87
87
|
signal: controller.signal,
|
|
@@ -151,7 +151,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
|
|
|
151
151
|
params: {
|
|
152
152
|
protocolVersion: PROTOCOL_VERSION,
|
|
153
153
|
capabilities: {},
|
|
154
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
154
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.6' },
|
|
155
155
|
},
|
|
156
156
|
});
|
|
157
157
|
const response = await post({
|
package/src/plugin.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
// IDEA MCP tools, and updates only IDEA MCP tool descriptions;
|
|
9
9
|
// 3. registers the `/open-in-idea` command, which opens the current project
|
|
10
10
|
// in IntelliJ IDEA (reusing a running instance) and then loads the IDE
|
|
11
|
-
// capabilities immediately
|
|
11
|
+
// capabilities immediately;
|
|
12
|
+
// 4. registers the `idea-run-config` skill (JetBrains projects only),
|
|
13
|
+
// which teaches the model how to manage `.run/*.run.xml` run
|
|
14
|
+
// configurations — a capability the IDE MCP server does not expose.
|
|
12
15
|
//
|
|
13
16
|
// Non-JetBrains projects are left untouched. Everything is manual: setup does
|
|
14
17
|
// NOT probe or connect the IDE (only the IDE-terminal env is read when a probe
|
|
@@ -37,16 +40,116 @@ import {
|
|
|
37
40
|
appendIdeGuidance,
|
|
38
41
|
appendIdeRecoveryGuidance,
|
|
39
42
|
} from './ide-guidance.js';
|
|
43
|
+
import { runConfigSkill } from './ide-run-config-skill.js';
|
|
40
44
|
|
|
41
45
|
const DEFAULT_LAUNCH_COOLDOWN_MS = 120000;
|
|
42
46
|
const DEFAULT_LAUNCH_MAX_ATTEMPTS = 5;
|
|
43
47
|
// MCP availability is a fast preflight. If it is not ready, fail fast and let
|
|
44
48
|
// the user enable MCP + Brave Mode in IDEA before retrying the command.
|
|
45
49
|
const DEFAULT_MCP_PROBE_TIMEOUT_MS = 1000;
|
|
50
|
+
// A cold IDE can take tens of seconds before its bundled MCP server listens.
|
|
51
|
+
// After `open -a`, poll for the endpoint instead of reporting failure once.
|
|
52
|
+
const DEFAULT_MCP_START_TIMEOUT_MS = 60000;
|
|
53
|
+
const MCP_START_POLL_MS = 1000;
|
|
54
|
+
const DEFAULT_TOOL_REFRESH_TIMEOUT_MS = 5000;
|
|
55
|
+
const TOOL_REFRESH_POLL_MS = 25;
|
|
56
|
+
const IDEA_READY_TOOL_IDS = new Set(['idea_read_file', 'idea_apply_patch']);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* IDEA MCP tools that must never reach the model.
|
|
60
|
+
*
|
|
61
|
+
* Grouped by the IDE's Exposed Tools page so the list stays auditable. Anything
|
|
62
|
+
* listed here is stripped from every model request, even while the IDE is up.
|
|
63
|
+
*
|
|
64
|
+
* - VCS: `idea_git_status` returns porcelain codes only and
|
|
65
|
+
* `idea_get_repositories` is covered by the native git tool, which also
|
|
66
|
+
* handles diff/log/blame.
|
|
67
|
+
* - Router: `idea_execute_tool` is the IDE's router-mode dispatcher. Router-only
|
|
68
|
+
* mode is off, so every routed tool is exposed directly with its full schema
|
|
69
|
+
* and this dispatcher is redundant. Revisit if router-only mode is enabled.
|
|
70
|
+
* - Debugger (`xdebug_*`): the Debugger MCP toolset. Rarely needed, and a real
|
|
71
|
+
* debug session is driven from the IDE window anyway.
|
|
72
|
+
* - Dev Kit MCP: IntelliJ Platform plugin-development tools (Split Mode module
|
|
73
|
+
* kinds, the New IntelliJ Module scaffold, Read/Write lock and EDT threading
|
|
74
|
+
* analysis). Useless outside IntelliJ Platform plugin work.
|
|
75
|
+
* - Inspection KTS MCP: inspection.kts authoring helpers (PSI tree, API docs,
|
|
76
|
+
* examples, run). Also IntelliJ Platform plugin-development only.
|
|
77
|
+
* - Python Environment MCP: Python interpreter detection and configuration.
|
|
78
|
+
* Hidden; the IDE-injected shell env already carries the SDK paths.
|
|
79
|
+
* - Database data sources: connections are configured by the user in the IDE,
|
|
80
|
+
* so the AI only reads and queries them; create/edit stay hidden.
|
|
81
|
+
*/
|
|
82
|
+
export const HIDDEN_IDEA_TOOLS = [
|
|
83
|
+
// VCS
|
|
84
|
+
'idea_git_status',
|
|
85
|
+
'idea_get_repositories',
|
|
86
|
+
// Router
|
|
87
|
+
'idea_execute_tool',
|
|
88
|
+
// Terminal: the plugin reads the IDE env through its own direct MCP call
|
|
89
|
+
// (`src/ide-env.js`), so the model never needs this tool. Measured value for
|
|
90
|
+
// the agent is nil: ~60s hard cutoff with no way to read the terminal buffer,
|
|
91
|
+
// and the native shell already keeps the output tail plus a full-output file.
|
|
92
|
+
'idea_execute_terminal_command',
|
|
93
|
+
// Debugger
|
|
94
|
+
'idea_xdebug_control_session',
|
|
95
|
+
'idea_xdebug_evaluate_expression',
|
|
96
|
+
'idea_xdebug_get_debugger_status',
|
|
97
|
+
'idea_xdebug_get_frame_values',
|
|
98
|
+
'idea_xdebug_get_stack',
|
|
99
|
+
'idea_xdebug_get_threads',
|
|
100
|
+
'idea_xdebug_get_value_by_path',
|
|
101
|
+
'idea_xdebug_list_breakpoints',
|
|
102
|
+
'idea_xdebug_remove_breakpoint',
|
|
103
|
+
'idea_xdebug_run_to_line',
|
|
104
|
+
'idea_xdebug_set_breakpoint',
|
|
105
|
+
'idea_xdebug_set_variable',
|
|
106
|
+
'idea_xdebug_start_debugger_session',
|
|
107
|
+
// Dev Kit MCP
|
|
108
|
+
'idea_collect_split_mode_compatibility_issues',
|
|
109
|
+
'idea_create_ij_module',
|
|
110
|
+
'idea_find_lock_requirements_usages',
|
|
111
|
+
'idea_find_threading_requirements_usages',
|
|
112
|
+
'idea_recognize_ij_module_kind',
|
|
113
|
+
'idea_recognize_split_mode_api_kind',
|
|
114
|
+
// Inspection KTS MCP
|
|
115
|
+
'idea_generate_inspection_kts_api',
|
|
116
|
+
'idea_generate_inspection_kts_examples',
|
|
117
|
+
'idea_generate_psi_tree',
|
|
118
|
+
'idea_run_inspection_kts',
|
|
119
|
+
// Python Environment MCP
|
|
120
|
+
'idea_configure_python_interpreter',
|
|
121
|
+
'idea_get_python_environment',
|
|
122
|
+
// Database data sources: the user configures them in the IDE, the AI only
|
|
123
|
+
// reads and queries them.
|
|
124
|
+
'idea_create_database_connection',
|
|
125
|
+
'idea_edit_database_connection',
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Remove the given IDEA MCP definitions from one request's tool record.
|
|
130
|
+
*
|
|
131
|
+
* @param {Record<string, unknown> | undefined} tools
|
|
132
|
+
* @param {readonly string[]} names
|
|
133
|
+
*/
|
|
134
|
+
function removeIdeaToolDefinitions(tools, names) {
|
|
135
|
+
if (!tools || typeof tools !== 'object') return 0;
|
|
136
|
+
let removed = 0;
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (!name.startsWith('idea_') || !(name in tools)) continue;
|
|
139
|
+
delete tools[name];
|
|
140
|
+
removed += 1;
|
|
141
|
+
}
|
|
142
|
+
return removed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @param {ReadonlyArray<{ id?: string }> | undefined} tools */
|
|
146
|
+
function toolIds(tools) {
|
|
147
|
+
return (tools ?? []).map((tool) => tool.id).filter((id) => typeof id === 'string').sort();
|
|
148
|
+
}
|
|
46
149
|
|
|
47
150
|
/**
|
|
48
151
|
* @typedef {{
|
|
49
|
-
* status: 'connected' | 'mcp-unavailable' | 'launch-failed' | 'disabled',
|
|
152
|
+
* status: 'connected' | 'tools-loading' | 'mcp-unavailable' | 'launch-failed' | 'disabled',
|
|
50
153
|
* port?: number,
|
|
51
154
|
* opened?: boolean,
|
|
52
155
|
* unavailableNoticeSent?: boolean
|
|
@@ -71,10 +174,12 @@ const asPositiveNumber = (value, fallback) =>
|
|
|
71
174
|
export function ideFeedback(result) {
|
|
72
175
|
switch (result?.status) {
|
|
73
176
|
case 'connected':
|
|
74
|
-
return 'IDE MCP 已连接;
|
|
177
|
+
return 'IDE MCP 已连接;IDEA 原生工具已注册并可用于后续请求。';
|
|
178
|
+
case 'tools-loading':
|
|
179
|
+
return 'IDE MCP 已连接,但原生 IDEA 工具列表尚未完成注册。请稍后再次执行 /open-in-idea。';
|
|
75
180
|
case 'mcp-unavailable':
|
|
76
181
|
return result?.opened
|
|
77
|
-
? '已请求打开/激活 IntelliJ IDEA
|
|
182
|
+
? '已请求打开/激活 IntelliJ IDEA 并等待 MCP 服务启动,但未检测到 MCP 服务。请在 IDEA 中开启 MCP 服务(Settings → MCP Server,需 2026.2+)并启用 Brave Mode,然后再次执行 /open-in-idea。'
|
|
78
183
|
: '未检测到 IntelliJ IDEA 的 MCP 服务。请确认 IDEA 已打开,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-idea。';
|
|
79
184
|
case 'launch-failed':
|
|
80
185
|
return '无法拉起 IntelliJ IDEA(未找到应用或启动失败)。请确认已安装 IDEA,或手动打开后重试 /open-in-idea。';
|
|
@@ -112,6 +217,8 @@ export default {
|
|
|
112
217
|
* launchCooldownMs?: number,
|
|
113
218
|
* launchMaxAttempts?: number,
|
|
114
219
|
* mcpProbeTimeoutMs?: number,
|
|
220
|
+
* mcpStartTimeoutMs?: number,
|
|
221
|
+
* toolRefreshTimeoutMs?: number,
|
|
115
222
|
* feedback?: false | 'message',
|
|
116
223
|
* },
|
|
117
224
|
* command?: {
|
|
@@ -121,11 +228,26 @@ export default {
|
|
|
121
228
|
* execute: (input: { sessionID: string, prompt: { text: string }, delivery: unknown }) => Promise<void>,
|
|
122
229
|
* }) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
123
230
|
* },
|
|
231
|
+
* skill?: {
|
|
232
|
+
* transform: (cb: (editor: { add: (definition: {
|
|
233
|
+
* id: string,
|
|
234
|
+
* name: string,
|
|
235
|
+
* description: string,
|
|
236
|
+
* location: string,
|
|
237
|
+
* content: string,
|
|
238
|
+
* }) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
239
|
+
* },
|
|
124
240
|
* session?: {
|
|
125
241
|
* prompt: (input: { sessionID: string, text: string, delivery: unknown }) => Promise<unknown>,
|
|
126
|
-
* hook: (name: string, cb: (event: {
|
|
242
|
+
* hook: (name: string, cb: (event: {
|
|
243
|
+
* sessionID?: string,
|
|
244
|
+
* agent?: string,
|
|
245
|
+
* system?: unknown[],
|
|
246
|
+
* tools?: Record<string, unknown>,
|
|
247
|
+
* }) => void | Promise<void>) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
127
248
|
* },
|
|
128
249
|
* tool?: {
|
|
250
|
+
* list?: () => Promise<ReadonlyArray<{ id?: string }>>,
|
|
129
251
|
* reload?: () => Promise<void>,
|
|
130
252
|
* },
|
|
131
253
|
* mcp: {
|
|
@@ -138,6 +260,11 @@ export default {
|
|
|
138
260
|
async setup(ctx) {
|
|
139
261
|
const projectPath = currentProjectPath(ctx.location);
|
|
140
262
|
if (!projectPath) return;
|
|
263
|
+
// Where IDEA tools can exist at all. Computed once: the recovery guidance is
|
|
264
|
+
// injected per model request, so it must not cost a filesystem check each
|
|
265
|
+
// time, and unrelated projects must not receive IDEA prompts they can never
|
|
266
|
+
// use.
|
|
267
|
+
const isIdeaProject = hasIdeaDirectory(projectPath);
|
|
141
268
|
|
|
142
269
|
const options = ctx.options || {};
|
|
143
270
|
const ports = asArray(options.ports, DEFAULT_PORTS);
|
|
@@ -145,6 +272,8 @@ export default {
|
|
|
145
272
|
const injectGuidance = options.injectGuidance !== false;
|
|
146
273
|
const ideApp = resolveIdeApp(options.openInIde);
|
|
147
274
|
const mcpProbeTimeoutMs = asPositiveNumber(options.mcpProbeTimeoutMs, DEFAULT_MCP_PROBE_TIMEOUT_MS);
|
|
275
|
+
const mcpStartTimeoutMs = asPositiveNumber(options.mcpStartTimeoutMs, DEFAULT_MCP_START_TIMEOUT_MS);
|
|
276
|
+
const toolRefreshTimeoutMs = asPositiveNumber(options.toolRefreshTimeoutMs, DEFAULT_TOOL_REFRESH_TIMEOUT_MS);
|
|
148
277
|
// "message" (default) injects a clearly-labelled notification that the
|
|
149
278
|
// model only acknowledges ("收到"); the user sees the clean notice via
|
|
150
279
|
// metadata.displayText. false is silent.
|
|
@@ -153,9 +282,6 @@ export default {
|
|
|
153
282
|
cooldownMs: asPositiveNumber(options.launchCooldownMs, DEFAULT_LAUNCH_COOLDOWN_MS),
|
|
154
283
|
maxAttempts: asPositiveNumber(options.launchMaxAttempts, DEFAULT_LAUNCH_MAX_ATTEMPTS),
|
|
155
284
|
});
|
|
156
|
-
|
|
157
|
-
/** @type {number | undefined} */
|
|
158
|
-
let activePort;
|
|
159
285
|
let currentEnv = { env: {}, prependPath: [] };
|
|
160
286
|
|
|
161
287
|
/** @type {Array<{ dispose: () => Promise<void> | void }>} */
|
|
@@ -166,10 +292,11 @@ export default {
|
|
|
166
292
|
let sessionRegistration;
|
|
167
293
|
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
168
294
|
let commandRegistration;
|
|
295
|
+
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
296
|
+
let skillRegistration;
|
|
169
297
|
let disposed = false;
|
|
170
298
|
|
|
171
299
|
const deactivateIde = async () => {
|
|
172
|
-
activePort = undefined;
|
|
173
300
|
const current = registrations.slice();
|
|
174
301
|
registrations.length = 0;
|
|
175
302
|
if (current.length === 0) return;
|
|
@@ -182,31 +309,66 @@ export default {
|
|
|
182
309
|
}
|
|
183
310
|
};
|
|
184
311
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
312
|
+
const waitForIdeaTools = async () => {
|
|
313
|
+
if (typeof ctx.tool?.list !== 'function') return false;
|
|
314
|
+
const deadline = Date.now() + toolRefreshTimeoutMs;
|
|
315
|
+
while (Date.now() < deadline) {
|
|
316
|
+
try {
|
|
317
|
+
const ids = toolIds(await ctx.tool.list());
|
|
318
|
+
if ([...IDEA_READY_TOOL_IDS].every((id) => ids.includes(id))) return true;
|
|
319
|
+
} catch {
|
|
320
|
+
// The MCP catalog may still be settling; retry within the bounded wait.
|
|
321
|
+
}
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, TOOL_REFRESH_POLL_MS));
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const refreshIdeTools = async () => {
|
|
191
328
|
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
192
329
|
// Reconcile the MCP servers above, then replay the tool registry so the
|
|
193
330
|
// refreshed catalog lands in the next model request's tool snapshot.
|
|
194
331
|
// Without this, a session that already captured a snapshot keeps the old
|
|
195
332
|
// tool list until the session is reopened.
|
|
196
333
|
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
334
|
+
return waitForIdeaTools();
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const activateIde = async (port) => {
|
|
338
|
+
const mcpRegistration = await ctx.mcp.transform((editor) => {
|
|
339
|
+
editor.set(IDEA_SERVER_NAME, serverConfig(port, projectPath));
|
|
340
|
+
});
|
|
341
|
+
registrations = [mcpRegistration];
|
|
342
|
+
return refreshIdeTools();
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Is the IDE MCP server contributing tools right now? OpenCode's registry is
|
|
347
|
+
* the source of truth: it adds the tools when a server connects and drops
|
|
348
|
+
* them when the connection stops (`stopServer` publishes `mcp.tools.changed`
|
|
349
|
+
* and the registry reloads). No probe of our own is involved, and only
|
|
350
|
+
* `/open-in-idea` asks, so the read is user-initiated.
|
|
351
|
+
*/
|
|
352
|
+
const ideaToolsRegistered = async () => {
|
|
353
|
+
if (typeof ctx.tool?.list !== 'function') return false;
|
|
354
|
+
try {
|
|
355
|
+
return toolIds(await ctx.tool.list()).some((id) => id.startsWith('idea_'));
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
197
359
|
};
|
|
198
360
|
|
|
199
361
|
const reconcileOnce = async ({ probeIde = true } = {}) => {
|
|
200
|
-
if (disposed) return;
|
|
362
|
+
if (disposed) return false;
|
|
201
363
|
|
|
202
364
|
// Manual: only `/open-in-idea` probes the IDE. Setup calls this with
|
|
203
365
|
// `probeIde: false` so it never connects until the command asks.
|
|
204
366
|
const port = probeIde ? await findIdePort(ports, { timeoutMs: mcpProbeTimeoutMs }) : undefined;
|
|
205
|
-
const isJetBrains =
|
|
367
|
+
const isJetBrains = isIdeaProject || port !== undefined;
|
|
206
368
|
if (!isJetBrains) {
|
|
207
369
|
currentEnv = { env: {}, prependPath: [] };
|
|
208
370
|
await deactivateIde();
|
|
209
|
-
return;
|
|
371
|
+
return false;
|
|
210
372
|
}
|
|
211
373
|
|
|
212
374
|
// Environment: read from the IDE integrated terminal, the single source
|
|
@@ -220,22 +382,46 @@ export default {
|
|
|
220
382
|
shellRegistration = await ctx.shell.hook('create.before', (input) => applyEnv(input, currentEnv));
|
|
221
383
|
}
|
|
222
384
|
|
|
223
|
-
|
|
385
|
+
// A live server is only refreshed; a dead one must be replaced, because
|
|
386
|
+
// `reconcile()` skips an unchanged config and would never reconnect it.
|
|
387
|
+
// The registry says which case this is.
|
|
388
|
+
if (port !== undefined && (await ideaToolsRegistered())) return refreshIdeTools();
|
|
224
389
|
await deactivateIde();
|
|
225
|
-
if (port
|
|
390
|
+
if (port === undefined) return false;
|
|
391
|
+
return activateIde(port);
|
|
226
392
|
};
|
|
227
393
|
|
|
228
394
|
// Serialize reconciles so overlapping callers can never register/activate
|
|
229
395
|
// the IDE concurrently (which would leak registrations).
|
|
230
396
|
let reconcileChain = Promise.resolve();
|
|
231
|
-
const
|
|
232
|
-
const next = reconcileChain.then(
|
|
397
|
+
const enqueue = (task) => {
|
|
398
|
+
const next = reconcileChain.then(task);
|
|
233
399
|
reconcileChain = next.then(
|
|
234
400
|
() => {},
|
|
235
401
|
() => {},
|
|
236
402
|
);
|
|
237
403
|
return next;
|
|
238
404
|
};
|
|
405
|
+
const reconcile = (options) => enqueue(() => reconcileOnce(options));
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* `open -a` only requests activation/opening. A cold IDE needs time before
|
|
409
|
+
* its bundled MCP server listens, so poll until the endpoint appears. This
|
|
410
|
+
* is the step that makes a single `/open-in-idea` enough on a cold start.
|
|
411
|
+
*
|
|
412
|
+
* @returns {Promise<number | undefined>}
|
|
413
|
+
*/
|
|
414
|
+
const waitForIdeServer = async () => {
|
|
415
|
+
const deadline = Date.now() + mcpStartTimeoutMs;
|
|
416
|
+
while (Date.now() < deadline) {
|
|
417
|
+
const port = await findIdePort(ports, { timeoutMs: mcpProbeTimeoutMs }).catch(() => undefined);
|
|
418
|
+
if (port !== undefined) return port;
|
|
419
|
+
const remaining = deadline - Date.now();
|
|
420
|
+
if (remaining <= 0) break;
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(MCP_START_POLL_MS, remaining)));
|
|
422
|
+
}
|
|
423
|
+
return undefined;
|
|
424
|
+
};
|
|
239
425
|
|
|
240
426
|
// Re-entrant: repeat invocations share one open request so the IDE is not
|
|
241
427
|
// spawned twice while the application is starting.
|
|
@@ -277,9 +463,15 @@ export default {
|
|
|
277
463
|
guard.markSpawn();
|
|
278
464
|
const launched = await openInIde(projectPath, { app: ideApp });
|
|
279
465
|
if (!launched) return { status: 'launch-failed' };
|
|
280
|
-
// `open -a` only requests activation/opening.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
466
|
+
// `open -a` only requests activation/opening. A cold IDE needs time to
|
|
467
|
+
// start its bundled MCP server, so wait for the endpoint instead of
|
|
468
|
+
// reporting failure immediately.
|
|
469
|
+
const started = await waitForIdeServer();
|
|
470
|
+
if (started !== undefined) {
|
|
471
|
+
guard.reset();
|
|
472
|
+
launchNoticeSent = false;
|
|
473
|
+
return { status: 'connected', port: started, unavailableNoticeSent: false };
|
|
474
|
+
}
|
|
283
475
|
await reportUnavailable(true);
|
|
284
476
|
return { status: 'mcp-unavailable', opened: true, unavailableNoticeSent: launchNoticeSent };
|
|
285
477
|
})();
|
|
@@ -312,6 +504,17 @@ export default {
|
|
|
312
504
|
}
|
|
313
505
|
};
|
|
314
506
|
|
|
507
|
+
// The run-configuration skill is static knowledge — where `.run/*.run.xml`
|
|
508
|
+
// live, how to bootstrap a schema from a real example, and how to verify a
|
|
509
|
+
// write. JetBrains projects only, and independent of `/open-in-idea`.
|
|
510
|
+
if (isIdeaProject && typeof ctx.skill?.transform === 'function') {
|
|
511
|
+
// Read the document before the transform: callbacks must stay synchronous.
|
|
512
|
+
const runConfigSkillDefinition = runConfigSkill();
|
|
513
|
+
skillRegistration = await ctx.skill.transform((editor) => {
|
|
514
|
+
editor.add(runConfigSkillDefinition);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
315
518
|
// `/open-in-idea`: manual, re-entrant trigger that also loads the IDE
|
|
316
519
|
// capabilities immediately.
|
|
317
520
|
if (typeof ctx.command?.transform === 'function') {
|
|
@@ -328,32 +531,50 @@ export default {
|
|
|
328
531
|
return unavailableNoticeSent;
|
|
329
532
|
},
|
|
330
533
|
});
|
|
331
|
-
const { port
|
|
534
|
+
const { port } = result;
|
|
535
|
+
let toolsReady = false;
|
|
332
536
|
try {
|
|
333
537
|
// Do not repeat the expensive probe when the preflight already
|
|
334
538
|
// found no MCP endpoint. The command will retry after the user
|
|
335
539
|
// enables MCP + Brave Mode.
|
|
336
|
-
await reconcile({ probeIde: port !== undefined });
|
|
540
|
+
toolsReady = (await reconcile({ probeIde: port !== undefined })) === true;
|
|
337
541
|
} catch {
|
|
338
542
|
// capability load is best effort; feedback still matters
|
|
339
543
|
}
|
|
544
|
+
const finalResult = result.status === 'connected' && !toolsReady
|
|
545
|
+
? { ...result, status: 'tools-loading' }
|
|
546
|
+
: result;
|
|
340
547
|
// The unavailable notice is the final result for this command. If
|
|
341
548
|
// it was already delivered, do not show the same warning twice.
|
|
342
|
-
const unavailableAlreadySent = unavailableNoticeSent ||
|
|
343
|
-
if (!unavailableAlreadySent || status !== 'mcp-unavailable') {
|
|
344
|
-
await sendFeedback(input,
|
|
549
|
+
const unavailableAlreadySent = unavailableNoticeSent || result.unavailableNoticeSent;
|
|
550
|
+
if (!unavailableAlreadySent || finalResult.status !== 'mcp-unavailable') {
|
|
551
|
+
await sendFeedback(input, finalResult);
|
|
345
552
|
}
|
|
346
553
|
},
|
|
347
554
|
});
|
|
348
555
|
});
|
|
349
556
|
}
|
|
350
557
|
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
558
|
+
// Hide the tools the model must not see, and point it at the rest. This hook
|
|
559
|
+
// runs for every agent-loop step (including tool-driven continuations), so
|
|
560
|
+
// it is deliberately a pure function of the request: no I/O, no cached
|
|
561
|
+
// connection state, and no action of its own.
|
|
562
|
+
//
|
|
563
|
+
// `event.tools` is this request's tool snapshot, and OpenCode's own registry
|
|
564
|
+
// adds IDEA tools when the server connects and drops them when it stops
|
|
565
|
+
// (`stopServer` publishes `mcp.tools.changed`, which the registry turns into
|
|
566
|
+
// a reload). So the snapshot is also the honest answer to "is the IDE
|
|
567
|
+
// serving?".
|
|
568
|
+
if (typeof ctx.session?.hook === 'function') {
|
|
354
569
|
sessionRegistration = await ctx.session.hook('context', (event) => {
|
|
355
|
-
|
|
356
|
-
|
|
570
|
+
const tools = event?.tools;
|
|
571
|
+
const serving = tools !== undefined && Object.keys(tools).some((name) => name.startsWith('idea_'));
|
|
572
|
+
if (serving) removeIdeaToolDefinitions(tools, HIDDEN_IDEA_TOOLS);
|
|
573
|
+
if (isIdeaProject || serving) appendIdeRecoveryGuidance(event?.system);
|
|
574
|
+
if (!injectGuidance) return;
|
|
575
|
+
// Guidance promises tools the model can call, so it only goes out when
|
|
576
|
+
// they are actually in this request.
|
|
577
|
+
if (!serving) return;
|
|
357
578
|
appendIdeGuidance(event?.system, projectPath);
|
|
358
579
|
});
|
|
359
580
|
}
|
|
@@ -381,6 +602,13 @@ export default {
|
|
|
381
602
|
// best effort
|
|
382
603
|
}
|
|
383
604
|
}
|
|
605
|
+
if (skillRegistration) {
|
|
606
|
+
try {
|
|
607
|
+
await skillRegistration.dispose();
|
|
608
|
+
} catch {
|
|
609
|
+
// best effort
|
|
610
|
+
}
|
|
611
|
+
}
|
|
384
612
|
await deactivateIde();
|
|
385
613
|
};
|
|
386
614
|
},
|