@hanhnd/agent-kit 1.0.42 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/mcp/bitbucket.test.d.ts +1 -0
- package/dist/mcp/bitbucket.test.js +335 -0
- package/dist/mcp/confluence.test.d.ts +1 -0
- package/dist/mcp/confluence.test.js +258 -0
- package/dist/mcp/integration.d.ts +154 -2
- package/dist/mcp/integration.js +751 -36
- package/dist/mcp/jira.test.d.ts +1 -0
- package/dist/mcp/jira.test.js +37 -0
- package/dist/mcp/registration.test.js +46 -1
- package/dist/server.js +1108 -48
- package/dist/services/integration/credentials.d.ts +1 -1
- package/dist/services/integration/credentials.js +1 -1
- package/dist/services/integration/parser/storage.d.ts +18 -0
- package/dist/services/integration/parser/storage.js +508 -0
- package/dist/services/integration/parser/storage.test.d.ts +1 -0
- package/dist/services/integration/parser/storage.test.js +211 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -36189,9 +36189,6 @@ import * as path6 from "path";
|
|
|
36189
36189
|
// src/core/security/index.ts
|
|
36190
36190
|
import { execFileSync } from "child_process";
|
|
36191
36191
|
import * as path5 from "path";
|
|
36192
|
-
function sanitize(input) {
|
|
36193
|
-
return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
|
|
36194
|
-
}
|
|
36195
36192
|
function validatePath(filePath, baseDir = process.cwd()) {
|
|
36196
36193
|
const resolved = path5.resolve(baseDir, filePath);
|
|
36197
36194
|
const root = path5.resolve(baseDir);
|
|
@@ -36479,19 +36476,19 @@ function adfToMarkdown(adf) {
|
|
|
36479
36476
|
if (!adf || typeof adf !== "object") return "No description";
|
|
36480
36477
|
const content = adf.content;
|
|
36481
36478
|
if (!content || !Array.isArray(content)) return "No description";
|
|
36482
|
-
function
|
|
36479
|
+
function renderNodes2(nodes, listDepth = 0, parentType) {
|
|
36483
36480
|
let result = "";
|
|
36484
36481
|
if (!nodes || !Array.isArray(nodes)) return result;
|
|
36485
36482
|
for (const node of nodes) {
|
|
36486
36483
|
switch (node.type) {
|
|
36487
36484
|
case "paragraph": {
|
|
36488
|
-
const text =
|
|
36485
|
+
const text = renderNodes2(node.content, listDepth, node.type);
|
|
36489
36486
|
result += parentType === "listItem" ? text : text + "\n\n";
|
|
36490
36487
|
break;
|
|
36491
36488
|
}
|
|
36492
36489
|
case "heading": {
|
|
36493
36490
|
const level = node.attrs?.level || 1;
|
|
36494
|
-
result += "#".repeat(level) + " " +
|
|
36491
|
+
result += "#".repeat(level) + " " + renderNodes2(node.content, listDepth, node.type) + "\n\n";
|
|
36495
36492
|
break;
|
|
36496
36493
|
}
|
|
36497
36494
|
case "text": {
|
|
@@ -36522,23 +36519,23 @@ function adfToMarkdown(adf) {
|
|
|
36522
36519
|
}
|
|
36523
36520
|
case "bulletList":
|
|
36524
36521
|
case "orderedList":
|
|
36525
|
-
result +=
|
|
36522
|
+
result += renderNodes2(node.content, listDepth + 1, node.type);
|
|
36526
36523
|
if (listDepth === 0) result += "\n";
|
|
36527
36524
|
break;
|
|
36528
36525
|
case "listItem": {
|
|
36529
36526
|
const indent = " ".repeat(Math.max(0, listDepth - 1));
|
|
36530
36527
|
const bullet = parentType === "orderedList" ? "1." : "-";
|
|
36531
|
-
const text =
|
|
36528
|
+
const text = renderNodes2(node.content, listDepth, node.type).trim();
|
|
36532
36529
|
result += `${indent}${bullet} ${text}
|
|
36533
36530
|
`;
|
|
36534
36531
|
break;
|
|
36535
36532
|
}
|
|
36536
36533
|
case "blockquote":
|
|
36537
|
-
result += "> " +
|
|
36534
|
+
result += "> " + renderNodes2(node.content, listDepth, node.type).trim().replace(/\n/g, "\n> ") + "\n\n";
|
|
36538
36535
|
break;
|
|
36539
36536
|
case "codeBlock":
|
|
36540
36537
|
result += `\`\`\`${node.attrs?.language || ""}
|
|
36541
|
-
${
|
|
36538
|
+
${renderNodes2(node.content, listDepth, node.type).trim()}
|
|
36542
36539
|
\`\`\`
|
|
36543
36540
|
|
|
36544
36541
|
`;
|
|
@@ -36550,7 +36547,7 @@ ${renderNodes(node.content, listDepth, node.type).trim()}
|
|
|
36550
36547
|
result += "\n";
|
|
36551
36548
|
break;
|
|
36552
36549
|
case "table":
|
|
36553
|
-
result +=
|
|
36550
|
+
result += renderTable2(node.content);
|
|
36554
36551
|
break;
|
|
36555
36552
|
case "mention":
|
|
36556
36553
|
result += `@${node.attrs?.text || node.attrs?.displayName || "user"}`;
|
|
@@ -36570,21 +36567,21 @@ ${renderNodes(node.content, listDepth, node.type).trim()}
|
|
|
36570
36567
|
break;
|
|
36571
36568
|
default:
|
|
36572
36569
|
if (node.content) {
|
|
36573
|
-
result +=
|
|
36570
|
+
result += renderNodes2(node.content, listDepth, node.type);
|
|
36574
36571
|
}
|
|
36575
36572
|
break;
|
|
36576
36573
|
}
|
|
36577
36574
|
}
|
|
36578
36575
|
return result;
|
|
36579
36576
|
}
|
|
36580
|
-
function
|
|
36577
|
+
function renderTable2(rows) {
|
|
36581
36578
|
if (!rows || !Array.isArray(rows)) return "";
|
|
36582
36579
|
let result = "\n";
|
|
36583
36580
|
let isFirstRow = true;
|
|
36584
36581
|
for (const row of rows) {
|
|
36585
36582
|
if (row.type !== "tableRow") continue;
|
|
36586
36583
|
const cells = row.content || [];
|
|
36587
|
-
const rendered = cells.map((c) =>
|
|
36584
|
+
const rendered = cells.map((c) => renderNodes2(c.content).trim().replace(/\n+/g, " ").replace(/\|/g, "\\|"));
|
|
36588
36585
|
result += "| " + rendered.join(" | ") + " |\n";
|
|
36589
36586
|
if (isFirstRow) {
|
|
36590
36587
|
result += "| " + cells.map(() => "---").join(" | ") + " |\n";
|
|
@@ -36593,10 +36590,474 @@ ${renderNodes(node.content, listDepth, node.type).trim()}
|
|
|
36593
36590
|
}
|
|
36594
36591
|
return result + "\n";
|
|
36595
36592
|
}
|
|
36596
|
-
return
|
|
36593
|
+
return renderNodes2(content).trim() || "No description";
|
|
36594
|
+
}
|
|
36595
|
+
|
|
36596
|
+
// src/services/integration/parser/storage.ts
|
|
36597
|
+
var MAX_STORAGE_DEPTH = 100;
|
|
36598
|
+
var DEPTH_TRUNCATION_MARKER = "[content nested too deeply - truncated]";
|
|
36599
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
36600
|
+
"br",
|
|
36601
|
+
"hr",
|
|
36602
|
+
"img",
|
|
36603
|
+
"col",
|
|
36604
|
+
"input",
|
|
36605
|
+
"meta",
|
|
36606
|
+
"link",
|
|
36607
|
+
"ri:page",
|
|
36608
|
+
"ri:attachment",
|
|
36609
|
+
"ri:url",
|
|
36610
|
+
"ri:user",
|
|
36611
|
+
"ri:space",
|
|
36612
|
+
"ri:blog-post",
|
|
36613
|
+
"ac:emoticon"
|
|
36614
|
+
]);
|
|
36615
|
+
var NAMED_ENTITIES = {
|
|
36616
|
+
// XML predefined
|
|
36617
|
+
nbsp: "\xA0",
|
|
36618
|
+
amp: "&",
|
|
36619
|
+
lt: "<",
|
|
36620
|
+
gt: ">",
|
|
36621
|
+
quot: '"',
|
|
36622
|
+
apos: "'",
|
|
36623
|
+
// dashes & punctuation
|
|
36624
|
+
ndash: "\u2013",
|
|
36625
|
+
mdash: "\u2014",
|
|
36626
|
+
hellip: "\u2026",
|
|
36627
|
+
middot: "\xB7",
|
|
36628
|
+
bull: "\u2022",
|
|
36629
|
+
dagger: "\u2020",
|
|
36630
|
+
Dagger: "\u2021",
|
|
36631
|
+
sect: "\xA7",
|
|
36632
|
+
para: "\xB6",
|
|
36633
|
+
// smart quotes
|
|
36634
|
+
lsquo: "\u2018",
|
|
36635
|
+
rsquo: "\u2019",
|
|
36636
|
+
sbquo: "\u201A",
|
|
36637
|
+
ldquo: "\u201C",
|
|
36638
|
+
rdquo: "\u201D",
|
|
36639
|
+
bdquo: "\u201E",
|
|
36640
|
+
laquo: "\xAB",
|
|
36641
|
+
raquo: "\xBB",
|
|
36642
|
+
prime: "\u2032",
|
|
36643
|
+
Prime: "\u2033",
|
|
36644
|
+
// arrows
|
|
36645
|
+
larr: "\u2190",
|
|
36646
|
+
rarr: "\u2192",
|
|
36647
|
+
uarr: "\u2191",
|
|
36648
|
+
darr: "\u2193",
|
|
36649
|
+
harr: "\u2194",
|
|
36650
|
+
// symbols & math
|
|
36651
|
+
copy: "\xA9",
|
|
36652
|
+
reg: "\xAE",
|
|
36653
|
+
trade: "\u2122",
|
|
36654
|
+
deg: "\xB0",
|
|
36655
|
+
plusmn: "\xB1",
|
|
36656
|
+
times: "\xD7",
|
|
36657
|
+
divide: "\xF7",
|
|
36658
|
+
micro: "\xB5",
|
|
36659
|
+
frac12: "\xBD",
|
|
36660
|
+
frac14: "\xBC",
|
|
36661
|
+
frac34: "\xBE",
|
|
36662
|
+
ne: "\u2260",
|
|
36663
|
+
le: "\u2264",
|
|
36664
|
+
ge: "\u2265",
|
|
36665
|
+
infin: "\u221E",
|
|
36666
|
+
// currency
|
|
36667
|
+
euro: "\u20AC",
|
|
36668
|
+
pound: "\xA3",
|
|
36669
|
+
yen: "\xA5",
|
|
36670
|
+
cent: "\xA2"
|
|
36671
|
+
};
|
|
36672
|
+
var PANEL_MACROS = /* @__PURE__ */ new Set(["info", "note", "warning", "tip", "panel"]);
|
|
36673
|
+
var PLACEHOLDER_MACROS = /* @__PURE__ */ new Set(["toc", "children", "excerpt-include"]);
|
|
36674
|
+
var ATTRIBUTE_PATTERN = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
36675
|
+
function codePointOrLiteral(code, literal2) {
|
|
36676
|
+
if (!Number.isInteger(code) || code < 1 || code > 1114111) return literal2;
|
|
36677
|
+
try {
|
|
36678
|
+
return String.fromCodePoint(code);
|
|
36679
|
+
} catch {
|
|
36680
|
+
return literal2;
|
|
36681
|
+
}
|
|
36682
|
+
}
|
|
36683
|
+
function decodeEntities(text) {
|
|
36684
|
+
if (!text.includes("&")) return text;
|
|
36685
|
+
return text.replace(/&(#[Xx][0-9A-Fa-f]+|#\d+|[A-Za-z][A-Za-z0-9]*);/g, (literal2, body) => {
|
|
36686
|
+
if (body[0] === "#") {
|
|
36687
|
+
const isHex = body[1] === "x" || body[1] === "X";
|
|
36688
|
+
const code = Number.parseInt(isHex ? body.slice(2) : body.slice(1), isHex ? 16 : 10);
|
|
36689
|
+
return codePointOrLiteral(code, literal2);
|
|
36690
|
+
}
|
|
36691
|
+
return NAMED_ENTITIES[body] ?? literal2;
|
|
36692
|
+
});
|
|
36693
|
+
}
|
|
36694
|
+
function parseAttributes(source) {
|
|
36695
|
+
const attrs = {};
|
|
36696
|
+
ATTRIBUTE_PATTERN.lastIndex = 0;
|
|
36697
|
+
let match;
|
|
36698
|
+
while ((match = ATTRIBUTE_PATTERN.exec(source)) !== null) {
|
|
36699
|
+
const raw = match[2] ?? match[3] ?? match[4] ?? "";
|
|
36700
|
+
attrs[match[1].toLowerCase()] = decodeEntities(raw);
|
|
36701
|
+
}
|
|
36702
|
+
return attrs;
|
|
36703
|
+
}
|
|
36704
|
+
function tokenize(input) {
|
|
36705
|
+
const tokens = [];
|
|
36706
|
+
let cursor = 0;
|
|
36707
|
+
let textStart = 0;
|
|
36708
|
+
const flushText = (end) => {
|
|
36709
|
+
if (end > textStart) tokens.push({ kind: "text", value: input.slice(textStart, end) });
|
|
36710
|
+
};
|
|
36711
|
+
while (cursor < input.length) {
|
|
36712
|
+
const lt = input.indexOf("<", cursor);
|
|
36713
|
+
if (lt === -1) break;
|
|
36714
|
+
if (input.startsWith("<![CDATA[", lt)) {
|
|
36715
|
+
const end = input.indexOf("]]>", lt + 9);
|
|
36716
|
+
if (end === -1) break;
|
|
36717
|
+
flushText(lt);
|
|
36718
|
+
tokens.push({ kind: "cdata", value: input.slice(lt + 9, end) });
|
|
36719
|
+
cursor = end + 3;
|
|
36720
|
+
textStart = cursor;
|
|
36721
|
+
continue;
|
|
36722
|
+
}
|
|
36723
|
+
if (input.startsWith("<!--", lt)) {
|
|
36724
|
+
const end = input.indexOf("-->", lt + 4);
|
|
36725
|
+
if (end === -1) break;
|
|
36726
|
+
flushText(lt);
|
|
36727
|
+
tokens.push({ kind: "comment" });
|
|
36728
|
+
cursor = end + 3;
|
|
36729
|
+
textStart = cursor;
|
|
36730
|
+
continue;
|
|
36731
|
+
}
|
|
36732
|
+
const gt = input.indexOf(">", lt + 1);
|
|
36733
|
+
if (gt === -1) break;
|
|
36734
|
+
const raw = input.slice(lt + 1, gt);
|
|
36735
|
+
if (!/^\/?[A-Za-z]/.test(raw)) {
|
|
36736
|
+
cursor = lt + 1;
|
|
36737
|
+
continue;
|
|
36738
|
+
}
|
|
36739
|
+
flushText(lt);
|
|
36740
|
+
if (raw[0] === "/") {
|
|
36741
|
+
tokens.push({ kind: "close", name: raw.slice(1).trim().toLowerCase() });
|
|
36742
|
+
} else {
|
|
36743
|
+
const selfClosing = raw.endsWith("/");
|
|
36744
|
+
const inner = selfClosing ? raw.slice(0, -1) : raw;
|
|
36745
|
+
const nameEnd = inner.search(/[\s/]/);
|
|
36746
|
+
const name = (nameEnd === -1 ? inner : inner.slice(0, nameEnd)).toLowerCase();
|
|
36747
|
+
const attrs = parseAttributes(nameEnd === -1 ? "" : inner.slice(nameEnd));
|
|
36748
|
+
tokens.push({ kind: selfClosing ? "selfclose" : "open", name, attrs });
|
|
36749
|
+
}
|
|
36750
|
+
cursor = gt + 1;
|
|
36751
|
+
textStart = cursor;
|
|
36752
|
+
}
|
|
36753
|
+
flushText(input.length);
|
|
36754
|
+
return tokens;
|
|
36755
|
+
}
|
|
36756
|
+
function buildTree(tokens) {
|
|
36757
|
+
const root = [];
|
|
36758
|
+
const stack = [];
|
|
36759
|
+
let suppressDepth = 0;
|
|
36760
|
+
const sink = () => stack.length > 0 ? stack[stack.length - 1].children : root;
|
|
36761
|
+
for (const token of tokens) {
|
|
36762
|
+
switch (token.kind) {
|
|
36763
|
+
case "comment":
|
|
36764
|
+
break;
|
|
36765
|
+
case "text":
|
|
36766
|
+
case "cdata":
|
|
36767
|
+
if (suppressDepth === 0) sink().push(token);
|
|
36768
|
+
break;
|
|
36769
|
+
case "open":
|
|
36770
|
+
case "selfclose": {
|
|
36771
|
+
const overDepth = stack.length >= MAX_STORAGE_DEPTH;
|
|
36772
|
+
if (suppressDepth === 0 && overDepth) {
|
|
36773
|
+
sink().push({ kind: "text", value: DEPTH_TRUNCATION_MARKER });
|
|
36774
|
+
}
|
|
36775
|
+
const suppressing = suppressDepth > 0 || overDepth;
|
|
36776
|
+
const isLeaf = token.kind === "selfclose" || VOID_TAGS.has(token.name);
|
|
36777
|
+
if (suppressing) {
|
|
36778
|
+
if (!isLeaf) {
|
|
36779
|
+
stack.push({ name: token.name, children: [], suppressing: true });
|
|
36780
|
+
suppressDepth++;
|
|
36781
|
+
}
|
|
36782
|
+
break;
|
|
36783
|
+
}
|
|
36784
|
+
const element = { kind: "element", name: token.name, attrs: token.attrs, children: [] };
|
|
36785
|
+
sink().push(element);
|
|
36786
|
+
if (!isLeaf) stack.push({ name: token.name, children: element.children, suppressing: false });
|
|
36787
|
+
break;
|
|
36788
|
+
}
|
|
36789
|
+
case "close": {
|
|
36790
|
+
let target = -1;
|
|
36791
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
36792
|
+
if (stack[i].name === token.name) {
|
|
36793
|
+
target = i;
|
|
36794
|
+
break;
|
|
36795
|
+
}
|
|
36796
|
+
}
|
|
36797
|
+
if (target === -1) break;
|
|
36798
|
+
while (stack.length > target) {
|
|
36799
|
+
const frame = stack.pop();
|
|
36800
|
+
if (frame?.suppressing) suppressDepth--;
|
|
36801
|
+
}
|
|
36802
|
+
break;
|
|
36803
|
+
}
|
|
36804
|
+
}
|
|
36805
|
+
}
|
|
36806
|
+
return root;
|
|
36807
|
+
}
|
|
36808
|
+
function isElement(node, name) {
|
|
36809
|
+
return node.kind === "element" && (name === void 0 || node.name === name);
|
|
36810
|
+
}
|
|
36811
|
+
function findChild(element, name) {
|
|
36812
|
+
return element.children.find((child) => isElement(child, name));
|
|
36813
|
+
}
|
|
36814
|
+
function rawText(nodes) {
|
|
36815
|
+
let result = "";
|
|
36816
|
+
for (const node of nodes) {
|
|
36817
|
+
if (node.kind === "element") result += rawText(node.children);
|
|
36818
|
+
else result += node.value;
|
|
36819
|
+
}
|
|
36820
|
+
return result;
|
|
36821
|
+
}
|
|
36822
|
+
function macroParameters(element) {
|
|
36823
|
+
const params = {};
|
|
36824
|
+
for (const child of element.children) {
|
|
36825
|
+
if (!isElement(child, "ac:parameter")) continue;
|
|
36826
|
+
const key = child.attrs["ac:name"];
|
|
36827
|
+
if (key) params[key] = rawText(child.children).trim();
|
|
36828
|
+
}
|
|
36829
|
+
return params;
|
|
36830
|
+
}
|
|
36831
|
+
function collectRows(nodes) {
|
|
36832
|
+
const rows = [];
|
|
36833
|
+
for (const node of nodes) {
|
|
36834
|
+
if (!isElement(node)) continue;
|
|
36835
|
+
if (node.name === "tr") rows.push(node);
|
|
36836
|
+
else if (node.name === "thead" || node.name === "tbody" || node.name === "tfoot") {
|
|
36837
|
+
rows.push(...collectRows(node.children));
|
|
36838
|
+
}
|
|
36839
|
+
}
|
|
36840
|
+
return rows;
|
|
36841
|
+
}
|
|
36842
|
+
function renderTable(nodes) {
|
|
36843
|
+
const rows = collectRows(nodes);
|
|
36844
|
+
if (rows.length === 0) return "";
|
|
36845
|
+
let result = "\n";
|
|
36846
|
+
let isFirstRow = true;
|
|
36847
|
+
for (const row of rows) {
|
|
36848
|
+
const cells = row.children.filter(
|
|
36849
|
+
(child) => isElement(child, "th") || isElement(child, "td")
|
|
36850
|
+
);
|
|
36851
|
+
const rendered = cells.map((cell) => renderNodes(cell.children).trim().replace(/\n+/g, " ").replace(/\|/g, "\\|"));
|
|
36852
|
+
result += "| " + rendered.join(" | ") + " |\n";
|
|
36853
|
+
if (isFirstRow) {
|
|
36854
|
+
result += "| " + cells.map(() => "---").join(" | ") + " |\n";
|
|
36855
|
+
isFirstRow = false;
|
|
36856
|
+
}
|
|
36857
|
+
}
|
|
36858
|
+
return result + "\n";
|
|
36859
|
+
}
|
|
36860
|
+
function renderMacro(element, listDepth) {
|
|
36861
|
+
const name = element.attrs["ac:name"] || "";
|
|
36862
|
+
const params = macroParameters(element);
|
|
36863
|
+
const richBody = findChild(element, "ac:rich-text-body");
|
|
36864
|
+
if (name === "code") {
|
|
36865
|
+
const plainBody = findChild(element, "ac:plain-text-body");
|
|
36866
|
+
const code = plainBody ? rawText(plainBody.children) : rawText(element.children);
|
|
36867
|
+
return `\`\`\`${params.language || ""}
|
|
36868
|
+
${code.replace(/^\n+|\n+$/g, "")}
|
|
36869
|
+
\`\`\`
|
|
36870
|
+
|
|
36871
|
+
`;
|
|
36872
|
+
}
|
|
36873
|
+
if (PANEL_MACROS.has(name)) {
|
|
36874
|
+
const body = richBody ? renderNodes(richBody.children, listDepth, name).trim() : "";
|
|
36875
|
+
const block = `**${name.toUpperCase()}**${body ? `
|
|
36876
|
+
${body}` : ""}`;
|
|
36877
|
+
return "> " + block.replace(/\n/g, "\n> ") + "\n\n";
|
|
36878
|
+
}
|
|
36879
|
+
if (name === "expand") {
|
|
36880
|
+
const body = richBody ? renderNodes(richBody.children, listDepth, name).trim() : "";
|
|
36881
|
+
return `**${params.title || "Details"}**
|
|
36882
|
+
|
|
36883
|
+
${body ? `${body}
|
|
36884
|
+
|
|
36885
|
+
` : ""}`;
|
|
36886
|
+
}
|
|
36887
|
+
if (name === "jira") {
|
|
36888
|
+
return params.key ? `\`${params.key}\`` : "[macro: jira]";
|
|
36889
|
+
}
|
|
36890
|
+
if (PLACEHOLDER_MACROS.has(name)) return `[macro: ${name}]`;
|
|
36891
|
+
return `[macro: ${name || "unknown"}]`;
|
|
36892
|
+
}
|
|
36893
|
+
function renderLink(element, listDepth) {
|
|
36894
|
+
const bodyElement = findChild(element, "ac:plain-text-link-body") ?? findChild(element, "ac:link-body");
|
|
36895
|
+
const bodyText = bodyElement ? renderNodes(bodyElement.children, listDepth, element.name).trim() : "";
|
|
36896
|
+
const page = findChild(element, "ri:page");
|
|
36897
|
+
if (page) return `[[${bodyText || page.attrs["ri:content-title"] || "page"}]]`;
|
|
36898
|
+
const attachment = findChild(element, "ri:attachment");
|
|
36899
|
+
if (attachment) return `[attachment: ${attachment.attrs["ri:filename"] || "unknown"}]`;
|
|
36900
|
+
const externalUrl = findChild(element, "ri:url");
|
|
36901
|
+
if (externalUrl) {
|
|
36902
|
+
const href = externalUrl.attrs["ri:value"] || "";
|
|
36903
|
+
return `[${bodyText || href}](${href})`;
|
|
36904
|
+
}
|
|
36905
|
+
if (findChild(element, "ri:user")) return "@user";
|
|
36906
|
+
return bodyText;
|
|
36907
|
+
}
|
|
36908
|
+
function renderTaskList(element, listDepth) {
|
|
36909
|
+
let result = "";
|
|
36910
|
+
for (const task of element.children) {
|
|
36911
|
+
if (!isElement(task, "ac:task")) continue;
|
|
36912
|
+
const status = findChild(task, "ac:task-status");
|
|
36913
|
+
const body = findChild(task, "ac:task-body");
|
|
36914
|
+
const marker = rawText(status?.children ?? []).trim() === "complete" ? "[x]" : "[ ]";
|
|
36915
|
+
const text = body ? renderNodes(body.children, listDepth, "ac:task").trim() : "";
|
|
36916
|
+
result += `- ${marker} ${text}
|
|
36917
|
+
`;
|
|
36918
|
+
}
|
|
36919
|
+
return result ? result + "\n" : "";
|
|
36920
|
+
}
|
|
36921
|
+
function renderNodes(nodes, listDepth = 0, parentType) {
|
|
36922
|
+
let result = "";
|
|
36923
|
+
for (const node of nodes) {
|
|
36924
|
+
if (node.kind === "cdata") {
|
|
36925
|
+
result += node.value;
|
|
36926
|
+
continue;
|
|
36927
|
+
}
|
|
36928
|
+
if (node.kind === "text") {
|
|
36929
|
+
result += decodeEntities(node.value);
|
|
36930
|
+
continue;
|
|
36931
|
+
}
|
|
36932
|
+
const children = node.children;
|
|
36933
|
+
switch (node.name) {
|
|
36934
|
+
case "h1":
|
|
36935
|
+
case "h2":
|
|
36936
|
+
case "h3":
|
|
36937
|
+
case "h4":
|
|
36938
|
+
case "h5":
|
|
36939
|
+
case "h6":
|
|
36940
|
+
result += "#".repeat(Number(node.name[1])) + " " + renderNodes(children, listDepth, node.name).trim() + "\n\n";
|
|
36941
|
+
break;
|
|
36942
|
+
case "p": {
|
|
36943
|
+
const text = renderNodes(children, listDepth, node.name);
|
|
36944
|
+
result += parentType === "li" ? text + "\n" : text + "\n\n";
|
|
36945
|
+
break;
|
|
36946
|
+
}
|
|
36947
|
+
case "strong":
|
|
36948
|
+
case "b":
|
|
36949
|
+
result += `**${renderNodes(children, listDepth, node.name)}**`;
|
|
36950
|
+
break;
|
|
36951
|
+
case "em":
|
|
36952
|
+
case "i":
|
|
36953
|
+
result += `*${renderNodes(children, listDepth, node.name)}*`;
|
|
36954
|
+
break;
|
|
36955
|
+
case "code":
|
|
36956
|
+
result += `\`${renderNodes(children, listDepth, node.name)}\``;
|
|
36957
|
+
break;
|
|
36958
|
+
case "s":
|
|
36959
|
+
case "strike":
|
|
36960
|
+
case "del":
|
|
36961
|
+
result += `~~${renderNodes(children, listDepth, node.name)}~~`;
|
|
36962
|
+
break;
|
|
36963
|
+
case "sup":
|
|
36964
|
+
result += `^${renderNodes(children, listDepth, node.name)}^`;
|
|
36965
|
+
break;
|
|
36966
|
+
case "sub":
|
|
36967
|
+
result += `~${renderNodes(children, listDepth, node.name)}~`;
|
|
36968
|
+
break;
|
|
36969
|
+
case "pre":
|
|
36970
|
+
result += `\`\`\`
|
|
36971
|
+
${renderNodes(children, listDepth, node.name).trim()}
|
|
36972
|
+
\`\`\`
|
|
36973
|
+
|
|
36974
|
+
`;
|
|
36975
|
+
break;
|
|
36976
|
+
case "blockquote":
|
|
36977
|
+
result += "> " + renderNodes(children, listDepth, node.name).trim().replace(/\n/g, "\n> ") + "\n\n";
|
|
36978
|
+
break;
|
|
36979
|
+
case "hr":
|
|
36980
|
+
result += "---\n\n";
|
|
36981
|
+
break;
|
|
36982
|
+
case "br":
|
|
36983
|
+
result += "\n";
|
|
36984
|
+
break;
|
|
36985
|
+
case "a": {
|
|
36986
|
+
const text = renderNodes(children, listDepth, node.name);
|
|
36987
|
+
const href = node.attrs.href ?? "";
|
|
36988
|
+
result += href ? `[${text}](${href})` : text;
|
|
36989
|
+
break;
|
|
36990
|
+
}
|
|
36991
|
+
case "ul":
|
|
36992
|
+
case "ol": {
|
|
36993
|
+
const inner = renderNodes(children, listDepth + 1, node.name);
|
|
36994
|
+
if (parentType === "li") {
|
|
36995
|
+
if (result.length > 0 && !result.endsWith("\n")) result += "\n";
|
|
36996
|
+
result += inner;
|
|
36997
|
+
} else {
|
|
36998
|
+
result += inner;
|
|
36999
|
+
if (listDepth === 0) result += "\n";
|
|
37000
|
+
}
|
|
37001
|
+
break;
|
|
37002
|
+
}
|
|
37003
|
+
case "li": {
|
|
37004
|
+
const indent = " ".repeat(Math.max(0, listDepth - 1));
|
|
37005
|
+
const bullet = parentType === "ol" ? "1." : "-";
|
|
37006
|
+
result += `${indent}${bullet} ${renderNodes(children, listDepth, "li").trim()}
|
|
37007
|
+
`;
|
|
37008
|
+
break;
|
|
37009
|
+
}
|
|
37010
|
+
case "table":
|
|
37011
|
+
result += renderTable(children);
|
|
37012
|
+
break;
|
|
37013
|
+
case "ac:structured-macro":
|
|
37014
|
+
result += renderMacro(node, listDepth);
|
|
37015
|
+
break;
|
|
37016
|
+
case "ac:link":
|
|
37017
|
+
result += renderLink(node, listDepth);
|
|
37018
|
+
break;
|
|
37019
|
+
case "ac:image": {
|
|
37020
|
+
const attachment = findChild(node, "ri:attachment");
|
|
37021
|
+
const externalUrl = findChild(node, "ri:url");
|
|
37022
|
+
const label = attachment?.attrs["ri:filename"] ?? externalUrl?.attrs["ri:value"] ?? "unknown";
|
|
37023
|
+
const marker = `[image: ${label}]`;
|
|
37024
|
+
if (parentType === "p") {
|
|
37025
|
+
result += marker;
|
|
37026
|
+
} else {
|
|
37027
|
+
if (result.length > 0 && !result.endsWith("\n")) result += "\n";
|
|
37028
|
+
result += `${marker}
|
|
37029
|
+
|
|
37030
|
+
`;
|
|
37031
|
+
}
|
|
37032
|
+
break;
|
|
37033
|
+
}
|
|
37034
|
+
case "ac:task-list":
|
|
37035
|
+
result += renderTaskList(node, listDepth);
|
|
37036
|
+
break;
|
|
37037
|
+
case "ac:emoticon":
|
|
37038
|
+
result += `:${node.attrs["ac:name"] || "emoticon"}:`;
|
|
37039
|
+
break;
|
|
37040
|
+
default:
|
|
37041
|
+
result += renderNodes(children, listDepth, node.name);
|
|
37042
|
+
break;
|
|
37043
|
+
}
|
|
37044
|
+
}
|
|
37045
|
+
return result;
|
|
37046
|
+
}
|
|
37047
|
+
function extractPlainText(xhtml) {
|
|
37048
|
+
return decodeEntities(xhtml.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
|
|
37049
|
+
}
|
|
37050
|
+
function storageToMarkdown(xhtml) {
|
|
37051
|
+
if (typeof xhtml !== "string" || xhtml.length === 0) return "";
|
|
37052
|
+
try {
|
|
37053
|
+
return renderNodes(buildTree(tokenize(xhtml))).trim();
|
|
37054
|
+
} catch {
|
|
37055
|
+
return extractPlainText(xhtml);
|
|
37056
|
+
}
|
|
36597
37057
|
}
|
|
36598
37058
|
|
|
36599
37059
|
// src/mcp/integration.ts
|
|
37060
|
+
var LARGE_PAYLOAD_THRESHOLD = 5e4;
|
|
36600
37061
|
var BitbucketPrSchema = external_exports.object({
|
|
36601
37062
|
id: external_exports.number(),
|
|
36602
37063
|
title: external_exports.string(),
|
|
@@ -36606,6 +37067,42 @@ var BitbucketPrSchema = external_exports.object({
|
|
|
36606
37067
|
source: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }),
|
|
36607
37068
|
destination: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) })
|
|
36608
37069
|
});
|
|
37070
|
+
var BitbucketCreatedPrSchema = external_exports.object({
|
|
37071
|
+
id: external_exports.number(),
|
|
37072
|
+
title: external_exports.string(),
|
|
37073
|
+
state: external_exports.string().optional(),
|
|
37074
|
+
draft: external_exports.boolean().optional(),
|
|
37075
|
+
source: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }).optional(),
|
|
37076
|
+
destination: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }).optional(),
|
|
37077
|
+
reviewers: external_exports.array(external_exports.object({ display_name: external_exports.string().optional() }).passthrough()).optional(),
|
|
37078
|
+
links: external_exports.object({ html: external_exports.object({ href: external_exports.string() }).optional() }).passthrough().optional()
|
|
37079
|
+
});
|
|
37080
|
+
var BitbucketCommentSchema = external_exports.object({
|
|
37081
|
+
id: external_exports.number(),
|
|
37082
|
+
created_on: external_exports.string().optional(),
|
|
37083
|
+
updated_on: external_exports.string().optional(),
|
|
37084
|
+
content: external_exports.object({ raw: external_exports.string().nullable().optional() }).passthrough().optional(),
|
|
37085
|
+
user: external_exports.object({ display_name: external_exports.string().optional() }).passthrough().nullable().optional(),
|
|
37086
|
+
deleted: external_exports.boolean().optional(),
|
|
37087
|
+
pending: external_exports.boolean().optional(),
|
|
37088
|
+
parent: external_exports.object({ id: external_exports.number() }).passthrough().nullable().optional(),
|
|
37089
|
+
inline: external_exports.object({
|
|
37090
|
+
path: external_exports.string(),
|
|
37091
|
+
from: external_exports.number().nullable().optional(),
|
|
37092
|
+
to: external_exports.number().nullable().optional(),
|
|
37093
|
+
start_from: external_exports.number().nullable().optional(),
|
|
37094
|
+
start_to: external_exports.number().nullable().optional()
|
|
37095
|
+
}).passthrough().nullable().optional(),
|
|
37096
|
+
resolution: external_exports.object({
|
|
37097
|
+
type: external_exports.string().optional(),
|
|
37098
|
+
user: external_exports.object({ display_name: external_exports.string().optional() }).passthrough().nullable().optional(),
|
|
37099
|
+
created_on: external_exports.string().optional()
|
|
37100
|
+
}).passthrough().nullable().optional()
|
|
37101
|
+
}).passthrough();
|
|
37102
|
+
var BitbucketCommentPageSchema = external_exports.object({
|
|
37103
|
+
values: external_exports.array(BitbucketCommentSchema),
|
|
37104
|
+
next: external_exports.string().optional()
|
|
37105
|
+
});
|
|
36609
37106
|
var AdfContentSchema = external_exports.object({
|
|
36610
37107
|
type: external_exports.string().optional(),
|
|
36611
37108
|
content: external_exports.array(external_exports.unknown()).optional(),
|
|
@@ -36634,19 +37131,540 @@ var JiraTicketSchema = external_exports.object({
|
|
|
36634
37131
|
errorMessages: external_exports.array(external_exports.string()).optional(),
|
|
36635
37132
|
fields: JiraFieldsSchema
|
|
36636
37133
|
});
|
|
37134
|
+
var ConfluencePageSchema = external_exports.object({
|
|
37135
|
+
id: external_exports.string(),
|
|
37136
|
+
title: external_exports.string(),
|
|
37137
|
+
status: external_exports.string().optional(),
|
|
37138
|
+
spaceId: external_exports.string().optional(),
|
|
37139
|
+
version: external_exports.object({
|
|
37140
|
+
number: external_exports.number().optional(),
|
|
37141
|
+
createdAt: external_exports.string().optional(),
|
|
37142
|
+
message: external_exports.string().optional(),
|
|
37143
|
+
minorEdit: external_exports.boolean().optional(),
|
|
37144
|
+
authorId: external_exports.string().optional()
|
|
37145
|
+
}).optional(),
|
|
37146
|
+
body: external_exports.object({
|
|
37147
|
+
storage: external_exports.object({ value: external_exports.string().optional() }).passthrough().optional()
|
|
37148
|
+
}).passthrough().optional(),
|
|
37149
|
+
labels: external_exports.object({ results: external_exports.array(external_exports.object({ name: external_exports.string() }).passthrough()).optional() }).passthrough().optional(),
|
|
37150
|
+
_links: external_exports.object({ base: external_exports.string().optional(), webui: external_exports.string().optional() }).passthrough().optional()
|
|
37151
|
+
});
|
|
37152
|
+
var REQUIRED_SCOPES = {
|
|
37153
|
+
// The PR diff endpoint redirects to the repository diff, which needs read:repository.
|
|
37154
|
+
kit_get_bitbucket_pr: ["read:pullrequest:bitbucket", "read:repository:bitbucket"],
|
|
37155
|
+
kit_get_bitbucket_pr_comments: ["read:pullrequest:bitbucket"],
|
|
37156
|
+
// read:workspace resolves reviewer names; read:user skips the author (optional — degrades, never fails).
|
|
37157
|
+
kit_create_bitbucket_pr: [
|
|
37158
|
+
"read:pullrequest:bitbucket",
|
|
37159
|
+
"write:pullrequest:bitbucket",
|
|
37160
|
+
"read:workspace:bitbucket",
|
|
37161
|
+
"read:user:bitbucket"
|
|
37162
|
+
],
|
|
37163
|
+
kit_find_bitbucket_reviewers: ["read:workspace:bitbucket", "read:user:bitbucket"],
|
|
37164
|
+
kit_jira_get_ticket: ["read:jira-work"],
|
|
37165
|
+
kit_confluence_get_page: ["read:page:confluence"]
|
|
37166
|
+
};
|
|
37167
|
+
function errorText(error2, tool) {
|
|
37168
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
37169
|
+
if (!/\((401|403)\)/.test(message) || message.includes(":bitbucket") || message.includes("scope")) return message;
|
|
37170
|
+
return `${message}
|
|
37171
|
+
|
|
37172
|
+
Check the API token has these scopes: ${REQUIRED_SCOPES[tool].join(", ")}.`;
|
|
37173
|
+
}
|
|
36637
37174
|
function buildBasicAuth(emailVar, tokenVar) {
|
|
36638
37175
|
const email2 = getCredential(emailVar);
|
|
36639
37176
|
const token = getCredential(tokenVar);
|
|
36640
37177
|
if (!email2 || !token) throw new Error(`Missing ${emailVar} or ${tokenVar}`);
|
|
36641
37178
|
return "Basic " + Buffer.from(`${email2}:${token}`).toString("base64");
|
|
36642
37179
|
}
|
|
36643
|
-
|
|
36644
|
-
|
|
37180
|
+
var PRODUCT_TOKEN_KEY = {
|
|
37181
|
+
jira: "JIRA_API_TOKEN",
|
|
37182
|
+
confluence: "CONFLUENCE_API_TOKEN"
|
|
37183
|
+
};
|
|
37184
|
+
function buildAtlassianContext(product) {
|
|
37185
|
+
const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
|
|
37186
|
+
if (!cloudId) throw new Error("Missing ATLASSIAN_CLOUD_ID");
|
|
37187
|
+
return { auth: buildBasicAuth("ATLASSIAN_USER_EMAIL", PRODUCT_TOKEN_KEY[product]), cloudId };
|
|
37188
|
+
}
|
|
37189
|
+
async function callRestApi(url, auth, accept = "application/json", body) {
|
|
37190
|
+
const headers = { Authorization: auth, Accept: accept };
|
|
37191
|
+
const init = { headers };
|
|
37192
|
+
if (body !== void 0) {
|
|
37193
|
+
init.method = "POST";
|
|
37194
|
+
headers["Content-Type"] = "application/json";
|
|
37195
|
+
init.body = JSON.stringify(body);
|
|
37196
|
+
}
|
|
37197
|
+
const resp = await fetch(url, init);
|
|
36645
37198
|
if (resp.status === 401) throw new Error(`\u274C Auth failed (401): ${url}`);
|
|
37199
|
+
if (resp.status === 403)
|
|
37200
|
+
throw new Error(`\u274C Access denied (403): ${url}
|
|
37201
|
+
|
|
37202
|
+
You lack permission for this resource, or it is restricted.`);
|
|
36646
37203
|
if (resp.status === 404) throw new Error(`\u274C Not found: ${url}`);
|
|
36647
|
-
if (!resp.ok) throw new Error(`\u274C API error ${resp.status}: ${await resp.text()}`);
|
|
37204
|
+
if (!resp.ok) throw new Error(`\u274C API error ${resp.status}: ${extractApiError(await resp.text())}`);
|
|
36648
37205
|
return accept === "text/plain" ? resp.text() : resp.json();
|
|
36649
37206
|
}
|
|
37207
|
+
function extractApiError(text) {
|
|
37208
|
+
try {
|
|
37209
|
+
const err = JSON.parse(text).error;
|
|
37210
|
+
if (err && typeof err.message === "string") {
|
|
37211
|
+
const parts = [err.message];
|
|
37212
|
+
if (typeof err.detail === "string" && err.detail) parts.push(err.detail);
|
|
37213
|
+
if (err.fields && typeof err.fields === "object") parts.push(JSON.stringify(err.fields));
|
|
37214
|
+
return parts.join("\n");
|
|
37215
|
+
}
|
|
37216
|
+
} catch {
|
|
37217
|
+
}
|
|
37218
|
+
return text;
|
|
37219
|
+
}
|
|
37220
|
+
var CONFLUENCE_INPUT_FORMS = [
|
|
37221
|
+
"Accepted input forms:",
|
|
37222
|
+
" \u2022 https://<site>.atlassian.net/wiki/spaces/<SPACEKEY>/pages/<pageId>/<Title>",
|
|
37223
|
+
" \u2022 https://<site>.atlassian.net/pages/viewpage.action?pageId=<pageId>",
|
|
37224
|
+
" \u2022 a bare numeric page ID (e.g. 123456789)"
|
|
37225
|
+
].join("\n");
|
|
37226
|
+
function resolveConfluencePageId(input) {
|
|
37227
|
+
const trimmed = input.trim();
|
|
37228
|
+
if (!trimmed) return { kind: "unknown" };
|
|
37229
|
+
if (/\/blog(?:posts?)?\//.test(trimmed)) return { kind: "blog" };
|
|
37230
|
+
if (/\/wiki\/x\/[A-Za-z0-9]+/.test(trimmed)) return { kind: "tiny" };
|
|
37231
|
+
const spacesPageMatch = trimmed.match(/\/wiki\/spaces\/[^/]+\/pages\/(\d+)(?:[/?#]|$)/);
|
|
37232
|
+
if (spacesPageMatch) return { kind: "id", pageId: spacesPageMatch[1] };
|
|
37233
|
+
const queryMatch = trimmed.match(/[?&]pageId=(\d+)(?:[&#]|$)/);
|
|
37234
|
+
if (queryMatch) return { kind: "id", pageId: queryMatch[1] };
|
|
37235
|
+
if (/^\d+$/.test(trimmed)) return { kind: "id", pageId: trimmed };
|
|
37236
|
+
return { kind: "unknown" };
|
|
37237
|
+
}
|
|
37238
|
+
function buildConfluenceHeader(page, absoluteUrl) {
|
|
37239
|
+
const webui = page._links?.webui ?? "";
|
|
37240
|
+
const spaceKey = webui.match(/\/spaces\/([^/]+)\//)?.[1];
|
|
37241
|
+
const lines = [`## \u{1F4C4} ${page.title}`, "", `**Status:** ${page.status || "Unknown"}`];
|
|
37242
|
+
if (spaceKey) lines.push(`**Space:** ${spaceKey}`);
|
|
37243
|
+
lines.push(`**Version:** ${page.version?.number ?? "Unknown"}`);
|
|
37244
|
+
lines.push(`**Last updated:** ${page.version?.createdAt || "Unknown"}`);
|
|
37245
|
+
lines.push(`**URL:** ${absoluteUrl || "Unknown"}`);
|
|
37246
|
+
return lines.join("\n");
|
|
37247
|
+
}
|
|
37248
|
+
async function handleConfluenceGetPage(args) {
|
|
37249
|
+
try {
|
|
37250
|
+
const resolution = resolveConfluencePageId(args.input);
|
|
37251
|
+
if (resolution.kind === "tiny") {
|
|
37252
|
+
return mcpText(
|
|
37253
|
+
`\u274C Confluence tiny links cannot be resolved without following a redirect.
|
|
37254
|
+
|
|
37255
|
+
Open the link in a browser and pass the full page URL instead.
|
|
37256
|
+
|
|
37257
|
+
${CONFLUENCE_INPUT_FORMS}`
|
|
37258
|
+
);
|
|
37259
|
+
}
|
|
37260
|
+
if (resolution.kind === "blog") {
|
|
37261
|
+
return mcpText(
|
|
37262
|
+
`\u274C Confluence blog posts are not supported \u2014 they live on a different endpoint than pages.
|
|
37263
|
+
|
|
37264
|
+
${CONFLUENCE_INPUT_FORMS}`
|
|
37265
|
+
);
|
|
37266
|
+
}
|
|
37267
|
+
if (resolution.kind === "unknown") {
|
|
37268
|
+
return mcpText(`\u274C Could not extract a Confluence page ID from: ${args.input}
|
|
37269
|
+
|
|
37270
|
+
${CONFLUENCE_INPUT_FORMS}`);
|
|
37271
|
+
}
|
|
37272
|
+
const { auth, cloudId } = buildAtlassianContext("confluence");
|
|
37273
|
+
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${resolution.pageId}?body-format=storage&include-labels=true&include-version=true`;
|
|
37274
|
+
const jsonData = await callRestApi(url, auth);
|
|
37275
|
+
const parseResult = ConfluencePageSchema.safeParse(jsonData);
|
|
37276
|
+
if (!parseResult.success) {
|
|
37277
|
+
return mcpText(`\u274C Invalid Confluence response format: ${parseResult.error.message}`);
|
|
37278
|
+
}
|
|
37279
|
+
const page = parseResult.data;
|
|
37280
|
+
const base = page._links?.base ?? "";
|
|
37281
|
+
const webui = page._links?.webui ?? "";
|
|
37282
|
+
const absoluteUrl = base && webui ? `${base}${webui}` : base || webui;
|
|
37283
|
+
const markdown = storageToMarkdown(page.body?.storage?.value);
|
|
37284
|
+
let contentSection;
|
|
37285
|
+
if (!markdown.trim()) {
|
|
37286
|
+
contentSection = `\u26A0\uFE0F Confluence returned no readable body for this page (status: ${page.status || "Unknown"}). Open ${absoluteUrl || "the page in Confluence"} to view it directly.`;
|
|
37287
|
+
} else if (markdown.length >= LARGE_PAYLOAD_THRESHOLD) {
|
|
37288
|
+
const filePath = `/tmp/kit-confluence-${resolution.pageId}-${Date.now()}.md`;
|
|
37289
|
+
try {
|
|
37290
|
+
writeFileSync4(filePath, sanitizeOutput(markdown), "utf8");
|
|
37291
|
+
contentSection = `Content is large (${markdown.length} chars). Full markdown written to: \`${filePath}\`. Read this file before proceeding.`;
|
|
37292
|
+
} catch {
|
|
37293
|
+
contentSection = `\u26A0\uFE0F Could not write page content to a temp file. Showing inline (may be very large).
|
|
37294
|
+
|
|
37295
|
+
${markdown}`;
|
|
37296
|
+
}
|
|
37297
|
+
} else {
|
|
37298
|
+
contentSection = markdown;
|
|
37299
|
+
}
|
|
37300
|
+
const labels = page.labels?.results?.map((label) => label.name).filter((name) => name.length > 0) ?? [];
|
|
37301
|
+
const output = `${buildConfluenceHeader(page, absoluteUrl)}
|
|
37302
|
+
|
|
37303
|
+
### Content
|
|
37304
|
+
${contentSection}
|
|
37305
|
+
|
|
37306
|
+
### Labels
|
|
37307
|
+
${labels.length > 0 ? labels.join(", ") : "None"}`;
|
|
37308
|
+
return mcpText(sanitizeOutput(output));
|
|
37309
|
+
} catch (error2) {
|
|
37310
|
+
return mcpText(`Error: ${errorText(error2, "kit_confluence_get_page")}`);
|
|
37311
|
+
}
|
|
37312
|
+
}
|
|
37313
|
+
var BITBUCKET_API = "https://api.bitbucket.org/2.0";
|
|
37314
|
+
var BITBUCKET_SLUG = /^(?!\.+$)[A-Za-z0-9._-]+$/;
|
|
37315
|
+
var MAX_PAGES = 20;
|
|
37316
|
+
var MISSING_WORKSPACE = `\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`;
|
|
37317
|
+
function checkSlugs(ws, repo) {
|
|
37318
|
+
if (!BITBUCKET_SLUG.test(ws)) return `\u274C Invalid workspace slug: ${ws}`;
|
|
37319
|
+
if (!BITBUCKET_SLUG.test(repo)) return `\u274C Invalid repo slug: ${repo}`;
|
|
37320
|
+
return void 0;
|
|
37321
|
+
}
|
|
37322
|
+
function resolveBitbucketPr(input, workspace, repoSlug) {
|
|
37323
|
+
const trimmed = input.trim();
|
|
37324
|
+
let ws;
|
|
37325
|
+
let repo;
|
|
37326
|
+
let prId;
|
|
37327
|
+
const urlMatch = trimmed.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
|
|
37328
|
+
if (urlMatch) {
|
|
37329
|
+
ws = urlMatch[1];
|
|
37330
|
+
repo = urlMatch[2];
|
|
37331
|
+
prId = parseInt(urlMatch[3], 10);
|
|
37332
|
+
} else if (/^\d+$/.test(trimmed)) {
|
|
37333
|
+
prId = parseInt(trimmed, 10);
|
|
37334
|
+
ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
|
|
37335
|
+
repo = repoSlug;
|
|
37336
|
+
}
|
|
37337
|
+
if (!ws) return { ok: false, error: MISSING_WORKSPACE };
|
|
37338
|
+
if (!repo || !prId) {
|
|
37339
|
+
return { ok: false, error: `\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}` };
|
|
37340
|
+
}
|
|
37341
|
+
const slugError = checkSlugs(ws, repo);
|
|
37342
|
+
if (slugError) return { ok: false, error: slugError };
|
|
37343
|
+
return { ok: true, ws, repo, prId };
|
|
37344
|
+
}
|
|
37345
|
+
function resolveBitbucketRepo(repository, workspace) {
|
|
37346
|
+
const trimmed = repository.trim().replace(/\.git$/, "");
|
|
37347
|
+
let ws;
|
|
37348
|
+
let repo;
|
|
37349
|
+
const urlMatch = trimmed.match(/bitbucket\.org[/:]([^/]+)\/([^/?#]+)/);
|
|
37350
|
+
const pairMatch = trimmed.match(/^([^/]+)\/([^/]+)$/);
|
|
37351
|
+
if (urlMatch) {
|
|
37352
|
+
ws = urlMatch[1];
|
|
37353
|
+
repo = urlMatch[2];
|
|
37354
|
+
} else if (pairMatch) {
|
|
37355
|
+
ws = pairMatch[1];
|
|
37356
|
+
repo = pairMatch[2];
|
|
37357
|
+
} else if (trimmed) {
|
|
37358
|
+
ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
|
|
37359
|
+
repo = trimmed;
|
|
37360
|
+
}
|
|
37361
|
+
if (!repo) {
|
|
37362
|
+
return { ok: false, error: `\u274C repository is required: a Bitbucket repo URL, "workspace/repo", or a repo slug.` };
|
|
37363
|
+
}
|
|
37364
|
+
if (!ws) return { ok: false, error: MISSING_WORKSPACE };
|
|
37365
|
+
const slugError = checkSlugs(ws, repo);
|
|
37366
|
+
if (slugError) return { ok: false, error: slugError };
|
|
37367
|
+
return { ok: true, ws, repo };
|
|
37368
|
+
}
|
|
37369
|
+
function normalizeReviewerUuid(raw) {
|
|
37370
|
+
const bare = raw.trim().replace(/^\{|\}$/g, "");
|
|
37371
|
+
return /^[0-9a-fA-F-]{36}$/.test(bare) ? `{${bare}}` : void 0;
|
|
37372
|
+
}
|
|
37373
|
+
var BitbucketMembersPageSchema = external_exports.object({
|
|
37374
|
+
values: external_exports.array(
|
|
37375
|
+
external_exports.object({
|
|
37376
|
+
user: external_exports.object({ uuid: external_exports.string(), display_name: external_exports.string().optional(), nickname: external_exports.string().optional() }).passthrough()
|
|
37377
|
+
}).passthrough()
|
|
37378
|
+
),
|
|
37379
|
+
next: external_exports.string().optional()
|
|
37380
|
+
});
|
|
37381
|
+
function normalizeName(value) {
|
|
37382
|
+
return value.normalize("NFD").replace(/\p{M}/gu, "").replace(/đ/gi, "d").toLowerCase().replace(/\s+/g, " ").trim();
|
|
37383
|
+
}
|
|
37384
|
+
function nameTokens(value) {
|
|
37385
|
+
return normalizeName(value).split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
37386
|
+
}
|
|
37387
|
+
function matchReviewers(queries, members) {
|
|
37388
|
+
const result = { resolved: [], ambiguous: [], unmatched: [] };
|
|
37389
|
+
for (const query of queries) {
|
|
37390
|
+
const q = normalizeName(query);
|
|
37391
|
+
const qTokens = nameTokens(query);
|
|
37392
|
+
if (qTokens.length === 0) continue;
|
|
37393
|
+
let hits = members.filter(
|
|
37394
|
+
(m) => normalizeName(m.displayName) === q || m.nickname !== void 0 && normalizeName(m.nickname) === q
|
|
37395
|
+
);
|
|
37396
|
+
if (hits.length === 0) {
|
|
37397
|
+
hits = members.filter((m) => {
|
|
37398
|
+
const words = /* @__PURE__ */ new Set([...nameTokens(m.displayName), ...nameTokens(m.nickname ?? "")]);
|
|
37399
|
+
return qTokens.every((t) => words.has(t));
|
|
37400
|
+
});
|
|
37401
|
+
}
|
|
37402
|
+
if (hits.length === 1) result.resolved.push({ query, member: hits[0] });
|
|
37403
|
+
else if (hits.length > 1) result.ambiguous.push({ query, candidates: hits });
|
|
37404
|
+
else result.unmatched.push(query);
|
|
37405
|
+
}
|
|
37406
|
+
return result;
|
|
37407
|
+
}
|
|
37408
|
+
async function fetchWorkspaceMembers(ws, auth) {
|
|
37409
|
+
let url = `${BITBUCKET_API}/workspaces/${ws}/members?pagelen=100`;
|
|
37410
|
+
const members = [];
|
|
37411
|
+
let pages = 0;
|
|
37412
|
+
while (url && pages < MAX_PAGES) {
|
|
37413
|
+
let data;
|
|
37414
|
+
try {
|
|
37415
|
+
data = await callRestApi(url, auth);
|
|
37416
|
+
} catch (error2) {
|
|
37417
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
37418
|
+
throw new Error(
|
|
37419
|
+
`${message}
|
|
37420
|
+
|
|
37421
|
+
Looking up reviewers by name lists workspace members \u2014 the API token needs the read:workspace:bitbucket scope.`
|
|
37422
|
+
);
|
|
37423
|
+
}
|
|
37424
|
+
const parseResult = BitbucketMembersPageSchema.safeParse(data);
|
|
37425
|
+
if (!parseResult.success) throw new Error(`\u274C Invalid workspace members response: ${parseResult.error.message}`);
|
|
37426
|
+
for (const { user } of parseResult.data.values) {
|
|
37427
|
+
members.push({ uuid: user.uuid, displayName: user.display_name ?? "", nickname: user.nickname });
|
|
37428
|
+
}
|
|
37429
|
+
pages++;
|
|
37430
|
+
url = parseResult.data.next?.startsWith(`${BITBUCKET_API}/`) ? parseResult.data.next : void 0;
|
|
37431
|
+
}
|
|
37432
|
+
return members;
|
|
37433
|
+
}
|
|
37434
|
+
async function fetchCurrentUserUuid(auth) {
|
|
37435
|
+
try {
|
|
37436
|
+
const data = await callRestApi(`${BITBUCKET_API}/user`, auth);
|
|
37437
|
+
return typeof data.uuid === "string" ? data.uuid : void 0;
|
|
37438
|
+
} catch {
|
|
37439
|
+
return void 0;
|
|
37440
|
+
}
|
|
37441
|
+
}
|
|
37442
|
+
async function lookupReviewers(ws, auth, queries) {
|
|
37443
|
+
const byUuid = [];
|
|
37444
|
+
const names = [];
|
|
37445
|
+
for (const query of queries) {
|
|
37446
|
+
const uuid2 = normalizeReviewerUuid(query);
|
|
37447
|
+
if (uuid2) byUuid.push({ uuid: uuid2, displayName: uuid2 });
|
|
37448
|
+
else if (query.trim()) names.push(query);
|
|
37449
|
+
}
|
|
37450
|
+
const [members, selfUuid] = await Promise.all([
|
|
37451
|
+
names.length > 0 ? fetchWorkspaceMembers(ws, auth) : Promise.resolve([]),
|
|
37452
|
+
fetchCurrentUserUuid(auth)
|
|
37453
|
+
]);
|
|
37454
|
+
const match = matchReviewers(names, members);
|
|
37455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
37456
|
+
const reviewers = [];
|
|
37457
|
+
let skippedSelf = false;
|
|
37458
|
+
for (const member of [...byUuid, ...match.resolved.map((r) => r.member)]) {
|
|
37459
|
+
const key = member.uuid.toLowerCase();
|
|
37460
|
+
if (seen.has(key)) continue;
|
|
37461
|
+
seen.add(key);
|
|
37462
|
+
if (selfUuid && key === selfUuid.toLowerCase()) {
|
|
37463
|
+
skippedSelf = true;
|
|
37464
|
+
continue;
|
|
37465
|
+
}
|
|
37466
|
+
reviewers.push(member);
|
|
37467
|
+
}
|
|
37468
|
+
return { reviewers, skippedSelf, ambiguous: match.ambiguous, unmatched: match.unmatched };
|
|
37469
|
+
}
|
|
37470
|
+
function describeMember(member) {
|
|
37471
|
+
return `${member.displayName}${member.nickname ? ` (@${member.nickname})` : ""} ${member.uuid}`;
|
|
37472
|
+
}
|
|
37473
|
+
function describeReviewerProblems(lookup) {
|
|
37474
|
+
const lines = [];
|
|
37475
|
+
for (const { query, candidates } of lookup.ambiguous) {
|
|
37476
|
+
lines.push(`- \u2753 "${query}" matches ${candidates.length} members \u2014 use a fuller name or a UUID:`);
|
|
37477
|
+
for (const candidate of candidates.slice(0, 10)) lines.push(` - ${describeMember(candidate)}`);
|
|
37478
|
+
if (candidates.length > 10) lines.push(` - \u2026and ${candidates.length - 10} more`);
|
|
37479
|
+
}
|
|
37480
|
+
for (const query of lookup.unmatched) lines.push(`- \u274C "${query}" matches no workspace member`);
|
|
37481
|
+
return lines;
|
|
37482
|
+
}
|
|
37483
|
+
async function handleFindBitbucketReviewers(args) {
|
|
37484
|
+
try {
|
|
37485
|
+
const ws = args.workspace?.trim() || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
|
|
37486
|
+
if (!ws) return mcpText(MISSING_WORKSPACE);
|
|
37487
|
+
if (!BITBUCKET_SLUG.test(ws)) return mcpText(`\u274C Invalid workspace slug: ${ws}`);
|
|
37488
|
+
if (args.names.every((name) => !name.trim())) return mcpText("\u274C names is required.");
|
|
37489
|
+
const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
|
|
37490
|
+
const lookup = await lookupReviewers(ws, auth, args.names);
|
|
37491
|
+
const lines = [`## Reviewer lookup (${ws})`];
|
|
37492
|
+
for (const member of lookup.reviewers) lines.push(`- \u2705 ${describeMember(member)}`);
|
|
37493
|
+
if (lookup.skippedSelf) lines.push("- \u23ED\uFE0F You (the PR author) were named \u2014 skipped, Bitbucket rejects self-review");
|
|
37494
|
+
lines.push(...describeReviewerProblems(lookup));
|
|
37495
|
+
return mcpText(lines.join("\n"));
|
|
37496
|
+
} catch (error2) {
|
|
37497
|
+
return mcpText(errorText(error2, "kit_find_bitbucket_reviewers"));
|
|
37498
|
+
}
|
|
37499
|
+
}
|
|
37500
|
+
async function handleCreateBitbucketPr(args) {
|
|
37501
|
+
try {
|
|
37502
|
+
const ref = resolveBitbucketRepo(args.repository, args.workspace);
|
|
37503
|
+
if (!ref.ok) return mcpText(ref.error);
|
|
37504
|
+
const title = args.title.trim();
|
|
37505
|
+
const sourceBranch = args.sourceBranch.trim();
|
|
37506
|
+
if (!title) return mcpText("\u274C title is required.");
|
|
37507
|
+
if (!sourceBranch) return mcpText("\u274C sourceBranch is required.");
|
|
37508
|
+
const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
|
|
37509
|
+
let reviewerUuids = [];
|
|
37510
|
+
let skippedSelf = false;
|
|
37511
|
+
if (args.reviewers && args.reviewers.some((r) => r.trim())) {
|
|
37512
|
+
const lookup = await lookupReviewers(ref.ws, auth, args.reviewers);
|
|
37513
|
+
const problems = describeReviewerProblems(lookup);
|
|
37514
|
+
if (problems.length > 0) {
|
|
37515
|
+
return mcpText(
|
|
37516
|
+
`\u274C PR not created \u2014 some reviewers did not resolve to exactly one member:
|
|
37517
|
+
${problems.join("\n")}`
|
|
37518
|
+
);
|
|
37519
|
+
}
|
|
37520
|
+
reviewerUuids = lookup.reviewers.map((member) => member.uuid);
|
|
37521
|
+
skippedSelf = lookup.skippedSelf;
|
|
37522
|
+
}
|
|
37523
|
+
const body = { title, source: { branch: { name: sourceBranch } } };
|
|
37524
|
+
const destinationBranch = args.destinationBranch?.trim();
|
|
37525
|
+
if (destinationBranch) body.destination = { branch: { name: destinationBranch } };
|
|
37526
|
+
if (args.description) body.description = args.description;
|
|
37527
|
+
if (args.closeSourceBranch !== void 0) body.close_source_branch = args.closeSourceBranch;
|
|
37528
|
+
if (args.draft !== void 0) body.draft = args.draft;
|
|
37529
|
+
if (reviewerUuids.length > 0) body.reviewers = reviewerUuids.map((uuid2) => ({ uuid: uuid2 }));
|
|
37530
|
+
const url = `${BITBUCKET_API}/repositories/${ref.ws}/${ref.repo}/pullrequests`;
|
|
37531
|
+
const jsonData = await callRestApi(url, auth, "application/json", body);
|
|
37532
|
+
const parseResult = BitbucketCreatedPrSchema.safeParse(jsonData);
|
|
37533
|
+
if (!parseResult.success) {
|
|
37534
|
+
return mcpText(`\u26A0\uFE0F PR was likely created, but the response could not be parsed: ${parseResult.error.message}`);
|
|
37535
|
+
}
|
|
37536
|
+
const pr = parseResult.data;
|
|
37537
|
+
const reviewers = pr.reviewers?.map((r) => r.display_name).filter(Boolean) ?? [];
|
|
37538
|
+
const lines = [
|
|
37539
|
+
`\u2705 Created PR #${pr.id}: ${pr.title}`,
|
|
37540
|
+
`**URL:** ${pr.links?.html?.href ?? `https://bitbucket.org/${ref.ws}/${ref.repo}/pull-requests/${pr.id}`}`,
|
|
37541
|
+
`**Branch:** ${pr.source?.branch.name ?? sourceBranch} \u2192 ${pr.destination?.branch.name ?? destinationBranch ?? "(main branch)"}`,
|
|
37542
|
+
`**State:** ${pr.state ?? "OPEN"}${pr.draft ? " (draft)" : ""}`
|
|
37543
|
+
];
|
|
37544
|
+
if (reviewers.length > 0) lines.push(`**Reviewers:** ${reviewers.join(", ")}`);
|
|
37545
|
+
if (skippedSelf)
|
|
37546
|
+
lines.push("\u23ED\uFE0F You (the PR author) were named as a reviewer \u2014 skipped, Bitbucket rejects self-review.");
|
|
37547
|
+
return mcpText(lines.join("\n"));
|
|
37548
|
+
} catch (error2) {
|
|
37549
|
+
return mcpText(errorText(error2, "kit_create_bitbucket_pr"));
|
|
37550
|
+
}
|
|
37551
|
+
}
|
|
37552
|
+
function describeInlineAnchor(inline) {
|
|
37553
|
+
if (inline.to) {
|
|
37554
|
+
return inline.start_to && inline.start_to !== inline.to ? `lines ${inline.start_to}\u2013${inline.to}` : `line ${inline.to}`;
|
|
37555
|
+
}
|
|
37556
|
+
if (inline.from) {
|
|
37557
|
+
return inline.start_from && inline.start_from !== inline.from ? `old lines ${inline.start_from}\u2013${inline.from}` : `old line ${inline.from}`;
|
|
37558
|
+
}
|
|
37559
|
+
return "file";
|
|
37560
|
+
}
|
|
37561
|
+
function renderComment(comment, depth, isRoot) {
|
|
37562
|
+
const indent = " ".repeat(depth);
|
|
37563
|
+
const meta = [`#${comment.id}`, comment.user?.display_name || "Unknown", comment.created_on?.slice(0, 10) ?? ""];
|
|
37564
|
+
if (isRoot && comment.inline) meta.push(describeInlineAnchor(comment.inline));
|
|
37565
|
+
if (comment.pending) meta.push("pending (unpublished)");
|
|
37566
|
+
if (isRoot) {
|
|
37567
|
+
meta.push(
|
|
37568
|
+
comment.resolution ? `\u2705 resolved${comment.resolution.user?.display_name ? ` by ${comment.resolution.user.display_name}` : ""}` : "\u{1F7E1} open"
|
|
37569
|
+
);
|
|
37570
|
+
}
|
|
37571
|
+
const header = `${indent}${depth > 0 ? "\u21B3 " : "- "}**${meta.filter(Boolean).join(" \xB7 ")}**`;
|
|
37572
|
+
const raw = comment.deleted ? "_(deleted)_" : comment.content?.raw?.trim() || "_(empty)_";
|
|
37573
|
+
const body = raw.split("\n").map((line) => `${indent} > ${line}`).join("\n");
|
|
37574
|
+
return `${header}
|
|
37575
|
+
${body}`;
|
|
37576
|
+
}
|
|
37577
|
+
async function handleGetBitbucketPrComments(args) {
|
|
37578
|
+
try {
|
|
37579
|
+
const ref = resolveBitbucketPr(args.input, args.workspace, args.repoSlug);
|
|
37580
|
+
if (!ref.ok) return mcpText(ref.error);
|
|
37581
|
+
const includeResolved = args.includeResolved ?? true;
|
|
37582
|
+
const includeDeleted = args.includeDeleted ?? false;
|
|
37583
|
+
const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
|
|
37584
|
+
let url = `${BITBUCKET_API}/repositories/${ref.ws}/${ref.repo}/pullrequests/${ref.prId}/comments?pagelen=100`;
|
|
37585
|
+
const comments = [];
|
|
37586
|
+
let pages = 0;
|
|
37587
|
+
while (url && pages < MAX_PAGES) {
|
|
37588
|
+
const parseResult = BitbucketCommentPageSchema.safeParse(await callRestApi(url, auth));
|
|
37589
|
+
if (!parseResult.success) {
|
|
37590
|
+
return mcpText(`\u274C Invalid Bitbucket comments response: ${parseResult.error.message}`);
|
|
37591
|
+
}
|
|
37592
|
+
comments.push(...parseResult.data.values);
|
|
37593
|
+
pages++;
|
|
37594
|
+
url = parseResult.data.next?.startsWith(`${BITBUCKET_API}/`) ? parseResult.data.next : void 0;
|
|
37595
|
+
}
|
|
37596
|
+
const truncated = Boolean(url);
|
|
37597
|
+
const byId = new Map(comments.map((c) => [c.id, c]));
|
|
37598
|
+
const children = /* @__PURE__ */ new Map();
|
|
37599
|
+
const roots = [];
|
|
37600
|
+
for (const comment of comments) {
|
|
37601
|
+
const parentId = comment.parent?.id;
|
|
37602
|
+
if (parentId !== void 0 && byId.has(parentId)) {
|
|
37603
|
+
children.set(parentId, [...children.get(parentId) ?? [], comment]);
|
|
37604
|
+
} else {
|
|
37605
|
+
roots.push(comment);
|
|
37606
|
+
}
|
|
37607
|
+
}
|
|
37608
|
+
const renderThread = (comment, depth) => {
|
|
37609
|
+
const replies = (children.get(comment.id) ?? []).flatMap((child) => renderThread(child, depth + 1));
|
|
37610
|
+
if (comment.deleted && !includeDeleted && replies.length === 0) return [];
|
|
37611
|
+
return [renderComment(comment, depth, depth === 0), ...replies];
|
|
37612
|
+
};
|
|
37613
|
+
const visibleRoots = roots.filter((root) => includeResolved || !root.resolution);
|
|
37614
|
+
const general = [];
|
|
37615
|
+
const inlineByFile = /* @__PURE__ */ new Map();
|
|
37616
|
+
for (const root of visibleRoots) {
|
|
37617
|
+
const rendered = renderThread(root, 0);
|
|
37618
|
+
if (rendered.length === 0) continue;
|
|
37619
|
+
if (root.inline) {
|
|
37620
|
+
inlineByFile.set(root.inline.path, [...inlineByFile.get(root.inline.path) ?? [], ...rendered]);
|
|
37621
|
+
} else {
|
|
37622
|
+
general.push(...rendered);
|
|
37623
|
+
}
|
|
37624
|
+
}
|
|
37625
|
+
const liveRoots = roots.filter((root) => !root.deleted);
|
|
37626
|
+
const unresolved = liveRoots.filter((root) => !root.resolution).length;
|
|
37627
|
+
const summary = `${comments.length} comments in ${liveRoots.length} threads \xB7 ${unresolved} open \xB7 ${liveRoots.length - unresolved} resolved`;
|
|
37628
|
+
let output = `## PR #${ref.prId} comments
|
|
37629
|
+
${summary}${includeResolved ? "" : " (resolved threads hidden)"}`;
|
|
37630
|
+
if (truncated) {
|
|
37631
|
+
output += `
|
|
37632
|
+
|
|
37633
|
+
\u26A0\uFE0F Stopped after ${MAX_PAGES} pages (${comments.length} comments); later comments are not shown.`;
|
|
37634
|
+
}
|
|
37635
|
+
if (general.length === 0 && inlineByFile.size === 0) {
|
|
37636
|
+
output += "\n\nNo comments to show.";
|
|
37637
|
+
} else {
|
|
37638
|
+
if (general.length > 0) output += `
|
|
37639
|
+
|
|
37640
|
+
### General
|
|
37641
|
+
${general.join("\n")}`;
|
|
37642
|
+
if (inlineByFile.size > 0) {
|
|
37643
|
+
output += "\n\n### Inline";
|
|
37644
|
+
for (const [path16, rendered] of inlineByFile) output += `
|
|
37645
|
+
|
|
37646
|
+
#### \`${path16}\`
|
|
37647
|
+
${rendered.join("\n")}`;
|
|
37648
|
+
}
|
|
37649
|
+
}
|
|
37650
|
+
if (output.length >= LARGE_PAYLOAD_THRESHOLD) {
|
|
37651
|
+
const filePath = `/tmp/kit-pr-${ref.prId}-comments-${Date.now()}.md`;
|
|
37652
|
+
try {
|
|
37653
|
+
writeFileSync4(filePath, sanitizeOutput(output), "utf8");
|
|
37654
|
+
return mcpText(
|
|
37655
|
+
`## PR #${ref.prId} comments
|
|
37656
|
+
${summary}
|
|
37657
|
+
|
|
37658
|
+
Comments are large (${output.length} chars). Full markdown written to: \`${filePath}\`. Read this file before proceeding.`
|
|
37659
|
+
);
|
|
37660
|
+
} catch {
|
|
37661
|
+
}
|
|
37662
|
+
}
|
|
37663
|
+
return mcpText(sanitizeOutput(output));
|
|
37664
|
+
} catch (error2) {
|
|
37665
|
+
return mcpText(errorText(error2, "kit_get_bitbucket_pr_comments"));
|
|
37666
|
+
}
|
|
37667
|
+
}
|
|
36650
37668
|
function registerIntegrationTools(server2) {
|
|
36651
37669
|
server2.registerTool(
|
|
36652
37670
|
"kit_get_bitbucket_pr",
|
|
@@ -36663,29 +37681,9 @@ function registerIntegrationTools(server2) {
|
|
|
36663
37681
|
},
|
|
36664
37682
|
async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
36665
37683
|
try {
|
|
36666
|
-
|
|
36667
|
-
|
|
36668
|
-
|
|
36669
|
-
const urlMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
|
|
36670
|
-
if (urlMatch) {
|
|
36671
|
-
ws = urlMatch[1];
|
|
36672
|
-
repo = urlMatch[2];
|
|
36673
|
-
prId = parseInt(urlMatch[3], 10);
|
|
36674
|
-
} else if (input.match(/^\d+$/)) {
|
|
36675
|
-
prId = parseInt(input, 10);
|
|
36676
|
-
ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
|
|
36677
|
-
repo = repoSlug;
|
|
36678
|
-
}
|
|
36679
|
-
if (!ws) {
|
|
36680
|
-
return mcpText(
|
|
36681
|
-
`\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`
|
|
36682
|
-
);
|
|
36683
|
-
}
|
|
36684
|
-
if (!repo || !prId) {
|
|
36685
|
-
return mcpText(`\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`);
|
|
36686
|
-
}
|
|
36687
|
-
const safeWs = sanitize(ws);
|
|
36688
|
-
const safeRepo = sanitize(repo);
|
|
37684
|
+
const ref = resolveBitbucketPr(input, workspace, repoSlug);
|
|
37685
|
+
if (!ref.ok) return mcpText(ref.error);
|
|
37686
|
+
const { ws: safeWs, repo: safeRepo, prId } = ref;
|
|
36689
37687
|
const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
|
|
36690
37688
|
const prUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}`;
|
|
36691
37689
|
const jsonData = await callRestApi(prUrl, auth);
|
|
@@ -36703,8 +37701,7 @@ ${pr.description || "No description"}`;
|
|
|
36703
37701
|
if (includeDiff) {
|
|
36704
37702
|
const diffUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}/diff`;
|
|
36705
37703
|
const diff = await callRestApi(diffUrl, auth, "text/plain");
|
|
36706
|
-
|
|
36707
|
-
if (diff.length < DIFF_FILE_THRESHOLD) {
|
|
37704
|
+
if (diff.length < LARGE_PAYLOAD_THRESHOLD) {
|
|
36708
37705
|
output += `
|
|
36709
37706
|
|
|
36710
37707
|
### Diff
|
|
@@ -36732,10 +37729,63 @@ ${diff}
|
|
|
36732
37729
|
}
|
|
36733
37730
|
return mcpText(output);
|
|
36734
37731
|
} catch (error2) {
|
|
36735
|
-
return mcpText(error2
|
|
37732
|
+
return mcpText(errorText(error2, "kit_get_bitbucket_pr"));
|
|
36736
37733
|
}
|
|
36737
37734
|
}
|
|
36738
37735
|
);
|
|
37736
|
+
server2.registerTool(
|
|
37737
|
+
"kit_create_bitbucket_pr",
|
|
37738
|
+
{
|
|
37739
|
+
title: "Create Bitbucket PR",
|
|
37740
|
+
description: "Create a Bitbucket Cloud pull request from an already-pushed source branch. If destinationBranch is omitted, Bitbucket targets the repository main branch. Returns the new PR id and URL.",
|
|
37741
|
+
inputSchema: {
|
|
37742
|
+
repository: external_exports.string().describe(
|
|
37743
|
+
'Bitbucket repo URL, "workspace/repo", or a repo slug (uses workspace / BITBUCKET_DEFAULT_WORKSPACE)'
|
|
37744
|
+
),
|
|
37745
|
+
workspace: external_exports.string().optional().describe("Bitbucket workspace slug, used when repository is a bare slug"),
|
|
37746
|
+
title: external_exports.string().describe("PR title"),
|
|
37747
|
+
sourceBranch: external_exports.string().describe("Source branch name (must already exist on the remote)"),
|
|
37748
|
+
destinationBranch: external_exports.string().optional().describe("Destination branch name; defaults to the repo main branch"),
|
|
37749
|
+
description: external_exports.string().optional().describe("PR description (markdown)"),
|
|
37750
|
+
closeSourceBranch: external_exports.boolean().optional().describe("Close the source branch when the PR is merged"),
|
|
37751
|
+
draft: external_exports.boolean().optional().describe("Create the PR as a draft"),
|
|
37752
|
+
reviewers: external_exports.array(external_exports.string()).optional().describe(
|
|
37753
|
+
'Reviewers as names, nicknames or account UUIDs, e.g. ["Dien", "Nguyen Tien Dung"]. Matching is case- and accent-insensitive; a name that matches no member or several aborts with no PR created. The PR author is skipped.'
|
|
37754
|
+
)
|
|
37755
|
+
},
|
|
37756
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
37757
|
+
},
|
|
37758
|
+
async (args) => handleCreateBitbucketPr(args)
|
|
37759
|
+
);
|
|
37760
|
+
server2.registerTool(
|
|
37761
|
+
"kit_find_bitbucket_reviewers",
|
|
37762
|
+
{
|
|
37763
|
+
title: "Find Bitbucket Reviewers",
|
|
37764
|
+
description: "Resolve reviewer names, nicknames or UUIDs to Bitbucket workspace members \u2014 the same matching kit_create_bitbucket_pr uses. Reports matched, ambiguous (with candidates) and unknown names, so reviewers can be verified before pushing or creating a PR.",
|
|
37765
|
+
inputSchema: {
|
|
37766
|
+
names: external_exports.array(external_exports.string()).describe('Names, nicknames or account UUIDs, e.g. ["Dien", "Dung"]'),
|
|
37767
|
+
workspace: external_exports.string().optional().describe("Bitbucket workspace slug (defaults to BITBUCKET_DEFAULT_WORKSPACE)")
|
|
37768
|
+
},
|
|
37769
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
37770
|
+
},
|
|
37771
|
+
async (args) => handleFindBitbucketReviewers(args)
|
|
37772
|
+
);
|
|
37773
|
+
server2.registerTool(
|
|
37774
|
+
"kit_get_bitbucket_pr_comments",
|
|
37775
|
+
{
|
|
37776
|
+
title: "Get Bitbucket PR Comments",
|
|
37777
|
+
description: "Get all comments on a Bitbucket PR as threads: general comments, then inline comments grouped by file with line anchors and resolved/open status. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.",
|
|
37778
|
+
inputSchema: {
|
|
37779
|
+
input: external_exports.string().describe("Bitbucket PR URL or numeric PR ID"),
|
|
37780
|
+
workspace: external_exports.string().optional().describe("Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)"),
|
|
37781
|
+
repoSlug: external_exports.string().optional().describe("Bitbucket repo slug (required for numeric ID)"),
|
|
37782
|
+
includeResolved: external_exports.boolean().optional().default(true).describe("Include resolved threads"),
|
|
37783
|
+
includeDeleted: external_exports.boolean().optional().default(false).describe("Include deleted comments")
|
|
37784
|
+
},
|
|
37785
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
37786
|
+
},
|
|
37787
|
+
async (args) => handleGetBitbucketPrComments(args)
|
|
37788
|
+
);
|
|
36739
37789
|
server2.registerTool(
|
|
36740
37790
|
"kit_jira_get_ticket",
|
|
36741
37791
|
{
|
|
@@ -36754,9 +37804,7 @@ ${diff}
|
|
|
36754
37804
|
|
|
36755
37805
|
Expected format: PROJ-123`);
|
|
36756
37806
|
}
|
|
36757
|
-
const cloudId =
|
|
36758
|
-
if (!cloudId) throw new Error("Missing ATLASSIAN_CLOUD_ID");
|
|
36759
|
-
const auth = buildBasicAuth("ATLASSIAN_USER_EMAIL", "ATLASSIAN_API_TOKEN");
|
|
37807
|
+
const { auth, cloudId } = buildAtlassianContext("jira");
|
|
36760
37808
|
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${safeTicketId}`;
|
|
36761
37809
|
const jsonData = await callRestApi(url, auth);
|
|
36762
37810
|
const parseResult = JiraTicketSchema.safeParse(jsonData);
|
|
@@ -36784,10 +37832,22 @@ ${typeof ticket.fields.description === "string" ? ticket.fields.description : ad
|
|
|
36784
37832
|
${ticket.fields.labels?.join(", ") || "None"}`;
|
|
36785
37833
|
return mcpText(output);
|
|
36786
37834
|
} catch (error2) {
|
|
36787
|
-
return mcpText(`Error: ${error2
|
|
37835
|
+
return mcpText(`Error: ${errorText(error2, "kit_jira_get_ticket")}`);
|
|
36788
37836
|
}
|
|
36789
37837
|
}
|
|
36790
37838
|
);
|
|
37839
|
+
server2.registerTool(
|
|
37840
|
+
"kit_confluence_get_page",
|
|
37841
|
+
{
|
|
37842
|
+
title: "Get Confluence Page",
|
|
37843
|
+
description: "Get a Confluence page as markdown using the Atlassian REST API. Accepts a full Confluence page URL or a numeric page ID.",
|
|
37844
|
+
inputSchema: {
|
|
37845
|
+
input: external_exports.string().describe("Confluence page URL or numeric page ID")
|
|
37846
|
+
},
|
|
37847
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
37848
|
+
},
|
|
37849
|
+
async ({ input }) => handleConfluenceGetPage({ input })
|
|
37850
|
+
);
|
|
36791
37851
|
}
|
|
36792
37852
|
|
|
36793
37853
|
// src/mcp/memory.ts
|