@hanhnd/agent-kit 1.0.43 → 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/integration.d.ts +111 -1
- package/dist/mcp/integration.js +588 -30
- package/dist/mcp/registration.test.js +35 -1
- package/dist/server.js +495 -32
- package/package.json +1 -1
|
@@ -10,6 +10,9 @@ const EXPECTED_NON_MEMORY_TOOLS = [
|
|
|
10
10
|
'kit_save_handoff',
|
|
11
11
|
'kit_trigger_agent',
|
|
12
12
|
'kit_get_bitbucket_pr',
|
|
13
|
+
'kit_create_bitbucket_pr',
|
|
14
|
+
'kit_find_bitbucket_reviewers',
|
|
15
|
+
'kit_get_bitbucket_pr_comments',
|
|
13
16
|
'kit_jira_get_ticket',
|
|
14
17
|
'kit_confluence_get_page',
|
|
15
18
|
];
|
|
@@ -24,7 +27,7 @@ async function buildClientWithTools() {
|
|
|
24
27
|
return client;
|
|
25
28
|
}
|
|
26
29
|
describe('MCP tool registration smoke tests', () => {
|
|
27
|
-
test('R1: all
|
|
30
|
+
test('R1: all 8 non-memory tool names are registered', async () => {
|
|
28
31
|
const client = await buildClientWithTools();
|
|
29
32
|
const { tools } = await client.listTools();
|
|
30
33
|
const names = tools.map((t) => t.name);
|
|
@@ -85,4 +88,35 @@ describe('MCP tool registration smoke tests', () => {
|
|
|
85
88
|
assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
|
|
86
89
|
assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
|
|
87
90
|
});
|
|
91
|
+
test('R7: kit_create_bitbucket_pr (readOnlyHint:false) has correct annotations', async () => {
|
|
92
|
+
const client = await buildClientWithTools();
|
|
93
|
+
const { tools } = await client.listTools();
|
|
94
|
+
const tool = tools.find((t) => t.name === 'kit_create_bitbucket_pr');
|
|
95
|
+
assert.ok(tool, 'kit_create_bitbucket_pr must be registered');
|
|
96
|
+
assert.ok(tool.annotations, 'kit_create_bitbucket_pr must have annotations');
|
|
97
|
+
assert.equal(tool.annotations.readOnlyHint, false, 'readOnlyHint must be false');
|
|
98
|
+
assert.equal(tool.annotations.destructiveHint, false, 'destructiveHint must be false');
|
|
99
|
+
assert.equal(tool.annotations.idempotentHint, false, 'idempotentHint must be false');
|
|
100
|
+
assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
|
|
101
|
+
});
|
|
102
|
+
test('R8: kit_get_bitbucket_pr_comments (readOnlyHint:true) has correct annotations', async () => {
|
|
103
|
+
const client = await buildClientWithTools();
|
|
104
|
+
const { tools } = await client.listTools();
|
|
105
|
+
const tool = tools.find((t) => t.name === 'kit_get_bitbucket_pr_comments');
|
|
106
|
+
assert.ok(tool, 'kit_get_bitbucket_pr_comments must be registered');
|
|
107
|
+
assert.ok(tool.annotations, 'kit_get_bitbucket_pr_comments must have annotations');
|
|
108
|
+
assert.equal(tool.annotations.readOnlyHint, true, 'readOnlyHint must be true');
|
|
109
|
+
assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
|
|
110
|
+
assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
|
|
111
|
+
});
|
|
112
|
+
test('R9: kit_find_bitbucket_reviewers (readOnlyHint:true) has correct annotations', async () => {
|
|
113
|
+
const client = await buildClientWithTools();
|
|
114
|
+
const { tools } = await client.listTools();
|
|
115
|
+
const tool = tools.find((t) => t.name === 'kit_find_bitbucket_reviewers');
|
|
116
|
+
assert.ok(tool, 'kit_find_bitbucket_reviewers must be registered');
|
|
117
|
+
assert.ok(tool.annotations, 'kit_find_bitbucket_reviewers must have annotations');
|
|
118
|
+
assert.equal(tool.annotations.readOnlyHint, true, 'readOnlyHint must be true');
|
|
119
|
+
assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
|
|
120
|
+
assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
|
|
121
|
+
});
|
|
88
122
|
});
|
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);
|
|
@@ -37070,6 +37067,42 @@ var BitbucketPrSchema = external_exports.object({
|
|
|
37070
37067
|
source: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }),
|
|
37071
37068
|
destination: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) })
|
|
37072
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
|
+
});
|
|
37073
37106
|
var AdfContentSchema = external_exports.object({
|
|
37074
37107
|
type: external_exports.string().optional(),
|
|
37075
37108
|
content: external_exports.array(external_exports.unknown()).optional(),
|
|
@@ -37116,6 +37149,28 @@ var ConfluencePageSchema = external_exports.object({
|
|
|
37116
37149
|
labels: external_exports.object({ results: external_exports.array(external_exports.object({ name: external_exports.string() }).passthrough()).optional() }).passthrough().optional(),
|
|
37117
37150
|
_links: external_exports.object({ base: external_exports.string().optional(), webui: external_exports.string().optional() }).passthrough().optional()
|
|
37118
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
|
+
}
|
|
37119
37174
|
function buildBasicAuth(emailVar, tokenVar) {
|
|
37120
37175
|
const email2 = getCredential(emailVar);
|
|
37121
37176
|
const token = getCredential(tokenVar);
|
|
@@ -37131,17 +37186,37 @@ function buildAtlassianContext(product) {
|
|
|
37131
37186
|
if (!cloudId) throw new Error("Missing ATLASSIAN_CLOUD_ID");
|
|
37132
37187
|
return { auth: buildBasicAuth("ATLASSIAN_USER_EMAIL", PRODUCT_TOKEN_KEY[product]), cloudId };
|
|
37133
37188
|
}
|
|
37134
|
-
async function callRestApi(url, auth, accept = "application/json") {
|
|
37135
|
-
const
|
|
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);
|
|
37136
37198
|
if (resp.status === 401) throw new Error(`\u274C Auth failed (401): ${url}`);
|
|
37137
37199
|
if (resp.status === 403)
|
|
37138
37200
|
throw new Error(`\u274C Access denied (403): ${url}
|
|
37139
37201
|
|
|
37140
37202
|
You lack permission for this resource, or it is restricted.`);
|
|
37141
37203
|
if (resp.status === 404) throw new Error(`\u274C Not found: ${url}`);
|
|
37142
|
-
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())}`);
|
|
37143
37205
|
return accept === "text/plain" ? resp.text() : resp.json();
|
|
37144
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
|
+
}
|
|
37145
37220
|
var CONFLUENCE_INPUT_FORMS = [
|
|
37146
37221
|
"Accepted input forms:",
|
|
37147
37222
|
" \u2022 https://<site>.atlassian.net/wiki/spaces/<SPACEKEY>/pages/<pageId>/<Title>",
|
|
@@ -37232,7 +37307,362 @@ ${contentSection}
|
|
|
37232
37307
|
${labels.length > 0 ? labels.join(", ") : "None"}`;
|
|
37233
37308
|
return mcpText(sanitizeOutput(output));
|
|
37234
37309
|
} catch (error2) {
|
|
37235
|
-
return mcpText(`Error: ${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"));
|
|
37236
37666
|
}
|
|
37237
37667
|
}
|
|
37238
37668
|
function registerIntegrationTools(server2) {
|
|
@@ -37251,29 +37681,9 @@ function registerIntegrationTools(server2) {
|
|
|
37251
37681
|
},
|
|
37252
37682
|
async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
37253
37683
|
try {
|
|
37254
|
-
|
|
37255
|
-
|
|
37256
|
-
|
|
37257
|
-
const urlMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
|
|
37258
|
-
if (urlMatch) {
|
|
37259
|
-
ws = urlMatch[1];
|
|
37260
|
-
repo = urlMatch[2];
|
|
37261
|
-
prId = parseInt(urlMatch[3], 10);
|
|
37262
|
-
} else if (input.match(/^\d+$/)) {
|
|
37263
|
-
prId = parseInt(input, 10);
|
|
37264
|
-
ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
|
|
37265
|
-
repo = repoSlug;
|
|
37266
|
-
}
|
|
37267
|
-
if (!ws) {
|
|
37268
|
-
return mcpText(
|
|
37269
|
-
`\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`
|
|
37270
|
-
);
|
|
37271
|
-
}
|
|
37272
|
-
if (!repo || !prId) {
|
|
37273
|
-
return mcpText(`\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`);
|
|
37274
|
-
}
|
|
37275
|
-
const safeWs = sanitize(ws);
|
|
37276
|
-
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;
|
|
37277
37687
|
const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
|
|
37278
37688
|
const prUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}`;
|
|
37279
37689
|
const jsonData = await callRestApi(prUrl, auth);
|
|
@@ -37319,10 +37729,63 @@ ${diff}
|
|
|
37319
37729
|
}
|
|
37320
37730
|
return mcpText(output);
|
|
37321
37731
|
} catch (error2) {
|
|
37322
|
-
return mcpText(error2
|
|
37732
|
+
return mcpText(errorText(error2, "kit_get_bitbucket_pr"));
|
|
37323
37733
|
}
|
|
37324
37734
|
}
|
|
37325
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
|
+
);
|
|
37326
37789
|
server2.registerTool(
|
|
37327
37790
|
"kit_jira_get_ticket",
|
|
37328
37791
|
{
|
|
@@ -37369,7 +37832,7 @@ ${typeof ticket.fields.description === "string" ? ticket.fields.description : ad
|
|
|
37369
37832
|
${ticket.fields.labels?.join(", ") || "None"}`;
|
|
37370
37833
|
return mcpText(output);
|
|
37371
37834
|
} catch (error2) {
|
|
37372
|
-
return mcpText(`Error: ${error2
|
|
37835
|
+
return mcpText(`Error: ${errorText(error2, "kit_jira_get_ticket")}`);
|
|
37373
37836
|
}
|
|
37374
37837
|
}
|
|
37375
37838
|
);
|