bamboo-mcp-server 1.0.10 → 1.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ ## [1.1.2](https://github.com/norus/atlassian-bamboo-mcp/compare/v1.1.1...v1.1.2) (2026-01-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **ci:** match original publish.yml setup for trusted publishers ([2ff9306](https://github.com/norus/atlassian-bamboo-mcp/commit/2ff930679ee15f91bd8e9b7de78428db3683ba29))
7
+
8
+ ## [1.1.1](https://github.com/norus/atlassian-bamboo-mcp/compare/v1.1.0...v1.1.1) (2026-01-09)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **ci:** remove registry-url to fix OIDC auth ([af9ff11](https://github.com/norus/atlassian-bamboo-mcp/commit/af9ff111d95206a422c7bfa90f355750aaacd4a8))
14
+
15
+ # [1.1.0](https://github.com/norus/atlassian-bamboo-mcp/compare/v1.0.11...v1.1.0) (2026-01-09)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **ci:** use native npm publish for trusted publishers ([a24ad47](https://github.com/norus/atlassian-bamboo-mcp/commit/a24ad4716b10284271a9e83b5a789acf6f140401))
21
+ * **ci:** use Node 22 for semantic-release ([8c9db06](https://github.com/norus/atlassian-bamboo-mcp/commit/8c9db06200ecd640b616cc4aaf3188c9159d49dd))
22
+
23
+
24
+ ### Features
25
+
26
+ * add automated releases with semantic-release ([9bcaa2d](https://github.com/norus/atlassian-bamboo-mcp/commit/9bcaa2d4e36cc8cccb171e068c709287bb5bf6ef))
@@ -1,4 +1,22 @@
1
1
  import { ProxyAgent, fetch as undiciFetch } from 'undici';
2
+ /**
3
+ * Build a URL query string from optional parameters
4
+ */
5
+ function buildQueryString(params) {
6
+ const searchParams = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(params)) {
8
+ if (value !== undefined) {
9
+ searchParams.set(key, String(value));
10
+ }
11
+ }
12
+ return searchParams.toString();
13
+ }
14
+ /**
15
+ * Append query string to endpoint if not empty
16
+ */
17
+ function appendQuery(endpoint, query) {
18
+ return query ? `${endpoint}?${query}` : endpoint;
19
+ }
2
20
  export class BambooClient {
3
21
  baseUrl;
4
22
  token;
@@ -47,46 +65,38 @@ export class BambooClient {
47
65
  }
48
66
  // Project endpoints
49
67
  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}` : ''}`);
68
+ const query = buildQueryString({
69
+ 'expand': params?.expand,
70
+ 'start-index': params?.startIndex,
71
+ 'max-result': params?.maxResults,
72
+ });
73
+ return this.request(appendQuery('/project', query));
59
74
  }
60
75
  async getProject(projectKey, expand) {
61
- const query = expand ? `?expand=${expand}` : '';
62
- return this.request(`/project/${projectKey}${query}`);
76
+ const query = buildQueryString({ expand });
77
+ return this.request(appendQuery(`/project/${projectKey}`, query));
63
78
  }
64
79
  // Plan endpoints
65
80
  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}` : ''}`);
81
+ const query = buildQueryString({
82
+ 'expand': params?.expand,
83
+ 'start-index': params?.startIndex,
84
+ 'max-result': params?.maxResults,
85
+ });
86
+ return this.request(appendQuery('/plan', query));
75
87
  }
76
88
  async getPlan(planKey, expand) {
77
- const query = expand ? `?expand=${expand}` : '';
78
- return this.request(`/plan/${planKey}${query}`);
89
+ const query = buildQueryString({ expand });
90
+ return this.request(appendQuery(`/plan/${planKey}`, query));
79
91
  }
80
92
  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()}`);
93
+ const query = buildQueryString({
94
+ 'searchTerm': name,
95
+ 'fuzzy': params?.fuzzy,
96
+ 'start-index': params?.startIndex,
97
+ 'max-result': params?.maxResults,
98
+ });
99
+ return this.request(`/search/plans?${query}`);
90
100
  }
91
101
  async enablePlan(planKey) {
92
102
  return this.request(`/plan/${planKey}/enable`, { method: 'POST' });
@@ -96,37 +106,31 @@ export class BambooClient {
96
106
  }
97
107
  // Branch endpoints
98
108
  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}` : ''}`);
109
+ const query = buildQueryString({
110
+ 'enabledOnly': params?.enabledOnly,
111
+ 'start-index': params?.startIndex,
112
+ 'max-result': params?.maxResults,
113
+ });
114
+ return this.request(appendQuery(`/plan/${planKey}/branch`, query));
108
115
  }
109
116
  async getPlanBranch(planKey, branchName) {
110
117
  return this.request(`/plan/${planKey}/branch/${encodeURIComponent(branchName)}`);
111
118
  }
112
119
  // Build endpoints
113
120
  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
121
+ const queryParams = {
122
+ 'stage': params?.stage,
123
+ 'executeAllStages': params?.executeAllStages,
124
+ 'customRevision': params?.customRevision,
125
+ };
126
+ // Add bamboo variables with prefixed keys
123
127
  if (params?.variables) {
124
128
  for (const [key, value] of Object.entries(params.variables)) {
125
- searchParams.set(`bamboo.variable.${key}`, value);
129
+ queryParams[`bamboo.variable.${key}`] = value;
126
130
  }
127
131
  }
128
- const query = searchParams.toString();
129
- return this.request(`/queue/${planKey}${query ? `?${query}` : ''}`, { method: 'POST' });
132
+ const query = buildQueryString(queryParams);
133
+ return this.request(appendQuery(`/queue/${planKey}`, query), { method: 'POST' });
130
134
  }
131
135
  async stopBuild(planKey) {
132
136
  // To stop a build, we need to DELETE the job keys (not the plan key)
@@ -181,25 +185,21 @@ export class BambooClient {
181
185
  };
182
186
  }
183
187
  async getBuildResult(buildKey, expand) {
184
- const query = expand ? `?expand=${expand}` : '';
185
- return this.request(`/result/${buildKey}${query}`);
188
+ const query = buildQueryString({ expand });
189
+ return this.request(appendQuery(`/result/${buildKey}`, query));
186
190
  }
187
191
  async getLatestBuildResult(planKey, expand) {
188
- const query = expand ? `?expand=${expand}` : '';
189
- return this.request(`/result/${planKey}/latest${query}`);
192
+ const query = buildQueryString({ expand });
193
+ return this.request(appendQuery(`/result/${planKey}/latest`, query));
190
194
  }
191
195
  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');
196
+ const query = buildQueryString({
197
+ 'buildstate': params?.buildState,
198
+ 'start-index': params?.startIndex,
199
+ 'max-result': params?.maxResults,
200
+ 'expand': params?.expand,
201
+ 'includeAllStates': params?.includeAllStates,
202
+ });
203
203
  let endpoint = '/result';
204
204
  if (params?.projectKey && params?.planKey) {
205
205
  endpoint = `/result/${params.projectKey}-${params.planKey}`;
@@ -207,8 +207,7 @@ export class BambooClient {
207
207
  else if (params?.projectKey) {
208
208
  endpoint = `/result/${params.projectKey}`;
209
209
  }
210
- const query = searchParams.toString();
211
- return this.request(`${endpoint}${query ? `?${query}` : ''}`);
210
+ return this.request(appendQuery(endpoint, query));
212
211
  }
213
212
  async getBuildLogs(buildKey, jobKey) {
214
213
  // Build logs are obtained by getting the job result with logFiles expand
@@ -324,22 +323,20 @@ export class BambooClient {
324
323
  return this.request(`/queue/deployment?versionId=${versionId}&environmentId=${environmentId}`, { method: 'POST' });
325
324
  }
326
325
  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}` : ''}`);
326
+ const query = buildQueryString({
327
+ 'start-index': params?.startIndex,
328
+ 'max-result': params?.maxResults,
329
+ });
330
+ return this.request(appendQuery(`/deploy/environment/${environmentId}/results`, query));
334
331
  }
335
332
  async getDeploymentResult(deploymentResultId, params) {
336
- const searchParams = new URLSearchParams();
337
- if (params?.includeLogs) {
338
- searchParams.set('includeLogs', 'true');
339
- searchParams.set('max-result', String(params?.maxLogLines || 1000));
340
- }
341
- const query = searchParams.toString();
342
- return this.request(`/deploy/result/${deploymentResultId}${query ? `?${query}` : ''}`);
333
+ const query = params?.includeLogs
334
+ ? buildQueryString({
335
+ 'includeLogs': true,
336
+ 'max-result': params?.maxLogLines || 1000,
337
+ })
338
+ : '';
339
+ return this.request(appendQuery(`/deploy/result/${deploymentResultId}`, query));
343
340
  }
344
341
  }
345
342
  // Factory function to create client from environment variables
package/dist/index.js CHANGED
@@ -29,14 +29,12 @@ async function main() {
29
29
  const transport = new StdioServerTransport();
30
30
  await server.connect(transport);
31
31
  // Handle graceful shutdown
32
- process.on('SIGINT', async () => {
32
+ async function shutdown() {
33
33
  await server.close();
34
34
  process.exit(0);
35
- });
36
- process.on('SIGTERM', async () => {
37
- await server.close();
38
- process.exit(0);
39
- });
35
+ }
36
+ process.on('SIGINT', shutdown);
37
+ process.on('SIGTERM', shutdown);
40
38
  }
41
39
  main().catch((error) => {
42
40
  console.error('Fatal error:', error);
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { formatError, jsonResponse } from './utils.js';
2
3
  export function registerBranchTools(server, client) {
3
- // List plan branches
4
4
  server.tool('bamboo_list_plan_branches', 'List all branches for a Bamboo build plan', {
5
5
  plan_key: z.string().describe('The plan key (e.g., "PROJ-PLAN")'),
6
6
  enabled_only: z.boolean().optional().describe('Only return enabled branches'),
@@ -13,53 +13,22 @@ export function registerBranchTools(server, client) {
13
13
  startIndex: start_index,
14
14
  maxResults: max_results,
15
15
  });
16
- return {
17
- content: [
18
- {
19
- type: 'text',
20
- text: JSON.stringify(branches, null, 2),
21
- },
22
- ],
23
- };
16
+ return jsonResponse(branches);
24
17
  }
25
18
  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
- };
19
+ return formatError(error);
35
20
  }
36
21
  });
37
- // Get plan branch
38
22
  server.tool('bamboo_get_plan_branch', 'Get details of a specific plan branch', {
39
23
  plan_key: z.string().describe('The plan key (e.g., "PROJ-PLAN")'),
40
24
  branch_name: z.string().describe('The branch name'),
41
25
  }, async ({ plan_key, branch_name }) => {
42
26
  try {
43
27
  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
- };
28
+ return jsonResponse(branch);
52
29
  }
53
30
  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
- };
31
+ return formatError(error);
63
32
  }
64
33
  });
65
34
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { formatError, jsonResponse } from './utils.js';
2
3
  export function registerBuildTools(server, client) {
3
- // Trigger build
4
4
  server.tool('bamboo_trigger_build', 'Trigger a build for a Bamboo plan', {
5
5
  plan_key: z.string().describe('The plan key to build (e.g., "PROJ-PLAN")'),
6
6
  stage: z.string().optional().describe('Specific stage to execute'),
@@ -15,111 +15,47 @@ export function registerBuildTools(server, client) {
15
15
  customRevision: custom_revision,
16
16
  variables,
17
17
  });
18
- return {
19
- content: [
20
- {
21
- type: 'text',
22
- text: JSON.stringify(result, null, 2),
23
- },
24
- ],
25
- };
18
+ return jsonResponse(result);
26
19
  }
27
20
  catch (error) {
28
- return {
29
- content: [
30
- {
31
- type: 'text',
32
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
33
- },
34
- ],
35
- isError: true,
36
- };
21
+ return formatError(error);
37
22
  }
38
23
  });
39
- // Stop build
40
24
  server.tool('bamboo_stop_build', 'Stop a running build for a Bamboo plan', {
41
25
  plan_key: z.string().describe('The plan key to stop (e.g., "PROJ-PLAN")'),
42
26
  }, async ({ plan_key }) => {
43
27
  try {
44
28
  const result = await client.stopBuild(plan_key);
45
- return {
46
- content: [
47
- {
48
- type: 'text',
49
- text: JSON.stringify(result, null, 2),
50
- },
51
- ],
52
- };
29
+ return jsonResponse(result);
53
30
  }
54
31
  catch (error) {
55
- return {
56
- content: [
57
- {
58
- type: 'text',
59
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
60
- },
61
- ],
62
- isError: true,
63
- };
32
+ return formatError(error);
64
33
  }
65
34
  });
66
- // Get build result
67
35
  server.tool('bamboo_get_build_result', 'Get the result of a specific build', {
68
36
  build_key: z.string().describe('The build result key (e.g., "PROJ-PLAN-123")'),
69
37
  expand: z.string().optional().describe('Fields to expand (e.g., "changes,artifacts,testResults")'),
70
38
  }, async ({ build_key, expand }) => {
71
39
  try {
72
40
  const result = await client.getBuildResult(build_key, expand);
73
- return {
74
- content: [
75
- {
76
- type: 'text',
77
- text: JSON.stringify(result, null, 2),
78
- },
79
- ],
80
- };
41
+ return jsonResponse(result);
81
42
  }
82
43
  catch (error) {
83
- return {
84
- content: [
85
- {
86
- type: 'text',
87
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
88
- },
89
- ],
90
- isError: true,
91
- };
44
+ return formatError(error);
92
45
  }
93
46
  });
94
- // Get latest build result
95
47
  server.tool('bamboo_get_latest_result', 'Get the latest build result for a plan', {
96
48
  plan_key: z.string().describe('The plan key (e.g., "PROJ-PLAN")'),
97
49
  expand: z.string().optional().describe('Fields to expand (e.g., "changes,artifacts,testResults")'),
98
50
  }, async ({ plan_key, expand }) => {
99
51
  try {
100
52
  const result = await client.getLatestBuildResult(plan_key, expand);
101
- return {
102
- content: [
103
- {
104
- type: 'text',
105
- text: JSON.stringify(result, null, 2),
106
- },
107
- ],
108
- };
53
+ return jsonResponse(result);
109
54
  }
110
55
  catch (error) {
111
- return {
112
- content: [
113
- {
114
- type: 'text',
115
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
116
- },
117
- ],
118
- isError: true,
119
- };
56
+ return formatError(error);
120
57
  }
121
58
  });
122
- // List build results
123
59
  server.tool('bamboo_list_build_results', 'List build results with optional filters', {
124
60
  project_key: z.string().optional().describe('Filter by project key'),
125
61
  plan_key: z.string().optional().describe('Filter by plan key (requires project_key)'),
@@ -139,56 +75,24 @@ export function registerBuildTools(server, client) {
139
75
  expand,
140
76
  includeAllStates: include_all_states,
141
77
  });
142
- return {
143
- content: [
144
- {
145
- type: 'text',
146
- text: JSON.stringify(results, null, 2),
147
- },
148
- ],
149
- };
78
+ return jsonResponse(results);
150
79
  }
151
80
  catch (error) {
152
- return {
153
- content: [
154
- {
155
- type: 'text',
156
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
157
- },
158
- ],
159
- isError: true,
160
- };
81
+ return formatError(error);
161
82
  }
162
83
  });
163
- // Get build logs
164
84
  server.tool('bamboo_get_build_logs', 'Get the build logs for a specific build. Returns log file URLs that can be accessed via browser.', {
165
85
  build_key: z.string().describe('The build result key (e.g., "PROJ-PLAN-123")'),
166
86
  job_key: z.string().optional().describe('Specific job key to get logs for'),
167
87
  }, async ({ build_key, job_key }) => {
168
88
  try {
169
89
  const logs = await client.getBuildLogs(build_key, job_key);
170
- return {
171
- content: [
172
- {
173
- type: 'text',
174
- text: JSON.stringify(logs, null, 2),
175
- },
176
- ],
177
- };
90
+ return jsonResponse(logs);
178
91
  }
179
92
  catch (error) {
180
- return {
181
- content: [
182
- {
183
- type: 'text',
184
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
185
- },
186
- ],
187
- isError: true,
188
- };
93
+ return formatError(error);
189
94
  }
190
95
  });
191
- // Get build result with log content
192
96
  server.tool('bamboo_get_build_result_logs', 'Get build result with actual log content. For plan builds, fetches logs from all jobs. For job builds, returns logs directly.', {
193
97
  build_key: z.string().describe('The build result key - can be plan level (e.g., "PROJ-PLAN-123") or job level (e.g., "PROJ-PLAN-JOB1-123")'),
194
98
  max_log_lines: z.number().optional().describe('Maximum number of log lines per job (default: 1000)'),
@@ -197,25 +101,10 @@ export function registerBuildTools(server, client) {
197
101
  const result = await client.getBuildResultWithLogs(build_key, {
198
102
  maxLogLines: max_log_lines,
199
103
  });
200
- return {
201
- content: [
202
- {
203
- type: 'text',
204
- text: JSON.stringify(result, null, 2),
205
- },
206
- ],
207
- };
104
+ return jsonResponse(result);
208
105
  }
209
106
  catch (error) {
210
- return {
211
- content: [
212
- {
213
- type: 'text',
214
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
215
- },
216
- ],
217
- isError: true,
218
- };
107
+ return formatError(error);
219
108
  }
220
109
  });
221
110
  }
@@ -1,86 +1,38 @@
1
1
  import { z } from 'zod';
2
+ import { formatError, jsonResponse } from './utils.js';
2
3
  export function registerDeploymentTools(server, client) {
3
- // List deployment projects
4
4
  server.tool('bamboo_list_deployment_projects', 'List all Bamboo deployment projects', {}, async () => {
5
5
  try {
6
6
  const projects = await client.listDeploymentProjects();
7
- return {
8
- content: [
9
- {
10
- type: 'text',
11
- text: JSON.stringify(projects, null, 2),
12
- },
13
- ],
14
- };
7
+ return jsonResponse(projects);
15
8
  }
16
9
  catch (error) {
17
- return {
18
- content: [
19
- {
20
- type: 'text',
21
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
22
- },
23
- ],
24
- isError: true,
25
- };
10
+ return formatError(error);
26
11
  }
27
12
  });
28
- // Get deployment project
29
13
  server.tool('bamboo_get_deployment_project', 'Get details of a specific deployment project', {
30
14
  project_id: z.string().describe('The deployment project ID'),
31
15
  }, async ({ project_id }) => {
32
16
  try {
33
17
  const project = await client.getDeploymentProject(project_id);
34
- return {
35
- content: [
36
- {
37
- type: 'text',
38
- text: JSON.stringify(project, null, 2),
39
- },
40
- ],
41
- };
18
+ return jsonResponse(project);
42
19
  }
43
20
  catch (error) {
44
- return {
45
- content: [
46
- {
47
- type: 'text',
48
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
49
- },
50
- ],
51
- isError: true,
52
- };
21
+ return formatError(error);
53
22
  }
54
23
  });
55
- // Trigger deployment
56
24
  server.tool('bamboo_trigger_deployment', 'Trigger a deployment to an environment', {
57
25
  version_id: z.string().describe('The release version ID to deploy'),
58
26
  environment_id: z.string().describe('The target environment ID'),
59
27
  }, async ({ version_id, environment_id }) => {
60
28
  try {
61
29
  const result = await client.triggerDeployment(version_id, environment_id);
62
- return {
63
- content: [
64
- {
65
- type: 'text',
66
- text: JSON.stringify(result, null, 2),
67
- },
68
- ],
69
- };
30
+ return jsonResponse(result);
70
31
  }
71
32
  catch (error) {
72
- return {
73
- content: [
74
- {
75
- type: 'text',
76
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
77
- },
78
- ],
79
- isError: true,
80
- };
33
+ return formatError(error);
81
34
  }
82
35
  });
83
- // Get deployment results
84
36
  server.tool('bamboo_get_deployment_results', 'Get deployment results for an environment', {
85
37
  environment_id: z.string().describe('The environment ID'),
86
38
  start_index: z.number().optional().describe('Starting index for pagination (default: 0)'),
@@ -91,28 +43,12 @@ export function registerDeploymentTools(server, client) {
91
43
  startIndex: start_index,
92
44
  maxResults: max_results,
93
45
  });
94
- return {
95
- content: [
96
- {
97
- type: 'text',
98
- text: JSON.stringify(results, null, 2),
99
- },
100
- ],
101
- };
46
+ return jsonResponse(results);
102
47
  }
103
48
  catch (error) {
104
- return {
105
- content: [
106
- {
107
- type: 'text',
108
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
109
- },
110
- ],
111
- isError: true,
112
- };
49
+ return formatError(error);
113
50
  }
114
51
  });
115
- // Get deployment result with logs
116
52
  server.tool('bamboo_get_deployment_result', 'Get a specific deployment result with optional logs', {
117
53
  deployment_result_id: z.string().describe('The deployment result ID'),
118
54
  include_logs: z.boolean().optional().describe('Include log entries (default: false)'),
@@ -123,25 +59,10 @@ export function registerDeploymentTools(server, client) {
123
59
  includeLogs: include_logs,
124
60
  maxLogLines: max_log_lines,
125
61
  });
126
- return {
127
- content: [
128
- {
129
- type: 'text',
130
- text: JSON.stringify(result, null, 2),
131
- },
132
- ],
133
- };
62
+ return jsonResponse(result);
134
63
  }
135
64
  catch (error) {
136
- return {
137
- content: [
138
- {
139
- type: 'text',
140
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
141
- },
142
- ],
143
- isError: true,
144
- };
65
+ return formatError(error);
145
66
  }
146
67
  });
147
68
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { formatError, jsonResponse, textResponse } from './utils.js';
2
3
  export function registerPlanTools(server, client) {
3
- // List plans
4
4
  server.tool('bamboo_list_plans', 'List all Bamboo build plans', {
5
5
  expand: z.string().optional().describe('Fields to expand in the response (e.g., "plans.plan.stages")'),
6
6
  start_index: z.number().optional().describe('Starting index for pagination (default: 0)'),
@@ -12,56 +12,24 @@ export function registerPlanTools(server, client) {
12
12
  startIndex: start_index,
13
13
  maxResults: max_results,
14
14
  });
15
- return {
16
- content: [
17
- {
18
- type: 'text',
19
- text: JSON.stringify(plans, null, 2),
20
- },
21
- ],
22
- };
15
+ return jsonResponse(plans);
23
16
  }
24
17
  catch (error) {
25
- return {
26
- content: [
27
- {
28
- type: 'text',
29
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
30
- },
31
- ],
32
- isError: true,
33
- };
18
+ return formatError(error);
34
19
  }
35
20
  });
36
- // Get plan
37
21
  server.tool('bamboo_get_plan', 'Get details of a specific Bamboo build plan by key', {
38
22
  plan_key: z.string().describe('The plan key (e.g., "PROJ-PLAN")'),
39
23
  expand: z.string().optional().describe('Fields to expand in the response'),
40
24
  }, async ({ plan_key, expand }) => {
41
25
  try {
42
26
  const plan = await client.getPlan(plan_key, expand);
43
- return {
44
- content: [
45
- {
46
- type: 'text',
47
- text: JSON.stringify(plan, null, 2),
48
- },
49
- ],
50
- };
27
+ return jsonResponse(plan);
51
28
  }
52
29
  catch (error) {
53
- return {
54
- content: [
55
- {
56
- type: 'text',
57
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
58
- },
59
- ],
60
- isError: true,
61
- };
30
+ return formatError(error);
62
31
  }
63
32
  });
64
- // Search plans
65
33
  server.tool('bamboo_search_plans', 'Search for Bamboo build plans by name', {
66
34
  name: z.string().describe('The plan name to search for'),
67
35
  fuzzy: z.boolean().optional().describe('Enable fuzzy matching (default: true)'),
@@ -74,79 +42,32 @@ export function registerPlanTools(server, client) {
74
42
  startIndex: start_index,
75
43
  maxResults: max_results,
76
44
  });
77
- return {
78
- content: [
79
- {
80
- type: 'text',
81
- text: JSON.stringify(plans, null, 2),
82
- },
83
- ],
84
- };
45
+ return jsonResponse(plans);
85
46
  }
86
47
  catch (error) {
87
- return {
88
- content: [
89
- {
90
- type: 'text',
91
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
92
- },
93
- ],
94
- isError: true,
95
- };
48
+ return formatError(error);
96
49
  }
97
50
  });
98
- // Enable plan
99
51
  server.tool('bamboo_enable_plan', 'Enable a Bamboo build plan', {
100
52
  plan_key: z.string().describe('The plan key to enable (e.g., "PROJ-PLAN")'),
101
53
  }, async ({ plan_key }) => {
102
54
  try {
103
55
  await client.enablePlan(plan_key);
104
- return {
105
- content: [
106
- {
107
- type: 'text',
108
- text: `Plan ${plan_key} has been enabled successfully.`,
109
- },
110
- ],
111
- };
56
+ return textResponse(`Plan ${plan_key} has been enabled successfully.`);
112
57
  }
113
58
  catch (error) {
114
- return {
115
- content: [
116
- {
117
- type: 'text',
118
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
119
- },
120
- ],
121
- isError: true,
122
- };
59
+ return formatError(error);
123
60
  }
124
61
  });
125
- // Disable plan
126
62
  server.tool('bamboo_disable_plan', 'Disable a Bamboo build plan', {
127
63
  plan_key: z.string().describe('The plan key to disable (e.g., "PROJ-PLAN")'),
128
64
  }, async ({ plan_key }) => {
129
65
  try {
130
66
  await client.disablePlan(plan_key);
131
- return {
132
- content: [
133
- {
134
- type: 'text',
135
- text: `Plan ${plan_key} has been disabled successfully.`,
136
- },
137
- ],
138
- };
67
+ return textResponse(`Plan ${plan_key} has been disabled successfully.`);
139
68
  }
140
69
  catch (error) {
141
- return {
142
- content: [
143
- {
144
- type: 'text',
145
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
146
- },
147
- ],
148
- isError: true,
149
- };
70
+ return formatError(error);
150
71
  }
151
72
  });
152
73
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { formatError, jsonResponse } from './utils.js';
2
3
  export function registerProjectTools(server, client) {
3
- // List projects
4
4
  server.tool('bamboo_list_projects', 'List all Bamboo projects', {
5
5
  expand: z.string().optional().describe('Fields to expand in the response (e.g., "projects.project.plans")'),
6
6
  start_index: z.number().optional().describe('Starting index for pagination (default: 0)'),
@@ -12,53 +12,22 @@ export function registerProjectTools(server, client) {
12
12
  startIndex: start_index,
13
13
  maxResults: max_results,
14
14
  });
15
- return {
16
- content: [
17
- {
18
- type: 'text',
19
- text: JSON.stringify(projects, null, 2),
20
- },
21
- ],
22
- };
15
+ return jsonResponse(projects);
23
16
  }
24
17
  catch (error) {
25
- return {
26
- content: [
27
- {
28
- type: 'text',
29
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
30
- },
31
- ],
32
- isError: true,
33
- };
18
+ return formatError(error);
34
19
  }
35
20
  });
36
- // Get project
37
21
  server.tool('bamboo_get_project', 'Get details of a specific Bamboo project by key', {
38
22
  project_key: z.string().describe('The project key (e.g., "PROJ")'),
39
23
  expand: z.string().optional().describe('Fields to expand in the response'),
40
24
  }, async ({ project_key, expand }) => {
41
25
  try {
42
26
  const project = await client.getProject(project_key, expand);
43
- return {
44
- content: [
45
- {
46
- type: 'text',
47
- text: JSON.stringify(project, null, 2),
48
- },
49
- ],
50
- };
27
+ return jsonResponse(project);
51
28
  }
52
29
  catch (error) {
53
- return {
54
- content: [
55
- {
56
- type: 'text',
57
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
58
- },
59
- ],
60
- isError: true,
61
- };
30
+ return formatError(error);
62
31
  }
63
32
  });
64
33
  }
@@ -1,55 +1,24 @@
1
1
  import { z } from 'zod';
2
+ import { formatError, jsonResponse } from './utils.js';
2
3
  export function registerQueueTools(server, client) {
3
- // Get build queue
4
4
  server.tool('bamboo_get_build_queue', 'Get the current Bamboo build queue', {
5
5
  expand: z.string().optional().describe('Fields to expand (default: "queuedBuilds")'),
6
6
  }, async ({ expand }) => {
7
7
  try {
8
8
  const queue = await client.getBuildQueue(expand);
9
- return {
10
- content: [
11
- {
12
- type: 'text',
13
- text: JSON.stringify(queue, null, 2),
14
- },
15
- ],
16
- };
9
+ return jsonResponse(queue);
17
10
  }
18
11
  catch (error) {
19
- return {
20
- content: [
21
- {
22
- type: 'text',
23
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
24
- },
25
- ],
26
- isError: true,
27
- };
12
+ return formatError(error);
28
13
  }
29
14
  });
30
- // Get deployment queue
31
15
  server.tool('bamboo_get_deployment_queue', 'Get the current Bamboo deployment queue', {}, async () => {
32
16
  try {
33
17
  const queue = await client.getDeploymentQueue();
34
- return {
35
- content: [
36
- {
37
- type: 'text',
38
- text: JSON.stringify(queue, null, 2),
39
- },
40
- ],
41
- };
18
+ return jsonResponse(queue);
42
19
  }
43
20
  catch (error) {
44
- return {
45
- content: [
46
- {
47
- type: 'text',
48
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
49
- },
50
- ],
51
- isError: true,
52
- };
21
+ return formatError(error);
53
22
  }
54
23
  });
55
24
  }
@@ -1,52 +1,21 @@
1
+ import { formatError, jsonResponse } from './utils.js';
1
2
  export function registerServerTools(server, client) {
2
- // Get server info
3
3
  server.tool('bamboo_server_info', 'Get Bamboo server information including version, edition, and state', {}, async () => {
4
4
  try {
5
5
  const info = await client.getServerInfo();
6
- return {
7
- content: [
8
- {
9
- type: 'text',
10
- text: JSON.stringify(info, null, 2),
11
- },
12
- ],
13
- };
6
+ return jsonResponse(info);
14
7
  }
15
8
  catch (error) {
16
- return {
17
- content: [
18
- {
19
- type: 'text',
20
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
21
- },
22
- ],
23
- isError: true,
24
- };
9
+ return formatError(error);
25
10
  }
26
11
  });
27
- // Health check
28
12
  server.tool('bamboo_health_check', 'Check Bamboo server health status', {}, async () => {
29
13
  try {
30
14
  const health = await client.healthCheck();
31
- return {
32
- content: [
33
- {
34
- type: 'text',
35
- text: JSON.stringify(health, null, 2),
36
- },
37
- ],
38
- };
15
+ return jsonResponse(health);
39
16
  }
40
17
  catch (error) {
41
- return {
42
- content: [
43
- {
44
- type: 'text',
45
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
46
- },
47
- ],
48
- isError: true,
49
- };
18
+ return formatError(error);
50
19
  }
51
20
  });
52
21
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared utilities for MCP tool handlers
3
+ */
4
+ interface ToolResponse {
5
+ [key: string]: unknown;
6
+ content: Array<{
7
+ type: 'text';
8
+ text: string;
9
+ }>;
10
+ isError?: boolean;
11
+ }
12
+ /**
13
+ * Format a successful JSON response for MCP tools
14
+ */
15
+ export declare function jsonResponse(data: unknown): ToolResponse;
16
+ /**
17
+ * Format a text response for MCP tools
18
+ */
19
+ export declare function textResponse(message: string): ToolResponse;
20
+ /**
21
+ * Format an error response for MCP tools
22
+ */
23
+ export declare function formatError(error: unknown): ToolResponse;
24
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared utilities for MCP tool handlers
3
+ */
4
+ /**
5
+ * Format a successful JSON response for MCP tools
6
+ */
7
+ export function jsonResponse(data) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: 'text',
12
+ text: JSON.stringify(data, null, 2),
13
+ },
14
+ ],
15
+ };
16
+ }
17
+ /**
18
+ * Format a text response for MCP tools
19
+ */
20
+ export function textResponse(message) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: message,
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ /**
31
+ * Format an error response for MCP tools
32
+ */
33
+ export function formatError(error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [
37
+ {
38
+ type: 'text',
39
+ text: `Error: ${message}`,
40
+ },
41
+ ],
42
+ isError: true,
43
+ };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bamboo-mcp-server",
3
- "version": "1.0.10",
3
+ "version": "1.1.2",
4
4
  "description": "MCP server for Atlassian Bamboo",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,6 +11,9 @@
11
11
  "build": "tsc",
12
12
  "start": "node dist/index.js",
13
13
  "dev": "tsc --watch",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage",
14
17
  "prepublishOnly": "npm run build"
15
18
  },
16
19
  "keywords": [
@@ -35,6 +38,7 @@
35
38
  "files": [
36
39
  "dist",
37
40
  "README.md",
41
+ "CHANGELOG.md",
38
42
  "LICENSE"
39
43
  ],
40
44
  "dependencies": {
@@ -43,8 +47,14 @@
43
47
  "zod": "^3.22.0"
44
48
  },
45
49
  "devDependencies": {
50
+ "@semantic-release/changelog": "^6.0.3",
51
+ "@semantic-release/git": "^10.0.1",
46
52
  "@types/node": "^20.0.0",
47
- "typescript": "^5.3.0"
53
+ "@vitest/coverage-v8": "^4.0.16",
54
+ "msw": "^2.12.7",
55
+ "semantic-release": "^24.2.9",
56
+ "typescript": "^5.3.0",
57
+ "vitest": "^4.0.16"
48
58
  },
49
59
  "engines": {
50
60
  "node": ">=18.0.0"