@abdelrahmanhsn/jira-mcp 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +55 -3
  2. package/package.json +1 -1
  3. package/server.js +248 -0
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @abdelrahmanhsn/jira-mcp
2
2
 
3
- A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that connects your AI IDE to Jira. Query your tickets, active sprint, and issue details directly from GitHub Copilot, Cursor, Claude Desktop, or any MCP-compatible client.
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that connects your AI IDE to Jira. Query tickets, manage sprints, and let AI autonomously implement, test, and ship Jira tickets all without leaving your editor.
4
+
5
+ Works with GitHub Copilot, Cursor, Claude Desktop, and any MCP-compatible client.
4
6
 
5
7
  ## Tools
6
8
 
@@ -14,12 +16,32 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that co
14
16
  | `get_sprint_summary` | Get all sprint tickets grouped by status (Todo / In Progress / Done) |
15
17
  | `search_tickets` | Search tickets with plain English or raw JQL |
16
18
  | `get_context_for_pr` | Extract Jira ticket from a branch name and return a ready-to-use PR description block |
19
+ | `start_ticket` | **🤖 Autonomous mode** — assigns ticket, moves to In Progress, creates git branch, then drives AI to implement, test, commit, push, open PR, and comment on Jira — non-stop |
17
20
 
18
21
  ## Prerequisites
19
22
 
20
23
  - Node.js 18 or later
21
24
  - A Jira Cloud account
22
25
  - A Jira API token ([generate one here](https://id.atlassian.com/manage-profile/security/api-tokens))
26
+ - **GitHub CLI** — required for `start_ticket` to create PRs automatically
27
+
28
+ ### Install GitHub CLI
29
+
30
+ ```bash
31
+ # macOS
32
+ brew install gh
33
+ gh auth login
34
+
35
+ # Windows
36
+ winget install --id GitHub.cli
37
+ gh auth login
38
+
39
+ # Linux
40
+ # See https://github.com/cli/cli/blob/trunk/docs/install_linux.md
41
+ gh auth login
42
+ ```
43
+
44
+ > If `gh` is not installed or not authenticated, `start_ticket` will return clear instructions instead of silently failing.
23
45
 
24
46
  ## Setup
25
47
 
@@ -115,17 +137,47 @@ Open `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
115
137
 
116
138
  ## Usage Examples
117
139
 
118
- Once configured, you can ask your AI assistant:
140
+ ### Everyday queries
119
141
 
120
142
  - *"Show me my current Jira tickets"*
121
143
  - *"What's in my active sprint?"*
122
144
  - *"Get me the details for PROJ-1234"*
123
- - *"Summarize the description of PROJ-5678"*
124
145
  - *"Add a comment to PROJ-123 saying the fix is deployed to staging"*
125
146
  - *"Give me my standup for today"*
126
147
  - *"Summarize the active sprint — how many tickets are done vs in progress?"*
127
148
  - *"Search for open bugs related to login"*
149
+
150
+ ### PR workflow
151
+
128
152
  - *"Get PR context for branch STUD-17891-add-email-icon"*
153
+ → Extracts the ticket key from the branch, fetches description + comments, returns a formatted PR description block ready to paste or expand.
154
+
155
+ ### Autonomous mode — `start_ticket`
156
+
157
+ The most powerful tool. One prompt and AI does everything:
158
+
159
+ ```
160
+ "Start working on STUD-17931"
161
+ ```
162
+
163
+ **What happens automatically, with no stops:**
164
+ 1. ✅ Self-assigns the Jira ticket to you
165
+ 2. ✅ Moves it to **In Progress**
166
+ 3. ✅ Creates and switches to a git branch (e.g. `stud-17931-content-preview-bug`)
167
+ 4. ✅ AI reads description, acceptance criteria, and comments
168
+ 5. ✅ Implements the feature/fix
169
+ 6. ✅ Runs the test suite — fixes failures automatically
170
+ 7. ✅ Commits and pushes the branch
171
+ 8. ✅ Opens a PR via `gh pr create`
172
+ 9. ✅ Posts the PR link as a comment on the Jira ticket
173
+
174
+ You can optionally pass the repo path:
175
+
176
+ ```
177
+ "Start working on STUD-17931 in /Users/you/code/my-project"
178
+ ```
179
+
180
+ If omitted, the tool auto-detects the git repo from the current working directory.
129
181
 
130
182
  ## Security
131
183
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abdelrahmanhsn/jira-mcp",
3
3
  "type": "module",
4
- "version": "1.3.0",
4
+ "version": "1.6.0",
5
5
  "description": "MCP server for Jira — query your tickets, active sprints, and issue details from any AI IDE (GitHub Copilot, Cursor, Claude Desktop)",
6
6
  "main": "server.js",
7
7
  "bin": {
package/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import axios from "axios";
3
+ import { execSync } from "child_process";
3
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
6
  import { z } from "zod";
@@ -269,6 +270,253 @@ server.tool(
269
270
  }
270
271
  );
271
272
 
273
+ // Tool: start_ticket
274
+ server.tool(
275
+ "start_ticket",
276
+ "Fully start a Jira ticket in AUTONOMOUS mode: self-assigns, moves to In Progress, creates the git branch, then returns agent instructions that drive non-stop autonomous implementation, testing, commit, push, PR creation, and Jira comment — no confirmation needed",
277
+ {
278
+ issueKey: z.string().describe("The Jira issue key, e.g. STUD-17891"),
279
+ workingDir: z.string().optional().describe("Absolute path to the git repo. If omitted, the tool auto-detects from the current working directory."),
280
+ },
281
+ async ({ issueKey, workingDir }) => {
282
+ const actions = [];
283
+
284
+ // ── Auto-detect workingDir if not provided ────────────────────────────────
285
+ if (!workingDir) {
286
+ try {
287
+ workingDir = execSync("git rev-parse --show-toplevel", { stdio: "pipe" }).toString().trim();
288
+ actions.push(`✅ Auto-detected repo: ${workingDir}`);
289
+ } catch {
290
+ return {
291
+ content: [{
292
+ type: "text",
293
+ text: JSON.stringify({
294
+ error: "Could not auto-detect the git repository.",
295
+ fix: "Re-run start_ticket and pass the workingDir parameter explicitly, e.g. /Users/you/code/my-project",
296
+ hint: "The AI should look at the currently open workspace/files in the editor to determine the correct path and retry.",
297
+ }, null, 2),
298
+ }],
299
+ };
300
+ }
301
+ }
302
+
303
+ // ── 0. Preflight: verify gh CLI is installed and authenticated ───────────
304
+ try {
305
+ execSync("gh --version", { stdio: "pipe" });
306
+ } catch {
307
+ return {
308
+ content: [{
309
+ type: "text",
310
+ text: JSON.stringify({
311
+ error: "GitHub CLI (gh) is not installed.",
312
+ fix: "Install it first, then re-run start_ticket.",
313
+ install_steps: {
314
+ macOS: "brew install gh",
315
+ windows: "winget install --id GitHub.cli",
316
+ linux: "https://github.com/cli/cli/blob/trunk/docs/install_linux.md",
317
+ },
318
+ after_install: "Run `gh auth login` to authenticate, then retry.",
319
+ }, null, 2),
320
+ }],
321
+ };
322
+ }
323
+
324
+ try {
325
+ execSync("gh auth status", { stdio: "pipe" });
326
+ } catch {
327
+ return {
328
+ content: [{
329
+ type: "text",
330
+ text: JSON.stringify({
331
+ error: "GitHub CLI is installed but not authenticated.",
332
+ fix: "Run `gh auth login` in your terminal, complete the browser flow, then retry start_ticket.",
333
+ }, null, 2),
334
+ }],
335
+ };
336
+ }
337
+
338
+ actions.push("✅ GitHub CLI (gh) is installed and authenticated");
339
+
340
+ // ── 1. Fetch ticket + comments + current user in parallel ────────────────
341
+ const [issueRes, commentsRes, myselfRes, transitionsRes] = await Promise.all([
342
+ jiraClient.get(`/issue/${issueKey}`, {
343
+ params: {
344
+ fields: "summary,description,issuetype,priority,status,assignee,reporter,labels,components,customfield_10016,customfield_10014,customfield_10021,subtasks,parent",
345
+ },
346
+ }),
347
+ jiraClient.get(`/issue/${issueKey}/comment`, {
348
+ params: { maxResults: 20, orderBy: "-created" },
349
+ }),
350
+ jiraClient.get("/myself"),
351
+ jiraClient.get(`/issue/${issueKey}/transitions`),
352
+ ]);
353
+
354
+ const f = issueRes.data.fields;
355
+ const me = myselfRes.data;
356
+
357
+ // Extract plain text from Atlassian Document Format
358
+ function extractText(node) {
359
+ if (!node) return "";
360
+ if (node.type === "text") return node.text || "";
361
+ if (node.type === "hardBreak") return "\n";
362
+ if (node.content) return node.content.map(extractText).join(node.type === "paragraph" ? "\n" : " ");
363
+ return "";
364
+ }
365
+
366
+ const description = extractText(f.description).trim() || "No description provided.";
367
+ const acceptanceCriteria = extractText(
368
+ f["customfield_10016"] || f["customfield_10014"] || f["customfield_10021"] || null
369
+ ).trim();
370
+
371
+ const comments = (commentsRes.data.comments || [])
372
+ .map(c => ({ author: c.author?.displayName, body: extractText(c.body).trim(), created: c.created?.split("T")[0] }))
373
+ .filter(c => c.body);
374
+
375
+ const subtasks = (f.subtasks || []).map(s => ({
376
+ key: s.key,
377
+ summary: s.fields?.summary,
378
+ status: s.fields?.status?.name,
379
+ }));
380
+
381
+ // ── 2. Self-assign the ticket ─────────────────────────────────────────────
382
+ try {
383
+ await jiraClient.put(`/issue/${issueKey}/assignee`, { accountId: me.accountId });
384
+ actions.push(`✅ Assigned to ${me.displayName}`);
385
+ } catch (e) {
386
+ actions.push(`⚠️ Could not assign: ${e.response?.data?.errorMessages?.[0] ?? e.message}`);
387
+ }
388
+
389
+ // ── 3. Transition to "In Progress" ────────────────────────────────────────
390
+ const transitions = transitionsRes.data.transitions || [];
391
+ const inProgress = transitions.find(t =>
392
+ /in.?progress|start|doing|in.?dev/i.test(t.name)
393
+ );
394
+ if (inProgress) {
395
+ try {
396
+ await jiraClient.post(`/issue/${issueKey}/transitions`, { transition: { id: inProgress.id } });
397
+ actions.push(`✅ Moved to "${inProgress.name}"`);
398
+ } catch (e) {
399
+ actions.push(`⚠️ Could not transition: ${e.response?.data?.errorMessages?.[0] ?? e.message}`);
400
+ }
401
+ } else {
402
+ const available = transitions.map(t => t.name).join(", ");
403
+ actions.push(`⚠️ No "In Progress" transition found. Available: ${available}`);
404
+ }
405
+
406
+ // ── 4. Create git branch ──────────────────────────────────────────────────
407
+ const slugifiedSummary = (f.summary || "")
408
+ .toLowerCase()
409
+ .replace(/[^a-z0-9\s-]/g, "")
410
+ .trim()
411
+ .replace(/\s+/g, "-")
412
+ .slice(0, 50)
413
+ .replace(/-+$/, "");
414
+ const branchName = `${issueKey.toLowerCase()}-${slugifiedSummary}`;
415
+
416
+ try {
417
+ execSync(`git -C "${workingDir}" checkout -b "${branchName}"`, { stdio: "pipe" });
418
+ actions.push(`✅ Created and switched to branch: ${branchName}`);
419
+ } catch (e) {
420
+ const stderr = e.stderr?.toString() ?? "";
421
+ if (stderr.includes("already exists")) {
422
+ try {
423
+ execSync(`git -C "${workingDir}" checkout "${branchName}"`, { stdio: "pipe" });
424
+ actions.push(`✅ Branch already exists — switched to: ${branchName}`);
425
+ } catch {
426
+ actions.push(`⚠️ Branch exists but could not switch: ${stderr}`);
427
+ }
428
+ } else {
429
+ actions.push(`⚠️ Git error: ${stderr || e.message}`);
430
+ }
431
+ }
432
+
433
+ const ticketUrl = `https://${JIRA_DOMAIN}/browse/${issueKey}`;
434
+
435
+ const agentInstructions = [
436
+ `You are now in AUTONOMOUS CODING MODE for ${issueKey}.`,
437
+ ``,
438
+ `RULES — follow these exactly, do NOT stop or ask for confirmation at any point:`,
439
+ `1. You are already on branch \`${branchName}\` in \`${workingDir}\` — start coding immediately.`,
440
+ `2. Read the description and acceptance criteria below — implement everything required.`,
441
+ `3. After implementing, run the test suite automatically. Fix any failures before moving on.`,
442
+ `4. If you encounter an ambiguity, make the most reasonable assumption and document it in a code comment — do NOT pause to ask.`,
443
+ `5. After all tests pass, run: \`git -C "${workingDir}" add -A && git -C "${workingDir}" commit -m "${issueKey}: ${f.summary}" && git -C "${workingDir}" push -u origin ${branchName}\``,
444
+ `6. Create the PR by running: \`gh pr create --repo $(git -C "${workingDir}" remote get-url origin) --title "${issueKey}: ${f.summary}" --body "Resolves ${ticketUrl}" --head ${branchName}\``,
445
+ `7. Capture the PR URL from the gh output and call the add_comment tool to post it on ${issueKey}.`,
446
+ `8. Only stop when the PR is open and the Jira ticket has been commented. That is the definition of done.`,
447
+ ].join("\n");
448
+
449
+ const brief = [
450
+ `# 🤖 AUTONOMOUS MODE — ${issueKey}: ${f.summary}`,
451
+ ``,
452
+ agentInstructions,
453
+ ``,
454
+ `**Type:** ${f.issuetype?.name} | **Priority:** ${f.priority?.name}`,
455
+ `**Assignee:** ${me.displayName} | **Reporter:** ${f.reporter?.displayName}`,
456
+ f.labels?.length ? `**Labels:** ${f.labels.join(", ")}` : "",
457
+ f.components?.length ? `**Components:** ${f.components.map(c => c.name).join(", ")}` : "",
458
+ `**Ticket:** ${ticketUrl}`,
459
+ ``,
460
+ `## ⚡ Actions Taken`,
461
+ ...actions.map(a => `- ${a}`),
462
+ ``,
463
+ `## 📋 Description`,
464
+ description,
465
+ acceptanceCriteria ? `\n## ✅ Acceptance Criteria\n${acceptanceCriteria}` : "",
466
+ subtasks.length > 0
467
+ ? `\n## 🔀 Subtasks\n${subtasks.map(s => `- [${s.status === "Done" ? "x" : " "}] ${s.key}: ${s.summary} (${s.status})`).join("\n")}`
468
+ : "",
469
+ comments.length > 0
470
+ ? `\n## 💬 Recent Comments\n${comments.slice(0, 5).map(c => `**${c.author}** (${c.created}):\n${c.body}`).join("\n\n---\n\n")}`
471
+ : "",
472
+ ``,
473
+ `## 🚀 Implementation Checklist`,
474
+ `- [ ] Implement the feature/fix on branch \`${branchName}\``,
475
+ `- [ ] Write unit tests`,
476
+ `- [ ] Write integration tests if applicable`,
477
+ `- [ ] Run full test suite`,
478
+ `- [ ] Create PR with title: \`${issueKey}: ${f.summary}\``,
479
+ `- [ ] Link PR to ticket: ${ticketUrl}`,
480
+ ``,
481
+ `## 📝 PR Description Template`,
482
+ `### What`,
483
+ description.split("\n")[0],
484
+ ``,
485
+ `### Why`,
486
+ `Resolves ${issueKey} — ${ticketUrl}`,
487
+ ``,
488
+ `### How`,
489
+ `<!-- Describe your implementation approach -->`,
490
+ ``,
491
+ `### Testing`,
492
+ `- [ ] Unit tests added/updated`,
493
+ `- [ ] Integration tests added/updated`,
494
+ `- [ ] Manually tested`,
495
+ ].filter(l => l !== null && l !== undefined).join("\n");
496
+
497
+ return {
498
+ content: [{
499
+ type: "text",
500
+ text: JSON.stringify({
501
+ issue_key: issueKey,
502
+ summary: f.summary,
503
+ type: f.issuetype?.name,
504
+ priority: f.priority?.name,
505
+ branch_name: branchName,
506
+ ticket_url: ticketUrl,
507
+ actions_taken: actions,
508
+ agent_instructions: agentInstructions,
509
+ description,
510
+ acceptance_criteria: acceptanceCriteria || null,
511
+ subtasks,
512
+ comments,
513
+ brief,
514
+ }, null, 2),
515
+ }],
516
+ };
517
+ }
518
+ );
519
+
272
520
  // ── Start ────────────────────────────────────────────────────────────────────
273
521
  const transport = new StdioServerTransport();
274
522
  await server.connect(transport);