@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 CHANGED
@@ -27,6 +27,29 @@ Claude Code and Codex plugin manifests already use this package:
27
27
  }
28
28
  ```
29
29
 
30
+ ## API Token Scopes
31
+
32
+ Integration tools call the Atlassian REST APIs with scoped API tokens. Create each token at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) ("Create API token with scopes") and grant the scopes below.
33
+
34
+ | Token | Scopes |
35
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
36
+ | `BITBUCKET_API_TOKEN` | `read:pullrequest:bitbucket`, `write:pullrequest:bitbucket`, `read:repository:bitbucket`, `read:workspace:bitbucket`, `read:user:bitbucket` |
37
+ | `JIRA_API_TOKEN` | `read:jira-work` |
38
+ | `CONFLUENCE_API_TOKEN` | `read:page:confluence` |
39
+
40
+ Per tool:
41
+
42
+ | Tool | Scopes |
43
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
44
+ | `kit_get_bitbucket_pr` | `read:pullrequest:bitbucket`; `read:repository:bitbucket` for the diff (the PR diff redirects to the repository diff) |
45
+ | `kit_get_bitbucket_pr_comments` | `read:pullrequest:bitbucket` |
46
+ | `kit_create_bitbucket_pr` | `read:pullrequest:bitbucket`, `write:pullrequest:bitbucket`; `read:workspace:bitbucket` for reviewers given by name; `read:user:bitbucket` to skip the PR author |
47
+ | `kit_find_bitbucket_reviewers` | `read:workspace:bitbucket`; `read:user:bitbucket` to flag the PR author |
48
+ | `kit_jira_get_ticket` | `read:jira-work` |
49
+ | `kit_confluence_get_page` | `read:page:confluence` |
50
+
51
+ A missing scope surfaces as a 401/403; the error message lists the scopes the tool needs.
52
+
30
53
  ## Development
31
54
 
32
55
  From the repository root:
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,335 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { afterEach, describe, mock, test } from 'node:test';
3
+ import { MAX_PAGES, handleCreateBitbucketPr, handleFindBitbucketReviewers, handleGetBitbucketPrComments, matchReviewers, normalizeName, resolveBitbucketPr, resolveBitbucketRepo, } from './integration.js';
4
+ const API = 'https://api.bitbucket.org/2.0';
5
+ const PR_URL = 'https://bitbucket.org/acme/web-app/pull-requests/42';
6
+ process.env.BITBUCKET_USER_EMAIL = 'dev@acme.test';
7
+ process.env.BITBUCKET_API_TOKEN = 'bb-token';
8
+ process.env.BITBUCKET_DEFAULT_WORKSPACE = 'acme';
9
+ /** Replace the global fetch for one test with a queue of responses; `afterEach` restores it. */
10
+ function stubFetch(responses) {
11
+ const calls = [];
12
+ mock.method(globalThis, 'fetch', async (input, init) => {
13
+ calls.push({
14
+ url: String(input),
15
+ method: init?.method ?? 'GET',
16
+ body: init?.body ? JSON.parse(String(init.body)) : undefined,
17
+ });
18
+ const next = responses[Math.min(calls.length - 1, responses.length - 1)];
19
+ return new Response(typeof next.body === 'string' ? next.body : JSON.stringify(next.body), {
20
+ status: next.status,
21
+ headers: { 'content-type': 'application/json' },
22
+ });
23
+ });
24
+ return calls;
25
+ }
26
+ afterEach(() => mock.restoreAll());
27
+ describe('resolveBitbucketPr / resolveBitbucketRepo', () => {
28
+ test('B1: PR URL resolves to workspace, repo and id', () => {
29
+ assert.deepEqual(resolveBitbucketPr(PR_URL), { ok: true, ws: 'acme', repo: 'web-app', prId: 42 });
30
+ });
31
+ test('B2: numeric PR id uses the default workspace', () => {
32
+ assert.deepEqual(resolveBitbucketPr('7', undefined, 'api'), { ok: true, ws: 'acme', repo: 'api', prId: 7 });
33
+ });
34
+ test('B3: numeric PR id without repoSlug is rejected', () => {
35
+ assert.equal(resolveBitbucketPr('7').ok, false);
36
+ });
37
+ test('B4: repo accepts URL, workspace/repo pair, clone URL and bare slug', () => {
38
+ const expected = { ok: true, ws: 'acme', repo: 'web-app' };
39
+ assert.deepEqual(resolveBitbucketRepo('https://bitbucket.org/acme/web-app/src/main/'), expected);
40
+ assert.deepEqual(resolveBitbucketRepo('acme/web-app'), expected);
41
+ assert.deepEqual(resolveBitbucketRepo('git@bitbucket.org:acme/web-app.git'), expected);
42
+ assert.deepEqual(resolveBitbucketRepo('web-app'), expected);
43
+ assert.deepEqual(resolveBitbucketRepo('web-app', 'other'), { ok: true, ws: 'other', repo: 'web-app' });
44
+ });
45
+ test('B5: slugs with URL-breaking characters are rejected', () => {
46
+ assert.equal(resolveBitbucketRepo('acme/web app').ok, false);
47
+ assert.equal(resolveBitbucketRepo('../evil').ok, false);
48
+ });
49
+ });
50
+ const SELF_UUID = '{11111111-1111-1111-1111-111111111111}';
51
+ const MEMBERS = [
52
+ { uuid: '{aaaaaaaa-0000-0000-0000-000000000001}', display_name: 'Trần Văn Điền', nickname: 'dientv' },
53
+ { uuid: '{aaaaaaaa-0000-0000-0000-000000000002}', display_name: 'Nguyễn Tiến Dũng', nickname: 'dungnt' },
54
+ { uuid: '{aaaaaaaa-0000-0000-0000-000000000003}', display_name: 'Lê Dũng', nickname: 'dungle' },
55
+ { uuid: SELF_UUID, display_name: 'Ngô Hạnh', nickname: 'hanhnd' },
56
+ ];
57
+ const member = (i) => ({
58
+ uuid: MEMBERS[i].uuid,
59
+ displayName: MEMBERS[i].display_name,
60
+ nickname: MEMBERS[i].nickname,
61
+ });
62
+ /** Route requests by URL (lookups run concurrently, so order is not stable); unrouted → 500. */
63
+ function stubRoutes(route) {
64
+ const calls = [];
65
+ mock.method(globalThis, 'fetch', async (input, init) => {
66
+ const url = String(input);
67
+ const method = init?.method ?? 'GET';
68
+ calls.push({ url, method, body: init?.body ? JSON.parse(String(init.body)) : undefined });
69
+ const res = route(url, method) ?? { status: 500, body: `unrouted ${method} ${url}` };
70
+ return new Response(typeof res.body === 'string' ? res.body : JSON.stringify(res.body), {
71
+ status: res.status,
72
+ headers: { 'content-type': 'application/json' },
73
+ });
74
+ });
75
+ return calls;
76
+ }
77
+ /** Workspace members split over two pages, the current user, and a 201 for PR creation. */
78
+ function bitbucketRoutes(overrides = () => undefined) {
79
+ return (url, method) => {
80
+ const hit = overrides(url, method);
81
+ if (hit)
82
+ return hit;
83
+ if (url === `${API}/workspaces/acme/members?pagelen=100`) {
84
+ return {
85
+ status: 200,
86
+ body: { values: MEMBERS.slice(0, 2).map((user) => ({ user })), next: `${API}/workspaces/acme/members?page=2` },
87
+ };
88
+ }
89
+ if (url === `${API}/workspaces/acme/members?page=2`) {
90
+ return { status: 200, body: { values: MEMBERS.slice(2).map((user) => ({ user })) } };
91
+ }
92
+ if (url === `${API}/user`)
93
+ return { status: 200, body: { uuid: SELF_UUID } };
94
+ if (method === 'POST')
95
+ return { status: 201, body: created };
96
+ return undefined;
97
+ };
98
+ }
99
+ const created = {
100
+ id: 101,
101
+ title: 'Add login',
102
+ state: 'OPEN',
103
+ draft: true,
104
+ source: { branch: { name: 'feature/login' } },
105
+ destination: { branch: { name: 'develop' } },
106
+ reviewers: [{ display_name: 'Bob' }],
107
+ links: { html: { href: 'https://bitbucket.org/acme/web-app/pull-requests/101' } },
108
+ };
109
+ const postOf = (calls) => calls.find((c) => c.method === 'POST');
110
+ describe('matchReviewers / normalizeName', () => {
111
+ const members = MEMBERS.map((_, i) => member(i));
112
+ test('N1: normalizeName strips Vietnamese diacritics including đ', () => {
113
+ assert.equal(normalizeName(' Trần Văn Điền '), 'tran van dien');
114
+ });
115
+ test('N2: a single word matches a whole word of the display name, accent-insensitive', () => {
116
+ const r = matchReviewers(['Dien'], members);
117
+ assert.deepEqual(r.resolved, [{ query: 'Dien', member: member(0) }]);
118
+ });
119
+ test('N3: a word shared by several members is ambiguous, never guessed', () => {
120
+ const r = matchReviewers(['Dung'], members);
121
+ assert.equal(r.resolved.length, 0);
122
+ assert.deepEqual(r.ambiguous[0].candidates.map((c) => c.uuid), [member(1).uuid, member(2).uuid]);
123
+ });
124
+ test('N4: full name, nickname and multi-word queries disambiguate', () => {
125
+ const r = matchReviewers(['nguyen tien dung', 'dungle', 'Le Dung'], members);
126
+ assert.deepEqual(r.resolved.map((x) => x.member.uuid), [member(1).uuid, member(2).uuid, member(2).uuid]);
127
+ });
128
+ test('N5: partial words do not match', () => {
129
+ assert.deepEqual(matchReviewers(['Die'], members).unmatched, ['Die']);
130
+ });
131
+ });
132
+ describe('handleCreateBitbucketPr', () => {
133
+ test('C1: POSTs the documented body and renders id + URL', async () => {
134
+ const calls = stubRoutes(bitbucketRoutes());
135
+ const text = (await handleCreateBitbucketPr({
136
+ repository: 'acme/web-app',
137
+ title: ' Add login ',
138
+ sourceBranch: 'feature/login',
139
+ destinationBranch: 'develop',
140
+ description: 'Body',
141
+ closeSourceBranch: true,
142
+ draft: true,
143
+ reviewers: ['504c3b62-8120-4f0c-a7bc-87800b9d6f70'],
144
+ })).content[0].text;
145
+ const post = postOf(calls);
146
+ assert.ok(post);
147
+ assert.equal(post.url, `${API}/repositories/acme/web-app/pullrequests`);
148
+ assert.deepEqual(post.body, {
149
+ title: 'Add login',
150
+ source: { branch: { name: 'feature/login' } },
151
+ destination: { branch: { name: 'develop' } },
152
+ description: 'Body',
153
+ close_source_branch: true,
154
+ draft: true,
155
+ reviewers: [{ uuid: '{504c3b62-8120-4f0c-a7bc-87800b9d6f70}' }],
156
+ });
157
+ assert.ok(!calls.some((c) => c.url.includes('/members')), 'UUID-only reviewers need no member lookup');
158
+ assert.match(text, /Created PR #101: Add login/);
159
+ assert.match(text, /pull-requests\/101/);
160
+ assert.match(text, /feature\/login → develop/);
161
+ assert.match(text, /\(draft\)/);
162
+ assert.match(text, /Reviewers:\*\* Bob/);
163
+ });
164
+ test('C2: omits destination so Bitbucket defaults to the main branch; no lookups without reviewers', async () => {
165
+ const calls = stubRoutes(bitbucketRoutes());
166
+ await handleCreateBitbucketPr({ repository: 'web-app', title: 'T', sourceBranch: 'feat' });
167
+ assert.equal(calls.length, 1);
168
+ assert.deepEqual(postOf(calls)?.body, { title: 'T', source: { branch: { name: 'feat' } } });
169
+ });
170
+ test('C3: names resolve across member pages, dedupe, and drop the PR author', async () => {
171
+ const calls = stubRoutes(bitbucketRoutes());
172
+ const text = (await handleCreateBitbucketPr({
173
+ repository: 'acme/web-app',
174
+ title: 'T',
175
+ sourceBranch: 'f',
176
+ reviewers: ['Dien', 'dientv', 'Le Dung', 'Hanh'],
177
+ })).content[0].text;
178
+ assert.deepEqual(postOf(calls)?.body, {
179
+ title: 'T',
180
+ source: { branch: { name: 'f' } },
181
+ reviewers: [{ uuid: member(0).uuid }, { uuid: member(2).uuid }],
182
+ });
183
+ assert.match(text, /PR author\) were named as a reviewer — skipped/);
184
+ });
185
+ test('C4: ambiguous or unknown names abort before any PR is created', async () => {
186
+ const calls = stubRoutes(bitbucketRoutes());
187
+ const text = (await handleCreateBitbucketPr({
188
+ repository: 'acme/web-app',
189
+ title: 'T',
190
+ sourceBranch: 'f',
191
+ reviewers: ['Dung', 'Zed'],
192
+ })).content[0].text;
193
+ assert.equal(postOf(calls), undefined);
194
+ assert.match(text, /PR not created/);
195
+ assert.match(text, /"Dung" matches 2 members/);
196
+ assert.match(text, /Nguyễn Tiến Dũng \(@dungnt\)/);
197
+ assert.match(text, /"Zed" matches no workspace member/);
198
+ });
199
+ test('C5: a 403 on the member list names the missing scope', async () => {
200
+ stubRoutes(bitbucketRoutes((url) => (url.includes('/members') ? { status: 403, body: {} } : undefined)));
201
+ const text = (await handleCreateBitbucketPr({ repository: 'acme/web-app', title: 'T', sourceBranch: 'f', reviewers: ['Dien'] })).content[0].text;
202
+ assert.match(text, /read:workspace:bitbucket/);
203
+ });
204
+ test('C5b: the member-list scope hint is not repeated by the generic scope hint', async () => {
205
+ stubRoutes(bitbucketRoutes((url) => (url.includes('/members') ? { status: 403, body: {} } : undefined)));
206
+ const text = (await handleCreateBitbucketPr({ repository: 'acme/web-app', title: 'T', sourceBranch: 'f', reviewers: ['Dien'] })).content[0].text;
207
+ assert.doesNotMatch(text, /Check the API token has these scopes/);
208
+ });
209
+ test('C6: without read:user scope the author is not filtered, but creation proceeds', async () => {
210
+ const calls = stubRoutes(bitbucketRoutes((url) => (url.endsWith('/user') ? { status: 403, body: {} } : undefined)));
211
+ await handleCreateBitbucketPr({ repository: 'acme/web-app', title: 'T', sourceBranch: 'f', reviewers: ['Dien'] });
212
+ assert.deepEqual((postOf(calls)?.body).reviewers, [{ uuid: member(0).uuid }]);
213
+ });
214
+ test('C7: 400 surfaces the Bitbucket error message', async () => {
215
+ stubRoutes(() => ({
216
+ status: 400,
217
+ body: { type: 'error', error: { message: 'There are no changes to be pulled' } },
218
+ }));
219
+ const text = (await handleCreateBitbucketPr({ repository: 'acme/web-app', title: 'T', sourceBranch: 'f' }))
220
+ .content[0].text;
221
+ assert.match(text, /API error 400: There are no changes to be pulled/);
222
+ });
223
+ });
224
+ describe('handleFindBitbucketReviewers', () => {
225
+ test('F1: reports matched, skipped-self, ambiguous and unknown names', async () => {
226
+ stubRoutes(bitbucketRoutes());
227
+ const text = (await handleFindBitbucketReviewers({ names: ['Dien', 'Hanh', 'Dung', 'Zed'] })).content[0].text;
228
+ assert.match(text, /## Reviewer lookup \(acme\)/);
229
+ assert.match(text, /✅ Trần Văn Điền \(@dientv\) \{aaaaaaaa-0000-0000-0000-000000000001\}/);
230
+ assert.match(text, /PR author\) were named — skipped/);
231
+ assert.match(text, /"Dung" matches 2 members/);
232
+ assert.match(text, /"Zed" matches no workspace member/);
233
+ });
234
+ test('F2: an invalid workspace slug is rejected before any request', async () => {
235
+ const calls = stubRoutes(bitbucketRoutes());
236
+ const text = (await handleFindBitbucketReviewers({ names: ['Dien'], workspace: '../x' })).content[0].text;
237
+ assert.equal(calls.length, 0);
238
+ assert.match(text, /Invalid workspace slug/);
239
+ });
240
+ });
241
+ function comment(id, extra = {}) {
242
+ return {
243
+ id,
244
+ created_on: '2026-10-01T10:00:00Z',
245
+ content: { raw: `body ${id}` },
246
+ user: { display_name: `user${id}` },
247
+ deleted: false,
248
+ ...extra,
249
+ };
250
+ }
251
+ describe('handleGetBitbucketPrComments', () => {
252
+ test('M1: follows next links and threads general + inline comments', async () => {
253
+ const calls = stubFetch([
254
+ {
255
+ status: 200,
256
+ body: {
257
+ values: [comment(1), comment(2, { inline: { path: 'src/a.ts', to: 12 } })],
258
+ next: `${API}/repositories/acme/web-app/pullrequests/42/comments?pagelen=100&page=2`,
259
+ },
260
+ },
261
+ {
262
+ status: 200,
263
+ body: {
264
+ values: [
265
+ comment(3, { parent: { id: 2 }, inline: { path: 'src/a.ts', to: 12 } }),
266
+ comment(4, {
267
+ inline: { path: 'src/b.ts', from: 5 },
268
+ resolution: { type: 'comment_resolution', user: { display_name: 'Ann' } },
269
+ }),
270
+ ],
271
+ },
272
+ },
273
+ ]);
274
+ const text = (await handleGetBitbucketPrComments({ input: PR_URL })).content[0].text;
275
+ assert.equal(calls.length, 2);
276
+ assert.equal(calls[0].url, `${API}/repositories/acme/web-app/pullrequests/42/comments?pagelen=100`);
277
+ assert.match(text, /4 comments in 3 threads · 2 open · 1 resolved/);
278
+ assert.match(text, /### General\n- \*\*#1 · user1 · 2026-10-01 · 🟡 open\*\*\n > body 1/);
279
+ assert.match(text, /#### `src\/a.ts`\n- \*\*#2 · user2 · 2026-10-01 · line 12 · 🟡 open\*\*/);
280
+ assert.match(text, / ↳ \*\*#3 · user3 · 2026-10-01\*\*\n > body 3/);
281
+ assert.match(text, /#4 · user4 · 2026-10-01 · old line 5 · ✅ resolved by Ann/);
282
+ });
283
+ test('M2: includeResolved=false hides resolved threads', async () => {
284
+ stubFetch([
285
+ {
286
+ status: 200,
287
+ body: { values: [comment(1), comment(2, { resolution: { type: 'comment_resolution' } })] },
288
+ },
289
+ ]);
290
+ const text = (await handleGetBitbucketPrComments({ input: PR_URL, includeResolved: false })).content[0].text;
291
+ assert.match(text, /#1 /);
292
+ assert.doesNotMatch(text, /#2 /);
293
+ });
294
+ test('M3: deleted comments are hidden unless they carry replies', async () => {
295
+ stubFetch([
296
+ {
297
+ status: 200,
298
+ body: {
299
+ values: [comment(1, { deleted: true }), comment(2, { deleted: true }), comment(3, { parent: { id: 2 } })],
300
+ },
301
+ },
302
+ ]);
303
+ const text = (await handleGetBitbucketPrComments({ input: PR_URL })).content[0].text;
304
+ assert.doesNotMatch(text, /#1 /);
305
+ assert.match(text, /#2 [^\n]*\n > _\(deleted\)_/);
306
+ assert.match(text, /↳ \*\*#3 /);
307
+ });
308
+ test('M4: a next link to another host is never followed', async () => {
309
+ const calls = stubFetch([{ status: 200, body: { values: [comment(1)], next: 'https://evil.example/steal' } }]);
310
+ await handleGetBitbucketPrComments({ input: PR_URL });
311
+ assert.equal(calls.length, 1);
312
+ });
313
+ test('M5: stops at the page cap and says so', async () => {
314
+ const calls = stubFetch([
315
+ {
316
+ status: 200,
317
+ body: { values: [comment(1)], next: `${API}/repositories/acme/web-app/pullrequests/42/comments?page=n` },
318
+ },
319
+ ]);
320
+ const text = (await handleGetBitbucketPrComments({ input: PR_URL })).content[0].text;
321
+ assert.equal(calls.length, MAX_PAGES);
322
+ assert.match(text, /Stopped after 20 pages/);
323
+ });
324
+ test('M7: a 403 appends the scopes the tool needs', async () => {
325
+ stubFetch([{ status: 403, body: {} }]);
326
+ const text = (await handleGetBitbucketPrComments({ input: PR_URL })).content[0].text;
327
+ assert.match(text, /Access denied \(403\)/);
328
+ assert.match(text, /Check the API token has these scopes: read:pullrequest:bitbucket\./);
329
+ });
330
+ test('M6: 404 maps to a not-found message', async () => {
331
+ stubFetch([{ status: 404, body: { type: 'error', error: { message: 'nope' } } }]);
332
+ const text = (await handleGetBitbucketPrComments({ input: PR_URL })).content[0].text;
333
+ assert.match(text, /Not found/);
334
+ });
335
+ });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Integration Tools - Bitbucket, Jira, Confluence
3
- * Tools: kit_get_bitbucket_pr, kit_jira_get_ticket, kit_confluence_get_page
3
+ * Tools: kit_get_bitbucket_pr, kit_create_bitbucket_pr, kit_find_bitbucket_reviewers, kit_get_bitbucket_pr_comments, kit_jira_get_ticket, kit_confluence_get_page
4
4
  */
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  /**
@@ -22,6 +22,18 @@ export interface AtlassianContext {
22
22
  auth: string;
23
23
  cloudId: string;
24
24
  }
25
+ /**
26
+ * API token scopes each tool needs, from Atlassian's OpenAPI specs (`x-atlassian-oauth2-scopes`).
27
+ * Used for 401/403 hints; mcp/README.md lists the same scopes for setup.
28
+ */
29
+ export declare const REQUIRED_SCOPES: {
30
+ readonly kit_get_bitbucket_pr: readonly ["read:pullrequest:bitbucket", "read:repository:bitbucket"];
31
+ readonly kit_get_bitbucket_pr_comments: readonly ["read:pullrequest:bitbucket"];
32
+ readonly kit_create_bitbucket_pr: readonly ["read:pullrequest:bitbucket", "write:pullrequest:bitbucket", "read:workspace:bitbucket", "read:user:bitbucket"];
33
+ readonly kit_find_bitbucket_reviewers: readonly ["read:workspace:bitbucket", "read:user:bitbucket"];
34
+ readonly kit_jira_get_ticket: readonly ["read:jira-work"];
35
+ readonly kit_confluence_get_page: readonly ["read:page:confluence"];
36
+ };
25
37
  /** Resolve the shared cloud id plus the product's basic-auth header (shared email, own token). */
26
38
  export declare function buildAtlassianContext(product: 'jira' | 'confluence'): AtlassianContext;
27
39
  /**
@@ -45,4 +57,102 @@ export declare function handleConfluenceGetPage(args: {
45
57
  text: string;
46
58
  }];
47
59
  }>;
60
+ /** Safety cap on followed pagination pages (100 items each). */
61
+ export declare const MAX_PAGES = 20;
62
+ export type BitbucketRef<T> = ({
63
+ ok: true;
64
+ } & T) | {
65
+ ok: false;
66
+ error: string;
67
+ };
68
+ /** Resolve a PR URL, or a numeric PR id plus workspace/repoSlug, to its coordinates. */
69
+ export declare function resolveBitbucketPr(input: string, workspace?: string, repoSlug?: string): BitbucketRef<{
70
+ ws: string;
71
+ repo: string;
72
+ prId: number;
73
+ }>;
74
+ /** Resolve a repo URL, `workspace/repo`, or a bare repo slug (plus workspace or default) to its coordinates. */
75
+ export declare function resolveBitbucketRepo(repository: string, workspace?: string): BitbucketRef<{
76
+ ws: string;
77
+ repo: string;
78
+ }>;
79
+ export interface BitbucketMember {
80
+ uuid: string;
81
+ displayName: string;
82
+ nickname?: string;
83
+ }
84
+ export interface ReviewerResolution {
85
+ resolved: Array<{
86
+ query: string;
87
+ member: BitbucketMember;
88
+ }>;
89
+ ambiguous: Array<{
90
+ query: string;
91
+ candidates: BitbucketMember[];
92
+ }>;
93
+ unmatched: string[];
94
+ }
95
+ /** Lowercase and strip diacritics so "Dũng" matches "Dung" and "Đức" matches "Duc". */
96
+ export declare function normalizeName(value: string): string;
97
+ /**
98
+ * Match reviewer queries against workspace members. A query matches a member by exact
99
+ * display name / nickname first; failing that, when every word of the query is a whole
100
+ * word of the member's name ("Dung" → "Nguyen Tien Dung"). More than one hit is ambiguous —
101
+ * never guessed. UUID queries must be resolved by the caller before this runs.
102
+ */
103
+ export declare function matchReviewers(queries: string[], members: BitbucketMember[]): ReviewerResolution;
104
+ export interface FindBitbucketReviewersArgs {
105
+ names: string[];
106
+ workspace?: string;
107
+ }
108
+ /**
109
+ * Handler for kit_find_bitbucket_reviewers: dry-run of the reviewer resolution that
110
+ * kit_create_bitbucket_pr performs, so callers can verify names before pushing anything.
111
+ * Never rejects — every failure path resolves to an actionable mcpText message.
112
+ */
113
+ export declare function handleFindBitbucketReviewers(args: FindBitbucketReviewersArgs): Promise<{
114
+ content: [{
115
+ type: 'text';
116
+ text: string;
117
+ }];
118
+ }>;
119
+ export interface CreateBitbucketPrArgs {
120
+ repository: string;
121
+ workspace?: string;
122
+ title: string;
123
+ sourceBranch: string;
124
+ destinationBranch?: string;
125
+ description?: string;
126
+ closeSourceBranch?: boolean;
127
+ draft?: boolean;
128
+ reviewers?: string[];
129
+ }
130
+ /**
131
+ * Handler for kit_create_bitbucket_pr. Exported so it is unit testable without a transport.
132
+ * Never rejects — every failure path resolves to an actionable mcpText message.
133
+ */
134
+ export declare function handleCreateBitbucketPr(args: CreateBitbucketPrArgs): Promise<{
135
+ content: [{
136
+ type: 'text';
137
+ text: string;
138
+ }];
139
+ }>;
140
+ export interface GetBitbucketPrCommentsArgs {
141
+ input: string;
142
+ workspace?: string;
143
+ repoSlug?: string;
144
+ includeResolved?: boolean;
145
+ includeDeleted?: boolean;
146
+ }
147
+ /**
148
+ * Handler for kit_get_bitbucket_pr_comments. Follows `next` links (pagelen=100) up to
149
+ * MAX_PAGES, then renders threads: general comments first, inline ones grouped by file.
150
+ * Never rejects — every failure path resolves to an actionable mcpText message.
151
+ */
152
+ export declare function handleGetBitbucketPrComments(args: GetBitbucketPrCommentsArgs): Promise<{
153
+ content: [{
154
+ type: 'text';
155
+ text: string;
156
+ }];
157
+ }>;
48
158
  export declare function registerIntegrationTools(server: McpServer): void;