mcp-server-bitbucket 0.11.0 → 0.19.0
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/Dockerfile +9 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +906 -139
- package/package.json +14 -12
- package/scripts/check-ca-bundles.cjs +27 -0
- package/smithery.yaml +10 -4
- package/src/auth.ts +330 -0
- package/src/client.ts +253 -123
- package/src/http-input.ts +36 -0
- package/src/index.ts +274 -13
- package/src/oauth-proxy.ts +298 -0
- package/src/prompts.ts +1 -1
- package/src/settings.ts +51 -10
- package/src/tools/commits.ts +65 -33
- package/src/tools/index.ts +0 -3
- package/src/tools/pipelines.ts +41 -0
- package/src/tools/source.ts +23 -0
- package/src/tools/tags.ts +21 -6
- package/src/types.ts +9 -0
- package/tests/auth.test.ts +277 -0
- package/tests/history.test.ts +284 -0
- package/tests/http-handler.test.ts +67 -0
- package/tests/http-input.test.ts +40 -0
- package/tests/network-resilience.test.ts +113 -0
- package/tests/oauth-proxy.test.ts +286 -0
- package/tests/settings.test.ts +110 -0
- package/tests/stdio.test.ts +20 -0
- package/tests/test-remote.mjs +22 -0
package/src/settings.ts
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings management for Bitbucket MCP Server
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Configuration via environment variables:
|
|
5
5
|
* - BITBUCKET_WORKSPACE: Bitbucket workspace slug (required)
|
|
6
|
-
* - BITBUCKET_EMAIL: Account email for Basic Auth (required)
|
|
7
|
-
* - BITBUCKET_API_TOKEN:
|
|
6
|
+
* - BITBUCKET_EMAIL: Account email for Basic Auth (required for basic auth)
|
|
7
|
+
* - BITBUCKET_API_TOKEN: Atlassian API token / access token (required)
|
|
8
|
+
* - BITBUCKET_OAUTH_TOKEN: Access token used with `Authorization: Bearer` (optional)
|
|
9
|
+
* - BITBUCKET_AUTH_TYPE: Force auth mode - 'basic' or 'bearer' (optional, auto-detected)
|
|
8
10
|
* - API_TIMEOUT: Request timeout in seconds (default: 30, max: 300)
|
|
9
11
|
* - MAX_RETRIES: Max retry attempts for rate limiting (default: 3, max: 10)
|
|
10
12
|
* - OUTPUT_FORMAT: Output format - 'json' or 'toon' (default: json)
|
|
13
|
+
*
|
|
14
|
+
* Auth modes:
|
|
15
|
+
* - basic: Atlassian API tokens (ATATT...) authenticate as `email:token`.
|
|
16
|
+
* - bearer: Workspace/project/repository access tokens (ATCTT...) only work as
|
|
17
|
+
* `Authorization: Bearer <token>`; Basic auth returns 401 for them.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
import { z } from 'zod';
|
|
14
21
|
|
|
22
|
+
export type AuthType = 'basic' | 'bearer';
|
|
23
|
+
|
|
15
24
|
const settingsSchema = z.object({
|
|
16
25
|
bitbucketWorkspace: z.string().min(1, 'BITBUCKET_WORKSPACE is required'),
|
|
17
|
-
bitbucketEmail: z.string().
|
|
18
|
-
bitbucketApiToken: z.string().min(1, 'BITBUCKET_API_TOKEN is required'),
|
|
26
|
+
bitbucketEmail: z.string().default(''),
|
|
27
|
+
bitbucketApiToken: z.string().min(1, 'BITBUCKET_API_TOKEN (or BITBUCKET_OAUTH_TOKEN) is required'),
|
|
28
|
+
bitbucketAuthType: z.enum(['basic', 'bearer']),
|
|
19
29
|
apiTimeout: z.number().min(1).max(300).default(30),
|
|
20
30
|
maxRetries: z.number().min(0).max(10).default(3),
|
|
21
31
|
outputFormat: z.enum(['json', 'toon']).default('json'),
|
|
22
|
-
})
|
|
32
|
+
}).refine(
|
|
33
|
+
(s) => s.bitbucketAuthType !== 'basic' || s.bitbucketEmail.length > 0,
|
|
34
|
+
{ message: 'BITBUCKET_EMAIL is required when using basic auth', path: ['bitbucketEmail'] }
|
|
35
|
+
);
|
|
23
36
|
|
|
24
37
|
export type Settings = z.infer<typeof settingsSchema>;
|
|
25
38
|
|
|
26
39
|
let cachedSettings: Settings | null = null;
|
|
27
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Resolve which auth mode to use.
|
|
43
|
+
*
|
|
44
|
+
* Explicit BITBUCKET_AUTH_TYPE wins. Otherwise bearer is used when an
|
|
45
|
+
* OAuth/access token is configured or when no email is available (an access
|
|
46
|
+
* token has no account to pair with), and basic otherwise.
|
|
47
|
+
*/
|
|
48
|
+
function resolveAuthType(
|
|
49
|
+
explicit: string,
|
|
50
|
+
oauthToken: string,
|
|
51
|
+
email: string
|
|
52
|
+
): AuthType {
|
|
53
|
+
const normalized = explicit.trim().toLowerCase();
|
|
54
|
+
if (normalized === 'bearer' || normalized === 'basic') {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
if (oauthToken.length > 0 || email.length === 0) {
|
|
58
|
+
return 'bearer';
|
|
59
|
+
}
|
|
60
|
+
return 'basic';
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
/**
|
|
29
64
|
* Load and validate settings from environment variables.
|
|
30
65
|
* Results are cached for subsequent calls.
|
|
@@ -34,17 +69,24 @@ export function getSettings(): Settings {
|
|
|
34
69
|
return cachedSettings;
|
|
35
70
|
}
|
|
36
71
|
|
|
72
|
+
const email = process.env.BITBUCKET_EMAIL || '';
|
|
73
|
+
const oauthToken = process.env.BITBUCKET_OAUTH_TOKEN || '';
|
|
74
|
+
const apiToken = process.env.BITBUCKET_API_TOKEN || '';
|
|
75
|
+
const authType = resolveAuthType(process.env.BITBUCKET_AUTH_TYPE || '', oauthToken, email);
|
|
76
|
+
|
|
37
77
|
const rawSettings = {
|
|
38
78
|
bitbucketWorkspace: process.env.BITBUCKET_WORKSPACE || '',
|
|
39
|
-
bitbucketEmail:
|
|
40
|
-
|
|
79
|
+
bitbucketEmail: email,
|
|
80
|
+
// Bearer mode prefers the dedicated OAuth/access token when both are set
|
|
81
|
+
bitbucketApiToken: authType === 'bearer' ? (oauthToken || apiToken) : apiToken,
|
|
82
|
+
bitbucketAuthType: authType,
|
|
41
83
|
apiTimeout: parseInt(process.env.API_TIMEOUT || '30', 10),
|
|
42
84
|
maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
|
|
43
85
|
outputFormat: (process.env.OUTPUT_FORMAT || 'json') as 'json' | 'toon',
|
|
44
86
|
};
|
|
45
87
|
|
|
46
88
|
const result = settingsSchema.safeParse(rawSettings);
|
|
47
|
-
|
|
89
|
+
|
|
48
90
|
if (!result.success) {
|
|
49
91
|
const errors = result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
50
92
|
throw new Error(`Configuration error: ${errors}`);
|
|
@@ -60,4 +102,3 @@ export function getSettings(): Settings {
|
|
|
60
102
|
export function resetSettings(): void {
|
|
61
103
|
cachedSettings = null;
|
|
62
104
|
}
|
|
63
|
-
|
package/src/tools/commits.ts
CHANGED
|
@@ -10,12 +10,15 @@ import { CommitStatusState } from '../types.js';
|
|
|
10
10
|
export const definitions: Tool[] = [
|
|
11
11
|
{
|
|
12
12
|
name: 'list_commits',
|
|
13
|
-
description: 'List commits
|
|
13
|
+
description: 'List a page of commits. Use include/exclude for commits between revisions (include=head, exclude=base); continue with next_cursor until has_more is false.',
|
|
14
14
|
inputSchema: {
|
|
15
15
|
type: 'object',
|
|
16
16
|
properties: {
|
|
17
17
|
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
18
|
-
branch: { type: 'string', description: '
|
|
18
|
+
branch: { type: 'string', description: 'Branch, tag or SHA to include (alias for include)' },
|
|
19
|
+
include: { type: 'array', items: { type: 'string' }, description: 'Include commits reachable from these branches, tags or SHAs' },
|
|
20
|
+
exclude: { type: 'array', items: { type: 'string' }, description: 'Exclude commits reachable from these revisions' },
|
|
21
|
+
cursor: { type: 'string', description: 'Opaque next_cursor from this tool; retains its page size and filters' },
|
|
19
22
|
path: { type: 'string', description: 'Filter by file path - only commits that modified this path (optional)' },
|
|
20
23
|
limit: { type: 'number', description: 'Maximum results (default: 20)', default: 20 },
|
|
21
24
|
},
|
|
@@ -36,17 +39,43 @@ export const definitions: Tool[] = [
|
|
|
36
39
|
},
|
|
37
40
|
{
|
|
38
41
|
name: 'compare_commits',
|
|
39
|
-
description: 'Compare
|
|
42
|
+
description: 'Compare base to head (branches, tags or SHAs). Returns a page of changed files and line counts; follow next_cursor for all files.',
|
|
40
43
|
inputSchema: {
|
|
41
44
|
type: 'object',
|
|
42
45
|
properties: {
|
|
43
46
|
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
44
47
|
base: { type: 'string', description: 'Base commit hash or branch name' },
|
|
45
|
-
head: { type: 'string', description: 'Head commit hash or branch name' },
|
|
48
|
+
head: { type: 'string', description: 'Head commit hash, tag or branch name' },
|
|
49
|
+
path: { type: 'string', description: 'Optional file path filter' },
|
|
50
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
|
51
|
+
cursor: { type: 'string', description: 'Opaque next_cursor; retains page size and filters' },
|
|
46
52
|
},
|
|
47
53
|
required: ['repo_slug', 'base', 'head'],
|
|
48
54
|
},
|
|
49
55
|
},
|
|
56
|
+
{
|
|
57
|
+
name: 'get_commit_diff',
|
|
58
|
+
description: 'Get the raw Git-style diff from base to head. Continue large diffs with next_offset; pin revisions to commit hashes while paging. API-side diff limits still apply.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
repo_slug: { type: 'string' }, base: { type: 'string' }, head: { type: 'string' },
|
|
63
|
+
path: { type: 'string', description: 'Optional file path filter' },
|
|
64
|
+
offset: { type: 'integer', minimum: 0, default: 0 },
|
|
65
|
+
max_chars: { type: 'integer', minimum: 1, maximum: 100000, default: 20000 },
|
|
66
|
+
},
|
|
67
|
+
required: ['repo_slug', 'base', 'head'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'get_merge_base',
|
|
72
|
+
description: 'Get a best common ancestor of two branches, tags or commit hashes. Bitbucket returns one ancestor when multiple best ancestors exist.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: { repo_slug: { type: 'string' }, base: { type: 'string' }, head: { type: 'string' } },
|
|
76
|
+
required: ['repo_slug', 'base', 'head'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
50
79
|
{
|
|
51
80
|
name: 'get_commit_statuses',
|
|
52
81
|
description: 'Get build/CI statuses for a commit.',
|
|
@@ -82,18 +111,18 @@ export const definitions: Tool[] = [
|
|
|
82
111
|
export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
|
|
83
112
|
list_commits: async (args) => {
|
|
84
113
|
const client = getClient();
|
|
85
|
-
const
|
|
114
|
+
const page = await client.listCommitsPage(args.repo_slug as string, {
|
|
86
115
|
branch: args.branch as string,
|
|
116
|
+
include: args.include as string[] | undefined,
|
|
117
|
+
exclude: args.exclude as string[] | undefined,
|
|
87
118
|
path: args.path as string,
|
|
88
|
-
|
|
119
|
+
cursor: args.cursor as string,
|
|
120
|
+
limit: validateLimit((args.limit as number) ?? 20),
|
|
89
121
|
});
|
|
90
122
|
return {
|
|
91
|
-
commits:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
author: c.author?.raw,
|
|
95
|
-
date: c.date,
|
|
96
|
-
})),
|
|
123
|
+
commits: page.values.map(c => ({ hash: truncateHash(c.hash), message: c.message, author: c.author?.raw, date: c.date })),
|
|
124
|
+
has_more: page.has_more,
|
|
125
|
+
next_cursor: page.next_cursor,
|
|
97
126
|
};
|
|
98
127
|
},
|
|
99
128
|
|
|
@@ -114,31 +143,34 @@ export const handlers: Record<string, (args: Record<string, unknown>) => Promise
|
|
|
114
143
|
|
|
115
144
|
compare_commits: async (args) => {
|
|
116
145
|
const client = getClient();
|
|
117
|
-
const
|
|
118
|
-
args.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
122
|
-
if (!result) {
|
|
123
|
-
return { error: `Could not compare ${args.base}..${args.head}` };
|
|
124
|
-
}
|
|
125
|
-
const files = (result.values as Array<{
|
|
126
|
-
new?: { path?: string };
|
|
127
|
-
old?: { path?: string };
|
|
128
|
-
status?: string;
|
|
129
|
-
lines_added?: number;
|
|
130
|
-
lines_removed?: number;
|
|
131
|
-
}>) || [];
|
|
146
|
+
const page = await client.compareCommitsPage(args.repo_slug as string, args.base as string, args.head as string, {
|
|
147
|
+
path: args.path as string, limit: validateLimit((args.limit as number) ?? 50), cursor: args.cursor as string,
|
|
148
|
+
});
|
|
149
|
+
const files = page.values as Array<{ new?: { path?: string }; old?: { path?: string }; status?: string; lines_added?: number; lines_removed?: number }>;
|
|
132
150
|
return {
|
|
133
|
-
files: files.
|
|
134
|
-
|
|
135
|
-
status: f.status,
|
|
136
|
-
'+': f.lines_added || 0,
|
|
137
|
-
'-': f.lines_removed || 0,
|
|
138
|
-
})),
|
|
151
|
+
files: files.map(f => ({ path: f.new?.path || f.old?.path, status: f.status, '+': f.lines_added || 0, '-': f.lines_removed || 0 })),
|
|
152
|
+
has_more: page.has_more, next_cursor: page.next_cursor,
|
|
139
153
|
};
|
|
140
154
|
},
|
|
141
155
|
|
|
156
|
+
get_commit_diff: async (args) => {
|
|
157
|
+
const offset = args.offset === undefined ? 0 : Number(args.offset);
|
|
158
|
+
const maxChars = args.max_chars === undefined ? 20000 : Number(args.max_chars);
|
|
159
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(maxChars) || maxChars < 1 || maxChars > 100000) {
|
|
160
|
+
return { success: false, error: 'offset must be a nonnegative integer; max_chars must be an integer between 1 and 100000' };
|
|
161
|
+
}
|
|
162
|
+
const diff = await getClient().getCommitDiff(args.repo_slug as string, args.base as string, args.head as string, { path: args.path as string });
|
|
163
|
+
if (diff === null) return notFoundResponse('Diff', `${args.base}..${args.head}`);
|
|
164
|
+
const end = Math.min(offset + maxChars, diff.length);
|
|
165
|
+
return { diff: diff.slice(offset, end), offset, next_offset: end < diff.length ? end : null, truncated: end < diff.length, total_chars: diff.length };
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
get_merge_base: async (args) => {
|
|
169
|
+
const result = await getClient().getMergeBase(args.repo_slug as string, args.base as string, args.head as string);
|
|
170
|
+
return result ? { hash: result.hash, message: result.message, author: result.author?.raw, date: result.date, parents: result.parents?.map(p => truncateHash(p.hash)) }
|
|
171
|
+
: notFoundResponse('Common ancestor', `${args.base}..${args.head}`);
|
|
172
|
+
},
|
|
173
|
+
|
|
142
174
|
get_commit_statuses: async (args) => {
|
|
143
175
|
const client = getClient();
|
|
144
176
|
const statuses = await client.getCommitStatuses(args.repo_slug as string, args.commit as string, {
|
package/src/tools/index.ts
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
-
import { getClient } from '../client.js';
|
|
7
|
-
import { validateLimit, notFoundResponse, truncateHash, sanitizeSearchTerm } from '../utils.js';
|
|
8
|
-
import { PRState, MergeStrategy, CommitStatusState } from '../types.js';
|
|
9
6
|
|
|
10
7
|
// Import tool handlers from separate files
|
|
11
8
|
import * as repositoryTools from './repositories.js';
|
package/src/tools/pipelines.ts
CHANGED
|
@@ -146,6 +146,29 @@ export const definitions: Tool[] = [
|
|
|
146
146
|
required: ['repo_slug', 'variable_uuid'],
|
|
147
147
|
},
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: 'get_pipeline_config',
|
|
151
|
+
description: 'Get pipeline configuration for a repository (check if pipelines are enabled).',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
156
|
+
},
|
|
157
|
+
required: ['repo_slug'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'update_pipeline_config',
|
|
162
|
+
description: 'Update pipeline configuration for a repository (enable or disable pipelines).',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
167
|
+
enabled: { type: 'boolean', description: 'Enable or disable pipelines' },
|
|
168
|
+
},
|
|
169
|
+
required: ['repo_slug', 'enabled'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
149
172
|
];
|
|
150
173
|
|
|
151
174
|
export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
|
|
@@ -297,5 +320,23 @@ export const handlers: Record<string, (args: Record<string, unknown>) => Promise
|
|
|
297
320
|
await client.deletePipelineVariable(args.repo_slug as string, args.variable_uuid as string);
|
|
298
321
|
return {};
|
|
299
322
|
},
|
|
323
|
+
|
|
324
|
+
get_pipeline_config: async (args) => {
|
|
325
|
+
const client = getClient();
|
|
326
|
+
const result = await client.getPipelineConfig(args.repo_slug as string);
|
|
327
|
+
return {
|
|
328
|
+
enabled: result.enabled,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
update_pipeline_config: async (args) => {
|
|
333
|
+
const client = getClient();
|
|
334
|
+
const result = await client.updatePipelineConfig(args.repo_slug as string, {
|
|
335
|
+
enabled: args.enabled as boolean,
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
enabled: result.enabled,
|
|
339
|
+
};
|
|
340
|
+
},
|
|
300
341
|
};
|
|
301
342
|
|
package/src/tools/source.ts
CHANGED
|
@@ -20,6 +20,19 @@ export const definitions: Tool[] = [
|
|
|
20
20
|
required: ['repo_slug', 'path'],
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
name: 'get_file_history',
|
|
25
|
+
description: 'List file-history paths and full commit hashes at a branch, tag or SHA. Use get_commit for author, message and date. Follow next_cursor for older changes.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
repo_slug: { type: 'string' }, path: { type: 'string' }, ref: { type: 'string', default: 'main' },
|
|
30
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
31
|
+
cursor: { type: 'string', description: 'Opaque next_cursor; retains page size and filters' },
|
|
32
|
+
},
|
|
33
|
+
required: ['repo_slug', 'path'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
23
36
|
{
|
|
24
37
|
name: 'list_directory',
|
|
25
38
|
description: 'List contents of a directory in a repository. Browse repository structure without cloning.',
|
|
@@ -54,6 +67,16 @@ export const handlers: Record<string, (args: Record<string, unknown>) => Promise
|
|
|
54
67
|
};
|
|
55
68
|
},
|
|
56
69
|
|
|
70
|
+
get_file_history: async (args) => {
|
|
71
|
+
const page = await getClient().getFileHistory(args.repo_slug as string, args.path as string, (args.ref as string) || 'main', {
|
|
72
|
+
limit: validateLimit((args.limit as number) ?? 20), cursor: args.cursor as string,
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
history: page.values.map(entry => ({ path: entry.path, commit: { hash: entry.commit.hash } })),
|
|
76
|
+
has_more: page.has_more, next_cursor: page.next_cursor,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
|
|
57
80
|
list_directory: async (args) => {
|
|
58
81
|
const client = getClient();
|
|
59
82
|
const ref = (args.ref as string) || 'main';
|
package/src/tools/tags.ts
CHANGED
|
@@ -4,21 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { getClient } from '../client.js';
|
|
7
|
-
import { validateLimit, truncateHash } from '../utils.js';
|
|
7
|
+
import { validateLimit, truncateHash, notFoundResponse } from '../utils.js';
|
|
8
8
|
|
|
9
9
|
export const definitions: Tool[] = [
|
|
10
10
|
{
|
|
11
11
|
name: 'list_tags',
|
|
12
|
-
description: 'List tags
|
|
12
|
+
description: 'List a page of tags, optionally filtered with Bitbucket query syntax and sorted (name uses natural version order). Follow next_cursor for all tags.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: 'object',
|
|
15
15
|
properties: {
|
|
16
16
|
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
17
|
-
limit: { type: '
|
|
17
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
|
18
|
+
query: { type: 'string', description: 'Bitbucket filter, e.g. name ~ \"v2\" or target.hash = \"full-sha\"' },
|
|
19
|
+
sort: { type: 'string', description: 'Sort field, e.g. name, -name, -target.date', default: '-target.date' },
|
|
20
|
+
cursor: { type: 'string', description: 'Opaque next_cursor; retains page size and filters' },
|
|
18
21
|
},
|
|
19
22
|
required: ['repo_slug'],
|
|
20
23
|
},
|
|
21
24
|
},
|
|
25
|
+
{
|
|
26
|
+
name: 'get_tag',
|
|
27
|
+
description: 'Get a specific tag and its target commit without scanning tag pages.',
|
|
28
|
+
inputSchema: { type: 'object', properties: { repo_slug: { type: 'string' }, tag_name: { type: 'string' } }, required: ['repo_slug', 'tag_name'] },
|
|
29
|
+
},
|
|
22
30
|
{
|
|
23
31
|
name: 'create_tag',
|
|
24
32
|
description: 'Create a new tag in a repository.',
|
|
@@ -50,20 +58,27 @@ export const definitions: Tool[] = [
|
|
|
50
58
|
export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
|
|
51
59
|
list_tags: async (args) => {
|
|
52
60
|
const client = getClient();
|
|
53
|
-
const
|
|
54
|
-
limit: validateLimit((args.limit as number)
|
|
61
|
+
const page = await client.listTagsPage(args.repo_slug as string, {
|
|
62
|
+
limit: validateLimit((args.limit as number) ?? 50),
|
|
63
|
+
query: args.query as string, sort: args.sort as string, cursor: args.cursor as string,
|
|
55
64
|
});
|
|
56
65
|
return {
|
|
57
|
-
tags:
|
|
66
|
+
tags: page.values.map(t => ({
|
|
58
67
|
name: t.name,
|
|
59
68
|
target: truncateHash(t.target?.hash),
|
|
60
69
|
message: t.message,
|
|
61
70
|
date: t.target?.date,
|
|
62
71
|
tagger: t.tagger?.raw,
|
|
63
72
|
})),
|
|
73
|
+
has_more: page.has_more, next_cursor: page.next_cursor,
|
|
64
74
|
};
|
|
65
75
|
},
|
|
66
76
|
|
|
77
|
+
get_tag: async (args) => {
|
|
78
|
+
const tag = await getClient().getTag(args.repo_slug as string, args.tag_name as string);
|
|
79
|
+
return tag ? { name: tag.name, target: tag.target?.hash, message: tag.message, date: tag.date, tagger: tag.tagger?.raw } : notFoundResponse('Tag', args.tag_name as string);
|
|
80
|
+
},
|
|
81
|
+
|
|
67
82
|
create_tag: async (args) => {
|
|
68
83
|
const client = getClient();
|
|
69
84
|
const result = await client.createTag(
|
package/src/types.ts
CHANGED
|
@@ -144,6 +144,15 @@ export interface BitbucketPipelineVariable {
|
|
|
144
144
|
secured: boolean;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export interface BitbucketPipelineConfig {
|
|
148
|
+
enabled: boolean;
|
|
149
|
+
type?: string;
|
|
150
|
+
repository?: {
|
|
151
|
+
uuid?: string;
|
|
152
|
+
full_name?: string;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
// ==================== PIPELINE TRIGGER OPTIONS ====================
|
|
148
157
|
|
|
149
158
|
export interface PipelineTriggerVariable {
|