@abdelrahmanhsn/jira-mcp 1.0.0 → 1.2.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 +136 -0
  2. package/package.json +23 -4
  3. package/server.js +118 -6
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @abdelrahmanhsn/jira-mcp
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.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Description |
8
+ |------|-------------|
9
+ | `get_my_tickets` | Get all Jira tickets assigned to you, ordered by last updated |
10
+ | `get_active_sprint_tickets` | Get your tickets in the currently active sprint |
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
+
17
+ ## Prerequisites
18
+
19
+ - Node.js 18 or later
20
+ - A Jira Cloud account
21
+ - A Jira API token ([generate one here](https://id.atlassian.com/manage-profile/security/api-tokens))
22
+
23
+ ## Setup
24
+
25
+ ### 1. Get your Jira API token
26
+
27
+ 1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
28
+ 2. Click **Create API token**
29
+ 3. Copy the token — you'll need it below
30
+
31
+ ### 2. Configure your MCP client
32
+
33
+ Pick your AI IDE below and add the config. Replace the `env` values with your own.
34
+
35
+ ---
36
+
37
+ #### GitHub Copilot (VS Code)
38
+
39
+ Open **User Settings (JSON)** via `Cmd+Shift+P` → `Open User Settings (JSON)` and add:
40
+
41
+ ```json
42
+ "mcp": {
43
+ "servers": {
44
+ "jira-mcp": {
45
+ "command": "npx",
46
+ "args": ["-y", "@abdelrahmanhsn/jira-mcp"],
47
+ "env": {
48
+ "JIRA_EMAIL": "you@company.com",
49
+ "JIRA_TOKEN": "your-api-token",
50
+ "JIRA_DOMAIN": "yourcompany.atlassian.net",
51
+ "JIRA_PROJECT": "PROJ"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ #### Cursor
61
+
62
+ Open `~/.cursor/mcp.json` (or `Cursor Settings → MCP`) and add:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "jira-mcp": {
68
+ "command": "npx",
69
+ "args": ["-y", "@abdelrahmanhsn/jira-mcp"],
70
+ "env": {
71
+ "JIRA_EMAIL": "you@company.com",
72
+ "JIRA_TOKEN": "your-api-token",
73
+ "JIRA_DOMAIN": "yourcompany.atlassian.net",
74
+ "JIRA_PROJECT": "PROJ"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ---
82
+
83
+ #### Claude Desktop
84
+
85
+ Open `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "jira-mcp": {
91
+ "command": "npx",
92
+ "args": ["-y", "@abdelrahmanhsn/jira-mcp"],
93
+ "env": {
94
+ "JIRA_EMAIL": "you@company.com",
95
+ "JIRA_TOKEN": "your-api-token",
96
+ "JIRA_DOMAIN": "yourcompany.atlassian.net",
97
+ "JIRA_PROJECT": "PROJ"
98
+ }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Environment Variables
107
+
108
+ | Variable | Required | Description |
109
+ |----------|----------|-------------|
110
+ | `JIRA_EMAIL` | ✅ | Your Jira account email |
111
+ | `JIRA_TOKEN` | ✅ | Your Jira API token |
112
+ | `JIRA_DOMAIN` | ✅ | Your Jira domain, e.g. `yourcompany.atlassian.net` |
113
+ | `JIRA_PROJECT` | ✅ | Your Jira project key, e.g. `PROJ` |
114
+
115
+ ## Usage Examples
116
+
117
+ Once configured, you can ask your AI assistant:
118
+
119
+ - *"Show me my current Jira tickets"*
120
+ - *"What's in my active sprint?"*
121
+ - *"Get me the details for PROJ-1234"*
122
+ - *"Summarize the description of PROJ-5678"*
123
+ - *"Add a comment to PROJ-123 saying the fix is deployed to staging"*
124
+ - *"Give me my standup for today"*
125
+ - *"Summarize the active sprint — how many tickets are done vs in progress?"*
126
+ - *"Search for open bugs related to login"*
127
+
128
+ ## Security
129
+
130
+ - Credentials are **never** stored in code — they are injected at runtime by your MCP client
131
+ - Each user provides their own credentials in their local MCP config
132
+ - Your API token is only sent to your own Jira domain over HTTPS
133
+
134
+ ## License
135
+
136
+ ISC
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@abdelrahmanhsn/jira-mcp",
3
3
  "type": "module",
4
- "version": "1.0.0",
5
- "description": "MCP server for Jira — query tickets, sprints, and issue details from any AI IDE",
4
+ "version": "1.2.0",
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": {
8
8
  "jira-mcp": "./server.js"
@@ -10,13 +10,32 @@
10
10
  "scripts": {
11
11
  "start": "node server.js"
12
12
  },
13
- "keywords": ["mcp", "jira", "atlassian", "model-context-protocol", "ai"],
14
- "author": "Abdelrahman Hassan",
13
+ "keywords": [
14
+ "mcp",
15
+ "jira",
16
+ "atlassian",
17
+ "model-context-protocol",
18
+ "ai",
19
+ "copilot",
20
+ "cursor",
21
+ "claude",
22
+ "sprint",
23
+ "tickets",
24
+ "devtools"
25
+ ],
26
+ "author": {
27
+ "name": "Abdelrahman Hassan",
28
+ "email": "abdelrahmanhsn1@gmail.com"
29
+ },
15
30
  "license": "ISC",
16
31
  "repository": {
17
32
  "type": "git",
18
33
  "url": ""
19
34
  },
35
+ "homepage": "https://www.npmjs.com/package/@abdelrahmanhsn/jira-mcp",
36
+ "bugs": {
37
+ "url": ""
38
+ },
20
39
  "engines": {
21
40
  "node": ">=18"
22
41
  },
package/server.js CHANGED
@@ -5,11 +5,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
  import { z } from "zod";
6
6
 
7
7
  // ── Jira credentials (injected by MCP client via env) ──────────────────────
8
- const JIRA_EMAIL = process.env.JIRA_EMAIL;
9
- const JIRA_TOKEN = process.env.JIRA_TOKEN;
10
- const JIRA_DOMAIN = process.env.JIRA_DOMAIN;
8
+ const JIRA_EMAIL = process.env.JIRA_EMAIL;
9
+ const JIRA_TOKEN = process.env.JIRA_TOKEN;
10
+ const JIRA_DOMAIN = process.env.JIRA_DOMAIN;
11
+ const JIRA_PROJECT = process.env.JIRA_PROJECT;
11
12
 
12
- const missing = ["JIRA_EMAIL", "JIRA_TOKEN", "JIRA_DOMAIN"].filter(k => !process.env[k]);
13
+ const missing = ["JIRA_EMAIL", "JIRA_TOKEN", "JIRA_DOMAIN", "JIRA_PROJECT"].filter(k => !process.env[k]);
13
14
  if (missing.length) {
14
15
  process.stderr.write(
15
16
  `[jira-mcp] Missing required environment variables: ${missing.join(", ")}\n` +
@@ -58,11 +59,11 @@ server.tool(
58
59
  // Tool: get_active_sprint_tickets
59
60
  server.tool(
60
61
  "get_active_sprint_tickets",
61
- "Get user tickets in the active sprint of the Core Team board (STUD project)",
62
+ "Get user tickets in the active sprint for the configured Jira project",
62
63
  {},
63
64
  async () => {
64
65
  const result = await searchJira(
65
- "project = 'STUD' AND sprint IN openSprints() AND assignee = currentUser()"
66
+ `project = '${JIRA_PROJECT}' AND sprint IN openSprints() AND assignee = currentUser()`
66
67
  );
67
68
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
68
69
  }
@@ -79,6 +80,117 @@ server.tool(
79
80
  }
80
81
  );
81
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
+
82
194
  // ── Start ────────────────────────────────────────────────────────────────────
83
195
  const transport = new StdioServerTransport();
84
196
  await server.connect(transport);