opencode-idea 0.0.5 → 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 +10 -0
- package/README.md +141 -50
- package/package.json +3 -1
- 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 +145 -76
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.0.5
|
|
4
14
|
|
|
5
15
|
- **冷启动等待 MCP 端点**:`/open-in-idea` 在 `open -a` 之后按 `mcpStartTimeoutMs`(默认 60s)轮询,直到 IDEA 自带的 MCP server 开始监听。冷启动时一次命令即可连上,不再像之前那样探测一次失败就报「未检测到 MCP 服务」。
|
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,6 +14,8 @@
|
|
|
14
14
|
"src/env.js",
|
|
15
15
|
"src/ide-launcher.js",
|
|
16
16
|
"src/ide-guidance.js",
|
|
17
|
+
"src/ide-run-config-skill.js",
|
|
18
|
+
"src/ide-run-config-skill.md",
|
|
17
19
|
"README.md",
|
|
18
20
|
"CHANGELOG.md"
|
|
19
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
|
|
@@ -23,7 +26,6 @@ import {
|
|
|
23
26
|
DEFAULT_PORTS,
|
|
24
27
|
IDEA_SERVER_NAME,
|
|
25
28
|
findIdePort,
|
|
26
|
-
probePort,
|
|
27
29
|
serverConfig,
|
|
28
30
|
} from './idea-mcp.js';
|
|
29
31
|
import { readIdeTerminalEnv } from './ide-env.js';
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
appendIdeGuidance,
|
|
39
41
|
appendIdeRecoveryGuidance,
|
|
40
42
|
} from './ide-guidance.js';
|
|
43
|
+
import { runConfigSkill } from './ide-run-config-skill.js';
|
|
41
44
|
|
|
42
45
|
const DEFAULT_LAUNCH_COOLDOWN_MS = 120000;
|
|
43
46
|
const DEFAULT_LAUNCH_MAX_ATTEMPTS = 5;
|
|
@@ -53,15 +56,86 @@ const TOOL_REFRESH_POLL_MS = 25;
|
|
|
53
56
|
const IDEA_READY_TOOL_IDS = new Set(['idea_read_file', 'idea_apply_patch']);
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
|
-
*
|
|
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.
|
|
57
130
|
*
|
|
58
131
|
* @param {Record<string, unknown> | undefined} tools
|
|
132
|
+
* @param {readonly string[]} names
|
|
59
133
|
*/
|
|
60
|
-
function removeIdeaToolDefinitions(tools) {
|
|
134
|
+
function removeIdeaToolDefinitions(tools, names) {
|
|
61
135
|
if (!tools || typeof tools !== 'object') return 0;
|
|
62
136
|
let removed = 0;
|
|
63
|
-
for (const name of
|
|
64
|
-
if (!name.startsWith('idea_')) continue;
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (!name.startsWith('idea_') || !(name in tools)) continue;
|
|
65
139
|
delete tools[name];
|
|
66
140
|
removed += 1;
|
|
67
141
|
}
|
|
@@ -154,6 +228,15 @@ export default {
|
|
|
154
228
|
* execute: (input: { sessionID: string, prompt: { text: string }, delivery: unknown }) => Promise<void>,
|
|
155
229
|
* }) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
156
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
|
+
* },
|
|
157
240
|
* session?: {
|
|
158
241
|
* prompt: (input: { sessionID: string, text: string, delivery: unknown }) => Promise<unknown>,
|
|
159
242
|
* hook: (name: string, cb: (event: {
|
|
@@ -166,11 +249,6 @@ export default {
|
|
|
166
249
|
* tool?: {
|
|
167
250
|
* list?: () => Promise<ReadonlyArray<{ id?: string }>>,
|
|
168
251
|
* reload?: () => Promise<void>,
|
|
169
|
-
* hook?: (name: string, cb: (event: {
|
|
170
|
-
* tool?: string,
|
|
171
|
-
* status?: string,
|
|
172
|
-
* error?: { message?: string },
|
|
173
|
-
* }) => void | Promise<void>) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
174
252
|
* },
|
|
175
253
|
* mcp: {
|
|
176
254
|
* transform: (cb: (editor: { set: (name: string, config: unknown) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
@@ -182,6 +260,11 @@ export default {
|
|
|
182
260
|
async setup(ctx) {
|
|
183
261
|
const projectPath = currentProjectPath(ctx.location);
|
|
184
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);
|
|
185
268
|
|
|
186
269
|
const options = ctx.options || {};
|
|
187
270
|
const ports = asArray(options.ports, DEFAULT_PORTS);
|
|
@@ -199,9 +282,6 @@ export default {
|
|
|
199
282
|
cooldownMs: asPositiveNumber(options.launchCooldownMs, DEFAULT_LAUNCH_COOLDOWN_MS),
|
|
200
283
|
maxAttempts: asPositiveNumber(options.launchMaxAttempts, DEFAULT_LAUNCH_MAX_ATTEMPTS),
|
|
201
284
|
});
|
|
202
|
-
/** @type {number | undefined} */
|
|
203
|
-
let activePort;
|
|
204
|
-
let activeToolsReady = false;
|
|
205
285
|
let currentEnv = { env: {}, prependPath: [] };
|
|
206
286
|
|
|
207
287
|
/** @type {Array<{ dispose: () => Promise<void> | void }>} */
|
|
@@ -213,12 +293,10 @@ export default {
|
|
|
213
293
|
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
214
294
|
let commandRegistration;
|
|
215
295
|
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
216
|
-
let
|
|
296
|
+
let skillRegistration;
|
|
217
297
|
let disposed = false;
|
|
218
298
|
|
|
219
299
|
const deactivateIde = async () => {
|
|
220
|
-
activePort = undefined;
|
|
221
|
-
activeToolsReady = false;
|
|
222
300
|
const current = registrations.slice();
|
|
223
301
|
registrations.length = 0;
|
|
224
302
|
if (current.length === 0) return;
|
|
@@ -247,15 +325,13 @@ export default {
|
|
|
247
325
|
};
|
|
248
326
|
|
|
249
327
|
const refreshIdeTools = async () => {
|
|
250
|
-
activeToolsReady = false;
|
|
251
328
|
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
252
329
|
// Reconcile the MCP servers above, then replay the tool registry so the
|
|
253
330
|
// refreshed catalog lands in the next model request's tool snapshot.
|
|
254
331
|
// Without this, a session that already captured a snapshot keeps the old
|
|
255
332
|
// tool list until the session is reopened.
|
|
256
333
|
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
257
|
-
|
|
258
|
-
return activeToolsReady;
|
|
334
|
+
return waitForIdeaTools();
|
|
259
335
|
};
|
|
260
336
|
|
|
261
337
|
const activateIde = async (port) => {
|
|
@@ -263,37 +339,23 @@ export default {
|
|
|
263
339
|
editor.set(IDEA_SERVER_NAME, serverConfig(port, projectPath));
|
|
264
340
|
});
|
|
265
341
|
registrations = [mcpRegistration];
|
|
266
|
-
activePort = port;
|
|
267
342
|
return refreshIdeTools();
|
|
268
343
|
};
|
|
269
344
|
|
|
270
|
-
/** @type {Promise<boolean> | undefined} */
|
|
271
|
-
let healthCheck;
|
|
272
345
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* `/open-in-idea`
|
|
278
|
-
*
|
|
279
|
-
* @returns {Promise<boolean>}
|
|
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.
|
|
280
351
|
*/
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const available = await probePort(port, { timeoutMs: mcpProbeTimeoutMs });
|
|
287
|
-
if (available) return true;
|
|
288
|
-
if (activePort !== port) return true;
|
|
289
|
-
await deactivateIde();
|
|
290
|
-
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
291
|
-
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
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 {
|
|
292
357
|
return false;
|
|
293
|
-
}
|
|
294
|
-
healthCheck = undefined;
|
|
295
|
-
});
|
|
296
|
-
return healthCheck;
|
|
358
|
+
}
|
|
297
359
|
};
|
|
298
360
|
|
|
299
361
|
const reconcileOnce = async ({ probeIde = true } = {}) => {
|
|
@@ -302,7 +364,7 @@ export default {
|
|
|
302
364
|
// Manual: only `/open-in-idea` probes the IDE. Setup calls this with
|
|
303
365
|
// `probeIde: false` so it never connects until the command asks.
|
|
304
366
|
const port = probeIde ? await findIdePort(ports, { timeoutMs: mcpProbeTimeoutMs }) : undefined;
|
|
305
|
-
const isJetBrains =
|
|
367
|
+
const isJetBrains = isIdeaProject || port !== undefined;
|
|
306
368
|
if (!isJetBrains) {
|
|
307
369
|
currentEnv = { env: {}, prependPath: [] };
|
|
308
370
|
await deactivateIde();
|
|
@@ -320,10 +382,10 @@ export default {
|
|
|
320
382
|
shellRegistration = await ctx.shell.hook('create.before', (input) => applyEnv(input, currentEnv));
|
|
321
383
|
}
|
|
322
384
|
|
|
323
|
-
// A
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
if (port !== undefined &&
|
|
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();
|
|
327
389
|
await deactivateIde();
|
|
328
390
|
if (port === undefined) return false;
|
|
329
391
|
return activateIde(port);
|
|
@@ -332,14 +394,15 @@ export default {
|
|
|
332
394
|
// Serialize reconciles so overlapping callers can never register/activate
|
|
333
395
|
// the IDE concurrently (which would leak registrations).
|
|
334
396
|
let reconcileChain = Promise.resolve();
|
|
335
|
-
const
|
|
336
|
-
const next = reconcileChain.then(
|
|
397
|
+
const enqueue = (task) => {
|
|
398
|
+
const next = reconcileChain.then(task);
|
|
337
399
|
reconcileChain = next.then(
|
|
338
400
|
() => {},
|
|
339
401
|
() => {},
|
|
340
402
|
);
|
|
341
403
|
return next;
|
|
342
404
|
};
|
|
405
|
+
const reconcile = (options) => enqueue(() => reconcileOnce(options));
|
|
343
406
|
|
|
344
407
|
/**
|
|
345
408
|
* `open -a` only requests activation/opening. A cold IDE needs time before
|
|
@@ -441,6 +504,17 @@ export default {
|
|
|
441
504
|
}
|
|
442
505
|
};
|
|
443
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
|
+
|
|
444
518
|
// `/open-in-idea`: manual, re-entrant trigger that also loads the IDE
|
|
445
519
|
// capabilities immediately.
|
|
446
520
|
if (typeof ctx.command?.transform === 'function') {
|
|
@@ -481,35 +555,30 @@ export default {
|
|
|
481
555
|
});
|
|
482
556
|
}
|
|
483
557
|
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
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?".
|
|
488
568
|
if (typeof ctx.session?.hook === 'function') {
|
|
489
|
-
sessionRegistration = await ctx.session.hook('context',
|
|
490
|
-
const ideActive = activePort !== undefined && activeToolsReady;
|
|
491
|
-
const available = ideActive ? await verifyActiveIde() : false;
|
|
569
|
+
sessionRegistration = await ctx.session.hook('context', (event) => {
|
|
492
570
|
const tools = event?.tools;
|
|
493
|
-
|
|
494
|
-
|
|
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);
|
|
495
574
|
if (!injectGuidance) return;
|
|
496
|
-
|
|
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;
|
|
497
578
|
appendIdeGuidance(event?.system, projectPath);
|
|
498
579
|
});
|
|
499
580
|
}
|
|
500
581
|
|
|
501
|
-
// A failed IDEA MCP call is also a signal that the IDE endpoint is gone.
|
|
502
|
-
// Unregister immediately so later requests cannot keep calling a dead tool.
|
|
503
|
-
if (typeof ctx.tool?.hook === 'function') {
|
|
504
|
-
toolRegistration = await ctx.tool.hook('execute.after', async (event) => {
|
|
505
|
-
if (typeof event?.tool !== 'string' || !event.tool.startsWith('idea_')) return;
|
|
506
|
-
if (event.status !== 'error') return;
|
|
507
|
-
await deactivateIde();
|
|
508
|
-
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
509
|
-
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
582
|
return async () => {
|
|
514
583
|
disposed = true;
|
|
515
584
|
if (shellRegistration) {
|
|
@@ -533,9 +602,9 @@ export default {
|
|
|
533
602
|
// best effort
|
|
534
603
|
}
|
|
535
604
|
}
|
|
536
|
-
if (
|
|
605
|
+
if (skillRegistration) {
|
|
537
606
|
try {
|
|
538
|
-
await
|
|
607
|
+
await skillRegistration.dispose();
|
|
539
608
|
} catch {
|
|
540
609
|
// best effort
|
|
541
610
|
}
|