bamboo-mcp-server 1.0.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.
@@ -0,0 +1,363 @@
1
+ import { ProxyAgent, fetch as undiciFetch } from 'undici';
2
+ export class BambooClient {
3
+ baseUrl;
4
+ token;
5
+ proxyAgent;
6
+ constructor(config) {
7
+ this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
8
+ this.token = config.token;
9
+ if (config.proxyUrl) {
10
+ this.proxyAgent = new ProxyAgent(config.proxyUrl);
11
+ }
12
+ }
13
+ async request(endpoint, options = {}) {
14
+ const url = `${this.baseUrl}/rest/api/latest${endpoint}`;
15
+ const headers = {
16
+ 'Authorization': `Bearer ${this.token}`,
17
+ 'Accept': 'application/json',
18
+ 'Content-Type': 'application/json',
19
+ ...(options.headers || {}),
20
+ };
21
+ const fetchOptions = {
22
+ ...options,
23
+ headers,
24
+ };
25
+ // Add proxy agent if configured
26
+ if (this.proxyAgent) {
27
+ fetchOptions.dispatcher = this.proxyAgent;
28
+ }
29
+ const response = await undiciFetch(url, fetchOptions);
30
+ if (!response.ok) {
31
+ const errorText = await response.text();
32
+ throw new Error(`Bamboo API error (${response.status}): ${errorText}`);
33
+ }
34
+ // Handle empty responses
35
+ const text = await response.text();
36
+ if (!text) {
37
+ return {};
38
+ }
39
+ return JSON.parse(text);
40
+ }
41
+ // Server endpoints
42
+ async getServerInfo() {
43
+ return this.request('/info');
44
+ }
45
+ async healthCheck() {
46
+ return this.request('/server');
47
+ }
48
+ // Project endpoints
49
+ async listProjects(params) {
50
+ const searchParams = new URLSearchParams();
51
+ if (params?.expand)
52
+ searchParams.set('expand', params.expand);
53
+ if (params?.startIndex)
54
+ searchParams.set('start-index', String(params.startIndex));
55
+ if (params?.maxResults)
56
+ searchParams.set('max-result', String(params.maxResults));
57
+ const query = searchParams.toString();
58
+ return this.request(`/project${query ? `?${query}` : ''}`);
59
+ }
60
+ async getProject(projectKey, expand) {
61
+ const query = expand ? `?expand=${expand}` : '';
62
+ return this.request(`/project/${projectKey}${query}`);
63
+ }
64
+ // Plan endpoints
65
+ async listPlans(params) {
66
+ const searchParams = new URLSearchParams();
67
+ if (params?.expand)
68
+ searchParams.set('expand', params.expand);
69
+ if (params?.startIndex)
70
+ searchParams.set('start-index', String(params.startIndex));
71
+ if (params?.maxResults)
72
+ searchParams.set('max-result', String(params.maxResults));
73
+ const query = searchParams.toString();
74
+ return this.request(`/plan${query ? `?${query}` : ''}`);
75
+ }
76
+ async getPlan(planKey, expand) {
77
+ const query = expand ? `?expand=${expand}` : '';
78
+ return this.request(`/plan/${planKey}${query}`);
79
+ }
80
+ async searchPlans(name, params) {
81
+ const searchParams = new URLSearchParams();
82
+ searchParams.set('searchTerm', name);
83
+ if (params?.fuzzy !== undefined)
84
+ searchParams.set('fuzzy', String(params.fuzzy));
85
+ if (params?.startIndex)
86
+ searchParams.set('start-index', String(params.startIndex));
87
+ if (params?.maxResults)
88
+ searchParams.set('max-result', String(params.maxResults));
89
+ return this.request(`/search/plans?${searchParams.toString()}`);
90
+ }
91
+ async enablePlan(planKey) {
92
+ return this.request(`/plan/${planKey}/enable`, { method: 'POST' });
93
+ }
94
+ async disablePlan(planKey) {
95
+ return this.request(`/plan/${planKey}/enable`, { method: 'DELETE' });
96
+ }
97
+ // Branch endpoints
98
+ async listPlanBranches(planKey, params) {
99
+ const searchParams = new URLSearchParams();
100
+ if (params?.enabledOnly !== undefined)
101
+ searchParams.set('enabledOnly', String(params.enabledOnly));
102
+ if (params?.startIndex)
103
+ searchParams.set('start-index', String(params.startIndex));
104
+ if (params?.maxResults)
105
+ searchParams.set('max-result', String(params.maxResults));
106
+ const query = searchParams.toString();
107
+ return this.request(`/plan/${planKey}/branch${query ? `?${query}` : ''}`);
108
+ }
109
+ async getPlanBranch(planKey, branchName) {
110
+ return this.request(`/plan/${planKey}/branch/${encodeURIComponent(branchName)}`);
111
+ }
112
+ // Build endpoints
113
+ async triggerBuild(planKey, params) {
114
+ const searchParams = new URLSearchParams();
115
+ if (params?.stage)
116
+ searchParams.set('stage', params.stage);
117
+ if (params?.executeAllStages !== undefined) {
118
+ searchParams.set('executeAllStages', String(params.executeAllStages));
119
+ }
120
+ if (params?.customRevision)
121
+ searchParams.set('customRevision', params.customRevision);
122
+ // Add bamboo variables
123
+ if (params?.variables) {
124
+ for (const [key, value] of Object.entries(params.variables)) {
125
+ searchParams.set(`bamboo.variable.${key}`, value);
126
+ }
127
+ }
128
+ const query = searchParams.toString();
129
+ return this.request(`/queue/${planKey}${query ? `?${query}` : ''}`, { method: 'POST' });
130
+ }
131
+ async stopBuild(planKey) {
132
+ // To stop a build, we need to DELETE the job keys (not the plan key)
133
+ // First, check if the plan is currently building
134
+ const plan = await this.request(`/plan/${planKey}`);
135
+ if (!plan.isBuilding) {
136
+ return {
137
+ message: `Plan ${planKey} is not currently building`,
138
+ success: false
139
+ };
140
+ }
141
+ // Get the list of results including in-progress builds
142
+ const results = await this.request(`/result/${planKey}?includeAllStates=true&max-result=5`);
143
+ // Find the in-progress build
144
+ const runningBuild = results.results?.result?.find(r => r.lifeCycleState === 'InProgress');
145
+ if (!runningBuild) {
146
+ return {
147
+ message: `No running build found for plan ${planKey}`,
148
+ success: false
149
+ };
150
+ }
151
+ // Get the running build with job details
152
+ const buildResult = await this.request(`/result/${runningBuild.key}?expand=stages.stage.results.result`);
153
+ // Find all jobs and stop them
154
+ const stoppedJobs = [];
155
+ const failedJobs = [];
156
+ if (buildResult.stages?.stage) {
157
+ for (const stage of buildResult.stages.stage) {
158
+ if (stage.results?.result) {
159
+ for (const job of stage.results.result) {
160
+ if (job.lifeCycleState === 'InProgress' || job.lifeCycleState === 'Pending' || job.lifeCycleState === 'Queued') {
161
+ try {
162
+ await this.request(`/queue/${job.key}`, { method: 'DELETE' });
163
+ stoppedJobs.push(job.key);
164
+ }
165
+ catch {
166
+ failedJobs.push(job.key);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ return {
174
+ message: stoppedJobs.length > 0
175
+ ? `Stopped ${stoppedJobs.length} job(s) for build ${runningBuild.key}`
176
+ : `No running jobs found to stop for build ${runningBuild.key}`,
177
+ buildKey: runningBuild.key,
178
+ stoppedJobs,
179
+ failedJobs,
180
+ success: stoppedJobs.length > 0
181
+ };
182
+ }
183
+ async getBuildResult(buildKey, expand) {
184
+ const query = expand ? `?expand=${expand}` : '';
185
+ return this.request(`/result/${buildKey}${query}`);
186
+ }
187
+ async getLatestBuildResult(planKey, expand) {
188
+ const query = expand ? `?expand=${expand}` : '';
189
+ return this.request(`/result/${planKey}/latest${query}`);
190
+ }
191
+ async listBuildResults(params) {
192
+ const searchParams = new URLSearchParams();
193
+ if (params?.buildState)
194
+ searchParams.set('buildstate', params.buildState);
195
+ if (params?.startIndex)
196
+ searchParams.set('start-index', String(params.startIndex));
197
+ if (params?.maxResults)
198
+ searchParams.set('max-result', String(params.maxResults));
199
+ if (params?.expand)
200
+ searchParams.set('expand', params.expand);
201
+ if (params?.includeAllStates)
202
+ searchParams.set('includeAllStates', 'true');
203
+ let endpoint = '/result';
204
+ if (params?.projectKey && params?.planKey) {
205
+ endpoint = `/result/${params.projectKey}-${params.planKey}`;
206
+ }
207
+ else if (params?.projectKey) {
208
+ endpoint = `/result/${params.projectKey}`;
209
+ }
210
+ const query = searchParams.toString();
211
+ return this.request(`${endpoint}${query ? `?${query}` : ''}`);
212
+ }
213
+ async getBuildLogs(buildKey, jobKey) {
214
+ // Build logs are obtained by getting the job result with logFiles expand
215
+ // The buildKey should be a job result key (e.g., PROJ-PLAN-JOB1-123)
216
+ // If only a plan result key is provided (e.g., PROJ-PLAN-123), we need to find the job keys first
217
+ if (jobKey) {
218
+ // If job key provided, construct the full job result key
219
+ const jobResultKey = `${buildKey.replace(/-\d+$/, '')}-${jobKey}-${buildKey.split('-').pop()}`;
220
+ const result = await this.request(`/result/${jobResultKey}?expand=logFiles`);
221
+ return {
222
+ buildKey: jobResultKey,
223
+ logFiles: result.logFiles || [],
224
+ message: result.logFiles?.length
225
+ ? 'Log files are available via the URLs below. These require browser authentication to download.'
226
+ : 'No log files found for this build.'
227
+ };
228
+ }
229
+ // Try to get the build result with stages to find job results
230
+ const result = await this.request(`/result/${buildKey}?expand=stages.stage.results.result`);
231
+ // Extract log files from all jobs
232
+ const allLogs = [];
233
+ if (result.stages?.stage) {
234
+ for (const stage of result.stages.stage) {
235
+ if (stage.results?.result) {
236
+ for (const job of stage.results.result) {
237
+ if (job.logFiles && job.logFiles.length > 0) {
238
+ allLogs.push({
239
+ jobKey: job.key,
240
+ logFiles: job.logFiles
241
+ });
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ return {
248
+ buildKey,
249
+ jobs: allLogs,
250
+ message: allLogs.length
251
+ ? 'Log files are available via the URLs below. These require browser authentication to download.'
252
+ : 'No log files found for this build.'
253
+ };
254
+ }
255
+ async getBuildResultWithLogs(buildKey, params) {
256
+ // First, try to get the result with logEntries (works for job-level keys)
257
+ const maxResult = params?.maxLogLines || 100;
258
+ const result = await this.request(`/result/${buildKey}?expand=logEntries,stages.stage.results.result&max-result=${maxResult}`);
259
+ // If logEntries exists and has entries, this is a job result - return directly
260
+ if (result.logEntries?.logEntry && result.logEntries.logEntry.length > 0) {
261
+ return result;
262
+ }
263
+ // This is a plan result - fetch logs from all jobs
264
+ const jobLogs = [];
265
+ if (result.stages?.stage) {
266
+ for (const stage of result.stages.stage) {
267
+ if (stage.results?.result) {
268
+ for (const job of stage.results.result) {
269
+ // Fetch logs for each job
270
+ const jobResult = await this.request(`/result/${job.key}?expand=logEntries&max-result=${maxResult}`);
271
+ jobLogs.push({
272
+ jobKey: job.key,
273
+ jobName: job.key.split('-').slice(-2, -1)[0], // Extract job name from key
274
+ buildState: jobResult.buildState,
275
+ logEntries: {
276
+ size: jobResult.logEntries?.size || 0,
277
+ logs: (jobResult.logEntries?.logEntry || []).map(entry => ({
278
+ date: entry.formattedDate,
279
+ log: entry.unstyledLog
280
+ }))
281
+ }
282
+ });
283
+ }
284
+ }
285
+ }
286
+ }
287
+ return {
288
+ key: result.key,
289
+ buildState: result.buildState,
290
+ lifeCycleState: result.lifeCycleState,
291
+ isJobResult: false,
292
+ jobs: jobLogs,
293
+ totalLogEntries: jobLogs.reduce((sum, job) => sum + job.logEntries.size, 0)
294
+ };
295
+ }
296
+ // Queue endpoints
297
+ async getBuildQueue(expand) {
298
+ const query = expand ? `?expand=${expand}` : '?expand=queuedBuilds';
299
+ return this.request(`/queue${query}`);
300
+ }
301
+ async getDeploymentQueue() {
302
+ // Note: The deployment queue endpoint is not available in all Bamboo versions
303
+ // We'll try to get it from the deployment dashboard instead
304
+ try {
305
+ // Try the standard endpoint first
306
+ return await this.request('/deploy/queue?expand=queuedDeployments');
307
+ }
308
+ catch {
309
+ // If that fails, return info about checking deployment results instead
310
+ return {
311
+ message: 'Deployment queue endpoint not available in this Bamboo version. Use bamboo_get_deployment_results to check deployment status for specific environments.',
312
+ available: false
313
+ };
314
+ }
315
+ }
316
+ // Deployment endpoints
317
+ async listDeploymentProjects() {
318
+ return this.request('/deploy/project/all');
319
+ }
320
+ async getDeploymentProject(projectId) {
321
+ return this.request(`/deploy/project/${projectId}`);
322
+ }
323
+ async triggerDeployment(versionId, environmentId) {
324
+ return this.request(`/queue/deployment?versionId=${versionId}&environmentId=${environmentId}`, { method: 'POST' });
325
+ }
326
+ async getDeploymentResults(environmentId, params) {
327
+ const searchParams = new URLSearchParams();
328
+ if (params?.startIndex)
329
+ searchParams.set('start-index', String(params.startIndex));
330
+ if (params?.maxResults)
331
+ searchParams.set('max-result', String(params.maxResults));
332
+ const query = searchParams.toString();
333
+ return this.request(`/deploy/environment/${environmentId}/results${query ? `?${query}` : ''}`);
334
+ }
335
+ async getDeploymentResult(deploymentResultId, params) {
336
+ const searchParams = new URLSearchParams();
337
+ if (params?.includeLogs) {
338
+ searchParams.set('includeLogs', 'true');
339
+ if (params?.maxLogLines) {
340
+ searchParams.set('max-result', String(params.maxLogLines));
341
+ }
342
+ }
343
+ const query = searchParams.toString();
344
+ return this.request(`/deploy/result/${deploymentResultId}${query ? `?${query}` : ''}`);
345
+ }
346
+ }
347
+ // Factory function to create client from environment variables
348
+ export function createBambooClientFromEnv() {
349
+ const baseUrl = process.env.BAMBOO_URL;
350
+ const token = process.env.BAMBOO_TOKEN;
351
+ const proxyUrl = process.env.BAMBOO_PROXY;
352
+ if (!baseUrl) {
353
+ throw new Error('BAMBOO_URL environment variable is required');
354
+ }
355
+ if (!token) {
356
+ throw new Error('BAMBOO_TOKEN environment variable is required');
357
+ }
358
+ return new BambooClient({
359
+ baseUrl,
360
+ token,
361
+ proxyUrl,
362
+ });
363
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { createBambooClientFromEnv } from './bamboo-client.js';
5
+ import { registerServerTools } from './tools/server.js';
6
+ import { registerProjectTools } from './tools/projects.js';
7
+ import { registerPlanTools } from './tools/plans.js';
8
+ import { registerBranchTools } from './tools/branches.js';
9
+ import { registerBuildTools } from './tools/builds.js';
10
+ import { registerQueueTools } from './tools/queue.js';
11
+ import { registerDeploymentTools } from './tools/deployments.js';
12
+ async function main() {
13
+ // Create the Bamboo client from environment variables
14
+ const client = createBambooClientFromEnv();
15
+ // Create the MCP server
16
+ const server = new McpServer({
17
+ name: 'bamboo-mcp-server',
18
+ version: '1.0.0',
19
+ });
20
+ // Register all tools
21
+ registerServerTools(server, client);
22
+ registerProjectTools(server, client);
23
+ registerPlanTools(server, client);
24
+ registerBranchTools(server, client);
25
+ registerBuildTools(server, client);
26
+ registerQueueTools(server, client);
27
+ registerDeploymentTools(server, client);
28
+ // Connect via stdio transport
29
+ const transport = new StdioServerTransport();
30
+ await server.connect(transport);
31
+ // Handle graceful shutdown
32
+ process.on('SIGINT', async () => {
33
+ await server.close();
34
+ process.exit(0);
35
+ });
36
+ process.on('SIGTERM', async () => {
37
+ await server.close();
38
+ process.exit(0);
39
+ });
40
+ }
41
+ main().catch((error) => {
42
+ console.error('Fatal error:', error);
43
+ process.exit(1);
44
+ });
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BambooClient } from '../bamboo-client.js';
3
+ export declare function registerBranchTools(server: McpServer, client: BambooClient): void;
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ export function registerBranchTools(server, client) {
3
+ // List plan branches
4
+ server.tool('bamboo_list_plan_branches', 'List all branches for a Bamboo build plan', {
5
+ plan_key: z.string().describe('The plan key (e.g., "PROJ-PLAN")'),
6
+ enabled_only: z.boolean().optional().describe('Only return enabled branches'),
7
+ start_index: z.number().optional().describe('Starting index for pagination (default: 0)'),
8
+ max_results: z.number().optional().describe('Maximum number of results to return (default: 25)'),
9
+ }, async ({ plan_key, enabled_only, start_index, max_results }) => {
10
+ try {
11
+ const branches = await client.listPlanBranches(plan_key, {
12
+ enabledOnly: enabled_only,
13
+ startIndex: start_index,
14
+ maxResults: max_results,
15
+ });
16
+ return {
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: JSON.stringify(branches, null, 2),
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ catch (error) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
31
+ },
32
+ ],
33
+ isError: true,
34
+ };
35
+ }
36
+ });
37
+ // Get plan branch
38
+ server.tool('bamboo_get_plan_branch', 'Get details of a specific plan branch', {
39
+ plan_key: z.string().describe('The plan key (e.g., "PROJ-PLAN")'),
40
+ branch_name: z.string().describe('The branch name'),
41
+ }, async ({ plan_key, branch_name }) => {
42
+ try {
43
+ const branch = await client.getPlanBranch(plan_key, branch_name);
44
+ return {
45
+ content: [
46
+ {
47
+ type: 'text',
48
+ text: JSON.stringify(branch, null, 2),
49
+ },
50
+ ],
51
+ };
52
+ }
53
+ catch (error) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
59
+ },
60
+ ],
61
+ isError: true,
62
+ };
63
+ }
64
+ });
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BambooClient } from '../bamboo-client.js';
3
+ export declare function registerBuildTools(server: McpServer, client: BambooClient): void;