jira-pilot 2.0.0 → 2.0.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/LICENSE +5 -5
- package/README.md +465 -404
- package/bin/jira.js +62 -55
- package/package.json +90 -84
- package/src/commands/ai-actions/plan.js +119 -0
- package/src/commands/ai-actions/review.js +109 -0
- package/src/commands/ai-actions/standup.js +42 -0
- package/src/commands/ai.js +232 -204
- package/src/commands/board.js +75 -66
- package/src/commands/bulk.js +108 -0
- package/src/commands/config.js +224 -154
- package/src/commands/dashboard.js +89 -0
- package/src/commands/git.js +63 -60
- package/src/commands/issue.js +985 -698
- package/src/commands/mcp.js +20 -20
- package/src/commands/project.js +59 -50
- package/src/commands/sprint.js +153 -78
- package/src/server/mcp-server.js +332 -332
- package/src/services/ai-service.js +165 -107
- package/src/services/api-service.js +115 -115
- package/src/utils/adf-parser.js +49 -49
- package/src/utils/config.js +97 -60
- package/src/utils/error-handler.js +41 -41
- package/src/utils/text-to-adf.js +34 -34
- package/src/utils/validators.js +88 -0
package/src/server/mcp-server.js
CHANGED
|
@@ -1,332 +1,332 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { api } from "../services/api-service.js";
|
|
5
|
-
import { textToADF } from "../utils/text-to-adf.js";
|
|
6
|
-
|
|
7
|
-
// Initialize MCP Server
|
|
8
|
-
const server = new Server(
|
|
9
|
-
{
|
|
10
|
-
name: "jira-pilot",
|
|
11
|
-
version: "1.0.0",
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
capabilities: {
|
|
15
|
-
tools: {},
|
|
16
|
-
},
|
|
17
|
-
}
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
// ── Tool Definitions ─────────────────────────────────────────────────
|
|
21
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
22
|
-
return {
|
|
23
|
-
tools: [
|
|
24
|
-
// ── Issues ──────────────────────────────────────
|
|
25
|
-
{
|
|
26
|
-
name: "jira_list_issues",
|
|
27
|
-
description: "List Jira issues using JQL. Returns key, summary, status, and assignee for each issue.",
|
|
28
|
-
inputSchema: {
|
|
29
|
-
type: "object",
|
|
30
|
-
properties: {
|
|
31
|
-
jql: { type: "string", description: "JQL query string (e.g., 'project = PROJ AND status = \"In Progress\"')" },
|
|
32
|
-
limit: { type: "number", description: "Max results (default: 10)", default: 10 }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: "jira_get_issue",
|
|
38
|
-
description: "Get full details of a specific Jira issue including summary, description, status, assignee, priority, and comments.",
|
|
39
|
-
inputSchema: {
|
|
40
|
-
type: "object",
|
|
41
|
-
properties: {
|
|
42
|
-
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" }
|
|
43
|
-
},
|
|
44
|
-
required: ["issueKey"]
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: "jira_create_issue",
|
|
49
|
-
description: "Create a new Jira issue in a project. Returns the created issue key.",
|
|
50
|
-
inputSchema: {
|
|
51
|
-
type: "object",
|
|
52
|
-
properties: {
|
|
53
|
-
projectKey: { type: "string", description: "Project Key (e.g., PROJ)" },
|
|
54
|
-
summary: { type: "string", description: "Issue summary/title" },
|
|
55
|
-
description: { type: "string", description: "Issue description (plain text, will be converted to ADF)" },
|
|
56
|
-
issueType: { type: "string", description: "Issue Type (Bug, Story, Task, Epic)", default: "Task" },
|
|
57
|
-
priority: { type: "string", description: "Priority name (e.g., High, Medium, Low)" },
|
|
58
|
-
assigneeId: { type: "string", description: "Assignee account ID" }
|
|
59
|
-
},
|
|
60
|
-
required: ["projectKey", "summary"]
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: "jira_transition_issue",
|
|
65
|
-
description: "Transition a Jira issue to a new status. First call with only issueKey to see available transitions, then call again with the transitionId.",
|
|
66
|
-
inputSchema: {
|
|
67
|
-
type: "object",
|
|
68
|
-
properties: {
|
|
69
|
-
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
70
|
-
transitionId: { type: "string", description: "Transition ID to execute. Omit to list available transitions." }
|
|
71
|
-
},
|
|
72
|
-
required: ["issueKey"]
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: "jira_assign_issue",
|
|
77
|
-
description: "Assign or unassign a Jira issue. Use accountId to assign, or null to unassign.",
|
|
78
|
-
inputSchema: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: {
|
|
81
|
-
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
82
|
-
accountId: { type: ["string", "null"], description: "Account ID of the assignee. Set to null to unassign. Use 'me' to assign to yourself." }
|
|
83
|
-
},
|
|
84
|
-
required: ["issueKey"]
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: "jira_add_comment",
|
|
89
|
-
description: "Add a comment to a Jira issue.",
|
|
90
|
-
inputSchema: {
|
|
91
|
-
type: "object",
|
|
92
|
-
properties: {
|
|
93
|
-
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
94
|
-
body: { type: "string", description: "Comment text (plain text, will be converted to ADF)" }
|
|
95
|
-
},
|
|
96
|
-
required: ["issueKey", "body"]
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
// ── Projects & Sprints ──────────────────────────
|
|
101
|
-
{
|
|
102
|
-
name: "jira_list_projects",
|
|
103
|
-
description: "List all accessible Jira projects. Returns project key, name, lead, and style.",
|
|
104
|
-
inputSchema: {
|
|
105
|
-
type: "object",
|
|
106
|
-
properties: {
|
|
107
|
-
limit: { type: "number", description: "Max results (default: 50)", default: 50 }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: "jira_list_sprints",
|
|
113
|
-
description: "List sprints for a Jira board. Requires a board ID.",
|
|
114
|
-
inputSchema: {
|
|
115
|
-
type: "object",
|
|
116
|
-
properties: {
|
|
117
|
-
boardId: { type: "number", description: "Board ID (numeric)" },
|
|
118
|
-
state: { type: "string", description: "Sprint state filter: active, future, closed (comma-separated)", default: "active,future" }
|
|
119
|
-
},
|
|
120
|
-
required: ["boardId"]
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
]
|
|
124
|
-
};
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// ── Tool Handlers ────────────────────────────────────────────────────
|
|
128
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
129
|
-
const { name, arguments: args } = request.params;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
// ── jira_list_issues ────────────────────────────────
|
|
133
|
-
if (name === "jira_list_issues") {
|
|
134
|
-
const jql = args.jql || "";
|
|
135
|
-
const limit = args.limit || 10;
|
|
136
|
-
const data = await api.post('/search/jql', {
|
|
137
|
-
jql,
|
|
138
|
-
maxResults: limit,
|
|
139
|
-
fields: ['summary', 'status', 'assignee', 'priority', 'created', 'updated']
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Return a cleaner format for LLM consumption
|
|
143
|
-
const issues = (data.issues || []).map(i => ({
|
|
144
|
-
key: i.key,
|
|
145
|
-
summary: i.fields.summary,
|
|
146
|
-
status: i.fields.status?.name,
|
|
147
|
-
assignee: i.fields.assignee?.displayName || 'Unassigned',
|
|
148
|
-
priority: i.fields.priority?.name,
|
|
149
|
-
created: i.fields.created?.split('T')[0],
|
|
150
|
-
updated: i.fields.updated?.split('T')[0]
|
|
151
|
-
}));
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
content: [{ type: "text", text: JSON.stringify(issues, null, 2) }]
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ── jira_get_issue ──────────────────────────────────
|
|
159
|
-
if (name === "jira_get_issue") {
|
|
160
|
-
const data = await api.get(`/issue/${args.issueKey}`);
|
|
161
|
-
|
|
162
|
-
// Return a cleaner summary for agents
|
|
163
|
-
const result = {
|
|
164
|
-
key: data.key,
|
|
165
|
-
summary: data.fields.summary,
|
|
166
|
-
status: data.fields.status?.name,
|
|
167
|
-
issueType: data.fields.issuetype?.name,
|
|
168
|
-
priority: data.fields.priority?.name,
|
|
169
|
-
assignee: data.fields.assignee?.displayName || 'Unassigned',
|
|
170
|
-
assigneeAccountId: data.fields.assignee?.accountId || null,
|
|
171
|
-
reporter: data.fields.reporter?.displayName,
|
|
172
|
-
created: data.fields.created,
|
|
173
|
-
updated: data.fields.updated,
|
|
174
|
-
description: data.fields.description,
|
|
175
|
-
labels: data.fields.labels,
|
|
176
|
-
comments: data.fields.comment?.comments?.map(c => ({
|
|
177
|
-
author: c.author.displayName,
|
|
178
|
-
body: c.body,
|
|
179
|
-
created: c.created
|
|
180
|
-
})) || []
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── jira_create_issue ───────────────────────────────
|
|
189
|
-
if (name === "jira_create_issue") {
|
|
190
|
-
const body = {
|
|
191
|
-
fields: {
|
|
192
|
-
project: { key: args.projectKey },
|
|
193
|
-
summary: args.summary,
|
|
194
|
-
issuetype: { name: args.issueType || 'Task' }
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// Convert plain text description to ADF
|
|
199
|
-
if (args.description) {
|
|
200
|
-
body.fields.description = textToADF(args.description);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (args.priority) {
|
|
204
|
-
body.fields.priority = { name: args.priority };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (args.assigneeId) {
|
|
208
|
-
body.fields.assignee = { accountId: args.assigneeId };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const data = await api.post('/issue', body);
|
|
212
|
-
return {
|
|
213
|
-
content: [{ type: "text", text: JSON.stringify({ key: data.key, self: data.self }, null, 2) }]
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ── jira_transition_issue ───────────────────────────
|
|
218
|
-
if (name === "jira_transition_issue") {
|
|
219
|
-
if (!args.transitionId) {
|
|
220
|
-
// List available transitions
|
|
221
|
-
const transData = await api.get(`/issue/${args.issueKey}/transitions`);
|
|
222
|
-
const issue = await api.get(`/issue/${args.issueKey}?fields=summary,status`);
|
|
223
|
-
|
|
224
|
-
const result = {
|
|
225
|
-
issueKey: args.issueKey,
|
|
226
|
-
summary: issue.fields.summary,
|
|
227
|
-
currentStatus: issue.fields.status?.name,
|
|
228
|
-
availableTransitions: (transData.transitions || []).map(t => ({
|
|
229
|
-
id: t.id,
|
|
230
|
-
name: t.name,
|
|
231
|
-
toStatus: t.to.name
|
|
232
|
-
}))
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Execute transition
|
|
241
|
-
await api.post(`/issue/${args.issueKey}/transitions`, {
|
|
242
|
-
transition: { id: args.transitionId }
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, transitionId: args.transitionId }) }]
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ── jira_assign_issue ───────────────────────────────
|
|
251
|
-
if (name === "jira_assign_issue") {
|
|
252
|
-
let accountId = args.accountId;
|
|
253
|
-
|
|
254
|
-
// Resolve "me" to actual account ID
|
|
255
|
-
if (accountId === 'me') {
|
|
256
|
-
const myself = await api.get('/myself');
|
|
257
|
-
accountId = myself.accountId;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
await api.put(`/issue/${args.issueKey}/assignee`, {
|
|
261
|
-
accountId: accountId || null
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, assignedTo: accountId || 'unassigned' }) }]
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ── jira_add_comment ────────────────────────────────
|
|
270
|
-
if (name === "jira_add_comment") {
|
|
271
|
-
const data = await api.post(`/issue/${args.issueKey}/comment`, {
|
|
272
|
-
body: textToADF(args.body)
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, commentId: data.id }) }]
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ── jira_list_projects ──────────────────────────────
|
|
281
|
-
if (name === "jira_list_projects") {
|
|
282
|
-
const limit = args.limit || 50;
|
|
283
|
-
const data = await api.get(`/project/search?maxResults=${limit}`);
|
|
284
|
-
|
|
285
|
-
const projects = (data.values || []).map(p => ({
|
|
286
|
-
key: p.key,
|
|
287
|
-
name: p.name,
|
|
288
|
-
lead: p.lead?.displayName || 'N/A',
|
|
289
|
-
style: p.style,
|
|
290
|
-
projectType: p.projectTypeKey
|
|
291
|
-
}));
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }]
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ── jira_list_sprints ───────────────────────────────
|
|
299
|
-
if (name === "jira_list_sprints") {
|
|
300
|
-
const state = args.state || 'active,future';
|
|
301
|
-
const data = await api.agileGet(`/board/${args.boardId}/sprint?state=${state}`);
|
|
302
|
-
|
|
303
|
-
const sprints = (data.values || []).map(s => ({
|
|
304
|
-
id: s.id,
|
|
305
|
-
name: s.name,
|
|
306
|
-
state: s.state,
|
|
307
|
-
startDate: s.startDate?.split('T')[0] || null,
|
|
308
|
-
endDate: s.endDate?.split('T')[0] || null,
|
|
309
|
-
goal: s.goal || null
|
|
310
|
-
}));
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
content: [{ type: "text", text: JSON.stringify(sprints, null, 2) }]
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
318
|
-
|
|
319
|
-
} catch (e) {
|
|
320
|
-
const errorMessage = e.response?.data ? JSON.stringify(e.response.data) : e.message;
|
|
321
|
-
return {
|
|
322
|
-
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
323
|
-
isError: true
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Start Server
|
|
329
|
-
export async function startServer() {
|
|
330
|
-
const transport = new StdioServerTransport();
|
|
331
|
-
await server.connect(transport);
|
|
332
|
-
}
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { api } from "../services/api-service.js";
|
|
5
|
+
import { textToADF } from "../utils/text-to-adf.js";
|
|
6
|
+
|
|
7
|
+
// Initialize MCP Server
|
|
8
|
+
const server = new Server(
|
|
9
|
+
{
|
|
10
|
+
name: "jira-pilot",
|
|
11
|
+
version: "1.0.0",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
capabilities: {
|
|
15
|
+
tools: {},
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// ── Tool Definitions ─────────────────────────────────────────────────
|
|
21
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
22
|
+
return {
|
|
23
|
+
tools: [
|
|
24
|
+
// ── Issues ──────────────────────────────────────
|
|
25
|
+
{
|
|
26
|
+
name: "jira_list_issues",
|
|
27
|
+
description: "List Jira issues using JQL. Returns key, summary, status, and assignee for each issue.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
jql: { type: "string", description: "JQL query string (e.g., 'project = PROJ AND status = \"In Progress\"')" },
|
|
32
|
+
limit: { type: "number", description: "Max results (default: 10)", default: 10 }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "jira_get_issue",
|
|
38
|
+
description: "Get full details of a specific Jira issue including summary, description, status, assignee, priority, and comments.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" }
|
|
43
|
+
},
|
|
44
|
+
required: ["issueKey"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "jira_create_issue",
|
|
49
|
+
description: "Create a new Jira issue in a project. Returns the created issue key.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
projectKey: { type: "string", description: "Project Key (e.g., PROJ)" },
|
|
54
|
+
summary: { type: "string", description: "Issue summary/title" },
|
|
55
|
+
description: { type: "string", description: "Issue description (plain text, will be converted to ADF)" },
|
|
56
|
+
issueType: { type: "string", description: "Issue Type (Bug, Story, Task, Epic)", default: "Task" },
|
|
57
|
+
priority: { type: "string", description: "Priority name (e.g., High, Medium, Low)" },
|
|
58
|
+
assigneeId: { type: "string", description: "Assignee account ID" }
|
|
59
|
+
},
|
|
60
|
+
required: ["projectKey", "summary"]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "jira_transition_issue",
|
|
65
|
+
description: "Transition a Jira issue to a new status. First call with only issueKey to see available transitions, then call again with the transitionId.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
70
|
+
transitionId: { type: "string", description: "Transition ID to execute. Omit to list available transitions." }
|
|
71
|
+
},
|
|
72
|
+
required: ["issueKey"]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "jira_assign_issue",
|
|
77
|
+
description: "Assign or unassign a Jira issue. Use accountId to assign, or null to unassign.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
82
|
+
accountId: { type: ["string", "null"], description: "Account ID of the assignee. Set to null to unassign. Use 'me' to assign to yourself." }
|
|
83
|
+
},
|
|
84
|
+
required: ["issueKey"]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "jira_add_comment",
|
|
89
|
+
description: "Add a comment to a Jira issue.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
94
|
+
body: { type: "string", description: "Comment text (plain text, will be converted to ADF)" }
|
|
95
|
+
},
|
|
96
|
+
required: ["issueKey", "body"]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ── Projects & Sprints ──────────────────────────
|
|
101
|
+
{
|
|
102
|
+
name: "jira_list_projects",
|
|
103
|
+
description: "List all accessible Jira projects. Returns project key, name, lead, and style.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
limit: { type: "number", description: "Max results (default: 50)", default: 50 }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "jira_list_sprints",
|
|
113
|
+
description: "List sprints for a Jira board. Requires a board ID.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
boardId: { type: "number", description: "Board ID (numeric)" },
|
|
118
|
+
state: { type: "string", description: "Sprint state filter: active, future, closed (comma-separated)", default: "active,future" }
|
|
119
|
+
},
|
|
120
|
+
required: ["boardId"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── Tool Handlers ────────────────────────────────────────────────────
|
|
128
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
129
|
+
const { name, arguments: args } = request.params;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// ── jira_list_issues ────────────────────────────────
|
|
133
|
+
if (name === "jira_list_issues") {
|
|
134
|
+
const jql = args.jql || "";
|
|
135
|
+
const limit = args.limit || 10;
|
|
136
|
+
const data = await api.post('/search/jql', {
|
|
137
|
+
jql,
|
|
138
|
+
maxResults: limit,
|
|
139
|
+
fields: ['summary', 'status', 'assignee', 'priority', 'created', 'updated']
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Return a cleaner format for LLM consumption
|
|
143
|
+
const issues = (data.issues || []).map(i => ({
|
|
144
|
+
key: i.key,
|
|
145
|
+
summary: i.fields.summary,
|
|
146
|
+
status: i.fields.status?.name,
|
|
147
|
+
assignee: i.fields.assignee?.displayName || 'Unassigned',
|
|
148
|
+
priority: i.fields.priority?.name,
|
|
149
|
+
created: i.fields.created?.split('T')[0],
|
|
150
|
+
updated: i.fields.updated?.split('T')[0]
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text: JSON.stringify(issues, null, 2) }]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── jira_get_issue ──────────────────────────────────
|
|
159
|
+
if (name === "jira_get_issue") {
|
|
160
|
+
const data = await api.get(`/issue/${args.issueKey}`);
|
|
161
|
+
|
|
162
|
+
// Return a cleaner summary for agents
|
|
163
|
+
const result = {
|
|
164
|
+
key: data.key,
|
|
165
|
+
summary: data.fields.summary,
|
|
166
|
+
status: data.fields.status?.name,
|
|
167
|
+
issueType: data.fields.issuetype?.name,
|
|
168
|
+
priority: data.fields.priority?.name,
|
|
169
|
+
assignee: data.fields.assignee?.displayName || 'Unassigned',
|
|
170
|
+
assigneeAccountId: data.fields.assignee?.accountId || null,
|
|
171
|
+
reporter: data.fields.reporter?.displayName,
|
|
172
|
+
created: data.fields.created,
|
|
173
|
+
updated: data.fields.updated,
|
|
174
|
+
description: data.fields.description,
|
|
175
|
+
labels: data.fields.labels,
|
|
176
|
+
comments: data.fields.comment?.comments?.map(c => ({
|
|
177
|
+
author: c.author.displayName,
|
|
178
|
+
body: c.body,
|
|
179
|
+
created: c.created
|
|
180
|
+
})) || []
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── jira_create_issue ───────────────────────────────
|
|
189
|
+
if (name === "jira_create_issue") {
|
|
190
|
+
const body = {
|
|
191
|
+
fields: {
|
|
192
|
+
project: { key: args.projectKey },
|
|
193
|
+
summary: args.summary,
|
|
194
|
+
issuetype: { name: args.issueType || 'Task' }
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Convert plain text description to ADF
|
|
199
|
+
if (args.description) {
|
|
200
|
+
body.fields.description = textToADF(args.description);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (args.priority) {
|
|
204
|
+
body.fields.priority = { name: args.priority };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (args.assigneeId) {
|
|
208
|
+
body.fields.assignee = { accountId: args.assigneeId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = await api.post('/issue', body);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: JSON.stringify({ key: data.key, self: data.self }, null, 2) }]
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── jira_transition_issue ───────────────────────────
|
|
218
|
+
if (name === "jira_transition_issue") {
|
|
219
|
+
if (!args.transitionId) {
|
|
220
|
+
// List available transitions
|
|
221
|
+
const transData = await api.get(`/issue/${args.issueKey}/transitions`);
|
|
222
|
+
const issue = await api.get(`/issue/${args.issueKey}?fields=summary,status`);
|
|
223
|
+
|
|
224
|
+
const result = {
|
|
225
|
+
issueKey: args.issueKey,
|
|
226
|
+
summary: issue.fields.summary,
|
|
227
|
+
currentStatus: issue.fields.status?.name,
|
|
228
|
+
availableTransitions: (transData.transitions || []).map(t => ({
|
|
229
|
+
id: t.id,
|
|
230
|
+
name: t.name,
|
|
231
|
+
toStatus: t.to.name
|
|
232
|
+
}))
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Execute transition
|
|
241
|
+
await api.post(`/issue/${args.issueKey}/transitions`, {
|
|
242
|
+
transition: { id: args.transitionId }
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, transitionId: args.transitionId }) }]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── jira_assign_issue ───────────────────────────────
|
|
251
|
+
if (name === "jira_assign_issue") {
|
|
252
|
+
let accountId = args.accountId;
|
|
253
|
+
|
|
254
|
+
// Resolve "me" to actual account ID
|
|
255
|
+
if (accountId === 'me') {
|
|
256
|
+
const myself = await api.get('/myself');
|
|
257
|
+
accountId = myself.accountId;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await api.put(`/issue/${args.issueKey}/assignee`, {
|
|
261
|
+
accountId: accountId || null
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, assignedTo: accountId || 'unassigned' }) }]
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── jira_add_comment ────────────────────────────────
|
|
270
|
+
if (name === "jira_add_comment") {
|
|
271
|
+
const data = await api.post(`/issue/${args.issueKey}/comment`, {
|
|
272
|
+
body: textToADF(args.body)
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, commentId: data.id }) }]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── jira_list_projects ──────────────────────────────
|
|
281
|
+
if (name === "jira_list_projects") {
|
|
282
|
+
const limit = args.limit || 50;
|
|
283
|
+
const data = await api.get(`/project/search?maxResults=${limit}`);
|
|
284
|
+
|
|
285
|
+
const projects = (data.values || []).map(p => ({
|
|
286
|
+
key: p.key,
|
|
287
|
+
name: p.name,
|
|
288
|
+
lead: p.lead?.displayName || 'N/A',
|
|
289
|
+
style: p.style,
|
|
290
|
+
projectType: p.projectTypeKey
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }]
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── jira_list_sprints ───────────────────────────────
|
|
299
|
+
if (name === "jira_list_sprints") {
|
|
300
|
+
const state = args.state || 'active,future';
|
|
301
|
+
const data = await api.agileGet(`/board/${args.boardId}/sprint?state=${state}`);
|
|
302
|
+
|
|
303
|
+
const sprints = (data.values || []).map(s => ({
|
|
304
|
+
id: s.id,
|
|
305
|
+
name: s.name,
|
|
306
|
+
state: s.state,
|
|
307
|
+
startDate: s.startDate?.split('T')[0] || null,
|
|
308
|
+
endDate: s.endDate?.split('T')[0] || null,
|
|
309
|
+
goal: s.goal || null
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: JSON.stringify(sprints, null, 2) }]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
318
|
+
|
|
319
|
+
} catch (e) {
|
|
320
|
+
const errorMessage = e.response?.data ? JSON.stringify(e.response.data) : e.message;
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
323
|
+
isError: true
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Start Server
|
|
329
|
+
export async function startServer() {
|
|
330
|
+
const transport = new StdioServerTransport();
|
|
331
|
+
await server.connect(transport);
|
|
332
|
+
}
|