opencode-idea 0.0.10 → 0.0.12
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/README.md +151 -88
- package/package.json +1 -3
- package/src/ide-guidance.js +29 -4
- package/src/ide-launcher.js +58 -0
- package/src/mcp/idea.js +91 -17
- package/src/plugin.js +331 -166
- package/src/env.js +0 -39
- package/src/mcp/ide-env.js +0 -101
package/src/mcp/idea.js
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
// * The IDE exposes a local MCP server. IntelliJ 2026.2+ exposes the modern
|
|
7
7
|
// Streamable-HTTP endpoint at `http://127.0.0.1:<port>/stream`; older builds
|
|
8
8
|
// only had the legacy SSE endpoint at `/sse`.
|
|
9
|
-
// *
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// `IJ_MCP_SERVER_PROJECT_PATH`.
|
|
9
|
+
// * The project a call targets is selected by the project path: the HTTP
|
|
10
|
+
// header `IJ_MCP_SERVER_PROJECT_PATH` for the remote transport, or the
|
|
11
|
+
// `IJ_MCP_SERVER_PROJECT_PATH` environment variable for the stdio bridge
|
|
12
|
+
// (see `serverConfig` / `stdioServerConfig`).
|
|
14
13
|
// * Availability probe: a single `initialize` POST to `/stream` succeeds when
|
|
15
14
|
// the MCP server is up. We do NOT check whether a specific project is open —
|
|
16
15
|
// the user triggers `/open-in-idea` for the project they want, so "server is
|
|
@@ -18,6 +17,15 @@
|
|
|
18
17
|
|
|
19
18
|
export const PROJECT_HEADER = 'IJ_MCP_SERVER_PROJECT_PATH';
|
|
20
19
|
|
|
20
|
+
/** Environment variable the IDE's stdio bridge uses to select the project. */
|
|
21
|
+
export const PROJECT_ENV = 'IJ_MCP_SERVER_PROJECT_PATH';
|
|
22
|
+
|
|
23
|
+
/** Environment variable the IDE's stdio bridge uses to reach the running server. */
|
|
24
|
+
export const PORT_ENV = 'IJ_MCP_SERVER_PORT';
|
|
25
|
+
|
|
26
|
+
/** Argument that puts the IDE launcher into its stdio MCP bridge mode. */
|
|
27
|
+
export const STDIO_ARG = 'stdioMcpServer';
|
|
28
|
+
|
|
21
29
|
/** OpenCode MCP server name registered for the IDE. */
|
|
22
30
|
export const IDEA_SERVER_NAME = 'idea';
|
|
23
31
|
|
|
@@ -44,16 +52,73 @@ const CALL_TIMEOUT_MS = 15000;
|
|
|
44
52
|
*
|
|
45
53
|
* @param {number} port
|
|
46
54
|
* @param {string} projectPath
|
|
55
|
+
* @param {{ executionTimeoutMs?: number }} [options]
|
|
47
56
|
*/
|
|
48
|
-
export function serverConfig(port, projectPath) {
|
|
49
|
-
|
|
57
|
+
export function serverConfig(port, projectPath, options = {}) {
|
|
58
|
+
/** @type {Record<string, unknown>} */
|
|
59
|
+
const config = {
|
|
50
60
|
type: 'remote',
|
|
51
61
|
url: `http://127.0.0.1:${port}${MCP_STREAM_PATH}`,
|
|
52
62
|
headers: { [PROJECT_HEADER]: projectPath },
|
|
53
63
|
codemode: false,
|
|
54
64
|
};
|
|
65
|
+
return withExecutionTimeout(config, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the OpenCode MCP server config for the IDE's **stdio bridge**.
|
|
70
|
+
*
|
|
71
|
+
* The IDE launcher (`<App>.app/Contents/MacOS/<binary>`) with the
|
|
72
|
+
* `stdioMcpServer` argument runs a small proxy that speaks MCP over
|
|
73
|
+
* stdin/stdout and connects to the running IDE MCP server selected by the
|
|
74
|
+
* environment. That makes the client side a child-process pipe instead of a
|
|
75
|
+
* Streamable-HTTP stream, so it does not suffer the idle-close that the remote
|
|
76
|
+
* transport does. The project and port are carried in the environment, matching
|
|
77
|
+
* what the remote config carries in a header and URL.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} executable absolute path to the IDE launcher
|
|
80
|
+
* @param {number} port IDE MCP port to bridge to
|
|
81
|
+
* @param {string} projectPath project root the bridge should target
|
|
82
|
+
* @param {{ executionTimeoutMs?: number }} [options]
|
|
83
|
+
*/
|
|
84
|
+
export function stdioServerConfig(executable, port, projectPath, options = {}) {
|
|
85
|
+
/** @type {Record<string, unknown>} */
|
|
86
|
+
const config = {
|
|
87
|
+
type: 'local',
|
|
88
|
+
// OpenCode's `LocalConfig.command` is a STRING ARRAY holding the executable
|
|
89
|
+
// followed by its arguments (`client.ts` does `const [command, ...args] =
|
|
90
|
+
// config.command`). There is no separate `args` field — passing one, or a
|
|
91
|
+
// bare string command, fails schema validation and the server never spawns.
|
|
92
|
+
command: [executable, STDIO_ARG],
|
|
93
|
+
environment: {
|
|
94
|
+
[PROJECT_ENV]: projectPath,
|
|
95
|
+
[PORT_ENV]: String(port),
|
|
96
|
+
},
|
|
97
|
+
codemode: false,
|
|
98
|
+
};
|
|
99
|
+
return withExecutionTimeout(config, options);
|
|
55
100
|
}
|
|
56
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Attach OpenCode's per-server execution timeout to an MCP config.
|
|
104
|
+
*
|
|
105
|
+
* OpenCode defaults this to **12 hours**. The IDE's stdio bridge never answers a
|
|
106
|
+
* call whose upstream IDE connection has dropped (it logs on stderr but sends no
|
|
107
|
+
* JSON-RPC response), so without a cap a call made after the IDE died would sit
|
|
108
|
+
* unresolved for the whole default. A finite cap turns that permanent hang into
|
|
109
|
+
* an ordinary timeout error.
|
|
110
|
+
*
|
|
111
|
+
* @param {Record<string, unknown>} config
|
|
112
|
+
* @param {{ executionTimeoutMs?: number }} options
|
|
113
|
+
*/
|
|
114
|
+
function withExecutionTimeout(config, options) {
|
|
115
|
+
if (typeof options.executionTimeoutMs === 'number') {
|
|
116
|
+
return { ...config, timeout: { execution: options.executionTimeoutMs } };
|
|
117
|
+
}
|
|
118
|
+
return config;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
57
122
|
/**
|
|
58
123
|
* Is an IDE MCP server listening on `port`? One Streamable-HTTP `initialize`
|
|
59
124
|
* POST decides it: any 2xx JSON reply means the server is up.
|
|
@@ -81,7 +146,7 @@ export async function probePort(port, options = {}) {
|
|
|
81
146
|
params: {
|
|
82
147
|
protocolVersion: PROTOCOL_VERSION,
|
|
83
148
|
capabilities: {},
|
|
84
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
149
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.12' },
|
|
85
150
|
},
|
|
86
151
|
}),
|
|
87
152
|
signal: controller.signal,
|
|
@@ -132,34 +197,43 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
|
|
|
132
197
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
133
198
|
const controller = new AbortController();
|
|
134
199
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? CALL_TIMEOUT_MS);
|
|
135
|
-
const post = (payload) =>
|
|
200
|
+
const post = (payload, sessionId) =>
|
|
136
201
|
fetchImpl(`http://127.0.0.1:${port}${MCP_STREAM_PATH}`, {
|
|
137
202
|
method: 'POST',
|
|
138
203
|
headers: {
|
|
139
204
|
'Content-Type': 'application/json',
|
|
140
205
|
Accept: 'application/json, text/event-stream',
|
|
141
206
|
[PROJECT_HEADER]: projectPath,
|
|
207
|
+
...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}),
|
|
142
208
|
},
|
|
143
209
|
body: JSON.stringify(payload),
|
|
144
210
|
signal: controller.signal,
|
|
145
211
|
});
|
|
146
212
|
try {
|
|
147
|
-
await post({
|
|
213
|
+
const initialized = await post({
|
|
148
214
|
jsonrpc: '2.0',
|
|
149
215
|
id: 1,
|
|
150
216
|
method: 'initialize',
|
|
151
217
|
params: {
|
|
152
218
|
protocolVersion: PROTOCOL_VERSION,
|
|
153
219
|
capabilities: {},
|
|
154
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
220
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.12' },
|
|
155
221
|
},
|
|
156
222
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
223
|
+
// The IDE's Streamable-HTTP endpoint binds follow-up requests to the session
|
|
224
|
+
// created by `initialize`: without echoing its `Mcp-Session-Id` header the
|
|
225
|
+
// server rejects the call with "Bad Request: Server not initialized"
|
|
226
|
+
// (verified against IntelliJ IDEA 2026.2.3).
|
|
227
|
+
const sessionId = initialized.headers?.get?.('mcp-session-id') ?? undefined;
|
|
228
|
+
const response = await post(
|
|
229
|
+
{
|
|
230
|
+
jsonrpc: '2.0',
|
|
231
|
+
id: 2,
|
|
232
|
+
method: 'tools/call',
|
|
233
|
+
params: { name, arguments: args },
|
|
234
|
+
},
|
|
235
|
+
sessionId,
|
|
236
|
+
);
|
|
163
237
|
if (!response.ok) return undefined;
|
|
164
238
|
const message = await parseToolResponse(response);
|
|
165
239
|
if (!message || message.error) return undefined;
|