@khalilgharbaoui/opencode-claude-code-plugin 0.3.1 → 0.4.1
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/dist/index.d.ts +39 -1
- package/dist/index.js +162 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,7 @@ interface ClaudeCodeConfig {
|
|
|
114
114
|
proxyTools?: string[];
|
|
115
115
|
webSearch?: WebSearchRouting;
|
|
116
116
|
hotReloadMcp?: boolean;
|
|
117
|
+
proxyOpencodeMcpTools?: boolean;
|
|
117
118
|
}
|
|
118
119
|
type WebSearchRouting = "claude" | "disabled" | (string & {});
|
|
119
120
|
interface ClaudeCodeProviderSettings {
|
|
@@ -191,6 +192,21 @@ interface ClaudeCodeProviderSettings {
|
|
|
191
192
|
* survives MCP changes until the chat is reset).
|
|
192
193
|
*/
|
|
193
194
|
hotReloadMcp?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Route opencode MCP server tools through the in-process `opencode_proxy`
|
|
197
|
+
* MCP server instead of bridging them directly into Claude CLI's
|
|
198
|
+
* `--mcp-config`. With both layers configured for the same MCP server,
|
|
199
|
+
* direct bridging causes each tool invocation to execute twice — once by
|
|
200
|
+
* Claude CLI's own MCP child process and once by opencode. Routing through
|
|
201
|
+
* the proxy keeps a single execution site (opencode) while preserving the
|
|
202
|
+
* tool-call/result surface in opencode's UI and its permission prompts.
|
|
203
|
+
*
|
|
204
|
+
* Defaults to `true`. Set to `false` to restore the prior direct-bridge
|
|
205
|
+
* behavior (Claude CLI executes MCP tools itself; opencode also re-executes
|
|
206
|
+
* — accept the duplication if you need Claude to invoke the tool without
|
|
207
|
+
* an opencode round-trip).
|
|
208
|
+
*/
|
|
209
|
+
proxyOpencodeMcpTools?: boolean;
|
|
194
210
|
}
|
|
195
211
|
type PermissionMode = "acceptEdits" | "auto" | "bypassPermissions" | "default" | "dontAsk" | "plan";
|
|
196
212
|
type ControlRequestBehavior = "allow" | "deny";
|
|
@@ -305,6 +321,16 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
|
|
|
305
321
|
private effectiveMcpConfig;
|
|
306
322
|
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
307
323
|
private resolvedProxyTools;
|
|
324
|
+
/**
|
|
325
|
+
* Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
|
|
326
|
+
* through the in-process proxy instead of being bridged into Claude CLI's
|
|
327
|
+
* `--mcp-config`. Direct bridging causes double execution because both
|
|
328
|
+
* Claude CLI's own MCP child and opencode hold their own connection to
|
|
329
|
+
* the same server; routing through the proxy keeps a single execution
|
|
330
|
+
* site (opencode). Returns null when the feature is disabled, the SDK
|
|
331
|
+
* client is unavailable, or no MCP servers are configured.
|
|
332
|
+
*/
|
|
333
|
+
private resolvedProxyMcpTools;
|
|
308
334
|
/**
|
|
309
335
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
310
336
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
@@ -339,6 +365,18 @@ interface BridgedMcp {
|
|
|
339
365
|
path: string;
|
|
340
366
|
/** Stable hash of the merged opencode mcp block (pre-translation). */
|
|
341
367
|
hash: string;
|
|
368
|
+
/**
|
|
369
|
+
* Names of opencode MCP servers that were bridged into Claude CLI's
|
|
370
|
+
* `--mcp-config`. Excludes any servers passed in `excludeServers`.
|
|
371
|
+
*/
|
|
372
|
+
serverNames: string[];
|
|
373
|
+
/**
|
|
374
|
+
* Names of every enabled opencode MCP server after merge + runtime
|
|
375
|
+
* overlay, regardless of whether they ended up bridged or excluded.
|
|
376
|
+
* Callers (e.g. the proxy-tool builder) use this to decide which
|
|
377
|
+
* `<server>_<tool>` IDs in opencode's tool catalog are MCP-origin.
|
|
378
|
+
*/
|
|
379
|
+
allEnabledServerNames: string[];
|
|
342
380
|
}
|
|
343
381
|
/**
|
|
344
382
|
* Per-server runtime status from opencode's `client.mcp.status()`. Used as
|
|
@@ -362,7 +400,7 @@ type RuntimeMcpStatus = Record<string, string>;
|
|
|
362
400
|
* return its path + a stable hash. Returns null when no enabled MCP servers
|
|
363
401
|
* remain after the merge + overlay.
|
|
364
402
|
*/
|
|
365
|
-
declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus): BridgedMcp | null;
|
|
403
|
+
declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string>): BridgedMcp | null;
|
|
366
404
|
|
|
367
405
|
declare const defaultModels: Record<string, OpenCodeModel>;
|
|
368
406
|
|
package/dist/index.js
CHANGED
|
@@ -637,7 +637,7 @@ function mergeMcp(target, source) {
|
|
|
637
637
|
}
|
|
638
638
|
return out;
|
|
639
639
|
}
|
|
640
|
-
function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
640
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
641
641
|
const worktree = detectWorktree(cwd);
|
|
642
642
|
let merged = {};
|
|
643
643
|
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
@@ -676,15 +676,37 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
676
676
|
merged[name] = { ...base, enabled: status === "connected" };
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
|
+
const allEnabledServerNames = [];
|
|
680
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
681
|
+
if (!spec || typeof spec !== "object") continue;
|
|
682
|
+
const enabled = spec.enabled;
|
|
683
|
+
if (enabled === false) continue;
|
|
684
|
+
allEnabledServerNames.push(name);
|
|
685
|
+
}
|
|
679
686
|
const servers = {};
|
|
687
|
+
const bridgedServerNames = [];
|
|
680
688
|
for (const [name, spec] of Object.entries(merged)) {
|
|
681
689
|
if (!spec || typeof spec !== "object") continue;
|
|
690
|
+
if (excludeServers?.has(name)) continue;
|
|
682
691
|
const translated = translateServer(name, spec);
|
|
683
|
-
if (translated)
|
|
692
|
+
if (translated) {
|
|
693
|
+
servers[name] = translated;
|
|
694
|
+
bridgedServerNames.push(name);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
698
|
+
const hash = crypto.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
699
|
+
if (Object.keys(servers).length === 0) {
|
|
700
|
+
const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
|
|
701
|
+
if (!allEnabledServersExcluded) return null;
|
|
702
|
+
return {
|
|
703
|
+
path: "",
|
|
704
|
+
hash,
|
|
705
|
+
serverNames: [],
|
|
706
|
+
allEnabledServerNames
|
|
707
|
+
};
|
|
684
708
|
}
|
|
685
|
-
if (Object.keys(servers).length === 0) return null;
|
|
686
709
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
687
|
-
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
688
710
|
const outPath = path2.join(
|
|
689
711
|
pluginTmpDir(),
|
|
690
712
|
`mcp-${hash}.json`
|
|
@@ -702,9 +724,15 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
702
724
|
log.info("bridged opencode MCP config", {
|
|
703
725
|
target: outPath,
|
|
704
726
|
hash,
|
|
705
|
-
servers:
|
|
727
|
+
servers: bridgedServerNames,
|
|
728
|
+
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
706
729
|
});
|
|
707
|
-
return {
|
|
730
|
+
return {
|
|
731
|
+
path: outPath,
|
|
732
|
+
hash,
|
|
733
|
+
serverNames: bridgedServerNames,
|
|
734
|
+
allEnabledServerNames
|
|
735
|
+
};
|
|
708
736
|
}
|
|
709
737
|
|
|
710
738
|
// src/runtime-status.ts
|
|
@@ -736,6 +764,35 @@ async function getRuntimeMcpStatus() {
|
|
|
736
764
|
return void 0;
|
|
737
765
|
}
|
|
738
766
|
}
|
|
767
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
768
|
+
const client = opencodeClient;
|
|
769
|
+
if (!client?.tool?.list) return void 0;
|
|
770
|
+
try {
|
|
771
|
+
const res = await client.tool.list({
|
|
772
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
773
|
+
});
|
|
774
|
+
const data = res.data;
|
|
775
|
+
if (!Array.isArray(data)) return void 0;
|
|
776
|
+
const out = [];
|
|
777
|
+
for (const entry of data) {
|
|
778
|
+
if (!entry || typeof entry !== "object") continue;
|
|
779
|
+
const e = entry;
|
|
780
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
781
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
782
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
783
|
+
if (!id) continue;
|
|
784
|
+
out.push({ id, description, parameters });
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
} catch (err) {
|
|
788
|
+
log.warn("failed to fetch opencode tool list", {
|
|
789
|
+
provider,
|
|
790
|
+
model,
|
|
791
|
+
error: err instanceof Error ? err.message : String(err)
|
|
792
|
+
});
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
739
796
|
|
|
740
797
|
// src/session-manager.ts
|
|
741
798
|
import { spawn } from "child_process";
|
|
@@ -1264,6 +1321,7 @@ function writeJson(res, body) {
|
|
|
1264
1321
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
1265
1322
|
var pendingBySession = /* @__PURE__ */ new Map();
|
|
1266
1323
|
var emitter = new EventEmitter3();
|
|
1324
|
+
var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1267
1325
|
function eventName(sessionKey2) {
|
|
1268
1326
|
return `pending:${sessionKey2}`;
|
|
1269
1327
|
}
|
|
@@ -1275,16 +1333,50 @@ function onPendingProxyCall(sessionKey2, handler) {
|
|
|
1275
1333
|
function queuePendingProxyCall(sessionKey2, call) {
|
|
1276
1334
|
const existing = pendingBySession.get(sessionKey2);
|
|
1277
1335
|
if (existing) {
|
|
1336
|
+
if (Date.now() - existing.createdAt < PENDING_PROXY_CALL_TIMEOUT_MS) {
|
|
1337
|
+
call.reject(
|
|
1338
|
+
new Error(`Another proxy tool call is already pending for ${sessionKey2}`)
|
|
1339
|
+
);
|
|
1340
|
+
log.warn("rejected overlapping proxy call", {
|
|
1341
|
+
sessionKey: sessionKey2,
|
|
1342
|
+
existingToolCallId: existing.toolCallId,
|
|
1343
|
+
existingToolName: existing.toolName,
|
|
1344
|
+
toolCallId: call.id,
|
|
1345
|
+
toolName: call.toolName
|
|
1346
|
+
});
|
|
1347
|
+
return existing;
|
|
1348
|
+
}
|
|
1349
|
+
clearTimeout(existing.timer);
|
|
1278
1350
|
existing.reject(
|
|
1279
|
-
new Error(
|
|
1351
|
+
new Error(
|
|
1352
|
+
`Stale proxy tool call expired after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms for ${sessionKey2}`
|
|
1353
|
+
)
|
|
1280
1354
|
);
|
|
1281
1355
|
pendingBySession.delete(sessionKey2);
|
|
1282
1356
|
}
|
|
1357
|
+
const timer = setTimeout(() => {
|
|
1358
|
+
const current = pendingBySession.get(sessionKey2);
|
|
1359
|
+
if (!current || current.toolCallId !== call.id) return;
|
|
1360
|
+
pendingBySession.delete(sessionKey2);
|
|
1361
|
+
current.reject(
|
|
1362
|
+
new Error(
|
|
1363
|
+
`Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
log.warn("timed out pending proxy call", {
|
|
1367
|
+
sessionKey: sessionKey2,
|
|
1368
|
+
toolCallId: call.id,
|
|
1369
|
+
toolName: call.toolName,
|
|
1370
|
+
timeoutMs: PENDING_PROXY_CALL_TIMEOUT_MS
|
|
1371
|
+
});
|
|
1372
|
+
}, PENDING_PROXY_CALL_TIMEOUT_MS);
|
|
1283
1373
|
const pending = {
|
|
1284
1374
|
sessionKey: sessionKey2,
|
|
1285
1375
|
toolCallId: call.id,
|
|
1286
1376
|
toolName: call.toolName,
|
|
1287
1377
|
input: call.input,
|
|
1378
|
+
createdAt: Date.now(),
|
|
1379
|
+
timer,
|
|
1288
1380
|
resolve: call.resolve,
|
|
1289
1381
|
reject: call.reject
|
|
1290
1382
|
};
|
|
@@ -1304,6 +1396,7 @@ function resolvePendingProxyCall(sessionKey2, result) {
|
|
|
1304
1396
|
const pending = pendingBySession.get(sessionKey2);
|
|
1305
1397
|
if (!pending) return false;
|
|
1306
1398
|
pendingBySession.delete(sessionKey2);
|
|
1399
|
+
clearTimeout(pending.timer);
|
|
1307
1400
|
pending.resolve(result);
|
|
1308
1401
|
log.info("resolved pending proxy call", {
|
|
1309
1402
|
sessionKey: sessionKey2,
|
|
@@ -1432,18 +1525,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1432
1525
|
* provided it overlays opencode's UI-toggled state on top of disk config
|
|
1433
1526
|
* so `/mcps` toggles propagate without a config file write.
|
|
1434
1527
|
*/
|
|
1435
|
-
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus) {
|
|
1528
|
+
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus, excludeServers) {
|
|
1436
1529
|
const paths = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
|
|
1437
1530
|
let bridgedHash = null;
|
|
1531
|
+
let allEnabledServerNames = [];
|
|
1438
1532
|
if (this.config.bridgeOpencodeMcp !== false) {
|
|
1439
|
-
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus);
|
|
1533
|
+
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
|
|
1440
1534
|
if (bridged) {
|
|
1441
|
-
paths.push(bridged.path);
|
|
1535
|
+
if (bridged.path) paths.push(bridged.path);
|
|
1442
1536
|
bridgedHash = bridged.hash;
|
|
1537
|
+
allEnabledServerNames = bridged.allEnabledServerNames;
|
|
1443
1538
|
}
|
|
1444
1539
|
}
|
|
1445
1540
|
if (proxyConfigPath) paths.push(proxyConfigPath);
|
|
1446
|
-
return { paths, bridgedHash };
|
|
1541
|
+
return { paths, bridgedHash, allEnabledServerNames };
|
|
1447
1542
|
}
|
|
1448
1543
|
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
1449
1544
|
resolvedProxyTools() {
|
|
@@ -1459,6 +1554,45 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1459
1554
|
}
|
|
1460
1555
|
return picked.length > 0 ? picked : null;
|
|
1461
1556
|
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
|
|
1559
|
+
* through the in-process proxy instead of being bridged into Claude CLI's
|
|
1560
|
+
* `--mcp-config`. Direct bridging causes double execution because both
|
|
1561
|
+
* Claude CLI's own MCP child and opencode hold their own connection to
|
|
1562
|
+
* the same server; routing through the proxy keeps a single execution
|
|
1563
|
+
* site (opencode). Returns null when the feature is disabled, the SDK
|
|
1564
|
+
* client is unavailable, or no MCP servers are configured.
|
|
1565
|
+
*/
|
|
1566
|
+
async resolvedProxyMcpTools(allEnabledServerNames) {
|
|
1567
|
+
if (this.config.proxyOpencodeMcpTools === false) return null;
|
|
1568
|
+
if (this.config.bridgeOpencodeMcp === false) return null;
|
|
1569
|
+
if (allEnabledServerNames.length === 0) return null;
|
|
1570
|
+
const items = await fetchOpencodeToolList(
|
|
1571
|
+
this.config.provider,
|
|
1572
|
+
this.modelId,
|
|
1573
|
+
this.config.cwd
|
|
1574
|
+
);
|
|
1575
|
+
if (!items || items.length === 0) return null;
|
|
1576
|
+
const serversByLengthDesc = [...allEnabledServerNames].sort(
|
|
1577
|
+
(a, b) => b.length - a.length
|
|
1578
|
+
);
|
|
1579
|
+
const out = [];
|
|
1580
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1581
|
+
for (const item of items) {
|
|
1582
|
+
const matchedServer = serversByLengthDesc.find(
|
|
1583
|
+
(name) => item.id === name || item.id.startsWith(`${name}_`)
|
|
1584
|
+
);
|
|
1585
|
+
if (!matchedServer) continue;
|
|
1586
|
+
if (seen.has(item.id)) continue;
|
|
1587
|
+
seen.add(item.id);
|
|
1588
|
+
out.push({
|
|
1589
|
+
name: item.id,
|
|
1590
|
+
description: item.description ?? "",
|
|
1591
|
+
inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return out.length > 0 ? out : null;
|
|
1595
|
+
}
|
|
1462
1596
|
/**
|
|
1463
1597
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
1464
1598
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
@@ -1740,7 +1874,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1740
1874
|
const scope = this.requestScope(options);
|
|
1741
1875
|
const affinity = this.sessionAffinity(options);
|
|
1742
1876
|
const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
|
|
1743
|
-
if (scope === "tools" && this.resolvedProxyTools()) {
|
|
1877
|
+
if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
|
|
1744
1878
|
return this.doGenerateViaStream(options);
|
|
1745
1879
|
}
|
|
1746
1880
|
if (scope === "no-tools") {
|
|
@@ -2137,8 +2271,18 @@ ${plan}
|
|
|
2137
2271
|
}
|
|
2138
2272
|
}
|
|
2139
2273
|
const setup = async () => {
|
|
2140
|
-
|
|
2141
|
-
|
|
2274
|
+
const discovery = self.effectiveMcpConfig(
|
|
2275
|
+
cwd,
|
|
2276
|
+
void 0,
|
|
2277
|
+
runtimeStatus
|
|
2278
|
+
);
|
|
2279
|
+
const proxyMcpTools = await self.resolvedProxyMcpTools(
|
|
2280
|
+
discovery.allEnabledServerNames
|
|
2281
|
+
);
|
|
2282
|
+
const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
|
|
2283
|
+
const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
|
|
2284
|
+
if (!proxyServer && combinedProxyTools) {
|
|
2285
|
+
proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
|
|
2142
2286
|
}
|
|
2143
2287
|
const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
|
|
2144
2288
|
const extraDisallowed = [];
|
|
@@ -2147,7 +2291,8 @@ ${plan}
|
|
|
2147
2291
|
const mcp = self.effectiveMcpConfig(
|
|
2148
2292
|
cwd,
|
|
2149
2293
|
proxyServer?.configPath(),
|
|
2150
|
-
runtimeStatus
|
|
2294
|
+
runtimeStatus,
|
|
2295
|
+
excludeServers
|
|
2151
2296
|
);
|
|
2152
2297
|
const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(cwd);
|
|
2153
2298
|
const cliArgs = buildCliArgs({
|
|
@@ -3225,7 +3370,8 @@ function createClaudeCode(settings = {}) {
|
|
|
3225
3370
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
3226
3371
|
proxyTools,
|
|
3227
3372
|
webSearch: settings.webSearch,
|
|
3228
|
-
hotReloadMcp: settings.hotReloadMcp ?? true
|
|
3373
|
+
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
3374
|
+
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true
|
|
3229
3375
|
});
|
|
3230
3376
|
};
|
|
3231
3377
|
const provider = function(modelId) {
|