dsh-idea-mcp 0.0.0-stage → 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 画野 (mtaech)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,156 @@
1
- # Temporary Holding Version
1
+ # dsh-idea-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DSH(DeepSeek Harness)插件:把 **IntelliJ IDEA 内置 MCP Server** 的能力包装成一组一等公民 agent 工具。
4
+
5
+ IDEA 的 MCP 对外只暴露 **一个** 路由工具 `execute_tool`——参数是一条 `工具名 --flag value` 的命令行字符串。模型得自己记住几十个工具名、参数名和引号规则,还得每次手工指定 `projectPath`,否则调用直接被拒。本插件直连 IDEA 的 MCP 服务(stdio 或 SSE),把这件事接管掉:
6
+
7
+ - **一个能力一个工具**:`idea_search_symbol`、`idea_lint_files`、`idea_build_project`…… 参数有 JSON Schema 校验,模型不用背命令语法;
8
+ - **跨工程自动解析**:按 agent 会话的 `cwd` 匹配 IDE 当前打开的工程(含 `.worktrees` 嵌套场景),自动带上 `projectPath`;
9
+ - **兜底逃生口**:`idea_execute_tool` 可以按原始命令行调用任意 IDEA MCP 工具(数据库、调试器、覆盖率等)。
10
+
11
+ ## 能力
12
+
13
+ | 工具 | 说明 |
14
+ |---|---|
15
+ | `idea_status` | 连接状态:传输方式、端点、握手到的 IDEA 版本、当前打开的工程 |
16
+ | `idea_projects` | 列出 IDE 当前打开的工程 |
17
+ | `idea_search_symbol` | 用 IDEA 索引按名字搜符号(类/方法/字段),返回文件+行列 |
18
+ | `idea_search_file` | 按文件名模糊搜文件 |
19
+ | `idea_get_symbol_info` | 对指定文件/行/列取 Quick Doc 与声明 |
20
+ | `idea_get_file_problems` | 单文件 inspections(含未解析引用等语义问题),可只看 error |
21
+ | `idea_lint_files` | 批量 inspections,一次汇总多个文件的问题 |
22
+ | `idea_analyze_calls` | 调用层次:谁调它(`INCOMING_CALLS`)/ 它调谁(`OUTGOING_CALLS`) |
23
+ | `idea_build_project` | 用 IDEA 的工程模型编译,返回错误与警告(可 `rebuild`、可只编指定文件) |
24
+ | `idea_project_modules` | 工程模块列表 |
25
+ | `idea_project_dependencies` | 工程依赖(库 + 模块间) |
26
+ | `idea_git_status` | IDEA 视角的 VCS 状态:仓库、当前分支、未提交变更 |
27
+ | `idea_execute_tool` | 逃生口:任意 IDEA MCP 工具的原生命令行(描述里附完整命令目录) |
28
+
29
+ ## 安装
30
+
31
+ 发布后:
32
+
33
+ ```bash
34
+ dsh plugin --profile <你的 profile> add dsh-idea-mcp
35
+ ```
36
+
37
+ **本地开发**装当前 checkout 时,要先打包再装——直接指向包目录会被 DSH 的 peer 兼容预检拒绝,因为本地 manifest 里的 `@deepseek-ai/dsh-*` 是 `catalog:` 协议,预检无法与运行时版本比对(`pnpm pack` 会把 `catalog:` 替换成真实范围):
38
+
39
+ ```bash
40
+ pnpm --filter dsh-idea-mcp pack --pack-destination packages/idea-mcp/dist
41
+ # 然后在会话里用 plugin_manager:install_bundle,target 指向
42
+ # <repo>/packages/idea-mcp/dist/dsh-idea-mcp-0.1.0.tgz
43
+ ```
44
+
45
+ (`dist/` 已在仓库 `.gitignore` 里。)改完 `src/` 要重新 build + pack + 重装才会生效。
46
+
47
+ 本地迭代有三个坑(都实测过):
48
+
49
+ 1. **必须 bump `version` 再 pack**。同版本同路径时 `plugin_manager` 会判定 `changed: false`(报 `ambiguous-install`),pnpm 不会重新解包,profile 里还是旧代码。
50
+ 2. **装完新版本要重启 DSH**。替换已安装的包会返回 `application: restart-required`——运行中的 host 仍持有旧模块实例,重启后才加载新 JS。
51
+ 3. **别删仍被 profile 引用的 tgz**。profile 的 `dependencies` 记着具体文件路径,删掉后 install 会直接 `ENOENT`;要等新版本装好、依赖指向新文件之后再清理旧包。
52
+
53
+ 装好后会话里直接出现 `idea_*` 工具。**前提**:IDEA 已打开,且 Settings | Tools | MCP Server 勾选了 *Enable MCP Server*。
54
+
55
+ ## 配置
56
+
57
+ bundle 的 patch 行可按需覆盖:
58
+
59
+ ```yaml
60
+ - insert:
61
+ - id: dsh-idea-mcp
62
+ name: 'dsh-idea-mcp'
63
+ config:
64
+ transport: stdio # stdio(默认)| sse
65
+ command: '' # 留空则自动探测 idea 启动器
66
+ args: ['stdioMcpServer']
67
+ url: '' # 留空则由 port 推导 http://127.0.0.1:<port>/sse
68
+ port: 0 # 0 表示用 IJ_MCP_SERVER_PORT,再退回 64342
69
+ projectPath: '' # 写死工程路径;留空则按会话 cwd 自动解析
70
+ requestTimeoutMs: 120000
71
+ idleTimeoutMs: 600000 # 空闲多久断开(stdio 会回收代理进程)
72
+ ```
73
+
74
+ | 字段 | 默认 | 说明 |
75
+ |---|---|---|
76
+ | `transport` | `stdio` | `stdio` 拉起 `idea stdioMcpServer` 代理;`sse` 直连 IDE 自己监听的 MCP 端口,少一个进程 |
77
+ | `command` / `args` | 自动探测 / `['stdioMcpServer']` | stdio 传输的可执行文件与参数 |
78
+ | `url` | `http://127.0.0.1:<port>/sse` | SSE 传输的端点 |
79
+ | `port` | `64342` | stdio 传给代理的 `IJ_MCP_SERVER_PORT`;也是 SSE 默认端口 |
80
+ | `projectPath` | 自动 | 写死后所有调用都作用于该工程 |
81
+ | `requestTimeoutMs` | `120000` | 单次 MCP 请求超时(`idea_build_project` 单独放宽到 15 分钟) |
82
+ | `idleTimeoutMs` | `600000` | 空闲这么久后断开连接,下次调用自动重连;`0` 表示常驻 |
83
+
84
+ ### 环境变量
85
+
86
+ 优先级:`DSH_IDEA_MCP_*` > 配置块 > IDEA 自己的 `IJ_MCP_SERVER_*` > 自动探测。
87
+
88
+ - `DSH_IDEA_MCP_ENABLED` / `DSH_IDEA_MCP_TRANSPORT` / `DSH_IDEA_MCP_URL` / `DSH_IDEA_MCP_COMMAND` / `DSH_IDEA_MCP_PORT` / `DSH_IDEA_MCP_PROJECT_PATH`
89
+ - `DSH_IDEA_MCP_REQUEST_TIMEOUT_MS` / `DSH_IDEA_MCP_IDLE_TIMEOUT_MS`
90
+ - `IJ_MCP_SERVER_PORT`(IDEA 标准变量)、`IJ_MCP_SERVER_PROJECT_PATH`
91
+
92
+ ## IDEA 没启动时会怎样
93
+
94
+ **插件本身不依赖 IDEA 在跑**:`apply()` 只把 13 个工具注册到 tools registry,不建立任何连接,所以 DSH 不会因为 IDEA 没开而起不来或启动变慢。连接是懒的——第一次调用 `idea_*` 时才连。
95
+
96
+ 连接前会先探测目标端口(`127.0.0.1:<port>`,或 SSE 端点的 loopback 主机):
97
+
98
+ | 情况 | 行为 |
99
+ |---|---|
100
+ | IDEA 没运行 / MCP Server 没启用 / 端口配错 | **毫秒级失败**,报 `127.0.0.1:<port> 上没有服务在监听`,并说明怎么处理。stdio 传输**不会**去 spawn 那个注定失败的代理进程 |
101
+ | IDEA 启动后重试 | 直接成功。失败状态不缓存,下一次调用自动重连 |
102
+ | IDEA 中途被关掉 | 当前调用报错(stdio 代理退出 / SSE 流断开),后续调用自动重连 |
103
+ | 只想看状态 | 调 `idea_status`:它不抛异常,返回 `connected: false` + 失败原因 + 已打开工程 |
104
+
105
+ 实测依据:`idea stdioMcpServer` 在端口无人监听时 **0.5 秒退出(exit 1)且不会启动新 IDE 进程**——它只是把 stdio 桥到 IDE 的 `IJ_MCP_SERVER_PORT` 上的纯代理。非 loopback 的 SSE 端点不做预检,直接让真实连接决定结果,避免误判。
106
+
107
+ ## 跨工程解析
108
+
109
+ IDEA 的 MCP 在不知道 `projectPath` 时会直接拒绝调用(**不会**因为只开了一个工程就自动选中),只在拒绝信息里附上已打开工程列表。本插件的处理:
110
+
111
+ 1. 命中 `projectPath` 配置 → 直接用;
112
+ 2. 否则探测 IDE 打开的工程列表(结果缓存 30 秒);
113
+ 3. 用会话 `cwd` 去匹配:取**路径最长**的那个包含 `cwd` 的工程(所以 `.worktrees/xxx` 子目录会命中 `.worktrees` 而不是外层 checkout);
114
+ 4. 只有一个工程且 cwd 不匹配 → 用这个工程(符合单人单窗口的直觉);
115
+ 5. 都不匹配 → 报错并列出所有打开工程,提示切 cwd 或显式传 `projectPath`。
116
+
117
+ ## 保证 agent 真的调用它
118
+
119
+ 工具schema 只解决"能发现",不解决"会被优先选"。除了 13 个工具定义,插件还通过 `ctx.systemPrompt.section()` 注入一段系统提示(顺序 2250,紧挨内置的 LSP 工具说明),说明这些工具何时比 grep 强、以及连不上时该退回普通文件工具。
120
+
121
+ 前提条件清单:
122
+
123
+ | 条件 | 说明 |
124
+ |---|---|
125
+ | IDEA 打开着**同一个工程目录**(或会话 cwd 的祖先) | 插件按会话 cwd 匹配打开的工程;对不上会报错并列出所有工程。多 worktree / 多工程时可写死 `projectPath` |
126
+ | Settings \| Tools \| MCP Server 勾选 *Enable MCP Server* | 没勾选时端口不监听,插件毫秒级报错 |
127
+ | **索引已就绪** | 刚切分支、Gradle sync、`idea` 冷启动后 IDEA 在重建索引:此时 `idea_search_symbol` 可能返回空/不全,`idea_get_file_problems` 可能返回 `"timedOut": true`。等 IDEA 右下角索引进度条走完再重试 |
128
+ | 前端半边 | 全栈项目里 Vue/JS 部分需要 IDEA 装了 JavaScript 插件、且前端模块在同一个工程内;`idea_project_dependencies` 会同时返回 npm 依赖 |
129
+
130
+ **模型会不会真的用它**:提示段落针对实测中模型的真实犹豫做了三件事——① 明确 `projectPath` 是按会话 cwd 自动解析的、对不上会带着工程列表报错,**不需要先探测**;② 明确什么样的问法该用它(跨重载/接口的符号解析、inspections、调用层次、编译验证),并承认唯一字面量用文本检索就够;③ 点出 `idea_analyze_calls` 只看 JVM 内调用方,**Spring/REST 端点的真实调用方在 HTTP 另一侧**,要配合前端里对 URL 的文本检索——这是 Java 全栈项目里最容易误判的一处。
131
+
132
+ **与 bash 脚本型 skill 的关系**:`ijidea-mcp` / `ijidea-mcp-database` / `ijidea-mcp-debugger` 这类 skill 会把 IDEA 包成 `~/.local/bin/ijidea-mcp` 脚本调用。同一次实测里模型并没有加载它们(直接上手工具),所以它是**潜在**而非主要的竞争路径;但在把 skill 当作强约束的会话里仍可能绕过插件。若你在同一 profile 里两者都装,建议把这些 skill 改成"优先用 `idea_*` 工具,脚本仅在未装插件时兜底"。
133
+
134
+ 自检只需一次调用:`idea_status` 会给出传输方式、端点、握手到的 IDEA 版本、已打开工程,以及在连不上时的确切原因。
135
+
136
+ ## 已知限制
137
+
138
+ - IDEA MCP 的工具集随 IDE 安装的插件而变。数据库、调试器(`xdebug_*`)、覆盖率工具来自可选插件,未安装时 `idea_execute_tool` 会拿到 `Tool '<name>' not found`。
139
+ - IDEA 的命令行解析要求**每个 flag 都带值**:`--errorsOnly true` 合法,裸 `--errorsOnly` 会被拒(插件已按此编码)。
140
+ - 服务端在 `invocationMode: VIA_ROUTER` 下不提供 `resources/*`、`prompts/*`,本插件只桥接 tools。
141
+ - 一次 `idea_build_project --rebuild true` 可能跑很久,注意 `requestTimeoutMs` 与工具自身的超时预算。
142
+
143
+ ## 开发
144
+
145
+ ```bash
146
+ pnpm --filter dsh-idea-mcp build # tsc -> lib/
147
+ pnpm --filter dsh-idea-mcp check # tsc --noEmit
148
+ pnpm --filter dsh-idea-mcp test # bun test(纯函数单测)
149
+ pnpm --filter dsh-idea-mcp smoke # 端到端:内置 mock IDEA MCP 服务,stdio + SSE 双跑
150
+ ```
151
+
152
+ `lib/` 是构建产物,不入版本控制。`scripts/smoke.mjs` 加载的是 `lib/`,改完 `src/` 要先 build。
153
+
154
+ ## 许可
155
+
156
+ MIT
@@ -0,0 +1,9 @@
1
+ # dsh-idea-mcp bundle layer: register the `idea_*` tools on the host tools
2
+ # registry so every session composed under this profile can drive IntelliJ IDEA
3
+ # through its built-in MCP server (Settings | Tools | MCP Server).
4
+ #
5
+ # Transport defaults to stdio (spawns `idea stdioMcpServer`). Set
6
+ # `transport: sse` to talk to the already-running IDE on the MCP port instead.
7
+ - insert:
8
+ - id: dsh-idea-mcp
9
+ name: 'dsh-idea-mcp'
package/icon.svg ADDED
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="IDEA MCP Tools">
2
+ <title>IDEA MCP Tools</title>
3
+ <defs>
4
+ <linearGradient id="ideaMcpPlate" x1="0" y1="0" x2="1" y2="1">
5
+ <stop offset="0" stop-color="#2b3a67"/>
6
+ <stop offset="1" stop-color="#0f1a33"/>
7
+ </linearGradient>
8
+ <linearGradient id="ideaMcpSpark" x1="0" y1="0" x2="1" y2="1">
9
+ <stop offset="0" stop-color="#6ee7ff"/>
10
+ <stop offset="1" stop-color="#4f8cff"/>
11
+ </linearGradient>
12
+ </defs>
13
+ <rect x="2" y="2" width="60" height="60" rx="15" fill="url(#ideaMcpPlate)"/>
14
+ <rect x="2.75" y="2.75" width="58.5" height="58.5" rx="14.25" fill="none" stroke="#7ea2ff" stroke-opacity="0.28" stroke-width="1.5"/>
15
+ <g fill="none" stroke="url(#ideaMcpSpark)" stroke-width="4.4" stroke-linecap="round" stroke-linejoin="round">
16
+ <path d="M21 20 L12.5 32 L21 44"/>
17
+ <path d="M43 20 L51.5 32 L43 44"/>
18
+ </g>
19
+ <circle cx="32" cy="30" r="7.4" fill="none" stroke="#eaf2ff" stroke-width="3.4"/>
20
+ <path d="M37.6 35.6 L43.4 41.4" fill="none" stroke="#eaf2ff" stroke-width="3.4" stroke-linecap="round"/>
21
+ <circle cx="32" cy="30" r="2.3" fill="#6ee7ff"/>
22
+ </svg>
@@ -0,0 +1,94 @@
1
+ /** Transport used to reach IDEA's MCP server. */
2
+ export type IdeaTransport = "stdio" | "sse";
3
+ /** Narrow logging surface, so the plugin can pass `ctx.logger` without a hard dep. */
4
+ export interface IdeaLogger {
5
+ debug?(message: string): void;
6
+ warn?(message: string): void;
7
+ }
8
+ /** Resolved client options (defaults applied by the plugin's Config). */
9
+ export interface IdeaClientOptions {
10
+ transport: IdeaTransport;
11
+ /** stdio: executable that speaks MCP over stdio (`idea`). */
12
+ command: string;
13
+ /** stdio: argv for `command`. */
14
+ args: readonly string[];
15
+ /** stdio: extra environment merged over the ambient one. */
16
+ env: Readonly<Record<string, string>>;
17
+ /** sse: full endpoint URL, e.g. `http://127.0.0.1:64342/sse`. */
18
+ url: string;
19
+ /** Per-request timeout in milliseconds. */
20
+ requestTimeoutMs: number;
21
+ /** Drop an idle connection after this many milliseconds (0 disables). */
22
+ idleTimeoutMs: number;
23
+ logger?: IdeaLogger;
24
+ }
25
+ /** One routed IDEA tool call. */
26
+ export interface IdeaCallResult {
27
+ /** Concatenated text content IDEA returned (usually a JSON string). */
28
+ text: string;
29
+ /** True when IDEA reported the call itself as failed. */
30
+ isError: boolean;
31
+ }
32
+ /** Connection facts for `idea_status`. */
33
+ export interface IdeaConnectionStatus {
34
+ transport: IdeaTransport;
35
+ /** Human-readable endpoint: the command line or the SSE URL. */
36
+ endpoint: string;
37
+ /** Whether a handshake completed and the session is still open. */
38
+ connected: boolean;
39
+ /** `serverInfo.name version` from the last handshake. */
40
+ server?: string;
41
+ /** Why the last status probe failed, when it did. */
42
+ error?: string;
43
+ /** Projects the IDE currently has open. */
44
+ projects: string[];
45
+ }
46
+ /** Raised when the transport died; callers drop it and reconnect. */
47
+ export declare class SessionClosedError extends Error {
48
+ constructor(message: string);
49
+ }
50
+ /**
51
+ * Keep only the actionable lines of the launcher's stderr.
52
+ *
53
+ * A dead IDE makes `idea stdioMcpServer` print a full Java stack trace
54
+ * (dozens of `at java.net.http...` frames) and exit 1. Those frames say
55
+ * nothing the model can act on, so they are dropped and the tail is bounded.
56
+ * @param lines - raw stderr lines, oldest first.
57
+ * @param maxLines - how many trailing lines to keep.
58
+ * @returns the condensed lines, newest last.
59
+ */
60
+ export declare function condenseStderr(lines: readonly string[], maxLines?: number): string[];
61
+ /** Resolve true when something accepts a TCP connection at `host:port`. */
62
+ export declare function probeTcp(host: string, port: number, timeoutMs?: number): Promise<boolean>;
63
+ /**
64
+ * A cached, lazily connected IDEA MCP client. One instance serves every tool
65
+ * call in the plugin; a dead transport is dropped and re-established on the
66
+ * next call.
67
+ */
68
+ export declare class IdeaClient {
69
+ #private;
70
+ constructor(options: IdeaClientOptions);
71
+ /** Endpoint description used in status output and error messages. */
72
+ get endpoint(): string;
73
+ /** Call IDEA's router tool with a ready-made command line. */
74
+ execute(command: string, call?: {
75
+ projectPath?: string;
76
+ timeoutMs?: number;
77
+ signal?: AbortSignal;
78
+ }): Promise<IdeaCallResult>;
79
+ /**
80
+ * Enumerate the IDE's open projects. IDEA only volunteers this list inside
81
+ * the rejection message of a call made without `projectPath`, so a harmless
82
+ * `search_file` probe is issued (and its rejection parsed) on purpose.
83
+ */
84
+ listOpenProjects(options?: {
85
+ signal?: AbortSignal;
86
+ force?: boolean;
87
+ }): Promise<string[]>;
88
+ /** Connection facts for `idea_status`; never throws. */
89
+ status(options?: {
90
+ signal?: AbortSignal;
91
+ }): Promise<IdeaConnectionStatus>;
92
+ /** Drop the current connection and clear cached project data. */
93
+ dispose(): void;
94
+ }