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.
@@ -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
+ }