@abdelrahmanhsn/jira-mcp 1.0.1 → 1.3.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 +10 -0
  2. package/package.json +1 -1
  3. package/server.js +189 -0
package/README.md CHANGED
@@ -9,6 +9,11 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that co
9
9
  | `get_my_tickets` | Get all Jira tickets assigned to you, ordered by last updated |
10
10
  | `get_active_sprint_tickets` | Get your tickets in the currently active sprint |
11
11
  | `get_issue_details` | Get full details (description + attachments) for a specific issue key |
12
+ | `add_comment` | Add a comment to any Jira issue |
13
+ | `get_my_standup` | Get a standup summary of tickets you updated since yesterday |
14
+ | `get_sprint_summary` | Get all sprint tickets grouped by status (Todo / In Progress / Done) |
15
+ | `search_tickets` | Search tickets with plain English or raw JQL |
16
+ | `get_context_for_pr` | Extract Jira ticket from a branch name and return a ready-to-use PR description block |
12
17
 
13
18
  ## Prerequisites
14
19
 
@@ -116,6 +121,11 @@ Once configured, you can ask your AI assistant:
116
121
  - *"What's in my active sprint?"*
117
122
  - *"Get me the details for PROJ-1234"*
118
123
  - *"Summarize the description of PROJ-5678"*
124
+ - *"Add a comment to PROJ-123 saying the fix is deployed to staging"*
125
+ - *"Give me my standup for today"*
126
+ - *"Summarize the active sprint — how many tickets are done vs in progress?"*
127
+ - *"Search for open bugs related to login"*
128
+ - *"Get PR context for branch STUD-17891-add-email-icon"*
119
129
 
120
130
  ## Security
121
131
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abdelrahmanhsn/jira-mcp",
3
3
  "type": "module",
4
- "version": "1.0.1",
4
+ "version": "1.3.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
@@ -80,6 +80,195 @@ server.tool(
80
80
  }
81
81
  );
82
82
 
83
+ // Tool: add_comment
84
+ server.tool(
85
+ "add_comment",
86
+ "Add a comment to a Jira issue",
87
+ {
88
+ issueKey: z.string().describe("The Jira issue key, e.g. PROJ-123"),
89
+ comment: z.string().describe("The comment text to add"),
90
+ },
91
+ async ({ issueKey, comment }) => {
92
+ await jiraClient.post(`/issue/${issueKey}/comment`, {
93
+ body: {
94
+ type: "doc",
95
+ version: 1,
96
+ content: [{ type: "paragraph", content: [{ type: "text", text: comment }] }],
97
+ },
98
+ });
99
+ return { content: [{ type: "text", text: `Comment added to ${issueKey}.` }] };
100
+ }
101
+ );
102
+
103
+ // Tool: get_my_standup
104
+ server.tool(
105
+ "get_my_standup",
106
+ "Get a standup summary: tickets you updated or commented on yesterday and today",
107
+ {},
108
+ async () => {
109
+ const yesterday = new Date();
110
+ yesterday.setDate(yesterday.getDate() - 1);
111
+ const since = yesterday.toISOString().split("T")[0]; // YYYY-MM-DD
112
+
113
+ const [updated, commented] = await Promise.all([
114
+ searchJira(
115
+ `assignee = currentUser() AND updated >= "${since}" ORDER BY updated DESC`,
116
+ "summary,status,priority,issuetype"
117
+ ),
118
+ searchJira(
119
+ `issueFunction in commented("by currentUser() after ${since}")`,
120
+ "summary,status,issuetype"
121
+ ).catch(() => []), // issueFunction requires ScriptRunner; gracefully skip if unavailable
122
+ ]);
123
+
124
+ const result = {
125
+ updated_tickets: updated,
126
+ commented_tickets: commented,
127
+ summary: `You updated ${updated.length} ticket(s) since ${since}.`,
128
+ };
129
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
130
+ }
131
+ );
132
+
133
+ // Tool: get_sprint_summary
134
+ server.tool(
135
+ "get_sprint_summary",
136
+ "Get a summary of all tickets in the active sprint grouped by status",
137
+ {},
138
+ async () => {
139
+ const issues = await searchJira(
140
+ `project = '${JIRA_PROJECT}' AND sprint IN openSprints()`,
141
+ "summary,status,priority,issuetype,assignee"
142
+ );
143
+
144
+ const grouped = issues.reduce((acc, issue) => {
145
+ const s = issue.status || "Unknown";
146
+ if (!acc[s]) acc[s] = [];
147
+ acc[s].push({ key: issue.key, summary: issue.summary, priority: issue.priority, type: issue.type });
148
+ return acc;
149
+ }, {});
150
+
151
+ const result = {
152
+ total: issues.length,
153
+ by_status: grouped,
154
+ };
155
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
156
+ }
157
+ );
158
+
159
+ // Tool: search_tickets
160
+ server.tool(
161
+ "search_tickets",
162
+ "Search Jira tickets using a plain-text query or a JQL string",
163
+ {
164
+ query: z.string().describe(
165
+ "Plain English query (e.g. 'open bugs assigned to me') or raw JQL (e.g. 'project = PROJ AND status = Open')"
166
+ ),
167
+ maxResults: z.number().optional().default(20).describe("Maximum number of results to return (default 20)"),
168
+ },
169
+ async ({ query, maxResults }) => {
170
+ // Use as raw JQL if it looks like JQL, otherwise wrap in a text search
171
+ const looksLikeJql = /\b(AND|OR|IN|=|!=|~|project|status|assignee|sprint|priority|issuetype)\b/i.test(query);
172
+ const jql = looksLikeJql
173
+ ? query
174
+ : `project = '${JIRA_PROJECT}' AND text ~ "${query.replace(/"/g, '\\"')}" ORDER BY updated DESC`;
175
+
176
+ const response = await jiraClient.get("/search/jql", {
177
+ params: { jql, fields: "summary,status,priority,issuetype,assignee,reporter", maxResults },
178
+ });
179
+
180
+ const issues = (response.data.issues || []).map(i => ({
181
+ key: i.key,
182
+ summary: i.fields?.summary,
183
+ status: i.fields?.status?.name,
184
+ priority: i.fields?.priority?.name,
185
+ type: i.fields?.issuetype?.name,
186
+ assignee: i.fields?.assignee?.displayName ?? "Unassigned",
187
+ reporter: i.fields?.reporter?.displayName,
188
+ }));
189
+
190
+ return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
191
+ }
192
+ );
193
+
194
+ // Tool: get_context_for_pr
195
+ server.tool(
196
+ "get_context_for_pr",
197
+ "Extract the Jira ticket key from a branch name, fetch its details and comments, and return a structured PR context block ready for AI to write a pull request description",
198
+ {
199
+ branch: z.string().describe("Git branch name, e.g. STUD-17891-add-email-icon or feature/STUD-17891"),
200
+ },
201
+ async ({ branch }) => {
202
+ // Extract issue key (e.g. STUD-17891) from anywhere in the branch name
203
+ const match = branch.match(/([A-Z][A-Z0-9]+-\d+)/i);
204
+ if (!match) {
205
+ return {
206
+ content: [{
207
+ type: "text",
208
+ text: `Could not find a Jira issue key in branch name: "${branch}". Expected format: PROJ-123 anywhere in the branch.`,
209
+ }],
210
+ };
211
+ }
212
+
213
+ const issueKey = match[1].toUpperCase();
214
+
215
+ const [issueRes, commentsRes] = await Promise.all([
216
+ jiraClient.get(`/issue/${issueKey}`, {
217
+ params: { fields: "summary,description,issuetype,priority,status,assignee,reporter,labels,components" },
218
+ }),
219
+ jiraClient.get(`/issue/${issueKey}/comment`, {
220
+ params: { maxResults: 10, orderBy: "-created" },
221
+ }),
222
+ ]);
223
+
224
+ const f = issueRes.data.fields;
225
+
226
+ // Extract plain text from Atlassian Document Format description
227
+ function extractText(node) {
228
+ if (!node) return "";
229
+ if (node.type === "text") return node.text || "";
230
+ if (node.content) return node.content.map(extractText).join(" ");
231
+ return "";
232
+ }
233
+
234
+ const description = extractText(f.description).trim() || "No description provided.";
235
+
236
+ const comments = (commentsRes.data.comments || []).map(c => ({
237
+ author: c.author?.displayName,
238
+ body: extractText(c.body).trim(),
239
+ created: c.created?.split("T")[0],
240
+ })).filter(c => c.body);
241
+
242
+ const context = {
243
+ issue_key: issueKey,
244
+ summary: f.summary,
245
+ type: f.issuetype?.name,
246
+ priority: f.priority?.name,
247
+ status: f.status?.name,
248
+ assignee: f.assignee?.displayName ?? "Unassigned",
249
+ reporter: f.reporter?.displayName,
250
+ labels: f.labels ?? [],
251
+ components: (f.components ?? []).map(c => c.name),
252
+ description,
253
+ recent_comments: comments,
254
+ pr_context: [
255
+ `## ${issueKey}: ${f.summary}`,
256
+ ``,
257
+ `**Type:** ${f.issuetype?.name} | **Priority:** ${f.priority?.name} | **Status:** ${f.status?.name}`,
258
+ ``,
259
+ `### What this PR does`,
260
+ description,
261
+ comments.length > 0 ? `\n### Discussion context\n${comments.map(c => `- **${c.author}** (${c.created}): ${c.body}`).join("\n")}` : "",
262
+ ``,
263
+ `### Jira ticket`,
264
+ `https://${JIRA_DOMAIN}/browse/${issueKey}`,
265
+ ].filter(Boolean).join("\n"),
266
+ };
267
+
268
+ return { content: [{ type: "text", text: JSON.stringify(context, null, 2) }] };
269
+ }
270
+ );
271
+
83
272
  // ── Start ────────────────────────────────────────────────────────────────────
84
273
  const transport = new StdioServerTransport();
85
274
  await server.connect(transport);