@khalilgharbaoui/opencode-claude-code-plugin 0.3.0 → 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.js CHANGED
@@ -19,7 +19,7 @@ var log = {
19
19
  console.error(fmt("NOTICE", msg, data));
20
20
  },
21
21
  warn(msg, data) {
22
- if (DEBUG) console.error(fmt("WARN", msg, data));
22
+ console.error(fmt("WARN", msg, data));
23
23
  },
24
24
  error(msg, data) {
25
25
  console.error(fmt("ERROR", msg, data));
@@ -381,22 +381,49 @@ Now continuing with the current message:
381
381
  }
382
382
 
383
383
  // src/mcp-bridge.ts
384
+ import * as fs2 from "fs";
385
+ import * as path2 from "path";
386
+ import * as os2 from "os";
387
+ import * as crypto from "crypto";
388
+
389
+ // src/tmp.ts
384
390
  import * as fs from "fs";
385
- import * as path from "path";
386
391
  import * as os from "os";
387
- import * as crypto from "crypto";
392
+ import * as path from "path";
393
+ var PLUGIN_TMP_DIR = path.join(
394
+ os.tmpdir(),
395
+ `opencode-claude-code-${process.pid}`
396
+ );
397
+ var registered = false;
398
+ function pluginTmpDir() {
399
+ if (!fs.existsSync(PLUGIN_TMP_DIR)) {
400
+ fs.mkdirSync(PLUGIN_TMP_DIR, { recursive: true });
401
+ }
402
+ if (!registered) {
403
+ registered = true;
404
+ process.on("exit", () => {
405
+ try {
406
+ fs.rmSync(PLUGIN_TMP_DIR, { recursive: true, force: true });
407
+ } catch {
408
+ }
409
+ });
410
+ }
411
+ return PLUGIN_TMP_DIR;
412
+ }
413
+
414
+ // src/mcp-bridge.ts
388
415
  var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
389
416
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
390
417
  function fileExists(p) {
391
418
  try {
392
- return fs.statSync(p).isFile();
419
+ return fs2.statSync(p).isFile();
393
420
  } catch {
394
421
  return false;
395
422
  }
396
423
  }
397
424
  function dirExists(p) {
398
425
  try {
399
- return fs.statSync(p).isDirectory();
426
+ return fs2.statSync(p).isDirectory();
400
427
  } catch {
401
428
  return false;
402
429
  }
@@ -442,7 +469,7 @@ function stripJsonComments(text) {
442
469
  }
443
470
  function readAndParse(file) {
444
471
  try {
445
- const raw = fs.readFileSync(file, "utf8");
472
+ const raw = fs2.readFileSync(file, "utf8");
446
473
  return JSON.parse(stripJsonComments(raw));
447
474
  } catch (e) {
448
475
  log.warn("failed to parse opencode config", {
@@ -470,14 +497,14 @@ function deepMerge(target, source) {
470
497
  }
471
498
  function walkUp(opts) {
472
499
  const out = [];
473
- let current = path.resolve(opts.start);
500
+ let current = path2.resolve(opts.start);
474
501
  while (true) {
475
502
  for (const target of opts.targets) {
476
- const candidate = path.join(current, target);
503
+ const candidate = path2.join(current, target);
477
504
  if (opts.predicate(candidate)) out.push(candidate);
478
505
  }
479
- if (opts.stop && current === path.resolve(opts.stop)) break;
480
- const parent = path.dirname(current);
506
+ if (opts.stop && current === path2.resolve(opts.stop)) break;
507
+ const parent = path2.dirname(current);
481
508
  if (parent === current) break;
482
509
  current = parent;
483
510
  }
@@ -485,28 +512,28 @@ function walkUp(opts) {
485
512
  }
486
513
  function detectWorktree(cwd) {
487
514
  const override = process.env.OPENCODE_WORKTREE;
488
- if (override) return path.resolve(override);
489
- let current = path.resolve(cwd);
515
+ if (override) return path2.resolve(override);
516
+ let current = path2.resolve(cwd);
490
517
  while (true) {
491
- const gitPath = path.join(current, ".git");
518
+ const gitPath = path2.join(current, ".git");
492
519
  try {
493
- if (fs.existsSync(gitPath)) return current;
520
+ if (fs2.existsSync(gitPath)) return current;
494
521
  } catch {
495
522
  }
496
- const parent = path.dirname(current);
523
+ const parent = path2.dirname(current);
497
524
  if (parent === current) return void 0;
498
525
  current = parent;
499
526
  }
500
527
  }
501
528
  function globalConfigDir() {
502
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
503
- return path.join(xdg, "opencode");
529
+ const xdg = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
530
+ return path2.join(xdg, "opencode");
504
531
  }
505
532
  function loadGlobalConfig() {
506
533
  const dir = globalConfigDir();
507
534
  let merged = {};
508
535
  for (const name of FILE_NAMES.slice().reverse()) {
509
- const file = path.join(dir, name);
536
+ const file = path2.join(dir, name);
510
537
  if (!fileExists(file)) continue;
511
538
  const parsed = readAndParse(file);
512
539
  if (parsed) merged = deepMerge(merged, parsed);
@@ -516,7 +543,7 @@ function loadGlobalConfig() {
516
543
  function loadProjectFilesInDir(dir) {
517
544
  let merged = {};
518
545
  for (const name of PROJECT_FILE_NAMES) {
519
- const file = path.join(dir, name);
546
+ const file = path2.join(dir, name);
520
547
  if (!fileExists(file)) continue;
521
548
  const parsed = readAndParse(file);
522
549
  if (parsed) merged = deepMerge(merged, parsed);
@@ -527,7 +554,7 @@ function dotOpencodeDirs(cwd, worktree) {
527
554
  const dirs = [];
528
555
  const seen = /* @__PURE__ */ new Set();
529
556
  const push = (p) => {
530
- const abs = path.resolve(p);
557
+ const abs = path2.resolve(p);
531
558
  if (!seen.has(abs) && dirExists(abs)) {
532
559
  seen.add(abs);
533
560
  dirs.push(abs);
@@ -541,9 +568,9 @@ function dotOpencodeDirs(cwd, worktree) {
541
568
  })) {
542
569
  push(dir);
543
570
  }
544
- const home = os.homedir();
571
+ const home = os2.homedir();
545
572
  if (home) {
546
- const homeDot = path.join(home, ".opencode");
573
+ const homeDot = path2.join(home, ".opencode");
547
574
  if (dirExists(homeDot)) push(homeDot);
548
575
  }
549
576
  const envDir = process.env.OPENCODE_CONFIG_DIR;
@@ -610,7 +637,7 @@ function mergeMcp(target, source) {
610
637
  }
611
638
  return out;
612
639
  }
613
- function bridgeOpencodeMcp(cwd, runtimeStatus) {
640
+ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
614
641
  const worktree = detectWorktree(cwd);
615
642
  let merged = {};
616
643
  merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
@@ -628,7 +655,7 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
628
655
  const projectDirs = [];
629
656
  const seenProjectDirs = /* @__PURE__ */ new Set();
630
657
  for (const f of projectFiles) {
631
- const d = path.dirname(f);
658
+ const d = path2.dirname(f);
632
659
  if (!seenProjectDirs.has(d)) {
633
660
  seenProjectDirs.add(d);
634
661
  projectDirs.push(d);
@@ -649,22 +676,44 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
649
676
  merged[name] = { ...base, enabled: status === "connected" };
650
677
  }
651
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
+ }
652
686
  const servers = {};
687
+ const bridgedServerNames = [];
653
688
  for (const [name, spec] of Object.entries(merged)) {
654
689
  if (!spec || typeof spec !== "object") continue;
690
+ if (excludeServers?.has(name)) continue;
655
691
  const translated = translateServer(name, spec);
656
- if (translated) servers[name] = 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
+ };
657
708
  }
658
- if (Object.keys(servers).length === 0) return null;
659
709
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
660
- const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
661
- const outPath = path.join(
662
- os.tmpdir(),
663
- `opencode-claude-code-mcp-${hash}.json`
710
+ const outPath = path2.join(
711
+ pluginTmpDir(),
712
+ `mcp-${hash}.json`
664
713
  );
665
714
  try {
666
715
  if (!fileExists(outPath)) {
667
- fs.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
716
+ fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
668
717
  }
669
718
  } catch (e) {
670
719
  log.warn("failed to write bridged MCP config", {
@@ -675,9 +724,15 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
675
724
  log.info("bridged opencode MCP config", {
676
725
  target: outPath,
677
726
  hash,
678
- servers: Object.keys(servers)
727
+ servers: bridgedServerNames,
728
+ excluded: excludeServers ? Array.from(excludeServers) : []
679
729
  });
680
- return { path: outPath, hash };
730
+ return {
731
+ path: outPath,
732
+ hash,
733
+ serverNames: bridgedServerNames,
734
+ allEnabledServerNames
735
+ };
681
736
  }
682
737
 
683
738
  // src/runtime-status.ts
@@ -709,6 +764,35 @@ async function getRuntimeMcpStatus() {
709
764
  return void 0;
710
765
  }
711
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
+ }
712
796
 
713
797
  // src/session-manager.ts
714
798
  import { spawn } from "child_process";
@@ -872,14 +956,14 @@ function sessionKey(cwd, modelId) {
872
956
 
873
957
  // src/proxy-mcp.ts
874
958
  import { createServer } from "http";
875
- import * as fs2 from "fs";
876
- import * as path2 from "path";
877
- import * as os2 from "os";
959
+ import * as fs3 from "fs";
960
+ import * as path3 from "path";
878
961
  import * as crypto2 from "crypto";
879
962
  import { EventEmitter as EventEmitter2 } from "events";
880
963
  var PROTOCOL_VERSION = "2024-11-05";
881
964
  var SERVER_NAME = "opencode_proxy";
882
965
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
966
+ var PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
883
967
  var DEFAULT_PROXY_TOOLS = [
884
968
  {
885
969
  name: "bash",
@@ -1050,6 +1134,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1050
1134
  toolName,
1051
1135
  hasInput: input != null
1052
1136
  });
1137
+ let timer = null;
1053
1138
  const result = await new Promise(
1054
1139
  (resolve3, reject) => {
1055
1140
  const entry = {
@@ -1060,9 +1145,24 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1060
1145
  reject
1061
1146
  };
1062
1147
  pending.set(callId, entry);
1148
+ timer = setTimeout(() => {
1149
+ if (!pending.has(callId)) return;
1150
+ pending.delete(callId);
1151
+ log.warn("proxy-mcp tool call timed out", {
1152
+ callId,
1153
+ toolName,
1154
+ timeoutMs: PROXY_CALL_TIMEOUT_MS
1155
+ });
1156
+ reject(
1157
+ new Error(
1158
+ `Proxy tool '${toolName}' timed out after ${PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
1159
+ )
1160
+ );
1161
+ }, PROXY_CALL_TIMEOUT_MS);
1063
1162
  calls.emit("call", entry);
1064
1163
  }
1065
1164
  ).finally(() => {
1165
+ if (timer) clearTimeout(timer);
1066
1166
  pending.delete(callId);
1067
1167
  });
1068
1168
  if (result.kind === "error") {
@@ -1151,11 +1251,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1151
1251
  2
1152
1252
  );
1153
1253
  const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
1154
- const outPath = path2.join(
1155
- os2.tmpdir(),
1156
- `opencode-claude-code-proxy-${hash}.json`
1254
+ const outPath = path3.join(
1255
+ pluginTmpDir(),
1256
+ `proxy-${hash}.json`
1157
1257
  );
1158
- fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
1258
+ fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
1159
1259
  configFilePath = outPath;
1160
1260
  return outPath;
1161
1261
  },
@@ -1167,6 +1267,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1167
1267
  await new Promise((resolve3) => {
1168
1268
  server2.close(() => resolve3());
1169
1269
  });
1270
+ if (configFilePath) {
1271
+ try {
1272
+ fs3.unlinkSync(configFilePath);
1273
+ } catch {
1274
+ }
1275
+ configFilePath = null;
1276
+ }
1170
1277
  }
1171
1278
  };
1172
1279
  return api;
@@ -1214,6 +1321,7 @@ function writeJson(res, body) {
1214
1321
  import { EventEmitter as EventEmitter3 } from "events";
1215
1322
  var pendingBySession = /* @__PURE__ */ new Map();
1216
1323
  var emitter = new EventEmitter3();
1324
+ var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
1217
1325
  function eventName(sessionKey2) {
1218
1326
  return `pending:${sessionKey2}`;
1219
1327
  }
@@ -1225,16 +1333,50 @@ function onPendingProxyCall(sessionKey2, handler) {
1225
1333
  function queuePendingProxyCall(sessionKey2, call) {
1226
1334
  const existing = pendingBySession.get(sessionKey2);
1227
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);
1228
1350
  existing.reject(
1229
- new Error(`Another proxy tool call is already pending for ${sessionKey2}`)
1351
+ new Error(
1352
+ `Stale proxy tool call expired after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms for ${sessionKey2}`
1353
+ )
1230
1354
  );
1231
1355
  pendingBySession.delete(sessionKey2);
1232
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);
1233
1373
  const pending = {
1234
1374
  sessionKey: sessionKey2,
1235
1375
  toolCallId: call.id,
1236
1376
  toolName: call.toolName,
1237
1377
  input: call.input,
1378
+ createdAt: Date.now(),
1379
+ timer,
1238
1380
  resolve: call.resolve,
1239
1381
  reject: call.reject
1240
1382
  };
@@ -1254,6 +1396,7 @@ function resolvePendingProxyCall(sessionKey2, result) {
1254
1396
  const pending = pendingBySession.get(sessionKey2);
1255
1397
  if (!pending) return false;
1256
1398
  pendingBySession.delete(sessionKey2);
1399
+ clearTimeout(pending.timer);
1257
1400
  pending.resolve(result);
1258
1401
  log.info("resolved pending proxy call", {
1259
1402
  sessionKey: sessionKey2,
@@ -1266,9 +1409,9 @@ function resolvePendingProxyCall(sessionKey2, result) {
1266
1409
  // src/claude-code-language-model.ts
1267
1410
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1268
1411
  import { unlink as unlink2 } from "fs/promises";
1269
- import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
1412
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1270
1413
  import { randomUUID as randomUUID2 } from "crypto";
1271
- import { dirname as dirname2, join as join3 } from "path";
1414
+ import { dirname as dirname2, join as join4 } from "path";
1272
1415
  function hasNewUserContent(prompt) {
1273
1416
  for (let i = prompt.length - 1; i >= 0; i--) {
1274
1417
  const msg = prompt[i];
@@ -1283,14 +1426,15 @@ function hasNewUserContent(prompt) {
1283
1426
  for (const part of content) {
1284
1427
  if (part.type === "text" && part.text && part.text.trim()) return true;
1285
1428
  if (part.type === "tool-result") return true;
1429
+ if (part.type === "image" || part.type === "file") return true;
1286
1430
  }
1287
1431
  }
1288
1432
  }
1289
1433
  return false;
1290
1434
  }
1291
- function readPromptFileIfPresent(path4) {
1435
+ function readPromptFileIfPresent(path5) {
1292
1436
  try {
1293
- const content = readFileSync2(path4, "utf8").trim();
1437
+ const content = readFileSync2(path5, "utf8").trim();
1294
1438
  return content || void 0;
1295
1439
  } catch {
1296
1440
  return void 0;
@@ -1299,7 +1443,7 @@ function readPromptFileIfPresent(path4) {
1299
1443
  function nearestWorkspaceAgentsPrompt(cwd) {
1300
1444
  let dir = cwd;
1301
1445
  while (true) {
1302
- const content = readPromptFileIfPresent(join3(dir, "AGENTS.md"));
1446
+ const content = readPromptFileIfPresent(join4(dir, "AGENTS.md"));
1303
1447
  if (content) return content;
1304
1448
  const parent = dirname2(dir);
1305
1449
  if (parent === dir) return void 0;
@@ -1308,17 +1452,17 @@ function nearestWorkspaceAgentsPrompt(cwd) {
1308
1452
  }
1309
1453
  function buildAppendedSystemPrompt(cwd) {
1310
1454
  const parts = [];
1311
- const configRoot = process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config");
1312
- const globalAgents = readPromptFileIfPresent(join3(configRoot, "opencode", "AGENTS.md"));
1455
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config");
1456
+ const globalAgents = readPromptFileIfPresent(join4(configRoot, "opencode", "AGENTS.md"));
1313
1457
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
1314
1458
  if (globalAgents) parts.push(globalAgents);
1315
1459
  if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
1316
1460
  const content = parts.join("\n\n");
1317
1461
  if (!content) return void 0;
1318
- const path4 = join3(tmpdir3(), `opencode-cc-sys-${randomUUID2()}.md`);
1462
+ const path5 = join4(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
1319
1463
  try {
1320
- writeFileSync3(path4, content, "utf8");
1321
- return path4;
1464
+ writeFileSync3(path5, content, "utf8");
1465
+ return path5;
1322
1466
  } catch (err) {
1323
1467
  log.warn("failed to write system prompt file", { error: String(err) });
1324
1468
  return void 0;
@@ -1381,18 +1525,20 @@ var ClaudeCodeLanguageModel = class {
1381
1525
  * provided it overlays opencode's UI-toggled state on top of disk config
1382
1526
  * so `/mcps` toggles propagate without a config file write.
1383
1527
  */
1384
- effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus) {
1528
+ effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus, excludeServers) {
1385
1529
  const paths = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
1386
1530
  let bridgedHash = null;
1531
+ let allEnabledServerNames = [];
1387
1532
  if (this.config.bridgeOpencodeMcp !== false) {
1388
- const bridged = bridgeOpencodeMcp(cwd, runtimeStatus);
1533
+ const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
1389
1534
  if (bridged) {
1390
- paths.push(bridged.path);
1535
+ if (bridged.path) paths.push(bridged.path);
1391
1536
  bridgedHash = bridged.hash;
1537
+ allEnabledServerNames = bridged.allEnabledServerNames;
1392
1538
  }
1393
1539
  }
1394
1540
  if (proxyConfigPath) paths.push(proxyConfigPath);
1395
- return { paths, bridgedHash };
1541
+ return { paths, bridgedHash, allEnabledServerNames };
1396
1542
  }
1397
1543
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
1398
1544
  resolvedProxyTools() {
@@ -1408,6 +1554,45 @@ var ClaudeCodeLanguageModel = class {
1408
1554
  }
1409
1555
  return picked.length > 0 ? picked : null;
1410
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
+ }
1411
1596
  /**
1412
1597
  * Create a proxy MCP server for a single active Claude process/session.
1413
1598
  * The process lifecycle owns the server lifecycle via session-manager.
@@ -1689,7 +1874,7 @@ var ClaudeCodeLanguageModel = class {
1689
1874
  const scope = this.requestScope(options);
1690
1875
  const affinity = this.sessionAffinity(options);
1691
1876
  const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
1692
- if (scope === "tools" && this.resolvedProxyTools()) {
1877
+ if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
1693
1878
  return this.doGenerateViaStream(options);
1694
1879
  }
1695
1880
  if (scope === "no-tools") {
@@ -2086,8 +2271,18 @@ ${plan}
2086
2271
  }
2087
2272
  }
2088
2273
  const setup = async () => {
2089
- if (!proxyServer && resolvedProxy) {
2090
- proxyServer = await self.ensureProxyServer(resolvedProxy, sk);
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);
2091
2286
  }
2092
2287
  const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
2093
2288
  const extraDisallowed = [];
@@ -2096,7 +2291,8 @@ ${plan}
2096
2291
  const mcp = self.effectiveMcpConfig(
2097
2292
  cwd,
2098
2293
  proxyServer?.configPath(),
2099
- runtimeStatus
2294
+ runtimeStatus,
2295
+ excludeServers
2100
2296
  );
2101
2297
  const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(cwd);
2102
2298
  const cliArgs = buildCliArgs({
@@ -2889,7 +3085,7 @@ var defaultModels = {
2889
3085
 
2890
3086
  // src/accounts.ts
2891
3087
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
2892
- import path3 from "path";
3088
+ import path4 from "path";
2893
3089
  var BASE_PROVIDER_ID = "claude-code";
2894
3090
  var DEFAULT_ACCOUNT = "default";
2895
3091
  var SHARED_CAPABILITY_ITEMS = [
@@ -2927,7 +3123,7 @@ function expandHome(value) {
2927
3123
  const home = process.env.HOME ?? process.env.USERPROFILE;
2928
3124
  if (value === "~") return home ?? value;
2929
3125
  if (value.startsWith("~/") || value.startsWith("~\\")) {
2930
- return home ? path3.join(home, value.slice(2)) : value;
3126
+ return home ? path4.join(home, value.slice(2)) : value;
2931
3127
  }
2932
3128
  return value;
2933
3129
  }
@@ -2959,8 +3155,8 @@ async function ensureSharedCapabilities(targetRoot) {
2959
3155
  }
2960
3156
  }
2961
3157
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
2962
- const source = path3.join(sourceRoot, item);
2963
- const target = path3.join(targetRoot, item);
3158
+ const source = path4.join(sourceRoot, item);
3159
+ const target = path4.join(targetRoot, item);
2964
3160
  let sourceStat;
2965
3161
  try {
2966
3162
  sourceStat = await lstat(source);
@@ -2971,8 +3167,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
2971
3167
  const targetStat = await lstat(target);
2972
3168
  if (targetStat.isSymbolicLink()) {
2973
3169
  const current = await readlink(target);
2974
- const resolvedCurrent = path3.resolve(path3.dirname(target), current);
2975
- const resolvedSource = path3.resolve(source);
3170
+ const resolvedCurrent = path4.resolve(path4.dirname(target), current);
3171
+ const resolvedSource = path4.resolve(source);
2976
3172
  if (resolvedCurrent === resolvedSource) return;
2977
3173
  }
2978
3174
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -2987,11 +3183,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
2987
3183
  await symlink(source, target, type);
2988
3184
  }
2989
3185
  async function writeAccountWrapper(account, baseCliPath, configDir) {
2990
- const cacheRoot = path3.join(
3186
+ const cacheRoot = path4.join(
2991
3187
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
2992
3188
  "opencode-claude-code-plugin"
2993
3189
  );
2994
- const wrapperPath = path3.join(cacheRoot, `claude-${account}`);
3190
+ const wrapperPath = path4.join(cacheRoot, `claude-${account}`);
2995
3191
  const suffix = `@${account}`;
2996
3192
  await mkdir(cacheRoot, { recursive: true });
2997
3193
  const script = `#!/usr/bin/env bash
@@ -3031,14 +3227,14 @@ function titleizeAccount(account) {
3031
3227
 
3032
3228
  // src/cleanup-stale.ts
3033
3229
  import {
3034
- existsSync as existsSync2,
3230
+ existsSync as existsSync3,
3035
3231
  readFileSync as readFileSync3,
3036
3232
  realpathSync,
3037
- rmSync,
3233
+ rmSync as rmSync2,
3038
3234
  writeFileSync as writeFileSync4
3039
3235
  } from "fs";
3040
3236
  import { homedir as homedir3 } from "os";
3041
- import { join as join4, resolve as resolve2 } from "path";
3237
+ import { join as join5, resolve as resolve2 } from "path";
3042
3238
  import { fileURLToPath } from "url";
3043
3239
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
3044
3240
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -3046,18 +3242,18 @@ var alreadyRan = false;
3046
3242
  function candidateCacheRoots() {
3047
3243
  const xdg = process.env.XDG_CACHE_HOME;
3048
3244
  return [
3049
- xdg ? join4(xdg, "opencode") : null,
3050
- join4(homedir3(), ".cache", "opencode"),
3051
- join4(homedir3(), "Library", "Caches", "opencode")
3245
+ xdg ? join5(xdg, "opencode") : null,
3246
+ join5(homedir3(), ".cache", "opencode"),
3247
+ join5(homedir3(), "Library", "Caches", "opencode")
3052
3248
  ].filter((p) => Boolean(p));
3053
3249
  }
3054
3250
  function userOpencodeJsonPath() {
3055
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join4(homedir3(), ".config");
3056
- return join4(xdgConfig, "opencode", "opencode.json");
3251
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
3252
+ return join5(xdgConfig, "opencode", "opencode.json");
3057
3253
  }
3058
3254
  function userIntendsToUseUnscoped() {
3059
3255
  const cfg = userOpencodeJsonPath();
3060
- if (!existsSync2(cfg)) return false;
3256
+ if (!existsSync3(cfg)) return false;
3061
3257
  try {
3062
3258
  const json = JSON.parse(readFileSync3(cfg, "utf8"));
3063
3259
  const plugins = json.plugin;
@@ -3095,17 +3291,17 @@ function cleanupStaleUnscopedInstall() {
3095
3291
  }
3096
3292
  }
3097
3293
  function cleanupOne(cacheRoot, ourDir) {
3098
- if (!existsSync2(cacheRoot)) return;
3099
- const stalePath = join4(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
3100
- if (!existsSync2(stalePath)) return;
3294
+ if (!existsSync3(cacheRoot)) return;
3295
+ const stalePath = join5(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
3296
+ if (!existsSync3(stalePath)) return;
3101
3297
  let realStalePath = stalePath;
3102
3298
  try {
3103
3299
  realStalePath = realpathSync(stalePath);
3104
3300
  } catch {
3105
3301
  }
3106
3302
  if (ourDir && realStalePath === ourDir) return;
3107
- const pkgJsonPath = join4(stalePath, "package.json");
3108
- if (!existsSync2(pkgJsonPath)) return;
3303
+ const pkgJsonPath = join5(stalePath, "package.json");
3304
+ if (!existsSync3(pkgJsonPath)) return;
3109
3305
  let pkg = {};
3110
3306
  try {
3111
3307
  pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
@@ -3116,7 +3312,7 @@ function cleanupOne(cacheRoot, ourDir) {
3116
3312
  if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
3117
3313
  log.info("cleanup-stale: removing unscoped install", { stalePath });
3118
3314
  try {
3119
- rmSync(stalePath, { recursive: true, force: true });
3315
+ rmSync2(stalePath, { recursive: true, force: true });
3120
3316
  } catch (err) {
3121
3317
  log.warn("cleanup-stale: rmSync failed", {
3122
3318
  stalePath,
@@ -3124,8 +3320,8 @@ function cleanupOne(cacheRoot, ourDir) {
3124
3320
  });
3125
3321
  return;
3126
3322
  }
3127
- const cachePkgJson = join4(cacheRoot, "package.json");
3128
- if (!existsSync2(cachePkgJson)) return;
3323
+ const cachePkgJson = join5(cacheRoot, "package.json");
3324
+ if (!existsSync3(cachePkgJson)) return;
3129
3325
  try {
3130
3326
  const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
3131
3327
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
@@ -3174,7 +3370,8 @@ function createClaudeCode(settings = {}) {
3174
3370
  controlRequestDenyMessage: settings.controlRequestDenyMessage,
3175
3371
  proxyTools,
3176
3372
  webSearch: settings.webSearch,
3177
- hotReloadMcp: settings.hotReloadMcp ?? true
3373
+ hotReloadMcp: settings.hotReloadMcp ?? true,
3374
+ proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true
3178
3375
  });
3179
3376
  };
3180
3377
  const provider = function(modelId) {
@@ -3342,12 +3539,12 @@ var server = async (input) => {
3342
3539
  config.provider ??= {};
3343
3540
  const expanded = await expandAccountProviders(config);
3344
3541
  if (expanded) {
3345
- const registered = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
3542
+ const registered2 = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
3346
3543
  id,
3347
3544
  name: p?.name ?? id,
3348
3545
  cwd: p?.options?.cwd
3349
3546
  }));
3350
- log.notice("registered claude-code providers", { providers: registered });
3547
+ log.notice("registered claude-code providers", { providers: registered2 });
3351
3548
  return;
3352
3549
  }
3353
3550
  const existing = config.provider[PROVIDER_ID2];