mcp-server-bitbucket 0.11.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 +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2976 -0
- package/docker-entrypoint.sh +6 -0
- package/package.json +57 -0
- package/smithery.yaml +47 -0
- package/src/client.ts +1102 -0
- package/src/index.ts +149 -0
- package/src/prompts.ts +208 -0
- package/src/resources.ts +160 -0
- package/src/settings.ts +63 -0
- package/src/tools/branches.ts +69 -0
- package/src/tools/commits.ts +186 -0
- package/src/tools/deployments.ts +100 -0
- package/src/tools/index.ts +112 -0
- package/src/tools/permissions.ts +205 -0
- package/src/tools/pipelines.ts +301 -0
- package/src/tools/projects.ts +63 -0
- package/src/tools/pull-requests.ts +321 -0
- package/src/tools/repositories.ts +208 -0
- package/src/tools/restrictions.ts +94 -0
- package/src/tools/source.ts +78 -0
- package/src/tools/tags.ts +88 -0
- package/src/tools/webhooks.ts +121 -0
- package/src/types.ts +369 -0
- package/src/utils.ts +83 -0
- package/tsconfig.json +25 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bitbucket MCP Server - TypeScript Implementation
|
|
4
|
+
*
|
|
5
|
+
* Provides tools for interacting with Bitbucket repositories,
|
|
6
|
+
* pull requests, pipelines, branches, commits, deployments, and webhooks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import {
|
|
12
|
+
CallToolRequestSchema,
|
|
13
|
+
ListToolsRequestSchema,
|
|
14
|
+
ListResourcesRequestSchema,
|
|
15
|
+
ReadResourceRequestSchema,
|
|
16
|
+
ListPromptsRequestSchema,
|
|
17
|
+
GetPromptRequestSchema,
|
|
18
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
+
|
|
20
|
+
import { getSettings } from './settings.js';
|
|
21
|
+
import { toolDefinitions, handleToolCall } from './tools/index.js';
|
|
22
|
+
import { resourceDefinitions, handleResourceRead } from './resources.js';
|
|
23
|
+
import { promptDefinitions, handlePromptGet } from './prompts.js';
|
|
24
|
+
|
|
25
|
+
const VERSION = '0.10.0';
|
|
26
|
+
|
|
27
|
+
function createServer(): Server {
|
|
28
|
+
const server = new Server(
|
|
29
|
+
{
|
|
30
|
+
name: 'bitbucket',
|
|
31
|
+
version: VERSION,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
capabilities: {
|
|
35
|
+
tools: {},
|
|
36
|
+
resources: {},
|
|
37
|
+
prompts: {},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// List available tools
|
|
43
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
44
|
+
return {
|
|
45
|
+
tools: toolDefinitions,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Handle tool calls
|
|
50
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
51
|
+
const { name, arguments: args } = request.params;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await handleToolCall(name, args || {});
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: JSON.stringify(result, null, 2),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: 'text',
|
|
69
|
+
text: JSON.stringify({ error: message }, null, 2),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// List available resources
|
|
78
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
79
|
+
return {
|
|
80
|
+
resources: resourceDefinitions,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Handle resource reads
|
|
85
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
86
|
+
const { uri } = request.params;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const content = await handleResourceRead(uri);
|
|
90
|
+
return {
|
|
91
|
+
contents: [
|
|
92
|
+
{
|
|
93
|
+
uri,
|
|
94
|
+
mimeType: 'text/markdown',
|
|
95
|
+
text: content,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
101
|
+
throw new Error(`Failed to read resource: ${message}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// List available prompts
|
|
106
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
107
|
+
return {
|
|
108
|
+
prompts: promptDefinitions,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Handle prompt gets
|
|
113
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
114
|
+
const { name, arguments: args } = request.params;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = handlePromptGet(name, args || {});
|
|
118
|
+
return result;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
121
|
+
throw new Error(`Failed to get prompt: ${message}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return server;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function main(): Promise<void> {
|
|
129
|
+
// Validate settings on startup
|
|
130
|
+
try {
|
|
131
|
+
getSettings();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Configuration error:', error instanceof Error ? error.message : error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const server = createServer();
|
|
138
|
+
const transport = new StdioServerTransport();
|
|
139
|
+
|
|
140
|
+
await server.connect(transport);
|
|
141
|
+
|
|
142
|
+
// Log to stderr so it doesn't interfere with MCP communication on stdout
|
|
143
|
+
console.error(`Bitbucket MCP Server v${VERSION} started`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main().catch((error) => {
|
|
147
|
+
console.error('Fatal error:', error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Prompts for Bitbucket Server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Prompt, GetPromptResult, PromptMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prompt definitions for the MCP server
|
|
9
|
+
*/
|
|
10
|
+
export const promptDefinitions: Prompt[] = [
|
|
11
|
+
{
|
|
12
|
+
name: 'code_review',
|
|
13
|
+
description: 'Generate a code review prompt for a pull request',
|
|
14
|
+
arguments: [
|
|
15
|
+
{
|
|
16
|
+
name: 'repo_slug',
|
|
17
|
+
description: 'Repository slug',
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'pr_id',
|
|
22
|
+
description: 'Pull request ID',
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'release_notes',
|
|
29
|
+
description: 'Generate release notes from commits between two refs',
|
|
30
|
+
arguments: [
|
|
31
|
+
{
|
|
32
|
+
name: 'repo_slug',
|
|
33
|
+
description: 'Repository slug',
|
|
34
|
+
required: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'base_tag',
|
|
38
|
+
description: 'Base tag or commit (e.g., "v1.0.0")',
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'head',
|
|
43
|
+
description: 'Head ref (default: "main")',
|
|
44
|
+
required: false,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'pipeline_debug',
|
|
50
|
+
description: 'Debug a failed pipeline',
|
|
51
|
+
arguments: [
|
|
52
|
+
{
|
|
53
|
+
name: 'repo_slug',
|
|
54
|
+
description: 'Repository slug',
|
|
55
|
+
required: true,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'repo_summary',
|
|
61
|
+
description: 'Get a comprehensive summary of a repository',
|
|
62
|
+
arguments: [
|
|
63
|
+
{
|
|
64
|
+
name: 'repo_slug',
|
|
65
|
+
description: 'Repository slug',
|
|
66
|
+
required: true,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle prompt get requests
|
|
74
|
+
*/
|
|
75
|
+
export function handlePromptGet(
|
|
76
|
+
name: string,
|
|
77
|
+
args: Record<string, string>
|
|
78
|
+
): GetPromptResult {
|
|
79
|
+
switch (name) {
|
|
80
|
+
case 'code_review':
|
|
81
|
+
return promptCodeReview(args.repo_slug, args.pr_id);
|
|
82
|
+
case 'release_notes':
|
|
83
|
+
return promptReleaseNotes(args.repo_slug, args.base_tag, args.head || 'main');
|
|
84
|
+
case 'pipeline_debug':
|
|
85
|
+
return promptPipelineDebug(args.repo_slug);
|
|
86
|
+
case 'repo_summary':
|
|
87
|
+
return promptRepoSummary(args.repo_slug);
|
|
88
|
+
default:
|
|
89
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function promptCodeReview(repoSlug: string, prId: string): GetPromptResult {
|
|
94
|
+
const content = `Please review pull request #${prId} in repository '${repoSlug}'.
|
|
95
|
+
|
|
96
|
+
Use the following tools to gather information:
|
|
97
|
+
1. get_pull_request(repo_slug="${repoSlug}", pr_id=${prId}) - Get PR details
|
|
98
|
+
2. get_pr_diff(repo_slug="${repoSlug}", pr_id=${prId}) - Get the code changes
|
|
99
|
+
3. list_pr_comments(repo_slug="${repoSlug}", pr_id=${prId}) - See existing comments
|
|
100
|
+
|
|
101
|
+
Then provide a thorough code review covering:
|
|
102
|
+
- Code quality and readability
|
|
103
|
+
- Potential bugs or edge cases
|
|
104
|
+
- Security concerns
|
|
105
|
+
- Performance considerations
|
|
106
|
+
- Suggestions for improvement
|
|
107
|
+
|
|
108
|
+
If you find issues, use add_pr_comment() to leave feedback on specific lines.`;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: {
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: content,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function promptReleaseNotes(repoSlug: string, baseTag: string, head: string): GetPromptResult {
|
|
124
|
+
const content = `Generate release notes for repository '${repoSlug}' comparing ${baseTag} to ${head}.
|
|
125
|
+
|
|
126
|
+
Use these tools:
|
|
127
|
+
1. compare_commits(repo_slug="${repoSlug}", base="${baseTag}", head="${head}") - See changed files
|
|
128
|
+
2. list_commits(repo_slug="${repoSlug}", branch="${head}", limit=50) - Get recent commits
|
|
129
|
+
|
|
130
|
+
Organize the release notes into sections:
|
|
131
|
+
- **New Features**: New functionality added
|
|
132
|
+
- **Bug Fixes**: Issues that were resolved
|
|
133
|
+
- **Improvements**: Enhancements to existing features
|
|
134
|
+
- **Breaking Changes**: Changes that require user action
|
|
135
|
+
|
|
136
|
+
Format as markdown suitable for a GitHub/Bitbucket release.`;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
messages: [
|
|
140
|
+
{
|
|
141
|
+
role: 'user',
|
|
142
|
+
content: {
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: content,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function promptPipelineDebug(repoSlug: string): GetPromptResult {
|
|
152
|
+
const content = `Help debug pipeline failures in repository '${repoSlug}'.
|
|
153
|
+
|
|
154
|
+
Use these tools:
|
|
155
|
+
1. list_pipelines(repo_slug="${repoSlug}", limit=5) - Get recent pipeline runs
|
|
156
|
+
2. get_pipeline(repo_slug="${repoSlug}", pipeline_uuid="<uuid>") - Get pipeline details
|
|
157
|
+
3. get_pipeline_logs(repo_slug="${repoSlug}", pipeline_uuid="<uuid>") - Get step list
|
|
158
|
+
4. get_pipeline_logs(repo_slug="${repoSlug}", pipeline_uuid="<uuid>", step_uuid="<step>") - Get logs
|
|
159
|
+
|
|
160
|
+
Analyze the failures and provide:
|
|
161
|
+
- Root cause of the failure
|
|
162
|
+
- Specific error messages
|
|
163
|
+
- Recommended fixes
|
|
164
|
+
- Commands to re-run the pipeline if appropriate`;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
messages: [
|
|
168
|
+
{
|
|
169
|
+
role: 'user',
|
|
170
|
+
content: {
|
|
171
|
+
type: 'text',
|
|
172
|
+
text: content,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function promptRepoSummary(repoSlug: string): GetPromptResult {
|
|
180
|
+
const content = `Provide a comprehensive summary of repository '${repoSlug}'.
|
|
181
|
+
|
|
182
|
+
Gather information using:
|
|
183
|
+
1. get_repository(repo_slug="${repoSlug}") - Basic repo info
|
|
184
|
+
2. list_branches(repo_slug="${repoSlug}", limit=10) - Active branches
|
|
185
|
+
3. list_pull_requests(repo_slug="${repoSlug}", state="OPEN") - Open PRs
|
|
186
|
+
4. list_pipelines(repo_slug="${repoSlug}", limit=5) - Recent CI/CD status
|
|
187
|
+
5. list_commits(repo_slug="${repoSlug}", limit=10) - Recent activity
|
|
188
|
+
|
|
189
|
+
Summarize:
|
|
190
|
+
- Repository description and purpose
|
|
191
|
+
- Current development activity
|
|
192
|
+
- Open pull requests needing attention
|
|
193
|
+
- CI/CD health
|
|
194
|
+
- Recent contributors`;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
messages: [
|
|
198
|
+
{
|
|
199
|
+
role: 'user',
|
|
200
|
+
content: {
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: content,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Resources for Bitbucket Server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Resource } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { getClient } from './client.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resource definitions for the MCP server
|
|
10
|
+
*/
|
|
11
|
+
export const resourceDefinitions: Resource[] = [
|
|
12
|
+
{
|
|
13
|
+
uri: 'bitbucket://repositories',
|
|
14
|
+
name: 'Repositories',
|
|
15
|
+
description: 'List all repositories in the workspace',
|
|
16
|
+
mimeType: 'text/markdown',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
uri: 'bitbucket://repositories/{repo_slug}',
|
|
20
|
+
name: 'Repository Details',
|
|
21
|
+
description: 'Get detailed information about a specific repository',
|
|
22
|
+
mimeType: 'text/markdown',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
uri: 'bitbucket://repositories/{repo_slug}/branches',
|
|
26
|
+
name: 'Repository Branches',
|
|
27
|
+
description: 'List branches in a repository',
|
|
28
|
+
mimeType: 'text/markdown',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
uri: 'bitbucket://repositories/{repo_slug}/pull-requests',
|
|
32
|
+
name: 'Pull Requests',
|
|
33
|
+
description: 'List open pull requests in a repository',
|
|
34
|
+
mimeType: 'text/markdown',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
uri: 'bitbucket://projects',
|
|
38
|
+
name: 'Projects',
|
|
39
|
+
description: 'List all projects in the workspace',
|
|
40
|
+
mimeType: 'text/markdown',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handle resource read requests
|
|
46
|
+
*/
|
|
47
|
+
export async function handleResourceRead(uri: string): Promise<string> {
|
|
48
|
+
const client = getClient();
|
|
49
|
+
|
|
50
|
+
// Parse the URI to extract parameters
|
|
51
|
+
if (uri === 'bitbucket://repositories') {
|
|
52
|
+
return await resourceRepositories(client);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (uri === 'bitbucket://projects') {
|
|
56
|
+
return await resourceProjects(client);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Match repository-specific URIs
|
|
60
|
+
const repoMatch = uri.match(/^bitbucket:\/\/repositories\/([^/]+)$/);
|
|
61
|
+
if (repoMatch) {
|
|
62
|
+
return await resourceRepository(client, repoMatch[1]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const branchesMatch = uri.match(/^bitbucket:\/\/repositories\/([^/]+)\/branches$/);
|
|
66
|
+
if (branchesMatch) {
|
|
67
|
+
return await resourceBranches(client, branchesMatch[1]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const prsMatch = uri.match(/^bitbucket:\/\/repositories\/([^/]+)\/pull-requests$/);
|
|
71
|
+
if (prsMatch) {
|
|
72
|
+
return await resourcePullRequests(client, prsMatch[1]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(`Unknown resource URI: ${uri}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resourceRepositories(client: ReturnType<typeof getClient>): Promise<string> {
|
|
79
|
+
const repos = await client.listRepositories({ limit: 50 });
|
|
80
|
+
const lines = [`# Repositories in ${client.workspace}`, ''];
|
|
81
|
+
|
|
82
|
+
for (const r of repos) {
|
|
83
|
+
const name = r.name || 'unknown';
|
|
84
|
+
const desc = (r.description || '').substring(0, 50) || 'No description';
|
|
85
|
+
const icon = r.is_private ? '🔒' : '🌐';
|
|
86
|
+
lines.push(`- ${icon} **${name}**: ${desc}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function resourceRepository(client: ReturnType<typeof getClient>, repoSlug: string): Promise<string> {
|
|
93
|
+
const repo = await client.getRepository(repoSlug);
|
|
94
|
+
if (!repo) {
|
|
95
|
+
return `Repository '${repoSlug}' not found`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lines = [
|
|
99
|
+
`# ${repo.name || repoSlug}`,
|
|
100
|
+
'',
|
|
101
|
+
`**Description**: ${repo.description || 'No description'}`,
|
|
102
|
+
`**Private**: ${repo.is_private ? 'Yes' : 'No'}`,
|
|
103
|
+
`**Project**: ${repo.project?.name || 'None'}`,
|
|
104
|
+
`**Main branch**: ${repo.mainbranch?.name || 'main'}`,
|
|
105
|
+
'',
|
|
106
|
+
'## Clone URLs',
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const clone of repo.links?.clone || []) {
|
|
110
|
+
lines.push(`- ${clone.name}: \`${clone.href}\``);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function resourceBranches(client: ReturnType<typeof getClient>, repoSlug: string): Promise<string> {
|
|
117
|
+
const branches = await client.listBranches(repoSlug, { limit: 30 });
|
|
118
|
+
const lines = [`# Branches in ${repoSlug}`, ''];
|
|
119
|
+
|
|
120
|
+
for (const b of branches) {
|
|
121
|
+
const name = b.name || 'unknown';
|
|
122
|
+
const commit = (b.target?.hash || '').substring(0, 7);
|
|
123
|
+
lines.push(`- **${name}** (${commit})`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function resourcePullRequests(client: ReturnType<typeof getClient>, repoSlug: string): Promise<string> {
|
|
130
|
+
const prs = await client.listPullRequests(repoSlug, { state: 'OPEN', limit: 20 });
|
|
131
|
+
const lines = [`# Open Pull Requests in ${repoSlug}`, ''];
|
|
132
|
+
|
|
133
|
+
if (prs.length === 0) {
|
|
134
|
+
lines.push('No open pull requests');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const pr of prs) {
|
|
138
|
+
const prId = pr.id;
|
|
139
|
+
const title = pr.title || 'Untitled';
|
|
140
|
+
const author = pr.author?.display_name || 'Unknown';
|
|
141
|
+
lines.push(`- **#${prId}**: ${title} (by ${author})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function resourceProjects(client: ReturnType<typeof getClient>): Promise<string> {
|
|
148
|
+
const projects = await client.listProjects({ limit: 50 });
|
|
149
|
+
const lines = [`# Projects in ${client.workspace}`, ''];
|
|
150
|
+
|
|
151
|
+
for (const p of projects) {
|
|
152
|
+
const key = p.key || '?';
|
|
153
|
+
const name = p.name || 'Unknown';
|
|
154
|
+
const desc = (p.description || '').substring(0, 40) || 'No description';
|
|
155
|
+
lines.push(`- **${key}** - ${name}: ${desc}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings management for Bitbucket MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Configuration via environment variables:
|
|
5
|
+
* - BITBUCKET_WORKSPACE: Bitbucket workspace slug (required)
|
|
6
|
+
* - BITBUCKET_EMAIL: Account email for Basic Auth (required)
|
|
7
|
+
* - BITBUCKET_API_TOKEN: Repository access token (required)
|
|
8
|
+
* - API_TIMEOUT: Request timeout in seconds (default: 30, max: 300)
|
|
9
|
+
* - MAX_RETRIES: Max retry attempts for rate limiting (default: 3, max: 10)
|
|
10
|
+
* - OUTPUT_FORMAT: Output format - 'json' or 'toon' (default: json)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
const settingsSchema = z.object({
|
|
16
|
+
bitbucketWorkspace: z.string().min(1, 'BITBUCKET_WORKSPACE is required'),
|
|
17
|
+
bitbucketEmail: z.string().min(1, 'BITBUCKET_EMAIL is required'),
|
|
18
|
+
bitbucketApiToken: z.string().min(1, 'BITBUCKET_API_TOKEN is required'),
|
|
19
|
+
apiTimeout: z.number().min(1).max(300).default(30),
|
|
20
|
+
maxRetries: z.number().min(0).max(10).default(3),
|
|
21
|
+
outputFormat: z.enum(['json', 'toon']).default('json'),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type Settings = z.infer<typeof settingsSchema>;
|
|
25
|
+
|
|
26
|
+
let cachedSettings: Settings | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load and validate settings from environment variables.
|
|
30
|
+
* Results are cached for subsequent calls.
|
|
31
|
+
*/
|
|
32
|
+
export function getSettings(): Settings {
|
|
33
|
+
if (cachedSettings) {
|
|
34
|
+
return cachedSettings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rawSettings = {
|
|
38
|
+
bitbucketWorkspace: process.env.BITBUCKET_WORKSPACE || '',
|
|
39
|
+
bitbucketEmail: process.env.BITBUCKET_EMAIL || '',
|
|
40
|
+
bitbucketApiToken: process.env.BITBUCKET_API_TOKEN || '',
|
|
41
|
+
apiTimeout: parseInt(process.env.API_TIMEOUT || '30', 10),
|
|
42
|
+
maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
|
|
43
|
+
outputFormat: (process.env.OUTPUT_FORMAT || 'json') as 'json' | 'toon',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = settingsSchema.safeParse(rawSettings);
|
|
47
|
+
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
const errors = result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
50
|
+
throw new Error(`Configuration error: ${errors}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cachedSettings = result.data;
|
|
54
|
+
return cachedSettings;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reset cached settings (useful for testing)
|
|
59
|
+
*/
|
|
60
|
+
export function resetSettings(): void {
|
|
61
|
+
cachedSettings = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch tools for Bitbucket MCP Server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { getClient } from '../client.js';
|
|
7
|
+
import { validateLimit, notFoundResponse } from '../utils.js';
|
|
8
|
+
|
|
9
|
+
export const definitions: Tool[] = [
|
|
10
|
+
{
|
|
11
|
+
name: 'list_branches',
|
|
12
|
+
description: 'List branches in a repository.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
17
|
+
limit: { type: 'number', description: 'Maximum results (default: 50)', default: 50 },
|
|
18
|
+
},
|
|
19
|
+
required: ['repo_slug'],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'get_branch',
|
|
24
|
+
description: 'Get information about a specific branch.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
repo_slug: { type: 'string', description: 'Repository slug' },
|
|
29
|
+
branch_name: { type: 'string', description: 'Branch name' },
|
|
30
|
+
},
|
|
31
|
+
required: ['repo_slug', 'branch_name'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
|
|
37
|
+
list_branches: async (args) => {
|
|
38
|
+
const client = getClient();
|
|
39
|
+
const branches = await client.listBranches(args.repo_slug as string, {
|
|
40
|
+
limit: validateLimit((args.limit as number) || 50),
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
branches: branches.map(b => ({
|
|
44
|
+
name: b.name,
|
|
45
|
+
commit: b.target?.hash?.substring(0, 7),
|
|
46
|
+
message: b.target?.message,
|
|
47
|
+
date: b.target?.date,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
get_branch: async (args) => {
|
|
53
|
+
const client = getClient();
|
|
54
|
+
const result = await client.getBranch(args.repo_slug as string, args.branch_name as string);
|
|
55
|
+
if (!result) {
|
|
56
|
+
return notFoundResponse('Branch', args.branch_name as string);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
name: result.name,
|
|
60
|
+
latest_commit: {
|
|
61
|
+
hash: result.target?.hash,
|
|
62
|
+
message: result.target?.message || '',
|
|
63
|
+
author: result.target?.author?.raw,
|
|
64
|
+
date: result.target?.date,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|