@stubbedev/atlassian-mcp 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/bitbucket.js +17 -9
- package/dist/index.js +120 -0
- package/dist/jira.js +317 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# atlassian-mcp
|
|
2
2
|
|
|
3
|
-
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **self-hosted Jira** (Server / Data Center) and **self-hosted Bitbucket** (Server / Data Center). Exposes
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **self-hosted Jira** (Server / Data Center) and **self-hosted Bitbucket** (Server / Data Center). Exposes tools for natural-language workflows around tickets, pull requests, review threads, and git context.
|
|
4
4
|
|
|
5
5
|
> **Note:** This server only supports self-hosted instances. Jira Cloud and Bitbucket Cloud use different APIs and are not supported.
|
|
6
6
|
|
|
@@ -22,9 +22,14 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
22
22
|
| `jira_my_issues` | List issues assigned to you, ordered by last updated |
|
|
23
23
|
| `jira_get_projects` | List all accessible projects |
|
|
24
24
|
| `jira_get_issue_types` | List issue types and their available statuses for a project |
|
|
25
|
+
| `jira_get_sprints` | List sprints for a board (with sprint IDs for assignment) |
|
|
25
26
|
| `jira_get_issue` | Get issue details by key |
|
|
27
|
+
| `jira_issue_overview` | Get one-call issue overview (details, transitions, sprint context, optional comments) |
|
|
28
|
+
| `jira_board_overview` | Get one-call board overview (board info, sprints, optional sprint issues) |
|
|
26
29
|
| `jira_create_issue` | Create a new issue |
|
|
27
30
|
| `jira_update_issue` | Update summary, description, assignee, or priority |
|
|
31
|
+
| `jira_add_issues_to_sprint` | Add one or more issues to a sprint by sprint ID |
|
|
32
|
+
| `jira_mutate_issue` | Bundle create/update/sprint/transition/comment actions into one call |
|
|
28
33
|
| `jira_search_users` | Search for users by name or email |
|
|
29
34
|
| `jira_get_comments` | List comments on an issue |
|
|
30
35
|
| `jira_add_comment` | Add a comment to an issue |
|
package/dist/bitbucket.js
CHANGED
|
@@ -158,6 +158,13 @@ export class BitbucketClient {
|
|
|
158
158
|
Accept: 'application/json',
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
+
pullRequestUrl(projectKey, repoSlug, prId, pr) {
|
|
162
|
+
const apiUrl = pr?.links?.self?.[0]?.href?.trim();
|
|
163
|
+
if (apiUrl) {
|
|
164
|
+
return apiUrl;
|
|
165
|
+
}
|
|
166
|
+
return `${this.baseUrl}/projects/${encodeURIComponent(projectKey)}/repos/${encodeURIComponent(repoSlug)}/pull-requests/${prId}`;
|
|
167
|
+
}
|
|
161
168
|
resolveProjectAndRepo(projectKey, repoSlug) {
|
|
162
169
|
if (projectKey && repoSlug)
|
|
163
170
|
return { projectKey, repoSlug };
|
|
@@ -375,20 +382,21 @@ export class BitbucketClient {
|
|
|
375
382
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests`, body);
|
|
376
383
|
if (!data)
|
|
377
384
|
return text('Pull request created.');
|
|
378
|
-
const url = data.
|
|
379
|
-
return text(`Created PR #${data.id}: "${data.title}"
|
|
385
|
+
const url = this.pullRequestUrl(projectKey, repoSlug, data.id, data);
|
|
386
|
+
return text(`Created PR #${data.id}: "${data.title}"\n${url}`);
|
|
380
387
|
}
|
|
381
388
|
async approvePr(args) {
|
|
382
389
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
383
390
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
|
|
391
|
+
const url = this.pullRequestUrl(projectKey, repoSlug, args.prId);
|
|
384
392
|
if (!data)
|
|
385
|
-
return text(`Approved PR #${args.prId}
|
|
386
|
-
return text(`Approved PR #${args.prId} as ${data.user.displayName}
|
|
393
|
+
return text(`Approved PR #${args.prId}.\n${url}`);
|
|
394
|
+
return text(`Approved PR #${args.prId} as ${data.user.displayName}.\n${url}`);
|
|
387
395
|
}
|
|
388
396
|
async unapprovePr(args) {
|
|
389
397
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
390
398
|
await this.request('DELETE', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
|
|
391
|
-
return text(`Approval removed from PR #${args.prId}
|
|
399
|
+
return text(`Approval removed from PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
|
|
392
400
|
}
|
|
393
401
|
async declinePr(args) {
|
|
394
402
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
@@ -400,8 +408,8 @@ export class BitbucketClient {
|
|
|
400
408
|
body.message = args.message;
|
|
401
409
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/decline`, body);
|
|
402
410
|
if (!data)
|
|
403
|
-
return text(`Declined PR #${args.prId}
|
|
404
|
-
return text(`Declined PR #${data.id}: "${data.title}"
|
|
411
|
+
return text(`Declined PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
|
|
412
|
+
return text(`Declined PR #${data.id}: "${data.title}".\n${this.pullRequestUrl(projectKey, repoSlug, data.id, data)}`);
|
|
405
413
|
}
|
|
406
414
|
async mergePr(args) {
|
|
407
415
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
@@ -415,8 +423,8 @@ export class BitbucketClient {
|
|
|
415
423
|
body.message = args.message;
|
|
416
424
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/merge`, body);
|
|
417
425
|
if (!data)
|
|
418
|
-
return text(`Merged PR #${args.prId}
|
|
419
|
-
return text(`Merged PR #${data.id}: "${data.title}" (${data.fromRef.displayId} → ${data.toRef.displayId})
|
|
426
|
+
return text(`Merged PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
|
|
427
|
+
return text(`Merged PR #${data.id}: "${data.title}" (${data.fromRef.displayId} → ${data.toRef.displayId}).\n${this.pullRequestUrl(projectKey, repoSlug, data.id, data)}`);
|
|
420
428
|
}
|
|
421
429
|
async getBranches(args) {
|
|
422
430
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,16 @@ function normalizeJiraProjectArgs(args) {
|
|
|
29
29
|
out.projectKey = out.project;
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
|
+
function normalizeJiraMutateArgs(args) {
|
|
33
|
+
const out = normalizeJiraProjectArgs(args);
|
|
34
|
+
if (out.create && typeof out.create === 'object') {
|
|
35
|
+
const create = { ...out.create };
|
|
36
|
+
if (typeof create.project === 'string' && typeof create.projectKey !== 'string')
|
|
37
|
+
create.projectKey = create.project;
|
|
38
|
+
out.create = create;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
32
42
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
33
43
|
tools: [
|
|
34
44
|
// ── Context ───────────────────────────────────────────────────────────
|
|
@@ -92,6 +102,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
92
102
|
},
|
|
93
103
|
},
|
|
94
104
|
},
|
|
105
|
+
{
|
|
106
|
+
name: 'jira_get_sprints',
|
|
107
|
+
description: 'Use when you need sprint IDs for planning or assignment. Returns sprints for a Jira board.',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
boardId: { type: 'number', description: 'Jira board ID' },
|
|
112
|
+
state: { type: 'string', description: 'Optional sprint state filter, e.g. "active", "future", or "closed"' },
|
|
113
|
+
maxResults: { type: 'number', description: 'Max sprints per page (default 20)', default: 20 },
|
|
114
|
+
startAt: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
|
|
115
|
+
},
|
|
116
|
+
required: ['boardId'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
95
119
|
{
|
|
96
120
|
name: 'jira_search_users',
|
|
97
121
|
description: 'Use when assigning tickets and you need to find the correct Jira username by name or email.',
|
|
@@ -115,6 +139,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
115
139
|
required: ['issueKey'],
|
|
116
140
|
},
|
|
117
141
|
},
|
|
142
|
+
{
|
|
143
|
+
name: 'jira_issue_overview',
|
|
144
|
+
description: 'Use when you want one Jira issue snapshot in a single call: details, transitions, sprint context, and optional comments.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
issueKey: { type: 'string', description: 'Jira issue key, e.g. "FOO-123"' },
|
|
149
|
+
includeComments: { type: 'boolean', description: 'Include comments in the overview (default true)', default: true },
|
|
150
|
+
commentsMaxResults: { type: 'number', description: 'Max comments when includeComments=true (default 10)', default: 10 },
|
|
151
|
+
commentsStartAt: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
|
|
152
|
+
includeTransitions: { type: 'boolean', description: 'Include available transitions (default true)', default: true },
|
|
153
|
+
includeSprint: { type: 'boolean', description: 'Include sprint data via Jira Agile API (default true)', default: true },
|
|
154
|
+
},
|
|
155
|
+
required: ['issueKey'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
118
158
|
{
|
|
119
159
|
name: 'jira_create_issue',
|
|
120
160
|
description: 'Use when you want to create a new Jira ticket (bug, story, task, etc.). If projectKey/project is omitted, the server auto-picks from branch context or asks you to choose.',
|
|
@@ -128,6 +168,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
128
168
|
description: { type: 'string', description: 'Issue description (optional)' },
|
|
129
169
|
assignee: { type: 'string', description: 'Username to assign to (optional)' },
|
|
130
170
|
priority: { type: 'string', description: 'Priority name, e.g. "High" (optional)' },
|
|
171
|
+
sprintId: { type: 'number', description: 'Sprint ID to immediately add the new issue into (optional)' },
|
|
131
172
|
},
|
|
132
173
|
required: ['issueType', 'summary'],
|
|
133
174
|
},
|
|
@@ -143,10 +184,79 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
143
184
|
description: { type: 'string', description: 'New description (optional)' },
|
|
144
185
|
assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
|
|
145
186
|
priority: { type: 'string', description: 'New priority name (optional)' },
|
|
187
|
+
sprintId: { type: 'number', description: 'Sprint ID to add this issue into (optional)' },
|
|
146
188
|
},
|
|
147
189
|
required: ['issueKey'],
|
|
148
190
|
},
|
|
149
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: 'jira_add_issues_to_sprint',
|
|
194
|
+
description: 'Use when you want to assign one or more Jira issues to a sprint by sprint ID.',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
sprintId: { type: 'number', description: 'Sprint ID' },
|
|
199
|
+
issueKey: { type: 'string', description: 'Single issue key (optional)' },
|
|
200
|
+
issueKeys: { type: 'array', items: { type: 'string' }, description: 'Multiple issue keys (optional)' },
|
|
201
|
+
},
|
|
202
|
+
required: ['sprintId'],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'jira_mutate_issue',
|
|
207
|
+
description: 'Use when you want to bundle Jira mutations in one call: create or target an issue, then optional update, sprint assignment, transition, and comment.',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
issueKey: { type: 'string', description: 'Existing issue key to mutate (optional if create is provided)' },
|
|
212
|
+
create: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
projectKey: { type: 'string', description: 'Jira project code (optional, auto-resolved when omitted)' },
|
|
216
|
+
project: { type: 'string', description: 'Alias for projectKey' },
|
|
217
|
+
issueType: { type: 'string', description: 'Issue type name, e.g. Bug, Story, Task' },
|
|
218
|
+
summary: { type: 'string', description: 'Issue title' },
|
|
219
|
+
description: { type: 'string', description: 'Issue description (optional)' },
|
|
220
|
+
assignee: { type: 'string', description: 'Username to assign to (optional)' },
|
|
221
|
+
priority: { type: 'string', description: 'Priority name (optional)' },
|
|
222
|
+
},
|
|
223
|
+
required: ['issueType', 'summary'],
|
|
224
|
+
},
|
|
225
|
+
update: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
summary: { type: 'string', description: 'New summary (optional)' },
|
|
229
|
+
description: { type: 'string', description: 'New description (optional)' },
|
|
230
|
+
assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
|
|
231
|
+
priority: { type: 'string', description: 'New priority name (optional)' },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
sprintId: { type: 'number', description: 'Sprint ID to add the issue into (optional)' },
|
|
235
|
+
transitionId: { type: 'string', description: 'Transition ID (optional if transitionName is provided)' },
|
|
236
|
+
transitionName: { type: 'string', description: 'Transition name, e.g. In Progress (optional if transitionId is provided)' },
|
|
237
|
+
comment: { type: 'string', description: 'Comment to add after other mutations (optional, no emoji)' },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'jira_board_overview',
|
|
243
|
+
description: 'Use when you want one board-level planning snapshot: board info, sprints, and optional sprint issues in one call.',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
boardId: { type: 'number', description: 'Jira board ID' },
|
|
248
|
+
sprintState: { type: 'string', description: 'Sprint state filter, e.g. "active,future" (default active,future)' },
|
|
249
|
+
sprintMaxResults: { type: 'number', description: 'Max sprints per page (default 10)', default: 10 },
|
|
250
|
+
sprintStartAt: { type: 'number', description: 'Sprints pagination offset (default 0)', default: 0 },
|
|
251
|
+
includeIssues: { type: 'boolean', description: 'Include sprint issues (default true)', default: true },
|
|
252
|
+
issueMaxResults: { type: 'number', description: 'Max issues per sprint when includeIssues=true (default 25)', default: 25 },
|
|
253
|
+
issueStartAt: { type: 'number', description: 'Issue pagination offset per sprint (default 0)', default: 0 },
|
|
254
|
+
assignee: { type: 'string', description: 'Optional assignee filter for sprint issues' },
|
|
255
|
+
status: { type: 'string', description: 'Optional status filter for sprint issues' },
|
|
256
|
+
},
|
|
257
|
+
required: ['boardId'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
150
260
|
{
|
|
151
261
|
name: 'jira_get_comments',
|
|
152
262
|
description: 'Use when you want the discussion thread on a Jira ticket, with pagination for long threads.',
|
|
@@ -542,14 +652,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
542
652
|
return await jira.getProjects(args);
|
|
543
653
|
case 'jira_get_issue_types':
|
|
544
654
|
return await jira.getIssueTypes(normalizeJiraProjectArgs(args));
|
|
655
|
+
case 'jira_get_sprints':
|
|
656
|
+
return await jira.getSprints(args);
|
|
545
657
|
case 'jira_search_users':
|
|
546
658
|
return await jira.searchUsers(args);
|
|
547
659
|
case 'jira_get_issue':
|
|
548
660
|
return await jira.getIssue(args);
|
|
661
|
+
case 'jira_issue_overview':
|
|
662
|
+
return await jira.issueOverview(args);
|
|
549
663
|
case 'jira_create_issue':
|
|
550
664
|
return await jira.createIssue(normalizeJiraProjectArgs(args));
|
|
551
665
|
case 'jira_update_issue':
|
|
552
666
|
return await jira.updateIssue(args);
|
|
667
|
+
case 'jira_add_issues_to_sprint':
|
|
668
|
+
return await jira.addIssuesToSprint(args);
|
|
669
|
+
case 'jira_mutate_issue':
|
|
670
|
+
return await jira.mutateIssue(normalizeJiraMutateArgs(args));
|
|
671
|
+
case 'jira_board_overview':
|
|
672
|
+
return await jira.boardOverview(args);
|
|
553
673
|
case 'jira_get_comments':
|
|
554
674
|
return await jira.getComments(args);
|
|
555
675
|
case 'jira_add_comment':
|
package/dist/jira.js
CHANGED
|
@@ -98,8 +98,20 @@ export class JiraClient {
|
|
|
98
98
|
Accept: 'application/json',
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
issueUrl(issueKey) {
|
|
102
|
+
return `${this.baseUrl}/browse/${encodeURIComponent(issueKey)}`;
|
|
103
|
+
}
|
|
104
|
+
projectUrl(projectKey) {
|
|
105
|
+
return `${this.baseUrl}/projects/${encodeURIComponent(projectKey)}`;
|
|
106
|
+
}
|
|
107
|
+
boardUrl(boardId) {
|
|
108
|
+
return `${this.baseUrl}/secure/RapidBoard.jspa?rapidView=${boardId}`;
|
|
109
|
+
}
|
|
110
|
+
sprintUrl(boardId, sprintId) {
|
|
111
|
+
return `${this.boardUrl(boardId)}&sprint=${sprintId}`;
|
|
112
|
+
}
|
|
113
|
+
async requestWithBase(apiBase, method, path, body) {
|
|
114
|
+
const url = `${this.baseUrl}${apiBase}${path}`;
|
|
103
115
|
const opts = { method, headers: this.headers };
|
|
104
116
|
if (body !== undefined)
|
|
105
117
|
opts.body = JSON.stringify(body);
|
|
@@ -111,6 +123,67 @@ export class JiraClient {
|
|
|
111
123
|
}
|
|
112
124
|
return res.status === 204 ? null : res.json();
|
|
113
125
|
}
|
|
126
|
+
async request(method, path, body) {
|
|
127
|
+
return this.requestWithBase('/rest/api/2', method, path, body);
|
|
128
|
+
}
|
|
129
|
+
async requestAgile(method, path, body) {
|
|
130
|
+
return this.requestWithBase('/rest/agile/1.0', method, path, body);
|
|
131
|
+
}
|
|
132
|
+
async addIssuesToSprintInternal(sprintId, issueKeys) {
|
|
133
|
+
await this.requestAgile('POST', `/sprint/${sprintId}/issue`, { issues: issueKeys });
|
|
134
|
+
}
|
|
135
|
+
async createIssueInternal(args) {
|
|
136
|
+
const projectKey = await this.resolveProjectKey(args.projectKey);
|
|
137
|
+
const fields = {
|
|
138
|
+
project: { key: projectKey },
|
|
139
|
+
issuetype: { name: args.issueType },
|
|
140
|
+
summary: args.summary,
|
|
141
|
+
};
|
|
142
|
+
if (args.description)
|
|
143
|
+
fields.description = args.description;
|
|
144
|
+
if (args.assignee)
|
|
145
|
+
fields.assignee = { name: args.assignee };
|
|
146
|
+
if (args.priority)
|
|
147
|
+
fields.priority = { name: args.priority };
|
|
148
|
+
return this.request('POST', '/issue', { fields });
|
|
149
|
+
}
|
|
150
|
+
async updateIssueFieldsInternal(args) {
|
|
151
|
+
const fields = {};
|
|
152
|
+
if (args.summary !== undefined)
|
|
153
|
+
fields.summary = args.summary;
|
|
154
|
+
if (args.description !== undefined)
|
|
155
|
+
fields.description = args.description;
|
|
156
|
+
if (args.assignee !== undefined)
|
|
157
|
+
fields.assignee = { name: args.assignee };
|
|
158
|
+
if (args.priority !== undefined)
|
|
159
|
+
fields.priority = { name: args.priority };
|
|
160
|
+
if (Object.keys(fields).length === 0)
|
|
161
|
+
return false;
|
|
162
|
+
await this.request('PUT', `/issue/${args.issueKey}`, { fields });
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
async resolveTransitionId(issueKey, transitionId, transitionName) {
|
|
166
|
+
if (transitionId)
|
|
167
|
+
return transitionId;
|
|
168
|
+
const requestedName = transitionName?.trim();
|
|
169
|
+
if (!requestedName) {
|
|
170
|
+
throw new Error('Provide transitionId or transitionName');
|
|
171
|
+
}
|
|
172
|
+
const data = await this.request('GET', `/issue/${issueKey}/transitions`);
|
|
173
|
+
const transitions = data?.transitions ?? [];
|
|
174
|
+
const lowered = requestedName.toLowerCase();
|
|
175
|
+
const match = transitions.find((t) => t.name.toLowerCase() === lowered);
|
|
176
|
+
if (!match) {
|
|
177
|
+
const available = transitions.map((t) => t.name).join(', ') || '(none)';
|
|
178
|
+
throw new Error(`Transition "${requestedName}" not found for ${issueKey}. Available: ${available}`);
|
|
179
|
+
}
|
|
180
|
+
return match.id;
|
|
181
|
+
}
|
|
182
|
+
async transitionIssueInternal(issueKey, transitionId) {
|
|
183
|
+
await this.request('POST', `/issue/${issueKey}/transitions`, {
|
|
184
|
+
transition: { id: transitionId },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
114
187
|
async resolveProjectKey(projectKey) {
|
|
115
188
|
if (projectKey)
|
|
116
189
|
return projectKey;
|
|
@@ -147,7 +220,7 @@ export class JiraClient {
|
|
|
147
220
|
return text('No results.');
|
|
148
221
|
const lines = data.issues.map((i, idx) => {
|
|
149
222
|
const assignee = i.fields.assignee?.displayName ?? 'Unassigned';
|
|
150
|
-
return `${startAt + idx + 1}. [${i.key}] ${i.fields.summary} | ${i.fields.status.name} | ${assignee}`;
|
|
223
|
+
return `${startAt + idx + 1}. [${i.key}] ${i.fields.summary} | ${i.fields.status.name} | ${assignee} | ${this.issueUrl(i.key)}`;
|
|
151
224
|
});
|
|
152
225
|
const page = pagination(data.total, startAt, data.issues.length);
|
|
153
226
|
return text(`Found ${data.total} issues${page}:\n${lines.join('\n')}`);
|
|
@@ -164,7 +237,7 @@ export class JiraClient {
|
|
|
164
237
|
const data = await this.request('GET', `/project?maxResults=${limit}`);
|
|
165
238
|
if (!data || data.length === 0)
|
|
166
239
|
return text('No projects found.');
|
|
167
|
-
const lines = data.map((p, i) => `${i + 1}. [${p.key}] ${p.name} (${p.projectTypeKey})`);
|
|
240
|
+
const lines = data.map((p, i) => `${i + 1}. [${p.key}] ${p.name} (${p.projectTypeKey}) | ${this.projectUrl(p.key)}`);
|
|
168
241
|
return text(`${data.length} project(s):\n${lines.join('\n')}`);
|
|
169
242
|
}
|
|
170
243
|
async getIssueTypes(args) {
|
|
@@ -178,6 +251,26 @@ export class JiraClient {
|
|
|
178
251
|
});
|
|
179
252
|
return text(`Issue types and statuses for ${projectKey}:\n${lines.join('\n')}`);
|
|
180
253
|
}
|
|
254
|
+
async getSprints(args) {
|
|
255
|
+
const { boardId, maxResults = 20, startAt = 0 } = args;
|
|
256
|
+
const params = new URLSearchParams({
|
|
257
|
+
maxResults: String(maxResults),
|
|
258
|
+
startAt: String(startAt),
|
|
259
|
+
});
|
|
260
|
+
if (args.state)
|
|
261
|
+
params.set('state', args.state);
|
|
262
|
+
const data = await this.requestAgile('GET', `/board/${boardId}/sprint?${params}`);
|
|
263
|
+
if (!data || data.values.length === 0)
|
|
264
|
+
return text(`No sprints found for board ${boardId}.`);
|
|
265
|
+
const lines = data.values.map((s, i) => {
|
|
266
|
+
const window = [s.startDate?.slice(0, 10), s.endDate?.slice(0, 10)].filter(Boolean).join(' -> ');
|
|
267
|
+
const goal = s.goal?.trim() ? ` | Goal: ${s.goal}` : '';
|
|
268
|
+
return `${startAt + i + 1}. [${s.id}] ${s.name} | ${s.state}${window ? ` | ${window}` : ''}${goal} | ${this.sprintUrl(boardId, s.id)}`;
|
|
269
|
+
});
|
|
270
|
+
const rangeEnd = startAt + data.values.length;
|
|
271
|
+
const page = data.isLast ? '' : ` (showing ${startAt + 1}-${rangeEnd}, use startAt=${rangeEnd} for next page)`;
|
|
272
|
+
return text(`Sprints for board ${boardId}${page}:\nBoard URL: ${this.boardUrl(boardId)}\n${lines.join('\n')}`);
|
|
273
|
+
}
|
|
181
274
|
async searchUsers(args) {
|
|
182
275
|
const params = new URLSearchParams({
|
|
183
276
|
username: args.query,
|
|
@@ -211,38 +304,229 @@ export class JiraClient {
|
|
|
211
304
|
];
|
|
212
305
|
return text(lines.join('\n'));
|
|
213
306
|
}
|
|
214
|
-
async
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
307
|
+
async issueOverview(args) {
|
|
308
|
+
const includeComments = args.includeComments ?? true;
|
|
309
|
+
const includeTransitions = args.includeTransitions ?? true;
|
|
310
|
+
const includeSprint = args.includeSprint ?? true;
|
|
311
|
+
const commentsMaxResults = args.commentsMaxResults ?? 10;
|
|
312
|
+
const commentsStartAt = args.commentsStartAt ?? 0;
|
|
313
|
+
const fields = 'summary,description,status,assignee,priority,issuetype,labels,components';
|
|
314
|
+
const issue = await this.request('GET', `/issue/${args.issueKey}?fields=${fields}`);
|
|
315
|
+
if (!issue)
|
|
316
|
+
return text('Issue not found.');
|
|
317
|
+
const f = issue.fields;
|
|
318
|
+
const lines = [
|
|
319
|
+
`Issue: ${issue.key} — ${f.summary}`,
|
|
320
|
+
`URL: ${this.issueUrl(issue.key)}`,
|
|
321
|
+
`Status: ${f.status.name}`,
|
|
322
|
+
`Type: ${f.issuetype.name}`,
|
|
323
|
+
`Priority: ${f.priority?.name ?? 'None'}`,
|
|
324
|
+
`Assignee: ${f.assignee?.displayName ?? 'Unassigned'}`,
|
|
325
|
+
`Labels: ${f.labels?.join(', ') || 'None'}`,
|
|
326
|
+
`Components: ${f.components?.map((c) => c.name).join(', ') || 'None'}`,
|
|
327
|
+
];
|
|
328
|
+
if (includeSprint) {
|
|
329
|
+
try {
|
|
330
|
+
const agileIssue = await this.requestAgile('GET', `/issue/${args.issueKey}?fields=sprint,closedSprints`);
|
|
331
|
+
const activeSprint = agileIssue?.fields?.sprint;
|
|
332
|
+
const closedSprints = agileIssue?.fields?.closedSprints ?? [];
|
|
333
|
+
if (activeSprint) {
|
|
334
|
+
lines.push(`Sprint: [${activeSprint.id}] ${activeSprint.name} (${activeSprint.state})`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
lines.push('Sprint: None');
|
|
338
|
+
}
|
|
339
|
+
if (closedSprints.length > 0) {
|
|
340
|
+
const closed = closedSprints.slice(0, 5).map((s) => `[${s.id}] ${s.name}`).join(', ');
|
|
341
|
+
const more = closedSprints.length > 5 ? ` (+${closedSprints.length - 5} more)` : '';
|
|
342
|
+
lines.push(`History: ${closed}${more}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
lines.push(`Sprint: unavailable (${err.message})`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (includeTransitions) {
|
|
350
|
+
const transitions = await this.request('GET', `/issue/${args.issueKey}/transitions`);
|
|
351
|
+
const names = (transitions?.transitions ?? []).map((t) => `${t.name} -> ${t.to.name}`);
|
|
352
|
+
lines.push(`Transitions: ${names.length > 0 ? names.join(', ') : '(none)'}`);
|
|
353
|
+
}
|
|
354
|
+
lines.push('', 'Description:', f.description ?? '(no description)');
|
|
355
|
+
if (includeComments) {
|
|
356
|
+
const comments = await this.request('GET', `/issue/${args.issueKey}/comment?startAt=${commentsStartAt}&maxResults=${commentsMaxResults}`);
|
|
357
|
+
const items = comments?.comments ?? [];
|
|
358
|
+
const total = comments?.total ?? 0;
|
|
359
|
+
const page = comments ? pagination(total, commentsStartAt, items.length) : '';
|
|
360
|
+
lines.push('', `Comments: ${total}${page}`);
|
|
361
|
+
if (items.length === 0) {
|
|
362
|
+
lines.push('(none)');
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
for (const c of items) {
|
|
366
|
+
const date = c.created.slice(0, 10);
|
|
367
|
+
lines.push(`--- ${c.author.displayName} (${date}) ---`, c.body, '');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return text(lines.join('\n').trimEnd());
|
|
372
|
+
}
|
|
373
|
+
async boardOverview(args) {
|
|
374
|
+
const includeIssues = args.includeIssues ?? true;
|
|
375
|
+
const sprintMaxResults = args.sprintMaxResults ?? 10;
|
|
376
|
+
const sprintStartAt = args.sprintStartAt ?? 0;
|
|
377
|
+
const issueMaxResults = args.issueMaxResults ?? 25;
|
|
378
|
+
const issueStartAt = args.issueStartAt ?? 0;
|
|
379
|
+
const board = await this.requestAgile('GET', `/board/${args.boardId}`);
|
|
380
|
+
const sprintParams = new URLSearchParams({
|
|
381
|
+
maxResults: String(sprintMaxResults),
|
|
382
|
+
startAt: String(sprintStartAt),
|
|
383
|
+
});
|
|
384
|
+
if (args.sprintState) {
|
|
385
|
+
sprintParams.set('state', args.sprintState);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
sprintParams.set('state', 'active,future');
|
|
389
|
+
}
|
|
390
|
+
const sprints = await this.requestAgile('GET', `/board/${args.boardId}/sprint?${sprintParams}`);
|
|
391
|
+
if (!sprints || sprints.values.length === 0) {
|
|
392
|
+
return text(`Board ${args.boardId}${board?.name ? ` (${board.name})` : ''}: no matching sprints.`);
|
|
393
|
+
}
|
|
394
|
+
const issueFilterClauses = [];
|
|
223
395
|
if (args.assignee)
|
|
224
|
-
|
|
225
|
-
if (args.
|
|
226
|
-
|
|
227
|
-
const
|
|
396
|
+
issueFilterClauses.push(`assignee = ${JSON.stringify(args.assignee)}`);
|
|
397
|
+
if (args.status)
|
|
398
|
+
issueFilterClauses.push(`status = ${JSON.stringify(args.status)}`);
|
|
399
|
+
const issueJql = issueFilterClauses.length > 0 ? issueFilterClauses.join(' AND ') : '';
|
|
400
|
+
const sprintIssueData = includeIssues
|
|
401
|
+
? await Promise.all(sprints.values.map(async (sprint) => {
|
|
402
|
+
const params = new URLSearchParams({
|
|
403
|
+
maxResults: String(issueMaxResults),
|
|
404
|
+
startAt: String(issueStartAt),
|
|
405
|
+
fields: 'summary,status,assignee,priority,issuetype',
|
|
406
|
+
});
|
|
407
|
+
if (issueJql)
|
|
408
|
+
params.set('jql', issueJql);
|
|
409
|
+
const issues = await this.requestAgile('GET', `/sprint/${sprint.id}/issue?${params}`);
|
|
410
|
+
return { sprintId: sprint.id, issues };
|
|
411
|
+
}))
|
|
412
|
+
: [];
|
|
413
|
+
const issueBySprint = new Map(sprintIssueData.map((entry) => [entry.sprintId, entry.issues]));
|
|
414
|
+
const lines = [
|
|
415
|
+
`Board: [${args.boardId}] ${board?.name ?? '(unknown)'} | ${board?.type ?? '(unknown type)'}`,
|
|
416
|
+
`URL: ${this.boardUrl(args.boardId)}`,
|
|
417
|
+
`Sprints: ${sprints.values.length}`,
|
|
418
|
+
'',
|
|
419
|
+
];
|
|
420
|
+
sprints.values.forEach((sprint, idx) => {
|
|
421
|
+
const window = [sprint.startDate?.slice(0, 10), sprint.endDate?.slice(0, 10)].filter(Boolean).join(' -> ');
|
|
422
|
+
lines.push(`${sprintStartAt + idx + 1}. [${sprint.id}] ${sprint.name} | ${sprint.state}${window ? ` | ${window}` : ''} | ${this.sprintUrl(args.boardId, sprint.id)}`);
|
|
423
|
+
if (sprint.goal?.trim()) {
|
|
424
|
+
lines.push(` Goal: ${sprint.goal}`);
|
|
425
|
+
}
|
|
426
|
+
if (includeIssues) {
|
|
427
|
+
const issueData = issueBySprint.get(sprint.id);
|
|
428
|
+
const issues = issueData?.issues ?? [];
|
|
429
|
+
lines.push(` Issues: ${issueData?.total ?? 0}`);
|
|
430
|
+
for (const issue of issues) {
|
|
431
|
+
const assignee = issue.fields.assignee?.displayName ?? 'Unassigned';
|
|
432
|
+
lines.push(` - [${issue.key}] ${issue.fields.summary} | ${issue.fields.status.name} | ${assignee} | ${this.issueUrl(issue.key)}`);
|
|
433
|
+
}
|
|
434
|
+
if ((issueData?.total ?? 0) > issues.length) {
|
|
435
|
+
lines.push(` ...and ${(issueData?.total ?? 0) - issues.length} more (adjust issueStartAt/issueMaxResults).`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
lines.push('');
|
|
439
|
+
});
|
|
440
|
+
const sprintRangeEnd = sprintStartAt + sprints.values.length;
|
|
441
|
+
if (!sprints.isLast) {
|
|
442
|
+
lines.push(`More sprints available: use sprintStartAt=${sprintRangeEnd}.`);
|
|
443
|
+
}
|
|
444
|
+
return text(lines.join('\n').trimEnd());
|
|
445
|
+
}
|
|
446
|
+
async createIssue(args) {
|
|
447
|
+
const data = await this.createIssueInternal(args);
|
|
228
448
|
if (!data)
|
|
229
449
|
return text('Issue created.');
|
|
230
|
-
|
|
450
|
+
const url = this.issueUrl(data.key);
|
|
451
|
+
if (args.sprintId !== undefined) {
|
|
452
|
+
await this.addIssuesToSprintInternal(args.sprintId, [data.key]);
|
|
453
|
+
return text(`Created ${data.key} and added it to sprint ${args.sprintId}.\n${url}`);
|
|
454
|
+
}
|
|
455
|
+
return text(`Created ${data.key}.\n${url}`);
|
|
231
456
|
}
|
|
232
457
|
async updateIssue(args) {
|
|
233
|
-
const
|
|
234
|
-
if (args.
|
|
235
|
-
fields.summary = args.summary;
|
|
236
|
-
if (args.description !== undefined)
|
|
237
|
-
fields.description = args.description;
|
|
238
|
-
if (args.assignee !== undefined)
|
|
239
|
-
fields.assignee = { name: args.assignee };
|
|
240
|
-
if (args.priority !== undefined)
|
|
241
|
-
fields.priority = { name: args.priority };
|
|
242
|
-
if (Object.keys(fields).length === 0)
|
|
458
|
+
const hasFieldUpdates = await this.updateIssueFieldsInternal(args);
|
|
459
|
+
if (!hasFieldUpdates && args.sprintId === undefined)
|
|
243
460
|
return text('Nothing to update.');
|
|
244
|
-
|
|
245
|
-
|
|
461
|
+
if (args.sprintId !== undefined) {
|
|
462
|
+
await this.addIssuesToSprintInternal(args.sprintId, [args.issueKey]);
|
|
463
|
+
}
|
|
464
|
+
if (hasFieldUpdates && args.sprintId !== undefined) {
|
|
465
|
+
return text(`Updated ${args.issueKey} and added it to sprint ${args.sprintId}.\n${this.issueUrl(args.issueKey)}`);
|
|
466
|
+
}
|
|
467
|
+
if (hasFieldUpdates) {
|
|
468
|
+
return text(`Updated ${args.issueKey}.\n${this.issueUrl(args.issueKey)}`);
|
|
469
|
+
}
|
|
470
|
+
return text(`Added ${args.issueKey} to sprint ${args.sprintId}.\n${this.issueUrl(args.issueKey)}`);
|
|
471
|
+
}
|
|
472
|
+
async addIssuesToSprint(args) {
|
|
473
|
+
const keys = new Set();
|
|
474
|
+
if (args.issueKey?.trim())
|
|
475
|
+
keys.add(args.issueKey.trim());
|
|
476
|
+
for (const issueKey of args.issueKeys ?? []) {
|
|
477
|
+
const trimmed = issueKey.trim();
|
|
478
|
+
if (trimmed)
|
|
479
|
+
keys.add(trimmed);
|
|
480
|
+
}
|
|
481
|
+
if (keys.size === 0) {
|
|
482
|
+
throw new Error('Provide issueKey or issueKeys with at least one Jira issue key.');
|
|
483
|
+
}
|
|
484
|
+
const issueKeys = Array.from(keys);
|
|
485
|
+
await this.addIssuesToSprintInternal(args.sprintId, issueKeys);
|
|
486
|
+
if (issueKeys.length === 1) {
|
|
487
|
+
return text(`Added ${issueKeys[0]} to sprint ${args.sprintId}.\n${this.issueUrl(issueKeys[0])}`);
|
|
488
|
+
}
|
|
489
|
+
const lines = [
|
|
490
|
+
`Added ${issueKeys.length} issue(s) to sprint ${args.sprintId}.`,
|
|
491
|
+
...issueKeys.map((issueKey) => `${issueKey}: ${this.issueUrl(issueKey)}`),
|
|
492
|
+
];
|
|
493
|
+
return text(lines.join('\n'));
|
|
494
|
+
}
|
|
495
|
+
async mutateIssue(args) {
|
|
496
|
+
let issueKey = args.issueKey?.trim();
|
|
497
|
+
const actions = [];
|
|
498
|
+
if (args.create) {
|
|
499
|
+
const created = await this.createIssueInternal(args.create);
|
|
500
|
+
if (!created)
|
|
501
|
+
throw new Error('Issue creation did not return an issue key.');
|
|
502
|
+
issueKey = created.key;
|
|
503
|
+
actions.push('created issue');
|
|
504
|
+
}
|
|
505
|
+
if (!issueKey) {
|
|
506
|
+
throw new Error('Provide issueKey, or provide create with issueType and summary.');
|
|
507
|
+
}
|
|
508
|
+
if (args.update) {
|
|
509
|
+
const updated = await this.updateIssueFieldsInternal({ issueKey, ...args.update });
|
|
510
|
+
if (updated)
|
|
511
|
+
actions.push('updated fields');
|
|
512
|
+
}
|
|
513
|
+
if (args.sprintId !== undefined) {
|
|
514
|
+
await this.addIssuesToSprintInternal(args.sprintId, [issueKey]);
|
|
515
|
+
actions.push(`added to sprint ${args.sprintId}`);
|
|
516
|
+
}
|
|
517
|
+
if (args.transitionId || args.transitionName) {
|
|
518
|
+
const transitionId = await this.resolveTransitionId(issueKey, args.transitionId, args.transitionName);
|
|
519
|
+
await this.transitionIssueInternal(issueKey, transitionId);
|
|
520
|
+
actions.push(`transitioned via ${transitionId}`);
|
|
521
|
+
}
|
|
522
|
+
if (args.comment !== undefined) {
|
|
523
|
+
await this.request('POST', `/issue/${issueKey}/comment`, { body: validateCommentBody(args.comment) });
|
|
524
|
+
actions.push('added comment');
|
|
525
|
+
}
|
|
526
|
+
if (actions.length === 0) {
|
|
527
|
+
return text('Nothing to mutate.');
|
|
528
|
+
}
|
|
529
|
+
return text(`Mutated ${issueKey}: ${actions.join(', ')}.\n${this.issueUrl(issueKey)}`);
|
|
246
530
|
}
|
|
247
531
|
async getComments(args) {
|
|
248
532
|
const { issueKey, maxResults = 50, startAt = 0 } = args;
|
|
@@ -261,25 +545,8 @@ export class JiraClient {
|
|
|
261
545
|
return text(`Comment added to ${args.issueKey}.`);
|
|
262
546
|
}
|
|
263
547
|
async transitionIssue(args) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!requestedName) {
|
|
268
|
-
throw new Error('Provide transitionId or transitionName');
|
|
269
|
-
}
|
|
270
|
-
const data = await this.request('GET', `/issue/${args.issueKey}/transitions`);
|
|
271
|
-
const transitions = data?.transitions ?? [];
|
|
272
|
-
const lowered = requestedName.toLowerCase();
|
|
273
|
-
const match = transitions.find((t) => t.name.toLowerCase() === lowered);
|
|
274
|
-
if (!match) {
|
|
275
|
-
const available = transitions.map((t) => t.name).join(', ') || '(none)';
|
|
276
|
-
throw new Error(`Transition "${requestedName}" not found for ${args.issueKey}. Available: ${available}`);
|
|
277
|
-
}
|
|
278
|
-
transitionId = match.id;
|
|
279
|
-
}
|
|
280
|
-
await this.request('POST', `/issue/${args.issueKey}/transitions`, {
|
|
281
|
-
transition: { id: transitionId },
|
|
282
|
-
});
|
|
283
|
-
return text(`Transitioned ${args.issueKey} using transition ${transitionId}.`);
|
|
548
|
+
const transitionId = await this.resolveTransitionId(args.issueKey, args.transitionId, args.transitionName);
|
|
549
|
+
await this.transitionIssueInternal(args.issueKey, transitionId);
|
|
550
|
+
return text(`Transitioned ${args.issueKey} using transition ${transitionId}.\n${this.issueUrl(args.issueKey)}`);
|
|
284
551
|
}
|
|
285
552
|
}
|