@sideboard-ai/core 0.1.160 → 0.1.161
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/{chunk-QYKADGPP.js → chunk-EZANYS5D.js} +5 -5
- package/dist/{chunk-BQ4PEXIF.js → chunk-MIX4R72K.js} +5 -5
- package/dist/index.cjs +741 -607
- package/dist/index.js +732 -601
- package/dist/mcp/run-stdio.cjs +741 -607
- package/dist/mcp/run-stdio.js +732 -601
- package/dist/{orchestrator-KVKPIUVU.js → orchestrator-6FZBSP5B.js} +1 -1
- package/dist/{orchestrator-BSVSOTBK.js → orchestrator-QREP2CFO.js} +1 -1
- package/package.json +1 -1
package/dist/mcp/run-stdio.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
syncBoardPins,
|
|
37
37
|
updateSchedule,
|
|
38
38
|
waitForDetachedJob
|
|
39
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-EZANYS5D.js";
|
|
40
40
|
import {
|
|
41
41
|
isInternalAgentStatusText,
|
|
42
42
|
listModelsForAgent,
|
|
@@ -107,7 +107,7 @@ import {
|
|
|
107
107
|
// src/mcp/server.ts
|
|
108
108
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
109
109
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
110
|
-
import { z as
|
|
110
|
+
import { z as z8 } from "zod";
|
|
111
111
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
112
112
|
import { basename } from "path";
|
|
113
113
|
|
|
@@ -1189,13 +1189,52 @@ ${githubUrl.trim()}` : githubUrl.trim();
|
|
|
1189
1189
|
${link}` : link;
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
|
-
// src/mcp/
|
|
1193
|
-
|
|
1192
|
+
// src/mcp/issue-list.ts
|
|
1193
|
+
var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
|
|
1194
|
+
var MCP_ISSUE_LIST_MAX_LIMIT = 250;
|
|
1195
|
+
function clampMcpIssueLimit(limit) {
|
|
1196
|
+
if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
|
|
1197
|
+
return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
|
|
1198
|
+
}
|
|
1199
|
+
function applyIssueListWindow(items, limit) {
|
|
1200
|
+
const truncated = items.length > limit;
|
|
1201
|
+
return {
|
|
1202
|
+
items: truncated ? items.slice(0, limit) : items,
|
|
1203
|
+
truncated
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function compactIssueRow(issue) {
|
|
1207
|
+
return {
|
|
1208
|
+
identifier: issue.identifier,
|
|
1209
|
+
title: issue.title,
|
|
1210
|
+
...issue.assignee ? { assignee: issue.assignee } : {},
|
|
1211
|
+
...issue.labels.length > 0 ? { labels: issue.labels } : {},
|
|
1212
|
+
...issue.teamKey ? { team: issue.teamKey } : {},
|
|
1213
|
+
...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function formatMcpIssueList(input) {
|
|
1217
|
+
return {
|
|
1218
|
+
...input.source ? { source: input.source } : {},
|
|
1219
|
+
...input.viewer ? { viewer: input.viewer } : {},
|
|
1220
|
+
limit: input.limit,
|
|
1221
|
+
returned: input.issues.length,
|
|
1222
|
+
truncated: input.truncated,
|
|
1223
|
+
...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
|
|
1224
|
+
issues: input.issues.map(compactIssueRow)
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function mcpJson(payload, isError = false) {
|
|
1194
1228
|
return {
|
|
1195
|
-
content: [{ type: "text", text: JSON.stringify(payload
|
|
1229
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
1196
1230
|
...isError ? { isError: true } : {}
|
|
1197
1231
|
};
|
|
1198
1232
|
}
|
|
1233
|
+
|
|
1234
|
+
// src/mcp/slack-tools.ts
|
|
1235
|
+
function text(payload, isError = false) {
|
|
1236
|
+
return mcpJson(payload, isError);
|
|
1237
|
+
}
|
|
1199
1238
|
function fail(err) {
|
|
1200
1239
|
return text(
|
|
1201
1240
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
@@ -1461,48 +1500,329 @@ function registerSlackTools(server) {
|
|
|
1461
1500
|
}
|
|
1462
1501
|
|
|
1463
1502
|
// src/mcp/abletime-tools.ts
|
|
1503
|
+
import { z as z3 } from "zod";
|
|
1504
|
+
|
|
1505
|
+
// src/mcp/issue-payload.ts
|
|
1464
1506
|
import { z as z2 } from "zod";
|
|
1465
1507
|
|
|
1466
|
-
// src/mcp/
|
|
1467
|
-
var
|
|
1468
|
-
var
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1508
|
+
// src/mcp/smart-crush.ts
|
|
1509
|
+
var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
|
|
1510
|
+
var CRUSH_MIN_ITEMS = 5;
|
|
1511
|
+
var CRUSH_MIN_CHARS = 800;
|
|
1512
|
+
var CRUSH_COMMENT_MAX_CHARS = 16e3;
|
|
1513
|
+
var CRUSH_BODY_MIN_CHARS = 12e3;
|
|
1514
|
+
var CRUSH_BODY_MAX_CHARS = 8e3;
|
|
1515
|
+
var NEAR_DUP_JACCARD = 0.92;
|
|
1516
|
+
function looksLikeMustKeep(text7) {
|
|
1517
|
+
return MUST_KEEP_TEXT_RE.test(text7);
|
|
1518
|
+
}
|
|
1519
|
+
function normalizeText(text7) {
|
|
1520
|
+
return text7.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1521
|
+
}
|
|
1522
|
+
function wordSet(text7) {
|
|
1523
|
+
return new Set(
|
|
1524
|
+
text7.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
|
|
1525
|
+
);
|
|
1472
1526
|
}
|
|
1473
|
-
function
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1527
|
+
function jaccard(a, b) {
|
|
1528
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
1529
|
+
let inter = 0;
|
|
1530
|
+
for (const w of a) {
|
|
1531
|
+
if (b.has(w)) inter += 1;
|
|
1532
|
+
}
|
|
1533
|
+
const union = a.size + b.size - inter;
|
|
1534
|
+
return union === 0 ? 0 : inter / union;
|
|
1535
|
+
}
|
|
1536
|
+
function importance(text7) {
|
|
1537
|
+
let score = text7.length;
|
|
1538
|
+
if (/```/.test(text7)) score += 400;
|
|
1539
|
+
if (/https?:\/\//.test(text7)) score += 50;
|
|
1540
|
+
if (looksLikeMustKeep(text7)) score += 1e4;
|
|
1541
|
+
return score;
|
|
1542
|
+
}
|
|
1543
|
+
function meanStd(values) {
|
|
1544
|
+
if (values.length === 0) return { mean: 0, std: 0 };
|
|
1545
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
1546
|
+
const variance = values.reduce((a, b) => a + (b - mean) * (b - mean), 0) / values.length;
|
|
1547
|
+
return { mean, std: Math.sqrt(variance) };
|
|
1548
|
+
}
|
|
1549
|
+
function dedupTextItems(items, getText) {
|
|
1550
|
+
const kept = [];
|
|
1551
|
+
const keptNorm = [];
|
|
1552
|
+
const keptWords = [];
|
|
1553
|
+
for (const item of items) {
|
|
1554
|
+
const text7 = getText(item);
|
|
1555
|
+
const norm = normalizeText(text7);
|
|
1556
|
+
if (!norm) {
|
|
1557
|
+
kept.push(item);
|
|
1558
|
+
keptNorm.push(norm);
|
|
1559
|
+
keptWords.push(wordSet(text7));
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const words = wordSet(text7);
|
|
1563
|
+
let dupAt = -1;
|
|
1564
|
+
for (let i = 0; i < kept.length; i++) {
|
|
1565
|
+
if (keptNorm[i] === norm) {
|
|
1566
|
+
dupAt = i;
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
if (keptWords[i].size >= 8 && words.size >= 8 && jaccard(keptWords[i], words) >= NEAR_DUP_JACCARD) {
|
|
1570
|
+
dupAt = i;
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (dupAt < 0) {
|
|
1575
|
+
kept.push(item);
|
|
1576
|
+
keptNorm.push(norm);
|
|
1577
|
+
keptWords.push(words);
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (text7.length > getText(kept[dupAt]).length) {
|
|
1581
|
+
kept[dupAt] = item;
|
|
1582
|
+
keptNorm[dupAt] = norm;
|
|
1583
|
+
keptWords[dupAt] = words;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return { items: kept, dropped: items.length - kept.length };
|
|
1587
|
+
}
|
|
1588
|
+
function crushTextItems(items, getText, opts = {}) {
|
|
1589
|
+
const minItems = opts.minItems ?? CRUSH_MIN_ITEMS;
|
|
1590
|
+
const minChars = opts.minChars ?? CRUSH_MIN_CHARS;
|
|
1591
|
+
const maxChars = opts.maxChars ?? CRUSH_COMMENT_MAX_CHARS;
|
|
1592
|
+
const original = items;
|
|
1593
|
+
const totalChars = items.reduce((n, item) => n + getText(item).length, 0);
|
|
1594
|
+
if (items.length < minItems || totalChars < minChars) {
|
|
1595
|
+
return {
|
|
1596
|
+
items,
|
|
1597
|
+
truncated: false,
|
|
1598
|
+
kept: items.length,
|
|
1599
|
+
dropped: 0,
|
|
1600
|
+
reason: "passthrough"
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
const deduped = dedupTextItems(items, getText);
|
|
1604
|
+
const unique = deduped.items;
|
|
1605
|
+
const uniqueChars = unique.reduce((n, item) => n + getText(item).length, 0);
|
|
1606
|
+
if (uniqueChars <= maxChars) {
|
|
1607
|
+
const changed = unique.length < original.length;
|
|
1608
|
+
return {
|
|
1609
|
+
items: unique,
|
|
1610
|
+
truncated: changed,
|
|
1611
|
+
kept: unique.length,
|
|
1612
|
+
dropped: original.length - unique.length,
|
|
1613
|
+
reason: changed ? "dedup" : "passthrough"
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
const lengths = unique.map((item) => getText(item).length);
|
|
1617
|
+
const { mean, std } = meanStd(lengths);
|
|
1618
|
+
const outlierFloor = mean + 2 * std;
|
|
1619
|
+
const mustKeep = /* @__PURE__ */ new Set();
|
|
1620
|
+
unique.forEach((item, i) => {
|
|
1621
|
+
const text7 = getText(item);
|
|
1622
|
+
if (looksLikeMustKeep(text7) || text7.length > outlierFloor) mustKeep.add(i);
|
|
1623
|
+
});
|
|
1624
|
+
const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
|
|
1625
|
+
let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
|
|
1626
|
+
const pick = new Set(mustKeep);
|
|
1627
|
+
const canTake = (i) => {
|
|
1628
|
+
if (pick.has(i)) return false;
|
|
1629
|
+
const next = used + lengths[i];
|
|
1630
|
+
if (next > maxChars && pick.size > mustKeep.size) return false;
|
|
1631
|
+
used = next;
|
|
1632
|
+
pick.add(i);
|
|
1633
|
+
return true;
|
|
1478
1634
|
};
|
|
1479
|
-
|
|
1480
|
-
|
|
1635
|
+
const k = remainingIdx.length;
|
|
1636
|
+
const headN = Math.max(1, Math.ceil(k * 0.3));
|
|
1637
|
+
const tailN = Math.max(1, Math.ceil(k * 0.15));
|
|
1638
|
+
for (const i of remainingIdx.slice(0, headN)) canTake(i);
|
|
1639
|
+
for (const i of remainingIdx.slice(-tailN).reverse()) canTake(i);
|
|
1640
|
+
const middle = remainingIdx.filter((i) => !pick.has(i)).sort((a, b) => importance(getText(unique[b])) - importance(getText(unique[a])));
|
|
1641
|
+
for (const i of middle) {
|
|
1642
|
+
if (used >= maxChars) break;
|
|
1643
|
+
canTake(i);
|
|
1644
|
+
}
|
|
1645
|
+
const crushed = unique.filter((_, i) => pick.has(i));
|
|
1646
|
+
const crushedChars = crushed.reduce((n, item) => n + getText(item).length, 0);
|
|
1647
|
+
if (crushed.length >= original.length && crushedChars >= totalChars) {
|
|
1648
|
+
return {
|
|
1649
|
+
items: original,
|
|
1650
|
+
truncated: false,
|
|
1651
|
+
kept: original.length,
|
|
1652
|
+
dropped: 0,
|
|
1653
|
+
reason: "passthrough"
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1481
1656
|
return {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
|
|
1657
|
+
items: crushed,
|
|
1658
|
+
truncated: crushed.length < original.length,
|
|
1659
|
+
kept: crushed.length,
|
|
1660
|
+
dropped: original.length - crushed.length,
|
|
1661
|
+
reason: "budget"
|
|
1488
1662
|
};
|
|
1489
1663
|
}
|
|
1490
|
-
function
|
|
1491
|
-
return
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1664
|
+
function splitBlocks(text7) {
|
|
1665
|
+
if (/\n\n/.test(text7)) return text7.split(/\n\n+/);
|
|
1666
|
+
return text7.split("\n");
|
|
1667
|
+
}
|
|
1668
|
+
function capOversizedBlock(block, maxChars) {
|
|
1669
|
+
if (block.length <= maxChars) return block;
|
|
1670
|
+
const head = Math.floor(maxChars * 0.3);
|
|
1671
|
+
const tail = Math.floor(maxChars * 0.15);
|
|
1672
|
+
return `${block.slice(0, head)}
|
|
1673
|
+
|
|
1674
|
+
[\u2026crushed ${block.length - head - tail} chars; pass include=full to read the rest\u2026]
|
|
1675
|
+
|
|
1676
|
+
${block.slice(-tail)}`;
|
|
1677
|
+
}
|
|
1678
|
+
function crushMarkdown(text7, opts) {
|
|
1679
|
+
const original = text7 ?? "";
|
|
1680
|
+
const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
|
|
1681
|
+
const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
|
|
1682
|
+
if (original.length < minChars) {
|
|
1683
|
+
return { text: original, truncated: false, originalChars: original.length };
|
|
1684
|
+
}
|
|
1685
|
+
const rawBlocks = splitBlocks(original).filter((b) => b.length > 0);
|
|
1686
|
+
const blocks = rawBlocks.map((b) => capOversizedBlock(b, maxChars));
|
|
1687
|
+
const cappedABlock = blocks.some((b, i) => b !== rawBlocks[i]);
|
|
1688
|
+
if (blocks.length <= 1) {
|
|
1689
|
+
const cut = blocks[0] ?? original;
|
|
1690
|
+
if (cut.length >= original.length) {
|
|
1691
|
+
return { text: original, truncated: false, originalChars: original.length };
|
|
1692
|
+
}
|
|
1693
|
+
return { text: cut, truncated: true, originalChars: original.length };
|
|
1694
|
+
}
|
|
1695
|
+
const must = blocks.filter((b) => looksLikeMustKeep(b));
|
|
1696
|
+
const rest = blocks.filter((b) => !looksLikeMustKeep(b));
|
|
1697
|
+
const marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
|
|
1698
|
+
let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
|
|
1699
|
+
const keptRest = [];
|
|
1700
|
+
const headN = Math.max(1, Math.ceil(rest.length * 0.3));
|
|
1701
|
+
const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
|
|
1702
|
+
const head = rest.slice(0, headN);
|
|
1703
|
+
const tail = rest.slice(-tailN);
|
|
1704
|
+
const middle = rest.slice(headN, Math.max(headN, rest.length - tailN));
|
|
1705
|
+
const take = (block) => {
|
|
1706
|
+
if (used + block.length + 2 > maxChars && keptRest.length + must.length > 0) {
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
used += block.length + 2;
|
|
1710
|
+
keptRest.push(block);
|
|
1711
|
+
return true;
|
|
1499
1712
|
};
|
|
1713
|
+
for (const b of head) take(b);
|
|
1714
|
+
for (const b of tail) take(b);
|
|
1715
|
+
const ranked = [...middle].sort((a, b) => importance(b) - importance(a));
|
|
1716
|
+
for (const b of ranked) {
|
|
1717
|
+
if (used >= maxChars) break;
|
|
1718
|
+
take(b);
|
|
1719
|
+
}
|
|
1720
|
+
const keepSet = /* @__PURE__ */ new Set([...must, ...keptRest]);
|
|
1721
|
+
const ordered = blocks.filter((b) => keepSet.has(b));
|
|
1722
|
+
const omitted = blocks.length - ordered.length;
|
|
1723
|
+
if (omitted <= 0) {
|
|
1724
|
+
const joined = blocks.join("\n\n");
|
|
1725
|
+
if (cappedABlock && joined.length < original.length) {
|
|
1726
|
+
return { text: joined, truncated: true, originalChars: original.length };
|
|
1727
|
+
}
|
|
1728
|
+
return { text: original, truncated: false, originalChars: original.length };
|
|
1729
|
+
}
|
|
1730
|
+
const out = [];
|
|
1731
|
+
let inserted = false;
|
|
1732
|
+
for (const block of blocks) {
|
|
1733
|
+
if (keepSet.has(block)) {
|
|
1734
|
+
out.push(block);
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (!inserted) {
|
|
1738
|
+
out.push(marker);
|
|
1739
|
+
inserted = true;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const textOut = out.join("\n\n");
|
|
1743
|
+
if (textOut.length >= original.length) {
|
|
1744
|
+
return { text: original, truncated: false, originalChars: original.length };
|
|
1745
|
+
}
|
|
1746
|
+
return { text: textOut, truncated: true, originalChars: original.length };
|
|
1500
1747
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1748
|
+
|
|
1749
|
+
// src/mcp/issue-payload.ts
|
|
1750
|
+
var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
|
|
1751
|
+
var mcpIssueIncludeSchema = z2.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
|
|
1752
|
+
"default applies SmartCrusher-style crush (dedup + budget; small unique tickets pass through). full returns the uncompressed vendor payload. Re-call with include=full if crush.truncated."
|
|
1753
|
+
);
|
|
1754
|
+
var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
|
|
1755
|
+
function emptyMeta() {
|
|
1756
|
+
return { truncated: true, hint: MCP_ISSUE_CRUSH_HINT };
|
|
1757
|
+
}
|
|
1758
|
+
function withComments(comments, meta) {
|
|
1759
|
+
const crushed = crushTextItems(comments, (c) => c.body);
|
|
1760
|
+
if (crushed.truncated) {
|
|
1761
|
+
meta.comments = {
|
|
1762
|
+
kept: crushed.kept,
|
|
1763
|
+
dropped: crushed.dropped,
|
|
1764
|
+
reason: crushed.reason
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
return { comments: crushed.items, meta };
|
|
1768
|
+
}
|
|
1769
|
+
function withBody(text7, meta) {
|
|
1770
|
+
const crushed = crushMarkdown(text7);
|
|
1771
|
+
if (crushed.truncated) {
|
|
1772
|
+
meta.body = { originalChars: crushed.originalChars };
|
|
1773
|
+
return { value: crushed.text, meta };
|
|
1774
|
+
}
|
|
1775
|
+
return { value: text7, meta };
|
|
1776
|
+
}
|
|
1777
|
+
function attachCrush(payload, meta) {
|
|
1778
|
+
if (!meta.comments && !meta.body) return payload;
|
|
1779
|
+
return { ...payload, crush: meta };
|
|
1780
|
+
}
|
|
1781
|
+
function formatLinearIssuePayload(issue, include) {
|
|
1782
|
+
if (include === "full") return issue;
|
|
1783
|
+
const meta = emptyMeta();
|
|
1784
|
+
const comments = withComments(issue.comments, meta);
|
|
1785
|
+
const description = withBody(issue.description, comments.meta);
|
|
1786
|
+
const { team, ...rest } = issue;
|
|
1787
|
+
const next = {
|
|
1788
|
+
...rest,
|
|
1789
|
+
...team ? { team: { id: team.id, key: team.key, name: team.name } } : {},
|
|
1790
|
+
description: description.value,
|
|
1791
|
+
comments: comments.comments
|
|
1792
|
+
};
|
|
1793
|
+
return attachCrush(next, description.meta);
|
|
1794
|
+
}
|
|
1795
|
+
function formatGitHubIssuePayload(issue, include) {
|
|
1796
|
+
if (include === "full") return issue;
|
|
1797
|
+
const meta = emptyMeta();
|
|
1798
|
+
const comments = withComments(issue.comments, meta);
|
|
1799
|
+
const body = withBody(issue.body, comments.meta);
|
|
1800
|
+
const next = {
|
|
1801
|
+
...issue,
|
|
1802
|
+
body: body.value,
|
|
1803
|
+
comments: comments.comments
|
|
1505
1804
|
};
|
|
1805
|
+
return attachCrush(next, body.meta);
|
|
1806
|
+
}
|
|
1807
|
+
function formatAbleTimeTaskPayload(task, include) {
|
|
1808
|
+
const base = {
|
|
1809
|
+
...toAbleTimeIssueInfo(task),
|
|
1810
|
+
description: task.description,
|
|
1811
|
+
state: task.state,
|
|
1812
|
+
comments: task.comments
|
|
1813
|
+
};
|
|
1814
|
+
if (include === "full") return base;
|
|
1815
|
+
const meta = emptyMeta();
|
|
1816
|
+
const comments = withComments(task.comments, meta);
|
|
1817
|
+
const description = withBody(task.description, comments.meta);
|
|
1818
|
+
return attachCrush(
|
|
1819
|
+
{
|
|
1820
|
+
...base,
|
|
1821
|
+
description: description.value,
|
|
1822
|
+
comments: comments.comments
|
|
1823
|
+
},
|
|
1824
|
+
description.meta
|
|
1825
|
+
);
|
|
1506
1826
|
}
|
|
1507
1827
|
|
|
1508
1828
|
// src/mcp/abletime-tools.ts
|
|
@@ -1548,7 +1868,7 @@ function registerAbleTimeTools(server) {
|
|
|
1548
1868
|
server.tool(
|
|
1549
1869
|
"abletime_list_tasks",
|
|
1550
1870
|
"List open AbleTime tasks (excludes done/archived). Optional projectId filter.",
|
|
1551
|
-
{ projectId:
|
|
1871
|
+
{ projectId: z3.string().optional() },
|
|
1552
1872
|
async ({ projectId }) => {
|
|
1553
1873
|
try {
|
|
1554
1874
|
const tasks = await listAbleTimeTasks({ projectId });
|
|
@@ -1561,7 +1881,7 @@ function registerAbleTimeTools(server) {
|
|
|
1561
1881
|
server.tool(
|
|
1562
1882
|
"abletime_search_tasks",
|
|
1563
1883
|
"Search AbleTime tasks by text, reference, or tag.",
|
|
1564
|
-
{ query:
|
|
1884
|
+
{ query: z3.string() },
|
|
1565
1885
|
async ({ query }) => {
|
|
1566
1886
|
try {
|
|
1567
1887
|
const tasks = await searchAbleTimeTasks(query);
|
|
@@ -1573,17 +1893,11 @@ function registerAbleTimeTools(server) {
|
|
|
1573
1893
|
);
|
|
1574
1894
|
server.tool(
|
|
1575
1895
|
"abletime_get_task",
|
|
1576
|
-
"Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments.",
|
|
1577
|
-
{ id:
|
|
1578
|
-
async ({ id }) => {
|
|
1896
|
+
"Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload.",
|
|
1897
|
+
{ id: z3.string(), include: mcpIssueIncludeSchema },
|
|
1898
|
+
async ({ id, include }) => {
|
|
1579
1899
|
try {
|
|
1580
|
-
|
|
1581
|
-
return text2({
|
|
1582
|
-
...toAbleTimeIssueInfo(task),
|
|
1583
|
-
description: task.description,
|
|
1584
|
-
state: task.state,
|
|
1585
|
-
comments: task.comments
|
|
1586
|
-
});
|
|
1900
|
+
return text2(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
|
|
1587
1901
|
} catch (err) {
|
|
1588
1902
|
return fail2(err);
|
|
1589
1903
|
}
|
|
@@ -1592,7 +1906,7 @@ function registerAbleTimeTools(server) {
|
|
|
1592
1906
|
server.tool(
|
|
1593
1907
|
"abletime_comment",
|
|
1594
1908
|
"Add a markdown comment on an AbleTime task (id or CRM-232).",
|
|
1595
|
-
{ id:
|
|
1909
|
+
{ id: z3.string(), body: z3.string() },
|
|
1596
1910
|
async (args) => {
|
|
1597
1911
|
try {
|
|
1598
1912
|
return text2(await commentAbleTimeTask(args));
|
|
@@ -1605,10 +1919,10 @@ function registerAbleTimeTools(server) {
|
|
|
1605
1919
|
"abletime_update_task",
|
|
1606
1920
|
"Update an AbleTime task (id or CRM-232). Pass title, description, and/or state.",
|
|
1607
1921
|
{
|
|
1608
|
-
id:
|
|
1609
|
-
title:
|
|
1610
|
-
description:
|
|
1611
|
-
state:
|
|
1922
|
+
id: z3.string(),
|
|
1923
|
+
title: z3.string().optional(),
|
|
1924
|
+
description: z3.string().optional(),
|
|
1925
|
+
state: z3.string().optional()
|
|
1612
1926
|
},
|
|
1613
1927
|
async (args) => {
|
|
1614
1928
|
try {
|
|
@@ -1627,12 +1941,12 @@ function registerAbleTimeTools(server) {
|
|
|
1627
1941
|
"abletime_create_task",
|
|
1628
1942
|
"Create an AbleTime task in a project. Call abletime_list_projects if you do not have a project id. Pass parent (CRM-232) for a spin-off. New tasks start in todo (or backlog).",
|
|
1629
1943
|
{
|
|
1630
|
-
title:
|
|
1631
|
-
description:
|
|
1632
|
-
projectId:
|
|
1633
|
-
categoryId:
|
|
1634
|
-
state:
|
|
1635
|
-
parent:
|
|
1944
|
+
title: z3.string(),
|
|
1945
|
+
description: z3.string().optional(),
|
|
1946
|
+
projectId: z3.string().optional(),
|
|
1947
|
+
categoryId: z3.string().optional(),
|
|
1948
|
+
state: z3.enum(["backlog", "todo"]).optional(),
|
|
1949
|
+
parent: z3.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
|
|
1636
1950
|
},
|
|
1637
1951
|
async (args) => {
|
|
1638
1952
|
try {
|
|
@@ -1646,10 +1960,10 @@ function registerAbleTimeTools(server) {
|
|
|
1646
1960
|
"abletime_ensure_task",
|
|
1647
1961
|
"Find or create an AbleTime task for this work, then create_thread with sourceType=ticket.",
|
|
1648
1962
|
{
|
|
1649
|
-
title:
|
|
1650
|
-
description:
|
|
1651
|
-
projectId:
|
|
1652
|
-
categoryId:
|
|
1963
|
+
title: z3.string(),
|
|
1964
|
+
description: z3.string().optional(),
|
|
1965
|
+
projectId: z3.string().optional(),
|
|
1966
|
+
categoryId: z3.string().optional()
|
|
1653
1967
|
},
|
|
1654
1968
|
async (args) => {
|
|
1655
1969
|
try {
|
|
@@ -1663,7 +1977,7 @@ function registerAbleTimeTools(server) {
|
|
|
1663
1977
|
}
|
|
1664
1978
|
|
|
1665
1979
|
// src/mcp/github-tools.ts
|
|
1666
|
-
import { z as
|
|
1980
|
+
import { z as z4 } from "zod";
|
|
1667
1981
|
|
|
1668
1982
|
// src/integrations/github-issues.ts
|
|
1669
1983
|
function requireGhOk(result, label) {
|
|
@@ -1847,15 +2161,15 @@ function fail3(err) {
|
|
|
1847
2161
|
true
|
|
1848
2162
|
);
|
|
1849
2163
|
}
|
|
1850
|
-
var repoPathSchema =
|
|
2164
|
+
var repoPathSchema = z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
|
|
1851
2165
|
function registerGithubIssueTools(server) {
|
|
1852
2166
|
server.tool(
|
|
1853
2167
|
"github_get_issue",
|
|
1854
|
-
"Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Uses Account gh.",
|
|
1855
|
-
{ id:
|
|
1856
|
-
async ({ id, repoPath }) => {
|
|
2168
|
+
"Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. Uses Account gh.",
|
|
2169
|
+
{ id: z4.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
|
|
2170
|
+
async ({ id, include, repoPath }) => {
|
|
1857
2171
|
try {
|
|
1858
|
-
return text3(await getGitHubIssue(id, { repoPath }));
|
|
2172
|
+
return text3(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
|
|
1859
2173
|
} catch (err) {
|
|
1860
2174
|
return fail3(err);
|
|
1861
2175
|
}
|
|
@@ -1864,7 +2178,7 @@ function registerGithubIssueTools(server) {
|
|
|
1864
2178
|
server.tool(
|
|
1865
2179
|
"github_comment",
|
|
1866
2180
|
"Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh.",
|
|
1867
|
-
{ id:
|
|
2181
|
+
{ id: z4.string(), body: z4.string(), repoPath: repoPathSchema },
|
|
1868
2182
|
async ({ id, body, repoPath }) => {
|
|
1869
2183
|
try {
|
|
1870
2184
|
return text3(await commentGitHubIssue({ id, body }, { repoPath }));
|
|
@@ -1877,10 +2191,10 @@ function registerGithubIssueTools(server) {
|
|
|
1877
2191
|
"github_update_issue",
|
|
1878
2192
|
"Update a GitHub issue (#123). Pass title, body, and/or state (open|closed).",
|
|
1879
2193
|
{
|
|
1880
|
-
id:
|
|
1881
|
-
title:
|
|
1882
|
-
body:
|
|
1883
|
-
state:
|
|
2194
|
+
id: z4.string(),
|
|
2195
|
+
title: z4.string().optional(),
|
|
2196
|
+
body: z4.string().optional(),
|
|
2197
|
+
state: z4.string().optional().describe("open or closed"),
|
|
1884
2198
|
repoPath: repoPathSchema
|
|
1885
2199
|
},
|
|
1886
2200
|
async (args) => {
|
|
@@ -1895,9 +2209,9 @@ function registerGithubIssueTools(server) {
|
|
|
1895
2209
|
"github_create_issue",
|
|
1896
2210
|
"Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
|
|
1897
2211
|
{
|
|
1898
|
-
title:
|
|
1899
|
-
body:
|
|
1900
|
-
parent:
|
|
2212
|
+
title: z4.string(),
|
|
2213
|
+
body: z4.string().optional(),
|
|
2214
|
+
parent: z4.string().optional().describe("Parent issue #123 or URL for a spin-off."),
|
|
1901
2215
|
repoPath: repoPathSchema
|
|
1902
2216
|
},
|
|
1903
2217
|
async (args) => {
|
|
@@ -1911,7 +2225,7 @@ function registerGithubIssueTools(server) {
|
|
|
1911
2225
|
}
|
|
1912
2226
|
|
|
1913
2227
|
// src/mcp/linear-tools.ts
|
|
1914
|
-
import { z as
|
|
2228
|
+
import { z as z5 } from "zod";
|
|
1915
2229
|
function text4(payload, isError = false) {
|
|
1916
2230
|
return mcpJson(payload, isError);
|
|
1917
2231
|
}
|
|
@@ -1921,7 +2235,7 @@ function fail4(err) {
|
|
|
1921
2235
|
true
|
|
1922
2236
|
);
|
|
1923
2237
|
}
|
|
1924
|
-
var prioritySchema =
|
|
2238
|
+
var prioritySchema = z5.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
|
|
1925
2239
|
function registerLinearTools(server) {
|
|
1926
2240
|
server.tool(
|
|
1927
2241
|
"linear_list_teams",
|
|
@@ -1939,9 +2253,9 @@ function registerLinearTools(server) {
|
|
|
1939
2253
|
"linear_search_issues",
|
|
1940
2254
|
"Search or list open Linear issues. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all (default with query), or a user id/name.",
|
|
1941
2255
|
{
|
|
1942
|
-
query:
|
|
1943
|
-
assignee:
|
|
1944
|
-
limit:
|
|
2256
|
+
query: z5.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
|
|
2257
|
+
assignee: z5.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
|
|
2258
|
+
limit: z5.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
|
|
1945
2259
|
},
|
|
1946
2260
|
async ({ query, assignee, limit }) => {
|
|
1947
2261
|
try {
|
|
@@ -1968,11 +2282,11 @@ function registerLinearTools(server) {
|
|
|
1968
2282
|
);
|
|
1969
2283
|
server.tool(
|
|
1970
2284
|
"linear_get_issue",
|
|
1971
|
-
"Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children.",
|
|
1972
|
-
{ id:
|
|
1973
|
-
async ({ id }) => {
|
|
2285
|
+
"Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload.",
|
|
2286
|
+
{ id: z5.string(), include: mcpIssueIncludeSchema },
|
|
2287
|
+
async ({ id, include }) => {
|
|
1974
2288
|
try {
|
|
1975
|
-
return text4(await getLinearIssue(id));
|
|
2289
|
+
return text4(formatLinearIssuePayload(await getLinearIssue(id), include));
|
|
1976
2290
|
} catch (err) {
|
|
1977
2291
|
return fail4(err);
|
|
1978
2292
|
}
|
|
@@ -1982,13 +2296,13 @@ function registerLinearTools(server) {
|
|
|
1982
2296
|
"linear_create_issue",
|
|
1983
2297
|
'Create a Linear issue. Call linear_list_teams first. team is id/key/name; state is name/type/id; assignee is "me" or a user id. Pass parent (ENG-123 or uuid) to nest a spin-off under the current ticket.',
|
|
1984
2298
|
{
|
|
1985
|
-
team:
|
|
1986
|
-
title:
|
|
1987
|
-
description:
|
|
1988
|
-
state:
|
|
1989
|
-
assignee:
|
|
2299
|
+
team: z5.string(),
|
|
2300
|
+
title: z5.string(),
|
|
2301
|
+
description: z5.string().optional(),
|
|
2302
|
+
state: z5.string().optional(),
|
|
2303
|
+
assignee: z5.string().nullable().optional(),
|
|
1990
2304
|
priority: prioritySchema,
|
|
1991
|
-
parent:
|
|
2305
|
+
parent: z5.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
|
|
1992
2306
|
},
|
|
1993
2307
|
async (args) => {
|
|
1994
2308
|
try {
|
|
@@ -2002,11 +2316,11 @@ function registerLinearTools(server) {
|
|
|
2002
2316
|
"linear_update_issue",
|
|
2003
2317
|
"Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority.",
|
|
2004
2318
|
{
|
|
2005
|
-
id:
|
|
2006
|
-
title:
|
|
2007
|
-
description:
|
|
2008
|
-
state:
|
|
2009
|
-
assignee:
|
|
2319
|
+
id: z5.string(),
|
|
2320
|
+
title: z5.string().optional(),
|
|
2321
|
+
description: z5.string().optional(),
|
|
2322
|
+
state: z5.string().optional(),
|
|
2323
|
+
assignee: z5.string().nullable().optional(),
|
|
2010
2324
|
priority: prioritySchema
|
|
2011
2325
|
},
|
|
2012
2326
|
async (args) => {
|
|
@@ -2021,8 +2335,8 @@ function registerLinearTools(server) {
|
|
|
2021
2335
|
"linear_comment",
|
|
2022
2336
|
"Add a markdown comment on a Linear issue (uuid or ENG-123).",
|
|
2023
2337
|
{
|
|
2024
|
-
id:
|
|
2025
|
-
body:
|
|
2338
|
+
id: z5.string(),
|
|
2339
|
+
body: z5.string()
|
|
2026
2340
|
},
|
|
2027
2341
|
async (args) => {
|
|
2028
2342
|
try {
|
|
@@ -2042,7 +2356,7 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
|
|
|
2042
2356
|
}
|
|
2043
2357
|
|
|
2044
2358
|
// src/mcp/viewer-context-tools.ts
|
|
2045
|
-
import { z as
|
|
2359
|
+
import { z as z6 } from "zod";
|
|
2046
2360
|
function text5(payload, isError = false) {
|
|
2047
2361
|
return mcpJson(payload, isError);
|
|
2048
2362
|
}
|
|
@@ -2074,7 +2388,7 @@ function registerViewerContextTools(server) {
|
|
|
2074
2388
|
"get_viewer_context",
|
|
2075
2389
|
"Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_workspaces for project context. Worktree turns may omit repoPath (uses cwd).",
|
|
2076
2390
|
{
|
|
2077
|
-
repoPath:
|
|
2391
|
+
repoPath: z6.string().optional().describe(
|
|
2078
2392
|
"Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
|
|
2079
2393
|
)
|
|
2080
2394
|
},
|
|
@@ -2094,10 +2408,10 @@ function registerViewerContextTools(server) {
|
|
|
2094
2408
|
"update_viewer_context",
|
|
2095
2409
|
"Replace account or project user context (Settings \u2192 Agents / Projects). Never call until the user confirmed. Show the proposed text, ask_user (Save this context / Do not save), wait, then pass confirmed=true. Empty context clears that field.",
|
|
2096
2410
|
{
|
|
2097
|
-
scope:
|
|
2098
|
-
context:
|
|
2099
|
-
confirmed:
|
|
2100
|
-
repoPath:
|
|
2411
|
+
scope: z6.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
|
|
2412
|
+
context: z6.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
|
|
2413
|
+
confirmed: z6.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
|
|
2414
|
+
repoPath: z6.string().optional().describe(
|
|
2101
2415
|
"Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
|
|
2102
2416
|
)
|
|
2103
2417
|
},
|
|
@@ -2174,12 +2488,9 @@ function formatMcpPrList(input) {
|
|
|
2174
2488
|
}
|
|
2175
2489
|
|
|
2176
2490
|
// src/mcp/schedule-tools.ts
|
|
2177
|
-
import { z as
|
|
2491
|
+
import { z as z7 } from "zod";
|
|
2178
2492
|
function text6(payload, isError = false) {
|
|
2179
|
-
return
|
|
2180
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
2181
|
-
...isError ? { isError: true } : {}
|
|
2182
|
-
};
|
|
2493
|
+
return mcpJson(payload, isError);
|
|
2183
2494
|
}
|
|
2184
2495
|
function fail5(err) {
|
|
2185
2496
|
return text6(
|
|
@@ -2217,17 +2528,17 @@ function registerScheduleTools(server) {
|
|
|
2217
2528
|
"create_schedule",
|
|
2218
2529
|
"Create a local schedule that, when due, sends a prompt to an orchestration chat (threadId) or starts a new Global orchestration chat (omit threadId). Pass threadId=self to continue this coordinator. Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without threadId open a new chat each run. Overnight/unattended runs need Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or set_caffeinate. Sideboard.app must be running for the job to fire.",
|
|
2219
2530
|
{
|
|
2220
|
-
prompt:
|
|
2221
|
-
name:
|
|
2222
|
-
at:
|
|
2223
|
-
every:
|
|
2224
|
-
cron:
|
|
2225
|
-
tz:
|
|
2226
|
-
threadId:
|
|
2531
|
+
prompt: z7.string().describe("User message / goal queued when the schedule fires"),
|
|
2532
|
+
name: z7.string().optional(),
|
|
2533
|
+
at: z7.string().optional().describe("ISO datetime for a one-shot"),
|
|
2534
|
+
every: z7.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
|
|
2535
|
+
cron: z7.string().optional().describe("5-field cron expression"),
|
|
2536
|
+
tz: z7.string().optional().describe("IANA timezone for cron (default: system)"),
|
|
2537
|
+
threadId: z7.string().optional().describe(
|
|
2227
2538
|
'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
|
|
2228
2539
|
),
|
|
2229
|
-
agent:
|
|
2230
|
-
model:
|
|
2540
|
+
agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
|
|
2541
|
+
model: z7.string().optional()
|
|
2231
2542
|
},
|
|
2232
2543
|
async (args) => {
|
|
2233
2544
|
try {
|
|
@@ -2260,17 +2571,17 @@ function registerScheduleTools(server) {
|
|
|
2260
2571
|
"update_schedule",
|
|
2261
2572
|
"Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
|
|
2262
2573
|
{
|
|
2263
|
-
id:
|
|
2264
|
-
prompt:
|
|
2265
|
-
name:
|
|
2266
|
-
at:
|
|
2267
|
-
every:
|
|
2268
|
-
cron:
|
|
2269
|
-
tz:
|
|
2270
|
-
threadId:
|
|
2271
|
-
agent:
|
|
2272
|
-
model:
|
|
2273
|
-
enabled:
|
|
2574
|
+
id: z7.string(),
|
|
2575
|
+
prompt: z7.string().optional(),
|
|
2576
|
+
name: z7.string().optional(),
|
|
2577
|
+
at: z7.string().optional(),
|
|
2578
|
+
every: z7.string().optional(),
|
|
2579
|
+
cron: z7.string().optional(),
|
|
2580
|
+
tz: z7.string().optional(),
|
|
2581
|
+
threadId: z7.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
|
|
2582
|
+
agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional(),
|
|
2583
|
+
model: z7.string().optional(),
|
|
2584
|
+
enabled: z7.boolean().optional()
|
|
2274
2585
|
},
|
|
2275
2586
|
async (args) => {
|
|
2276
2587
|
try {
|
|
@@ -2308,7 +2619,7 @@ function registerScheduleTools(server) {
|
|
|
2308
2619
|
server.tool(
|
|
2309
2620
|
"delete_schedule",
|
|
2310
2621
|
"Delete a local schedule. Pass id from list_schedules.",
|
|
2311
|
-
{ id:
|
|
2622
|
+
{ id: z7.string() },
|
|
2312
2623
|
async ({ id }) => {
|
|
2313
2624
|
try {
|
|
2314
2625
|
deleteSchedule(id);
|
|
@@ -2321,7 +2632,7 @@ function registerScheduleTools(server) {
|
|
|
2321
2632
|
server.tool(
|
|
2322
2633
|
"run_schedule",
|
|
2323
2634
|
"Fire a schedule now (does not wait for nextRunAt). Queues the prompt or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
|
|
2324
|
-
{ id:
|
|
2635
|
+
{ id: z7.string() },
|
|
2325
2636
|
async ({ id }) => {
|
|
2326
2637
|
try {
|
|
2327
2638
|
if (!getSchedule(id)) {
|
|
@@ -2742,13 +3053,13 @@ async function startMcpServer() {
|
|
|
2742
3053
|
"list_board",
|
|
2743
3054
|
"Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), column, limit (default 40). create_thread adds a worktree (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
|
|
2744
3055
|
{
|
|
2745
|
-
query:
|
|
2746
|
-
repoPath:
|
|
2747
|
-
kind:
|
|
2748
|
-
column:
|
|
3056
|
+
query: z8.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
|
|
3057
|
+
repoPath: z8.string().optional().describe("Limit to one workspace path from list_workspaces"),
|
|
3058
|
+
kind: z8.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
|
|
3059
|
+
column: z8.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
|
|
2749
3060
|
"Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
|
|
2750
3061
|
),
|
|
2751
|
-
limit:
|
|
3062
|
+
limit: z8.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
|
|
2752
3063
|
},
|
|
2753
3064
|
async ({ query, repoPath, kind, column, limit }) => {
|
|
2754
3065
|
const workspaces = orch.listWorkspaces();
|
|
@@ -2763,30 +3074,19 @@ async function startMcpServer() {
|
|
|
2763
3074
|
limit,
|
|
2764
3075
|
workspaceName: (path) => names.get(path) ?? ""
|
|
2765
3076
|
});
|
|
2766
|
-
return {
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
hidden: snap.hidden,
|
|
2774
|
-
totals: snap.totals,
|
|
2775
|
-
columnDefs: BOARD_COLUMN_DEFS,
|
|
2776
|
-
hint: HOME_BOARD_AGENT_HINT
|
|
2777
|
-
},
|
|
2778
|
-
null,
|
|
2779
|
-
2
|
|
2780
|
-
)
|
|
2781
|
-
}
|
|
2782
|
-
]
|
|
2783
|
-
};
|
|
3077
|
+
return mcpJson({
|
|
3078
|
+
columns: snap.columns,
|
|
3079
|
+
hidden: snap.hidden,
|
|
3080
|
+
totals: snap.totals,
|
|
3081
|
+
columnDefs: BOARD_COLUMN_DEFS,
|
|
3082
|
+
hint: HOME_BOARD_AGENT_HINT
|
|
3083
|
+
});
|
|
2784
3084
|
}
|
|
2785
3085
|
);
|
|
2786
3086
|
server.tool(
|
|
2787
3087
|
"get_thread",
|
|
2788
3088
|
"Get a compact thread summary by id/ref. Includes last message preview, parentThreadId, and child worktree threads (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (thread billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
|
|
2789
|
-
{ ref:
|
|
3089
|
+
{ ref: z8.string() },
|
|
2790
3090
|
async ({ ref }) => {
|
|
2791
3091
|
const t = orch.getThread(ref);
|
|
2792
3092
|
if (!t) {
|
|
@@ -2820,7 +3120,7 @@ async function startMcpServer() {
|
|
|
2820
3120
|
usage: spend.usage,
|
|
2821
3121
|
lastTurnUsage: spend.lastTurnUsage
|
|
2822
3122
|
};
|
|
2823
|
-
return
|
|
3123
|
+
return mcpJson(summary);
|
|
2824
3124
|
}
|
|
2825
3125
|
);
|
|
2826
3126
|
}
|
|
@@ -2828,17 +3128,17 @@ async function startMcpServer() {
|
|
|
2828
3128
|
"present_artifact",
|
|
2829
3129
|
"Show a document or live log in Sideboard\u2019s side column. For html/svg/markdown/react, pass the FULL document and do not also fence that same body in chat. For type=log, pass only NEW lines (same artifact_id appends). Prefer type=log for long-running job output \u2014 do not resend HTML. type=react is a single default-export component (JSX/TSX); only react/react-dom imports.",
|
|
2830
3130
|
{
|
|
2831
|
-
title:
|
|
2832
|
-
type:
|
|
3131
|
+
title: z8.string().describe("Short title shown in the artifact pane header"),
|
|
3132
|
+
type: z8.enum(["html", "svg", "markdown", "react", "log"]).describe(
|
|
2833
3133
|
"html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
|
|
2834
3134
|
),
|
|
2835
|
-
content:
|
|
3135
|
+
content: z8.string().describe(
|
|
2836
3136
|
"html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
|
|
2837
3137
|
),
|
|
2838
|
-
artifact_id:
|
|
2839
|
-
status:
|
|
2840
|
-
phase:
|
|
2841
|
-
mode:
|
|
3138
|
+
artifact_id: z8.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
|
|
3139
|
+
status: z8.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
|
|
3140
|
+
phase: z8.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
|
|
3141
|
+
mode: z8.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
|
|
2842
3142
|
},
|
|
2843
3143
|
async ({ title, type, artifact_id, status, phase, mode }) => {
|
|
2844
3144
|
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -2852,22 +3152,22 @@ async function startMcpServer() {
|
|
|
2852
3152
|
mode: type === "log" ? mode ?? "append" : void 0,
|
|
2853
3153
|
message: type === "log" ? "Log accepted. Same artifact_id appends; send only new lines next time." : "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
|
|
2854
3154
|
};
|
|
2855
|
-
return
|
|
3155
|
+
return mcpJson(payload);
|
|
2856
3156
|
}
|
|
2857
3157
|
);
|
|
2858
3158
|
server.tool(
|
|
2859
3159
|
"ask_user",
|
|
2860
3160
|
"Ask the user a clarifying multiple-choice question in Sideboard\u2019s composer. Call only when work is blocked on choosing among a few concrete options (approach fork, which API, auth vs cookies). Do not call for greetings, check-ins, \u201Chello\u201D, open-ended how-can-I-help, or to invent a menu of possible next tasks \u2014 reply in chat instead. If one option is the obvious default, proceed without asking. Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. After calling, stop and wait for answers. Not for \u201Cis the plan ready?\u201D.",
|
|
2861
3161
|
{
|
|
2862
|
-
questions:
|
|
2863
|
-
|
|
2864
|
-
question:
|
|
2865
|
-
header:
|
|
2866
|
-
multiSelect:
|
|
2867
|
-
options:
|
|
2868
|
-
|
|
2869
|
-
label:
|
|
2870
|
-
description:
|
|
3162
|
+
questions: z8.array(
|
|
3163
|
+
z8.object({
|
|
3164
|
+
question: z8.string().describe("Full question text ending with ?"),
|
|
3165
|
+
header: z8.string().max(24).optional().describe("Short label shown above the question"),
|
|
3166
|
+
multiSelect: z8.boolean().optional().describe("Allow selecting multiple options"),
|
|
3167
|
+
options: z8.array(
|
|
3168
|
+
z8.object({
|
|
3169
|
+
label: z8.string(),
|
|
3170
|
+
description: z8.string().optional().describe("What this option means / when to choose it (strongly preferred)")
|
|
2871
3171
|
})
|
|
2872
3172
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
2873
3173
|
})
|
|
@@ -2879,16 +3179,16 @@ async function startMcpServer() {
|
|
|
2879
3179
|
questions,
|
|
2880
3180
|
message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
|
|
2881
3181
|
};
|
|
2882
|
-
return
|
|
3182
|
+
return mcpJson(payload);
|
|
2883
3183
|
}
|
|
2884
3184
|
);
|
|
2885
3185
|
server.tool(
|
|
2886
3186
|
"present_plan",
|
|
2887
3187
|
"Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
|
|
2888
3188
|
{
|
|
2889
|
-
title:
|
|
2890
|
-
content:
|
|
2891
|
-
thread_id:
|
|
3189
|
+
title: z8.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
3190
|
+
content: z8.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
3191
|
+
thread_id: z8.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
2892
3192
|
},
|
|
2893
3193
|
async ({ title, content, thread_id }) => {
|
|
2894
3194
|
const { writePlanFile } = await import("../plan-file-I24VJVP2.js");
|
|
@@ -2904,22 +3204,22 @@ async function startMcpServer() {
|
|
|
2904
3204
|
title: title?.trim() || "Plan",
|
|
2905
3205
|
message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
|
|
2906
3206
|
};
|
|
2907
|
-
return
|
|
3207
|
+
return mcpJson(payload);
|
|
2908
3208
|
}
|
|
2909
3209
|
);
|
|
2910
3210
|
server.tool(
|
|
2911
3211
|
"present_schema",
|
|
2912
3212
|
"Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
|
|
2913
3213
|
{
|
|
2914
|
-
title:
|
|
2915
|
-
mode:
|
|
2916
|
-
datasource:
|
|
2917
|
-
resource_id:
|
|
2918
|
-
record_id:
|
|
2919
|
-
resource:
|
|
2920
|
-
record:
|
|
2921
|
-
records:
|
|
2922
|
-
pane_id:
|
|
3214
|
+
title: z8.string().describe("Pane title"),
|
|
3215
|
+
mode: z8.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
|
|
3216
|
+
datasource: z8.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
|
|
3217
|
+
resource_id: z8.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
|
|
3218
|
+
record_id: z8.string().optional().describe("Record id when opening form mode"),
|
|
3219
|
+
resource: z8.record(z8.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
|
|
3220
|
+
record: z8.record(z8.unknown()).optional().describe("Inline record { id, data, published_at? }"),
|
|
3221
|
+
records: z8.array(z8.record(z8.unknown())).optional().describe("Inline records for table mode"),
|
|
3222
|
+
pane_id: z8.string().optional().describe("Stable pane id across updates")
|
|
2923
3223
|
},
|
|
2924
3224
|
async (args) => {
|
|
2925
3225
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -2933,17 +3233,17 @@ async function startMcpServer() {
|
|
|
2933
3233
|
record_id: args.record_id,
|
|
2934
3234
|
message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
|
|
2935
3235
|
};
|
|
2936
|
-
return
|
|
3236
|
+
return mcpJson(payload);
|
|
2937
3237
|
}
|
|
2938
3238
|
);
|
|
2939
3239
|
server.tool(
|
|
2940
3240
|
"present_files",
|
|
2941
3241
|
"Open Sideboard\u2019s Files column (CMS-style file manager: browse, upload, pick). Use datasource=brightsy when the user is logged into Brightsy account storage; datasource=memory for a session-local demo store. Prefer this over claiming a file manager UI is unavailable. Pair with present_schema when editing records that need media.",
|
|
2942
3242
|
{
|
|
2943
|
-
title:
|
|
2944
|
-
datasource:
|
|
2945
|
-
path:
|
|
2946
|
-
pane_id:
|
|
3243
|
+
title: z8.string().optional().describe("Pane title (default: Files)"),
|
|
3244
|
+
datasource: z8.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
|
|
3245
|
+
path: z8.string().optional().describe("Initial folder path (e.g. public)"),
|
|
3246
|
+
pane_id: z8.string().optional().describe("Stable pane id across updates")
|
|
2947
3247
|
},
|
|
2948
3248
|
async (args) => {
|
|
2949
3249
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -2955,21 +3255,21 @@ async function startMcpServer() {
|
|
|
2955
3255
|
path: args.path,
|
|
2956
3256
|
message: "Files pane accepted. Sideboard desktop opens the Files column beside chat."
|
|
2957
3257
|
};
|
|
2958
|
-
return
|
|
3258
|
+
return mcpJson(payload);
|
|
2959
3259
|
}
|
|
2960
3260
|
);
|
|
2961
3261
|
server.tool(
|
|
2962
3262
|
"wait_for_job",
|
|
2963
3263
|
"Wait on a detached long job (tests, pack, deploy) started with detached-job.js. MCP clients kill tools around 60s, so this returns within 45s. stillRunning is the source of truth \u2014 if true, present_artifact type=log with content=delta (same artifact_id) and call wait_for_job again. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
|
|
2964
3264
|
{
|
|
2965
|
-
id:
|
|
2966
|
-
timeoutMs:
|
|
3265
|
+
id: z8.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
|
|
3266
|
+
timeoutMs: z8.number().optional()
|
|
2967
3267
|
},
|
|
2968
3268
|
async ({ id, timeoutMs }) => {
|
|
2969
3269
|
const result = await waitForDetachedJob(process.cwd(), id, {
|
|
2970
3270
|
timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
|
|
2971
3271
|
});
|
|
2972
|
-
return
|
|
3272
|
+
return mcpJson(result);
|
|
2973
3273
|
}
|
|
2974
3274
|
);
|
|
2975
3275
|
if (!worktreeProfile) {
|
|
@@ -2984,56 +3284,44 @@ async function startMcpServer() {
|
|
|
2984
3284
|
"set_caffeinate",
|
|
2985
3285
|
"Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. Closing or archiving this orchestration chat also releases the hold. macOS only.",
|
|
2986
3286
|
{
|
|
2987
|
-
enabled:
|
|
3287
|
+
enabled: z8.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
|
|
2988
3288
|
},
|
|
2989
3289
|
async ({ enabled }) => {
|
|
2990
3290
|
const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
|
|
2991
3291
|
const state = setCaffeinateHold(enabled, { threadId });
|
|
2992
3292
|
if (enabled && !state.held) {
|
|
2993
|
-
return
|
|
2994
|
-
content: [
|
|
2995
|
-
{
|
|
2996
|
-
type: "text",
|
|
2997
|
-
text: JSON.stringify({
|
|
2998
|
-
...state,
|
|
2999
|
-
ok: false,
|
|
3000
|
-
message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
|
|
3001
|
-
})
|
|
3002
|
-
}
|
|
3003
|
-
],
|
|
3004
|
-
isError: true
|
|
3005
|
-
};
|
|
3006
|
-
}
|
|
3007
|
-
return {
|
|
3008
|
-
content: [
|
|
3293
|
+
return mcpJson(
|
|
3009
3294
|
{
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3295
|
+
...state,
|
|
3296
|
+
ok: false,
|
|
3297
|
+
message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
|
|
3298
|
+
},
|
|
3299
|
+
true
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
return mcpJson({
|
|
3303
|
+
...getCaffeinateHold(),
|
|
3304
|
+
ok: true,
|
|
3305
|
+
message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false, the user says they are done, or this orchestration chat is closed." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
|
|
3306
|
+
});
|
|
3019
3307
|
}
|
|
3020
3308
|
);
|
|
3021
3309
|
server.tool(
|
|
3022
3310
|
"create_thread",
|
|
3023
3311
|
`Create a worktree thread (chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_workspaces. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_thread to chat.`,
|
|
3024
3312
|
{
|
|
3025
|
-
sourceType:
|
|
3026
|
-
sourceRef:
|
|
3027
|
-
agent:
|
|
3028
|
-
model:
|
|
3313
|
+
sourceType: z8.enum(["branch", "pr", "ticket"]),
|
|
3314
|
+
sourceRef: z8.string(),
|
|
3315
|
+
agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
|
|
3316
|
+
model: z8.string().nullable().optional().describe(
|
|
3029
3317
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
3030
3318
|
),
|
|
3031
|
-
repoPath:
|
|
3032
|
-
title:
|
|
3033
|
-
cowboy:
|
|
3319
|
+
repoPath: z8.string(),
|
|
3320
|
+
title: z8.string().optional(),
|
|
3321
|
+
cowboy: z8.boolean().optional().describe(
|
|
3034
3322
|
"If true, work in the project folder on the default branch (no thread/* worktree). Requires Settings \u2192 Advanced \u2192 Cowboy mode. Land is commit+push to that branch. Archive does not delete the folder."
|
|
3035
3323
|
),
|
|
3036
|
-
parentThreadId:
|
|
3324
|
+
parentThreadId: z8.string().optional().describe(
|
|
3037
3325
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
3038
3326
|
)
|
|
3039
3327
|
},
|
|
@@ -3049,10 +3337,10 @@ async function startMcpServer() {
|
|
|
3049
3337
|
"start_board_card",
|
|
3050
3338
|
"Same as create_thread for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_thread.",
|
|
3051
3339
|
{
|
|
3052
|
-
kind:
|
|
3053
|
-
ref:
|
|
3054
|
-
repoPath:
|
|
3055
|
-
title:
|
|
3340
|
+
kind: z8.enum(["ticket", "pr", "branch"]),
|
|
3341
|
+
ref: z8.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
|
|
3342
|
+
repoPath: z8.string().describe("Workspace path from list_workspaces / list_board"),
|
|
3343
|
+
title: z8.string().optional()
|
|
3056
3344
|
},
|
|
3057
3345
|
async ({ kind, ref, repoPath, title }) => {
|
|
3058
3346
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -3069,20 +3357,13 @@ async function startMcpServer() {
|
|
|
3069
3357
|
}))
|
|
3070
3358
|
);
|
|
3071
3359
|
if (existing) {
|
|
3072
|
-
return {
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
title: existing.title,
|
|
3080
|
-
status: existing.status,
|
|
3081
|
-
link: `sideboard://thread/${existing.id}`
|
|
3082
|
-
})
|
|
3083
|
-
}
|
|
3084
|
-
]
|
|
3085
|
-
};
|
|
3360
|
+
return mcpJson({
|
|
3361
|
+
alreadyStarted: true,
|
|
3362
|
+
id: existing.id,
|
|
3363
|
+
title: existing.title,
|
|
3364
|
+
status: existing.status,
|
|
3365
|
+
link: `sideboard://thread/${existing.id}`
|
|
3366
|
+
});
|
|
3086
3367
|
}
|
|
3087
3368
|
const workspaces = orch.listWorkspaces();
|
|
3088
3369
|
const loaded = await getHomeBoardInputs(workspaces);
|
|
@@ -3156,9 +3437,9 @@ async function startMcpServer() {
|
|
|
3156
3437
|
"send_to_thread",
|
|
3157
3438
|
'Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not force_stop to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that stops the child mid-thought. Call wait_for_turn again instead.',
|
|
3158
3439
|
{
|
|
3159
|
-
ref:
|
|
3160
|
-
prompt:
|
|
3161
|
-
force_stop:
|
|
3440
|
+
ref: z8.string(),
|
|
3441
|
+
prompt: z8.string(),
|
|
3442
|
+
force_stop: z8.boolean().optional()
|
|
3162
3443
|
},
|
|
3163
3444
|
async ({ ref, prompt, force_stop }) => {
|
|
3164
3445
|
if (force_stop) {
|
|
@@ -3168,79 +3449,58 @@ async function startMcpServer() {
|
|
|
3168
3449
|
}
|
|
3169
3450
|
}
|
|
3170
3451
|
const thread = await orch.send(ref, prompt);
|
|
3171
|
-
return {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
status: thread.status,
|
|
3178
|
-
queueLength: thread.queue.length,
|
|
3179
|
-
forceStopped: Boolean(force_stop)
|
|
3180
|
-
})
|
|
3181
|
-
}
|
|
3182
|
-
]
|
|
3183
|
-
};
|
|
3452
|
+
return mcpJson({
|
|
3453
|
+
id: thread.id,
|
|
3454
|
+
status: thread.status,
|
|
3455
|
+
queueLength: thread.queue.length,
|
|
3456
|
+
forceStopped: Boolean(force_stop)
|
|
3457
|
+
});
|
|
3184
3458
|
}
|
|
3185
3459
|
);
|
|
3186
3460
|
server.tool(
|
|
3187
3461
|
"wait_for_turn",
|
|
3188
3462
|
"Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. stillRunning is the source of truth \u2014 if false, the child is not working (do not say it is waiting for a gate). If stillRunning is true, progress is tools/thinking (or \u201Cqueued, waiting for a concurrency slot\u201D if it has not started). Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume a hang. On status error, lastError/text is the failure. On status stopped or broken, the child did not finish \u2014 resume with send_to_thread or tell the user; do not treat that as success. When finished, usage is the last agent turn\u2019s tokens + costUsd (when the provider reported cost).",
|
|
3189
3463
|
{
|
|
3190
|
-
ref:
|
|
3191
|
-
timeoutMs:
|
|
3464
|
+
ref: z8.string(),
|
|
3465
|
+
timeoutMs: z8.number().optional()
|
|
3192
3466
|
},
|
|
3193
3467
|
async ({ ref, timeoutMs }) => {
|
|
3194
3468
|
const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
|
|
3195
3469
|
resolveIfStillRunning: true
|
|
3196
3470
|
});
|
|
3197
3471
|
const result = orch.getTurnResult(thread.id);
|
|
3198
|
-
return {
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
lastActivityAt: result.lastActivityAt,
|
|
3210
|
-
hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
|
|
3211
|
-
incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
|
|
3212
|
-
})
|
|
3213
|
-
}
|
|
3214
|
-
]
|
|
3215
|
-
};
|
|
3472
|
+
return mcpJson({
|
|
3473
|
+
id: thread.id,
|
|
3474
|
+
status: result.status,
|
|
3475
|
+
text: result.text,
|
|
3476
|
+
lastError: result.lastError,
|
|
3477
|
+
stillRunning: result.stillRunning,
|
|
3478
|
+
progress: result.progress,
|
|
3479
|
+
lastActivityAt: result.lastActivityAt,
|
|
3480
|
+
hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
|
|
3481
|
+
incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
|
|
3482
|
+
});
|
|
3216
3483
|
}
|
|
3217
3484
|
);
|
|
3218
3485
|
server.tool(
|
|
3219
3486
|
"get_turn_result",
|
|
3220
3487
|
"Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript. Includes usage for the last agent turn (tokens + costUsd when reported).",
|
|
3221
|
-
{ ref:
|
|
3488
|
+
{ ref: z8.string() },
|
|
3222
3489
|
async ({ ref }) => {
|
|
3223
3490
|
const result = orch.getTurnResult(ref);
|
|
3224
|
-
return {
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
...result,
|
|
3230
|
-
hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
|
|
3231
|
-
incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
|
|
3232
|
-
})
|
|
3233
|
-
}
|
|
3234
|
-
]
|
|
3235
|
-
};
|
|
3491
|
+
return mcpJson({
|
|
3492
|
+
...result,
|
|
3493
|
+
hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
|
|
3494
|
+
incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
|
|
3495
|
+
});
|
|
3236
3496
|
}
|
|
3237
3497
|
);
|
|
3238
3498
|
server.tool(
|
|
3239
3499
|
"stop_thread",
|
|
3240
3500
|
"Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
|
|
3241
3501
|
{
|
|
3242
|
-
ref:
|
|
3243
|
-
force:
|
|
3502
|
+
ref: z8.string(),
|
|
3503
|
+
force: z8.boolean().optional()
|
|
3244
3504
|
},
|
|
3245
3505
|
async ({ ref, force }) => {
|
|
3246
3506
|
const t = orch.getThread(ref);
|
|
@@ -3253,24 +3513,17 @@ async function startMcpServer() {
|
|
|
3253
3513
|
const clearQueue = force !== false;
|
|
3254
3514
|
const hadQueued = t.queue.length > 0;
|
|
3255
3515
|
const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
|
|
3256
|
-
return {
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
id: stopped.id,
|
|
3262
|
-
status: stopped.status,
|
|
3263
|
-
clearedQueue: clearQueue && hadQueued
|
|
3264
|
-
})
|
|
3265
|
-
}
|
|
3266
|
-
]
|
|
3267
|
-
};
|
|
3516
|
+
return mcpJson({
|
|
3517
|
+
id: stopped.id,
|
|
3518
|
+
status: stopped.status,
|
|
3519
|
+
clearedQueue: clearQueue && hadQueued
|
|
3520
|
+
});
|
|
3268
3521
|
}
|
|
3269
3522
|
);
|
|
3270
3523
|
server.tool(
|
|
3271
3524
|
"archive_thread",
|
|
3272
3525
|
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, and open PRs by asking the worktree agent (ask_git). Merge only when the user explicitly asked.",
|
|
3273
|
-
{ ref:
|
|
3526
|
+
{ ref: z8.string() },
|
|
3274
3527
|
async ({ ref }) => {
|
|
3275
3528
|
const t = orch.getThread(ref);
|
|
3276
3529
|
if (!t) {
|
|
@@ -3287,38 +3540,24 @@ async function startMcpServer() {
|
|
|
3287
3540
|
};
|
|
3288
3541
|
}
|
|
3289
3542
|
const archived = await orch.archive(ref);
|
|
3290
|
-
return {
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
text: JSON.stringify({
|
|
3295
|
-
id: archived.id,
|
|
3296
|
-
status: archived.status
|
|
3297
|
-
})
|
|
3298
|
-
}
|
|
3299
|
-
]
|
|
3300
|
-
};
|
|
3543
|
+
return mcpJson({
|
|
3544
|
+
id: archived.id,
|
|
3545
|
+
status: archived.status
|
|
3546
|
+
});
|
|
3301
3547
|
}
|
|
3302
3548
|
);
|
|
3303
3549
|
server.tool(
|
|
3304
3550
|
"restore_thread",
|
|
3305
3551
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
3306
|
-
{ ref:
|
|
3552
|
+
{ ref: z8.string() },
|
|
3307
3553
|
async ({ ref }) => {
|
|
3308
3554
|
try {
|
|
3309
3555
|
const restored = await orch.restore(ref);
|
|
3310
|
-
return {
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
id: restored.id,
|
|
3316
|
-
status: restored.status,
|
|
3317
|
-
worktreePath: restored.worktreePath
|
|
3318
|
-
})
|
|
3319
|
-
}
|
|
3320
|
-
]
|
|
3321
|
-
};
|
|
3556
|
+
return mcpJson({
|
|
3557
|
+
id: restored.id,
|
|
3558
|
+
status: restored.status,
|
|
3559
|
+
worktreePath: restored.worktreePath
|
|
3560
|
+
});
|
|
3322
3561
|
} catch (err) {
|
|
3323
3562
|
const message = err instanceof Error ? err.message : String(err);
|
|
3324
3563
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3329,34 +3568,25 @@ async function startMcpServer() {
|
|
|
3329
3568
|
"get_diff",
|
|
3330
3569
|
"Compact diff summary (capped hunks, paginated)",
|
|
3331
3570
|
{
|
|
3332
|
-
ref:
|
|
3333
|
-
maxFiles:
|
|
3571
|
+
ref: z8.string(),
|
|
3572
|
+
maxFiles: z8.number().optional()
|
|
3334
3573
|
},
|
|
3335
3574
|
async ({ ref, maxFiles }) => {
|
|
3336
3575
|
const summary = await orch.diffSummary(ref);
|
|
3337
|
-
return {
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
text: JSON.stringify({
|
|
3342
|
-
...summary,
|
|
3343
|
-
files: summary.files.slice(0, maxFiles ?? 10)
|
|
3344
|
-
})
|
|
3345
|
-
}
|
|
3346
|
-
]
|
|
3347
|
-
};
|
|
3576
|
+
return mcpJson({
|
|
3577
|
+
...summary,
|
|
3578
|
+
files: summary.files.slice(0, maxFiles ?? 10)
|
|
3579
|
+
});
|
|
3348
3580
|
}
|
|
3349
3581
|
);
|
|
3350
3582
|
server.tool(
|
|
3351
3583
|
"get_pr_checks",
|
|
3352
3584
|
"Snapshot of GitHub PR checks for a worktree thread (`gh pr checks` plus merge/review gates). null = no PR. If the user gave a goal (Greptile 5/5, CI green), the worktree agent watches with `gh pr checks --watch` via /long-running \u2014 this tool is a snapshot, not a waiter. Coordinators: do not run gh from the orchestration cwd.",
|
|
3353
|
-
{ ref:
|
|
3585
|
+
{ ref: z8.string().describe("Worktree thread id/ref") },
|
|
3354
3586
|
async ({ ref }) => {
|
|
3355
3587
|
try {
|
|
3356
3588
|
const checks = await orch.getPrChecks(ref);
|
|
3357
|
-
return
|
|
3358
|
-
content: [{ type: "text", text: JSON.stringify(checks) }]
|
|
3359
|
-
};
|
|
3589
|
+
return mcpJson(checks);
|
|
3360
3590
|
} catch (err) {
|
|
3361
3591
|
const message = err instanceof Error ? err.message : String(err);
|
|
3362
3592
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3366,25 +3596,18 @@ async function startMcpServer() {
|
|
|
3366
3596
|
server.tool(
|
|
3367
3597
|
"request_review",
|
|
3368
3598
|
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .claude/skills/review/SKILL.md when present (else copies .sideboard/review.md into .context/review.md, or seeds that file from the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
|
|
3369
|
-
{ ref:
|
|
3599
|
+
{ ref: z8.string().describe("Worktree thread id/ref to review") },
|
|
3370
3600
|
async ({ ref }) => {
|
|
3371
3601
|
try {
|
|
3372
3602
|
const tab = await orch.requestReview(ref);
|
|
3373
3603
|
const from = orch.getThread(ref);
|
|
3374
|
-
return {
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
status: tab.status,
|
|
3382
|
-
fromThreadId: from?.id ?? ref,
|
|
3383
|
-
link: `sideboard://thread/${tab.id}`
|
|
3384
|
-
})
|
|
3385
|
-
}
|
|
3386
|
-
]
|
|
3387
|
-
};
|
|
3604
|
+
return mcpJson({
|
|
3605
|
+
id: tab.id,
|
|
3606
|
+
title: tab.title,
|
|
3607
|
+
status: tab.status,
|
|
3608
|
+
fromThreadId: from?.id ?? ref,
|
|
3609
|
+
link: `sideboard://thread/${tab.id}`
|
|
3610
|
+
});
|
|
3388
3611
|
} catch (err) {
|
|
3389
3612
|
const message = err instanceof Error ? err.message : String(err);
|
|
3390
3613
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3395,53 +3618,37 @@ async function startMcpServer() {
|
|
|
3395
3618
|
"ask_git",
|
|
3396
3619
|
"Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` even when origin is SSH / Settings \u2192 Git is SSH). When dirty, queues the worktree agent to commit; then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd. If this tool errors that the GraphQL/PR body is too long, the branch is already pushed \u2014 have the worktree agent retry `gh pr create --body-file` with a short description (GitHub limit 65,536 characters). Do not invent SSH/auth failures from that error.",
|
|
3397
3620
|
{
|
|
3398
|
-
ref:
|
|
3399
|
-
action:
|
|
3621
|
+
ref: z8.string().describe("Worktree thread id/ref"),
|
|
3622
|
+
action: z8.enum(AGENT_GIT_ACTIONS).describe(
|
|
3400
3623
|
"commit-push | create-draft | create-web | resolve-conflicts | merge"
|
|
3401
3624
|
)
|
|
3402
3625
|
},
|
|
3403
3626
|
async ({ ref, action }) => {
|
|
3404
3627
|
try {
|
|
3405
3628
|
const thread = await orch.askGit(ref, action);
|
|
3406
|
-
return {
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
queueLength: thread.queue.length,
|
|
3414
|
-
action,
|
|
3415
|
-
link: `sideboard://thread/${thread.id}`
|
|
3416
|
-
})
|
|
3417
|
-
}
|
|
3418
|
-
]
|
|
3419
|
-
};
|
|
3629
|
+
return mcpJson({
|
|
3630
|
+
id: thread.id,
|
|
3631
|
+
status: thread.status,
|
|
3632
|
+
queueLength: thread.queue.length,
|
|
3633
|
+
action,
|
|
3634
|
+
link: `sideboard://thread/${thread.id}`
|
|
3635
|
+
});
|
|
3420
3636
|
} catch (err) {
|
|
3421
3637
|
const raw = err instanceof Error ? err.message : String(err);
|
|
3422
3638
|
const message = formatGhLandError(raw);
|
|
3423
|
-
return
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft -R <origin> --body-file <short.md>. Keep the description short." : void 0
|
|
3433
|
-
},
|
|
3434
|
-
null,
|
|
3435
|
-
2
|
|
3436
|
-
)
|
|
3437
|
-
}
|
|
3438
|
-
],
|
|
3439
|
-
isError: true
|
|
3440
|
-
};
|
|
3639
|
+
return mcpJson(
|
|
3640
|
+
{
|
|
3641
|
+
error: true,
|
|
3642
|
+
action,
|
|
3643
|
+
lastError: message,
|
|
3644
|
+
hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft -R <origin> --body-file <short.md>. Keep the description short." : void 0
|
|
3645
|
+
},
|
|
3646
|
+
true
|
|
3647
|
+
);
|
|
3441
3648
|
}
|
|
3442
3649
|
}
|
|
3443
3650
|
);
|
|
3444
|
-
const agentEnum =
|
|
3651
|
+
const agentEnum = z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
3445
3652
|
server.tool(
|
|
3446
3653
|
"list_models",
|
|
3447
3654
|
"List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
|
|
@@ -3451,9 +3658,7 @@ async function startMcpServer() {
|
|
|
3451
3658
|
async ({ agent }) => {
|
|
3452
3659
|
try {
|
|
3453
3660
|
const catalogs = await listModelsForAgent(agent);
|
|
3454
|
-
return
|
|
3455
|
-
content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
|
|
3456
|
-
};
|
|
3661
|
+
return mcpJson(catalogs);
|
|
3457
3662
|
} catch (err) {
|
|
3458
3663
|
const message = err instanceof Error ? err.message : String(err);
|
|
3459
3664
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3464,11 +3669,11 @@ async function startMcpServer() {
|
|
|
3464
3669
|
"fork_worktree",
|
|
3465
3670
|
"Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id.",
|
|
3466
3671
|
{
|
|
3467
|
-
ref:
|
|
3468
|
-
through_index:
|
|
3672
|
+
ref: z8.string().describe("Worktree thread id/ref to fork"),
|
|
3673
|
+
through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
3469
3674
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
3470
|
-
model:
|
|
3471
|
-
title:
|
|
3675
|
+
model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
3676
|
+
title: z8.string().optional()
|
|
3472
3677
|
},
|
|
3473
3678
|
async ({ ref, through_index, agent, model, title }) => {
|
|
3474
3679
|
try {
|
|
@@ -3481,24 +3686,17 @@ async function startMcpServer() {
|
|
|
3481
3686
|
model,
|
|
3482
3687
|
title
|
|
3483
3688
|
});
|
|
3484
|
-
return {
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
worktreePath: thread.worktreePath,
|
|
3496
|
-
fromThreadId: source?.id ?? ref,
|
|
3497
|
-
link: `sideboard://thread/${thread.id}`
|
|
3498
|
-
})
|
|
3499
|
-
}
|
|
3500
|
-
]
|
|
3501
|
-
};
|
|
3689
|
+
return mcpJson({
|
|
3690
|
+
id: thread.id,
|
|
3691
|
+
title: thread.title,
|
|
3692
|
+
status: thread.status,
|
|
3693
|
+
agent: thread.agent,
|
|
3694
|
+
model: thread.model,
|
|
3695
|
+
branchName: thread.branchName,
|
|
3696
|
+
worktreePath: thread.worktreePath,
|
|
3697
|
+
fromThreadId: source?.id ?? ref,
|
|
3698
|
+
link: `sideboard://thread/${thread.id}`
|
|
3699
|
+
});
|
|
3502
3700
|
} catch (err) {
|
|
3503
3701
|
const message = err instanceof Error ? err.message : String(err);
|
|
3504
3702
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3509,11 +3707,11 @@ async function startMcpServer() {
|
|
|
3509
3707
|
"fork_chat",
|
|
3510
3708
|
"Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
|
|
3511
3709
|
{
|
|
3512
|
-
ref:
|
|
3513
|
-
through_index:
|
|
3710
|
+
ref: z8.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
|
|
3711
|
+
through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
3514
3712
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
3515
|
-
model:
|
|
3516
|
-
title:
|
|
3713
|
+
model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
3714
|
+
title: z8.string().optional()
|
|
3517
3715
|
},
|
|
3518
3716
|
async ({ ref, through_index, agent, model, title }) => {
|
|
3519
3717
|
try {
|
|
@@ -3531,24 +3729,17 @@ async function startMcpServer() {
|
|
|
3531
3729
|
model,
|
|
3532
3730
|
title
|
|
3533
3731
|
});
|
|
3534
|
-
return {
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
worktreePath: tab.worktreePath,
|
|
3546
|
-
fromThreadId: source.id,
|
|
3547
|
-
link: `sideboard://thread/${tab.id}`
|
|
3548
|
-
})
|
|
3549
|
-
}
|
|
3550
|
-
]
|
|
3551
|
-
};
|
|
3732
|
+
return mcpJson({
|
|
3733
|
+
id: tab.id,
|
|
3734
|
+
title: tab.title,
|
|
3735
|
+
status: tab.status,
|
|
3736
|
+
agent: tab.agent,
|
|
3737
|
+
model: tab.model,
|
|
3738
|
+
sourceType: tab.sourceType,
|
|
3739
|
+
worktreePath: tab.worktreePath,
|
|
3740
|
+
fromThreadId: source.id,
|
|
3741
|
+
link: `sideboard://thread/${tab.id}`
|
|
3742
|
+
});
|
|
3552
3743
|
} catch (err) {
|
|
3553
3744
|
const message = err instanceof Error ? err.message : String(err);
|
|
3554
3745
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3559,49 +3750,35 @@ async function startMcpServer() {
|
|
|
3559
3750
|
"run_dev_script",
|
|
3560
3751
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
3561
3752
|
{
|
|
3562
|
-
ref:
|
|
3563
|
-
name:
|
|
3753
|
+
ref: z8.string(),
|
|
3754
|
+
name: z8.string().optional()
|
|
3564
3755
|
},
|
|
3565
3756
|
async ({ ref, name }) => {
|
|
3566
3757
|
const result = await orch.startDev(ref, name);
|
|
3567
|
-
return {
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
scriptName: result.scriptName,
|
|
3574
|
-
ports: result.ports,
|
|
3575
|
-
url: `http://localhost:${result.port}`
|
|
3576
|
-
})
|
|
3577
|
-
}
|
|
3578
|
-
]
|
|
3579
|
-
};
|
|
3758
|
+
return mcpJson({
|
|
3759
|
+
port: result.port,
|
|
3760
|
+
scriptName: result.scriptName,
|
|
3761
|
+
ports: result.ports,
|
|
3762
|
+
url: `http://localhost:${result.port}`
|
|
3763
|
+
});
|
|
3580
3764
|
}
|
|
3581
3765
|
);
|
|
3582
3766
|
server.tool(
|
|
3583
3767
|
"list_run_scripts",
|
|
3584
3768
|
"List named run scripts available for a thread",
|
|
3585
|
-
{ ref:
|
|
3769
|
+
{ ref: z8.string() },
|
|
3586
3770
|
async ({ ref }) => {
|
|
3587
3771
|
const scripts = orch.listThreadRunScripts(ref);
|
|
3588
3772
|
const active = orch.getActiveRuns(ref);
|
|
3589
|
-
return {
|
|
3590
|
-
content: [
|
|
3591
|
-
{
|
|
3592
|
-
type: "text",
|
|
3593
|
-
text: JSON.stringify({ scripts, active }, null, 2)
|
|
3594
|
-
}
|
|
3595
|
-
]
|
|
3596
|
-
};
|
|
3773
|
+
return mcpJson({ scripts, active });
|
|
3597
3774
|
}
|
|
3598
3775
|
);
|
|
3599
3776
|
server.tool(
|
|
3600
3777
|
"stop_dev_script",
|
|
3601
3778
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
3602
3779
|
{
|
|
3603
|
-
ref:
|
|
3604
|
-
name:
|
|
3780
|
+
ref: z8.string(),
|
|
3781
|
+
name: z8.string().optional()
|
|
3605
3782
|
},
|
|
3606
3783
|
async ({ ref, name }) => {
|
|
3607
3784
|
orch.stopDev(ref, name);
|
|
@@ -3611,27 +3788,25 @@ async function startMcpServer() {
|
|
|
3611
3788
|
server.tool(
|
|
3612
3789
|
"run_setup",
|
|
3613
3790
|
"Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
|
|
3614
|
-
{ ref:
|
|
3791
|
+
{ ref: z8.string() },
|
|
3615
3792
|
async ({ ref }) => {
|
|
3616
3793
|
const result = await orch.runSetup(ref);
|
|
3617
|
-
return
|
|
3618
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
3619
|
-
};
|
|
3794
|
+
return mcpJson(result);
|
|
3620
3795
|
}
|
|
3621
3796
|
);
|
|
3622
3797
|
server.tool(
|
|
3623
3798
|
"add_workspace",
|
|
3624
3799
|
"Register a git repo as a Sideboard workspace",
|
|
3625
|
-
{ repoPath:
|
|
3800
|
+
{ repoPath: z8.string() },
|
|
3626
3801
|
async ({ repoPath }) => {
|
|
3627
3802
|
const ws = await orch.addWorkspace(repoPath);
|
|
3628
|
-
return
|
|
3803
|
+
return mcpJson(ws);
|
|
3629
3804
|
}
|
|
3630
3805
|
);
|
|
3631
3806
|
server.tool(
|
|
3632
3807
|
"remove_workspace",
|
|
3633
3808
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
3634
|
-
{ repoPath:
|
|
3809
|
+
{ repoPath: z8.string() },
|
|
3635
3810
|
async ({ repoPath }) => {
|
|
3636
3811
|
orch.removeWorkspace(repoPath);
|
|
3637
3812
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -3641,40 +3816,31 @@ async function startMcpServer() {
|
|
|
3641
3816
|
"fanout",
|
|
3642
3817
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
3643
3818
|
{
|
|
3644
|
-
prompt:
|
|
3645
|
-
agents:
|
|
3646
|
-
repoPath:
|
|
3647
|
-
sourceType:
|
|
3648
|
-
sourceRef:
|
|
3649
|
-
title:
|
|
3819
|
+
prompt: z8.string(),
|
|
3820
|
+
agents: z8.array(z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
|
|
3821
|
+
repoPath: z8.string(),
|
|
3822
|
+
sourceType: z8.enum(["branch", "pr", "ticket"]).optional(),
|
|
3823
|
+
sourceRef: z8.string().optional(),
|
|
3824
|
+
title: z8.string().optional()
|
|
3650
3825
|
},
|
|
3651
3826
|
async (args) => {
|
|
3652
3827
|
const threads = await orch.bestOfN(args);
|
|
3653
|
-
return
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
branchName: t.branchName,
|
|
3662
|
-
worktreePath: t.worktreePath
|
|
3663
|
-
})),
|
|
3664
|
-
null,
|
|
3665
|
-
2
|
|
3666
|
-
)
|
|
3667
|
-
}
|
|
3668
|
-
]
|
|
3669
|
-
};
|
|
3828
|
+
return mcpJson(
|
|
3829
|
+
threads.map((t) => ({
|
|
3830
|
+
id: t.id,
|
|
3831
|
+
agent: t.agent,
|
|
3832
|
+
branchName: t.branchName,
|
|
3833
|
+
worktreePath: t.worktreePath
|
|
3834
|
+
}))
|
|
3835
|
+
);
|
|
3670
3836
|
}
|
|
3671
3837
|
);
|
|
3672
3838
|
server.tool(
|
|
3673
3839
|
"list_branches",
|
|
3674
3840
|
"List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
|
|
3675
3841
|
{
|
|
3676
|
-
repoPath:
|
|
3677
|
-
unmergedOnly:
|
|
3842
|
+
repoPath: z8.string(),
|
|
3843
|
+
unmergedOnly: z8.boolean().optional()
|
|
3678
3844
|
},
|
|
3679
3845
|
async ({ repoPath, unmergedOnly }) => {
|
|
3680
3846
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -3695,19 +3861,19 @@ async function startMcpServer() {
|
|
|
3695
3861
|
"list_prs",
|
|
3696
3862
|
'List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_workspaces. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled eng-review with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_thread with sourceType=pr.',
|
|
3697
3863
|
{
|
|
3698
|
-
repoPath:
|
|
3699
|
-
query:
|
|
3700
|
-
queue:
|
|
3864
|
+
repoPath: z8.string(),
|
|
3865
|
+
query: z8.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
|
|
3866
|
+
queue: z8.enum(["review", "mine", "approved", "changes"]).optional().describe(
|
|
3701
3867
|
'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
|
|
3702
3868
|
),
|
|
3703
|
-
state:
|
|
3704
|
-
label:
|
|
3869
|
+
state: z8.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
|
|
3870
|
+
label: z8.string().optional().describe(
|
|
3705
3871
|
"GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
|
|
3706
3872
|
),
|
|
3707
|
-
reviewer:
|
|
3873
|
+
reviewer: z8.string().optional().describe(
|
|
3708
3874
|
"me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
|
|
3709
3875
|
),
|
|
3710
|
-
limit:
|
|
3876
|
+
limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
|
|
3711
3877
|
},
|
|
3712
3878
|
async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
|
|
3713
3879
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -3739,89 +3905,65 @@ async function startMcpServer() {
|
|
|
3739
3905
|
server.tool(
|
|
3740
3906
|
"get_pr_stack",
|
|
3741
3907
|
"Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before ask_git merge on stacked PRs (and only merge when the user explicitly asked).",
|
|
3742
|
-
{ ref:
|
|
3908
|
+
{ ref: z8.string() },
|
|
3743
3909
|
async ({ ref }) => {
|
|
3744
3910
|
const stack = await orch.getPrStack(ref);
|
|
3745
|
-
return
|
|
3746
|
-
content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
|
|
3747
|
-
};
|
|
3911
|
+
return mcpJson(stack);
|
|
3748
3912
|
}
|
|
3749
3913
|
);
|
|
3750
3914
|
server.tool(
|
|
3751
3915
|
"open_pr_stack_layers",
|
|
3752
3916
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
3753
3917
|
{
|
|
3754
|
-
ref:
|
|
3755
|
-
layer:
|
|
3918
|
+
ref: z8.string(),
|
|
3919
|
+
layer: z8.number().int().positive().optional()
|
|
3756
3920
|
},
|
|
3757
3921
|
async ({ ref, layer }) => {
|
|
3758
3922
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
3759
|
-
return {
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
worktreePath: t.worktreePath,
|
|
3773
|
-
prUrl: t.prUrl,
|
|
3774
|
-
link: `sideboard://thread/${t.id}`
|
|
3775
|
-
}))
|
|
3776
|
-
},
|
|
3777
|
-
null,
|
|
3778
|
-
2
|
|
3779
|
-
)
|
|
3780
|
-
}
|
|
3781
|
-
]
|
|
3782
|
-
};
|
|
3923
|
+
return mcpJson({
|
|
3924
|
+
stackNumber: result.stack.stackNumber,
|
|
3925
|
+
trunk: result.stack.trunk,
|
|
3926
|
+
threads: result.threads.map((t) => ({
|
|
3927
|
+
id: t.id,
|
|
3928
|
+
title: t.title,
|
|
3929
|
+
branchName: t.branchName,
|
|
3930
|
+
stackLayer: t.stackLayer,
|
|
3931
|
+
worktreePath: t.worktreePath,
|
|
3932
|
+
prUrl: t.prUrl,
|
|
3933
|
+
link: `sideboard://thread/${t.id}`
|
|
3934
|
+
}))
|
|
3935
|
+
});
|
|
3783
3936
|
}
|
|
3784
3937
|
);
|
|
3785
3938
|
server.tool(
|
|
3786
3939
|
"add_stack_layer",
|
|
3787
3940
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
3788
3941
|
{
|
|
3789
|
-
ref:
|
|
3790
|
-
branchName:
|
|
3791
|
-
title:
|
|
3942
|
+
ref: z8.string(),
|
|
3943
|
+
branchName: z8.string(),
|
|
3944
|
+
title: z8.string().optional()
|
|
3792
3945
|
},
|
|
3793
3946
|
async ({ ref, branchName, title }) => {
|
|
3794
3947
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
3795
|
-
return {
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
branchName: result.thread.branchName,
|
|
3804
|
-
stackLayer: result.thread.stackLayer,
|
|
3805
|
-
worktreePath: result.thread.worktreePath,
|
|
3806
|
-
link: `sideboard://thread/${result.thread.id}`
|
|
3807
|
-
},
|
|
3808
|
-
null,
|
|
3809
|
-
2
|
|
3810
|
-
)
|
|
3811
|
-
}
|
|
3812
|
-
]
|
|
3813
|
-
};
|
|
3948
|
+
return mcpJson({
|
|
3949
|
+
id: result.thread.id,
|
|
3950
|
+
title: result.thread.title,
|
|
3951
|
+
branchName: result.thread.branchName,
|
|
3952
|
+
stackLayer: result.thread.stackLayer,
|
|
3953
|
+
worktreePath: result.thread.worktreePath,
|
|
3954
|
+
link: `sideboard://thread/${result.thread.id}`
|
|
3955
|
+
});
|
|
3814
3956
|
}
|
|
3815
3957
|
);
|
|
3816
3958
|
server.tool(
|
|
3817
3959
|
"create_pr_stack",
|
|
3818
3960
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
3819
3961
|
{
|
|
3820
|
-
repoPath:
|
|
3821
|
-
branches:
|
|
3822
|
-
agent:
|
|
3823
|
-
base:
|
|
3824
|
-
title:
|
|
3962
|
+
repoPath: z8.string(),
|
|
3963
|
+
branches: z8.array(z8.string()).min(1),
|
|
3964
|
+
agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
|
|
3965
|
+
base: z8.string().optional(),
|
|
3966
|
+
title: z8.string().optional()
|
|
3825
3967
|
},
|
|
3826
3968
|
async (args) => {
|
|
3827
3969
|
const result = await orch.createPrStack({
|
|
@@ -3831,39 +3973,28 @@ async function startMcpServer() {
|
|
|
3831
3973
|
base: args.base,
|
|
3832
3974
|
title: args.title
|
|
3833
3975
|
});
|
|
3834
|
-
return {
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
stackLayer: t.stackLayer,
|
|
3847
|
-
worktreePath: t.worktreePath,
|
|
3848
|
-
link: `sideboard://thread/${t.id}`
|
|
3849
|
-
}))
|
|
3850
|
-
},
|
|
3851
|
-
null,
|
|
3852
|
-
2
|
|
3853
|
-
)
|
|
3854
|
-
}
|
|
3855
|
-
]
|
|
3856
|
-
};
|
|
3976
|
+
return mcpJson({
|
|
3977
|
+
stackNumber: result.stack.stackNumber,
|
|
3978
|
+
trunk: result.stack.trunk,
|
|
3979
|
+
threads: result.threads.map((t) => ({
|
|
3980
|
+
id: t.id,
|
|
3981
|
+
title: t.title,
|
|
3982
|
+
branchName: t.branchName,
|
|
3983
|
+
stackLayer: t.stackLayer,
|
|
3984
|
+
worktreePath: t.worktreePath,
|
|
3985
|
+
link: `sideboard://thread/${t.id}`
|
|
3986
|
+
}))
|
|
3987
|
+
});
|
|
3857
3988
|
}
|
|
3858
3989
|
);
|
|
3859
3990
|
server.tool(
|
|
3860
3991
|
"list_issues",
|
|
3861
3992
|
"List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Then create_thread with sourceType=ticket.",
|
|
3862
3993
|
{
|
|
3863
|
-
repoPath:
|
|
3864
|
-
query:
|
|
3865
|
-
assignee:
|
|
3866
|
-
limit:
|
|
3994
|
+
repoPath: z8.string(),
|
|
3995
|
+
query: z8.string().optional().describe("Search title, identifier, or description"),
|
|
3996
|
+
assignee: z8.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
|
|
3997
|
+
limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
|
|
3867
3998
|
},
|
|
3868
3999
|
async ({ repoPath, query, assignee, limit }) => {
|
|
3869
4000
|
const root = await resolveRepoRoot(repoPath);
|