@mcpio/jira 1.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/index.js +822 -0
  4. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Volodymyr Press
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # Jira MCP Server with ADF Support
2
+
3
+ Model Context Protocol (MCP) server for Jira API integration with enhanced Atlassian Document Format (ADF) support.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)](https://nodejs.org/)
7
+
8
+ ## Features
9
+
10
+ - Full Jira API integration via MCP protocol
11
+ - Enhanced ADF formatting with **clickable issue links**
12
+ - Support for code blocks, lists, headers, and rich text formatting
13
+ - Complete CRUD operations: create, read, update, delete issues
14
+ - Issue linking, subtasks, comments, and JQL search
15
+ - Built-in security: input validation, HTTPS enforcement, error sanitization
16
+ - Automatic formatting prompts for AI models
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @mcpio/jira
22
+ ```
23
+
24
+ Or install globally:
25
+
26
+ ```bash
27
+ npm install -g @mcpio/jira
28
+ ```
29
+
30
+ ## Setup
31
+
32
+ 1. Create a `.env` file with your Jira credentials:
33
+
34
+ ```bash
35
+ JIRA_HOST=https://your-domain.atlassian.net
36
+ JIRA_EMAIL=your-email@example.com
37
+ JIRA_API_TOKEN=your-api-token
38
+ JIRA_PROJECT_KEY=YOUR-PROJECT-KEY
39
+ ```
40
+
41
+ 2. Get your Jira API token from: https://id.atlassian.com/manage-profile/security/api-tokens
42
+
43
+ 3. Run the server:
44
+
45
+ ```bash
46
+ mcpio-jira
47
+ ```
48
+
49
+ Or if installed locally:
50
+
51
+ ```bash
52
+ npm start
53
+ ```
54
+
55
+ ## Formatting Guide
56
+
57
+ ### Clickable Issue Links
58
+
59
+ **Format for clickable links:**
60
+ ```
61
+ - [ISSUE-KEY|URL] Description
62
+ ```
63
+
64
+ **Example:**
65
+ ```
66
+ - [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Implement authentication
67
+ ```
68
+
69
+ ### Basic Formatting
70
+
71
+ ```
72
+ h1. Heading Level 1
73
+ h2. Heading Level 2
74
+
75
+ * Bullet item
76
+ # Numbered item
77
+
78
+ *bold text*
79
+
80
+ ---- (horizontal rule)
81
+
82
+ ​```
83
+ Code block
84
+ ​```
85
+ ```
86
+
87
+ ## Available Tools
88
+
89
+ - `jira_create_issue` - Create new issue
90
+ - `jira_get_issue` - Get issue details
91
+ - `jira_search_issues` - Search with JQL
92
+ - `jira_update_issue` - Update issue (description, status, summary)
93
+ - `jira_add_comment` - Add comment to issue
94
+ - `jira_link_issues` - Link two issues
95
+ - `jira_get_project_info` - Get project information
96
+ - `jira_delete_issue` - Delete issue
97
+ - `jira_create_subtask` - Create subtask under parent
98
+
99
+ ## Environment Variables
100
+
101
+ - `JIRA_HOST` - Jira instance URL (HTTPS required)
102
+ - `JIRA_EMAIL` - Your email address
103
+ - `JIRA_API_TOKEN` - API token from Atlassian
104
+ - `JIRA_PROJECT_KEY` - Default project key (optional, defaults to "PROJ")
105
+
106
+ ## License
107
+
108
+ MIT - see [LICENSE](LICENSE) file
109
+
110
+ ## Author
111
+
112
+ Volodymyr Press - [volodymyr.press.gpt@gmail.com](mailto:volodymyr.press.gpt@gmail.com)
package/index.js ADDED
@@ -0,0 +1,822 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ ListPromptsRequestSchema,
9
+ GetPromptRequestSchema,
10
+ } from '@modelcontextprotocol/sdk/types.js';
11
+ import axios from 'axios';
12
+ import * as dotenv from 'dotenv';
13
+
14
+ dotenv.config();
15
+
16
+ function getRequiredEnv(name, fallback = null) {
17
+ const value = process.env[name] || fallback;
18
+ if (!value) {
19
+ throw new Error(`Required environment variable ${name} is not set. Please check your .env file.`);
20
+ }
21
+ return value;
22
+ }
23
+
24
+ const JIRA_URL = getRequiredEnv('JIRA_HOST', process.env.JIRA_URL);
25
+ const JIRA_EMAIL = getRequiredEnv('JIRA_EMAIL');
26
+ const JIRA_API_TOKEN = getRequiredEnv('JIRA_API_TOKEN');
27
+ const JIRA_PROJECT_KEY = process.env.JIRA_PROJECT_KEY || 'PROJ';
28
+ const STORY_POINTS_FIELD = process.env.JIRA_STORY_POINTS_FIELD || 'customfield_10016';
29
+
30
+ if (!JIRA_URL.startsWith('https://')) {
31
+ throw new Error('JIRA_HOST must use HTTPS protocol for security');
32
+ }
33
+
34
+ function validateIssueKey(key) {
35
+ if (!key || typeof key !== 'string') {
36
+ throw new Error('Invalid issue key: must be a string');
37
+ }
38
+ if (!/^[A-Z]+-\d+$/.test(key)) {
39
+ throw new Error(`Invalid issue key format: ${key}. Expected format: PROJECT-123`);
40
+ }
41
+ return key;
42
+ }
43
+
44
+ function validateJQL(jql) {
45
+ if (!jql || typeof jql !== 'string') {
46
+ throw new Error('Invalid JQL query: must be a string');
47
+ }
48
+ if (jql.length > 5000) {
49
+ throw new Error('JQL query too long: maximum 5000 characters');
50
+ }
51
+ return jql;
52
+ }
53
+
54
+ function sanitizeString(str, maxLength = 1000, fieldName = 'input') {
55
+ if (!str || typeof str !== 'string') {
56
+ throw new Error(`Invalid ${fieldName}: must be a string`);
57
+ }
58
+ if (str.length > maxLength) {
59
+ throw new Error(`${fieldName} exceeds maximum length of ${maxLength} characters`);
60
+ }
61
+ return str.trim();
62
+ }
63
+
64
+ function createSuccessResponse(data) {
65
+ return {
66
+ content: [{
67
+ type: 'text',
68
+ text: JSON.stringify(data, null, 2),
69
+ }],
70
+ };
71
+ }
72
+
73
+ function createIssueUrl(issueKey) {
74
+ return `${JIRA_URL}/browse/${issueKey}`;
75
+ }
76
+
77
+ function handleError(error) {
78
+ const isDevelopment = process.env.NODE_ENV === 'development';
79
+
80
+ const errorResponse = {
81
+ error: 'Operation failed',
82
+ message: error.message || 'An unexpected error occurred',
83
+ };
84
+
85
+ if (isDevelopment) {
86
+ errorResponse.details = error.response?.data;
87
+ errorResponse.stack = error.stack;
88
+ }
89
+
90
+ return {
91
+ content: [{
92
+ type: 'text',
93
+ text: JSON.stringify(errorResponse, null, 2),
94
+ }],
95
+ isError: true,
96
+ };
97
+ }
98
+
99
+ const jiraApi = axios.create({
100
+ baseURL: `${JIRA_URL}/rest/api/3`,
101
+ auth: {
102
+ username: JIRA_EMAIL,
103
+ password: JIRA_API_TOKEN,
104
+ },
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ },
108
+ timeout: 30000,
109
+ });
110
+
111
+ const server = new Server(
112
+ {
113
+ name: 'jira-mcp-server',
114
+ version: '1.0.0',
115
+ },
116
+ {
117
+ capabilities: {
118
+ tools: {},
119
+ prompts: {},
120
+ },
121
+ }
122
+ );
123
+
124
+ function createADFDocument(content) {
125
+ const nodes = [];
126
+ const lines = content.split('\n');
127
+
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const line = lines[i].trim();
130
+
131
+ if (!line) {
132
+ continue;
133
+ }
134
+
135
+ if (line.startsWith('h1. ')) {
136
+ nodes.push({
137
+ type: 'heading',
138
+ attrs: { level: 1 },
139
+ content: [{ type: 'text', text: line.substring(4) }]
140
+ });
141
+ } else if (line.startsWith('h2. ')) {
142
+ nodes.push({
143
+ type: 'heading',
144
+ attrs: { level: 2 },
145
+ content: [{ type: 'text', text: line.substring(4) }]
146
+ });
147
+ } else if (line.startsWith('h3. ')) {
148
+ nodes.push({
149
+ type: 'heading',
150
+ attrs: { level: 3 },
151
+ content: [{ type: 'text', text: line.substring(4) }]
152
+ });
153
+ } else if (line.startsWith('- [') && line.includes('|')) {
154
+ const match = line.match(/- \[([^\]]+)\|([^\]]+)\] (.+)/);
155
+ if (match) {
156
+ nodes.push({
157
+ type: 'bulletList',
158
+ content: [{
159
+ type: 'listItem',
160
+ content: [{
161
+ type: 'paragraph',
162
+ content: [
163
+ {
164
+ type: 'text',
165
+ text: match[1],
166
+ marks: [{
167
+ type: 'link',
168
+ attrs: { href: match[2] }
169
+ }]
170
+ },
171
+ { type: 'text', text: ' ' + match[3] }
172
+ ]
173
+ }]
174
+ }]
175
+ });
176
+ }
177
+ } else if (line.startsWith('* ')) {
178
+ nodes.push({
179
+ type: 'bulletList',
180
+ content: [{
181
+ type: 'listItem',
182
+ content: [{
183
+ type: 'paragraph',
184
+ content: [{ type: 'text', text: line.substring(2) }]
185
+ }]
186
+ }]
187
+ });
188
+ } else if (line === '----') {
189
+ nodes.push({
190
+ type: 'rule'
191
+ });
192
+ } else if (line === '```' || line.startsWith('```')) {
193
+ const codeLines = [];
194
+ i++;
195
+ while (i < lines.length && lines[i].trim() !== '```') {
196
+ codeLines.push(lines[i]);
197
+ i++;
198
+ }
199
+ nodes.push({
200
+ type: 'codeBlock',
201
+ content: [{
202
+ type: 'text',
203
+ text: codeLines.join('\n')
204
+ }]
205
+ });
206
+ } else if (line.includes('*') && line.includes(':')) {
207
+ const parts = [];
208
+ const regex = /\*([^*]+)\*/g;
209
+ let lastIndex = 0;
210
+ let match;
211
+
212
+ while ((match = regex.exec(line)) !== null) {
213
+ if (match.index > lastIndex) {
214
+ parts.push({ type: 'text', text: line.substring(lastIndex, match.index) });
215
+ }
216
+ parts.push({
217
+ type: 'text',
218
+ text: match[1],
219
+ marks: [{ type: 'strong' }]
220
+ });
221
+ lastIndex = regex.lastIndex;
222
+ }
223
+
224
+ if (lastIndex < line.length) {
225
+ parts.push({ type: 'text', text: line.substring(lastIndex) });
226
+ }
227
+
228
+ nodes.push({
229
+ type: 'paragraph',
230
+ content: parts
231
+ });
232
+ } else if (line.startsWith('*') && line.endsWith('*')) {
233
+ nodes.push({
234
+ type: 'paragraph',
235
+ content: [{
236
+ type: 'text',
237
+ text: line.substring(1, line.length - 1),
238
+ marks: [{ type: 'strong' }]
239
+ }]
240
+ });
241
+ } else {
242
+ nodes.push({
243
+ type: 'paragraph',
244
+ content: [{ type: 'text', text: line }]
245
+ });
246
+ }
247
+ }
248
+
249
+ return {
250
+ type: 'doc',
251
+ version: 1,
252
+ content: nodes
253
+ };
254
+ }
255
+
256
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
257
+ return {
258
+ prompts: [
259
+ {
260
+ name: 'jira-formatting-guide',
261
+ description: 'Essential Jira formatting rules for creating clickable links and properly formatted issues',
262
+ },
263
+ ],
264
+ };
265
+ });
266
+
267
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
268
+ if (request.params.name === 'jira-formatting-guide') {
269
+ return {
270
+ messages: [
271
+ {
272
+ role: 'user',
273
+ content: {
274
+ type: 'text',
275
+ text: `When working with Jira through this MCP server, you MUST ALWAYS follow these formatting rules:
276
+
277
+ CRITICAL: CLICKABLE ISSUE LINKS
278
+ ================================
279
+ To create clickable links to Jira issues, ALWAYS use this exact format:
280
+ - [ISSUE-KEY|FULL-URL] Description text
281
+
282
+ CORRECT Examples:
283
+ - [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Implement authentication
284
+ - [PROJ-124|https://your-domain.atlassian.net/browse/PROJ-124] Add unit tests
285
+
286
+ WRONG (these will NOT be clickable):
287
+ - PROJ-123 Implement authentication (plain text)
288
+ - * PROJ-123 Implement authentication (plain bullet)
289
+ - [PROJ-123](https://...) (markdown format)
290
+
291
+ JIRA FORMATTING REFERENCE:
292
+ ==========================
293
+ 1. Headers:
294
+ h1. Main Title
295
+ h2. Section Title
296
+ h3. Subsection
297
+
298
+ 2. Lists:
299
+ * Bullet point
300
+ * Another bullet
301
+
302
+ # Numbered item
303
+ # Another number
304
+
305
+ 3. Code Blocks:
306
+ \`\`\`
307
+ Error message or code here
308
+ Multiple lines supported
309
+ \`\`\`
310
+
311
+ 4. Bold Text:
312
+ *important text*
313
+
314
+ 5. Horizontal Line:
315
+ ----
316
+
317
+ IMPORTANT RULES:
318
+ ================
319
+ 1. NEVER reference Jira issues as plain text like "PROJ-123"
320
+ 2. ALWAYS use the format: - [KEY|URL] description
321
+ 3. ALWAYS include the full URL: https://domain.atlassian.net/browse/KEY
322
+ 4. The dash and space before the bracket are REQUIRED: "- ["
323
+ 5. Use the pipe character | to separate key from URL
324
+
325
+ When creating or updating Jira issues, descriptions, or comments, automatically apply this formatting without being asked.`,
326
+ },
327
+ },
328
+ ],
329
+ };
330
+ }
331
+
332
+ throw new Error(`Unknown prompt: ${request.params.name}`);
333
+ });
334
+
335
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
336
+ return {
337
+ tools: [
338
+ {
339
+ name: 'jira_create_issue',
340
+ description: `Create a new Jira issue with proper ADF formatting.
341
+
342
+ ⚠️ CRITICAL - ALWAYS Use Jira Formatting:
343
+ When writing descriptions, ALWAYS format Jira issue references as clickable links:
344
+ - [PROJECT-123|https://your-domain.atlassian.net/browse/PROJECT-123] Description
345
+
346
+ NEVER use plain text like "PROJECT-123" - it won't be clickable!
347
+
348
+ Supported formatting:
349
+ - h1. h2. h3. for headers
350
+ - * for bullet lists
351
+ - \`\`\` for code blocks
352
+ - *text* for bold
353
+ - ---- for horizontal rule
354
+
355
+ See the 'jira-formatting-guide' prompt for complete reference.`,
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ summary: {
360
+ type: 'string',
361
+ description: 'Issue summary/title',
362
+ },
363
+ description: {
364
+ type: 'string',
365
+ description: 'Issue description - use format: - [KEY|URL] text for clickable links',
366
+ },
367
+ issueType: {
368
+ type: 'string',
369
+ description: 'Issue type (Story, Task, Bug, etc.)',
370
+ default: 'Task',
371
+ },
372
+ priority: {
373
+ type: 'string',
374
+ description: 'Priority (Highest, High, Medium, Low, Lowest)',
375
+ default: 'Medium',
376
+ },
377
+ labels: {
378
+ type: 'array',
379
+ items: { type: 'string' },
380
+ description: 'Labels for the issue',
381
+ },
382
+ storyPoints: {
383
+ type: 'number',
384
+ description: 'Story points estimate',
385
+ },
386
+ },
387
+ required: ['summary', 'description'],
388
+ },
389
+ },
390
+ {
391
+ name: 'jira_get_issue',
392
+ description: 'Get details of a Jira issue',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ issueKey: {
397
+ type: 'string',
398
+ description: 'Issue key (e.g., TTC-123)',
399
+ },
400
+ },
401
+ required: ['issueKey'],
402
+ },
403
+ },
404
+ {
405
+ name: 'jira_search_issues',
406
+ description: 'Search for Jira issues using JQL',
407
+ inputSchema: {
408
+ type: 'object',
409
+ properties: {
410
+ jql: {
411
+ type: 'string',
412
+ description: 'JQL query string',
413
+ },
414
+ maxResults: {
415
+ type: 'number',
416
+ description: 'Maximum number of results',
417
+ default: 50,
418
+ },
419
+ },
420
+ required: ['jql'],
421
+ },
422
+ },
423
+ {
424
+ name: 'jira_update_issue',
425
+ description: `Update a Jira issue.
426
+
427
+ IMPORTANT - Description Formatting Guide:
428
+
429
+ The description field supports a special markup format that gets converted to Atlassian Document Format (ADF):
430
+
431
+ 1. HEADINGS:
432
+ h1. Heading 1
433
+ h2. Heading 2
434
+ h3. Heading 3
435
+ h4. Heading 4
436
+ h5. Heading 5
437
+
438
+ 2. LISTS:
439
+ * Bullet item (use asterisk + space)
440
+ # Numbered item (use hash + space)
441
+
442
+ 3. LINKS TO JIRA ISSUES (CREATES CLICKABLE LINKS):
443
+ - [ISSUE-KEY|URL] Description text
444
+ Example: - [PROJ-61|https://your-domain.atlassian.net/browse/PROJ-61] API rate limiting
445
+
446
+ This format is CRITICAL for creating active hyperlinks to Jira issues!
447
+ DO NOT use plain text like "PROJ-61" - it will not be clickable.
448
+ DO NOT use markdown format [text](url) - it will not work.
449
+ ALWAYS use the pipe format: [KEY|URL]
450
+
451
+ 4. TEXT FORMATTING:
452
+ *bold text* (asterisk before and after)
453
+
454
+ 5. HORIZONTAL RULE:
455
+ ---- (four dashes)
456
+
457
+ Example with links:
458
+ h2. Task List
459
+ h4. Security Tasks
460
+ - [PROJ-61|https://your-domain.atlassian.net/browse/PROJ-61] Implement rate limiting
461
+ - [PROJ-63|https://your-domain.atlassian.net/browse/PROJ-63] Configure CORS
462
+
463
+ This will create proper clickable links in Jira UI.`,
464
+ inputSchema: {
465
+ type: 'object',
466
+ properties: {
467
+ issueKey: {
468
+ type: 'string',
469
+ description: 'Issue key to update',
470
+ },
471
+ summary: {
472
+ type: 'string',
473
+ description: 'New summary',
474
+ },
475
+ description: {
476
+ type: 'string',
477
+ description: 'New description - see tool description for formatting guide with clickable links',
478
+ },
479
+ status: {
480
+ type: 'string',
481
+ description: 'New status (To Do, In Progress, Done, etc.)',
482
+ },
483
+ },
484
+ required: ['issueKey'],
485
+ },
486
+ },
487
+ {
488
+ name: 'jira_add_comment',
489
+ description: `Add a comment to a Jira issue.
490
+
491
+ IMPORTANT - Comment Formatting:
492
+ To create CLICKABLE LINKS to other Jira issues, use this format:
493
+ - [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Task description
494
+
495
+ See jira_update_issue tool description for complete formatting guide.`,
496
+ inputSchema: {
497
+ type: 'object',
498
+ properties: {
499
+ issueKey: {
500
+ type: 'string',
501
+ description: 'Issue key',
502
+ },
503
+ comment: {
504
+ type: 'string',
505
+ description: 'Comment text - use format: - [KEY|URL] text for clickable links',
506
+ },
507
+ },
508
+ required: ['issueKey', 'comment'],
509
+ },
510
+ },
511
+ {
512
+ name: 'jira_link_issues',
513
+ description: 'Create a link between two issues. IMPORTANT: When linking multiple issues, use sequential calls (2-3 at a time max) instead of parallel calls to avoid permission prompt issues in Claude Code.',
514
+ inputSchema: {
515
+ type: 'object',
516
+ properties: {
517
+ inwardIssue: {
518
+ type: 'string',
519
+ description: 'Issue key that will be linked from (e.g., TTC-260)',
520
+ },
521
+ outwardIssue: {
522
+ type: 'string',
523
+ description: 'Issue key that will be linked to (e.g., TTC-87)',
524
+ },
525
+ linkType: {
526
+ type: 'string',
527
+ description: 'Link type (Relates, Blocks, Cloners, Duplicate, etc.)',
528
+ default: 'Relates',
529
+ },
530
+ },
531
+ required: ['inwardIssue', 'outwardIssue'],
532
+ },
533
+ },
534
+ {
535
+ name: 'jira_get_project_info',
536
+ description: 'Get project information',
537
+ inputSchema: {
538
+ type: 'object',
539
+ properties: {
540
+ projectKey: {
541
+ type: 'string',
542
+ description: 'Project key',
543
+ default: JIRA_PROJECT_KEY,
544
+ },
545
+ },
546
+ },
547
+ },
548
+ {
549
+ name: 'jira_delete_issue',
550
+ description: 'Delete a Jira issue',
551
+ inputSchema: {
552
+ type: 'object',
553
+ properties: {
554
+ issueKey: {
555
+ type: 'string',
556
+ description: 'Issue key to delete (e.g., TTC-123)',
557
+ },
558
+ },
559
+ required: ['issueKey'],
560
+ },
561
+ },
562
+ {
563
+ name: 'jira_create_subtask',
564
+ description: `Create a subtask under a parent issue.
565
+
566
+ IMPORTANT - Description Formatting:
567
+ To create CLICKABLE LINKS to other Jira issues, use this format:
568
+ - [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Task description
569
+
570
+ See jira_update_issue tool description for complete formatting guide.`,
571
+ inputSchema: {
572
+ type: 'object',
573
+ properties: {
574
+ parentKey: {
575
+ type: 'string',
576
+ description: 'Parent issue key (e.g., TTC-261)',
577
+ },
578
+ summary: {
579
+ type: 'string',
580
+ description: 'Subtask summary/title',
581
+ },
582
+ description: {
583
+ type: 'string',
584
+ description: 'Subtask description - use format: - [KEY|URL] text for clickable links',
585
+ },
586
+ priority: {
587
+ type: 'string',
588
+ description: 'Priority (Highest, High, Medium, Low, Lowest)',
589
+ default: 'Medium',
590
+ },
591
+ },
592
+ required: ['parentKey', 'summary', 'description'],
593
+ },
594
+ },
595
+ ],
596
+ };
597
+ });
598
+
599
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
600
+ const { name, arguments: args } = request.params;
601
+
602
+ try {
603
+ switch (name) {
604
+ case 'jira_create_issue': {
605
+ const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = args;
606
+
607
+ const issueData = {
608
+ fields: {
609
+ project: { key: JIRA_PROJECT_KEY },
610
+ summary: sanitizeString(summary, 500, 'summary'),
611
+ description: createADFDocument(description),
612
+ issuetype: { name: issueType },
613
+ priority: { name: priority },
614
+ labels,
615
+ },
616
+ };
617
+
618
+ if (storyPoints) {
619
+ issueData.fields[STORY_POINTS_FIELD] = storyPoints;
620
+ }
621
+
622
+ const response = await jiraApi.post('/issue', issueData);
623
+
624
+ return createSuccessResponse({
625
+ success: true,
626
+ key: response.data.key,
627
+ id: response.data.id,
628
+ url: createIssueUrl(response.data.key),
629
+ });
630
+ }
631
+
632
+ case 'jira_get_issue': {
633
+ const { issueKey } = args;
634
+ validateIssueKey(issueKey);
635
+ const response = await jiraApi.get(`/issue/${issueKey}`);
636
+
637
+ return createSuccessResponse({
638
+ key: response.data.key,
639
+ summary: response.data.fields.summary,
640
+ description: response.data.fields.description,
641
+ status: response.data.fields.status.name,
642
+ assignee: response.data.fields.assignee?.displayName,
643
+ reporter: response.data.fields.reporter?.displayName,
644
+ priority: response.data.fields.priority?.name,
645
+ issueType: response.data.fields.issuetype?.name,
646
+ parent: response.data.fields.parent?.key,
647
+ created: response.data.fields.created,
648
+ updated: response.data.fields.updated,
649
+ url: createIssueUrl(response.data.key),
650
+ });
651
+ }
652
+
653
+ case 'jira_search_issues': {
654
+ const { jql, maxResults = 50 } = args;
655
+ validateJQL(jql);
656
+ const response = await jiraApi.get('/search/jql', {
657
+ params: {
658
+ jql,
659
+ maxResults,
660
+ fields: 'summary,status,assignee,priority,created,updated,issuetype,parent',
661
+ },
662
+ });
663
+
664
+ return createSuccessResponse({
665
+ total: response.data.total,
666
+ issues: response.data.issues.map(issue => ({
667
+ key: issue.key,
668
+ summary: issue.fields.summary,
669
+ status: issue.fields.status.name,
670
+ assignee: issue.fields.assignee?.displayName,
671
+ priority: issue.fields.priority?.name,
672
+ issueType: issue.fields.issuetype?.name,
673
+ parent: issue.fields.parent?.key,
674
+ url: createIssueUrl(issue.key),
675
+ })),
676
+ });
677
+ }
678
+
679
+ case 'jira_update_issue': {
680
+ const { issueKey, summary, description, status } = args;
681
+ validateIssueKey(issueKey);
682
+
683
+ const updateData = { fields: {} };
684
+
685
+ if (summary) {
686
+ updateData.fields.summary = sanitizeString(summary, 500, 'summary');
687
+ }
688
+ if (description) {
689
+ updateData.fields.description = createADFDocument(description);
690
+ }
691
+
692
+ await jiraApi.put(`/issue/${issueKey}`, updateData);
693
+
694
+ if (status) {
695
+ const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
696
+ const transition = transitions.data.transitions.find(t => t.name === status);
697
+
698
+ if (transition) {
699
+ await jiraApi.post(`/issue/${issueKey}/transitions`, {
700
+ transition: { id: transition.id },
701
+ });
702
+ }
703
+ }
704
+
705
+ return createSuccessResponse({
706
+ success: true,
707
+ message: `Issue ${issueKey} updated successfully`,
708
+ url: createIssueUrl(issueKey),
709
+ });
710
+ }
711
+
712
+ case 'jira_add_comment': {
713
+ const { issueKey, comment } = args;
714
+ validateIssueKey(issueKey);
715
+
716
+ await jiraApi.post(`/issue/${issueKey}/comment`, {
717
+ body: createADFDocument(comment),
718
+ });
719
+
720
+ return createSuccessResponse({
721
+ success: true,
722
+ message: `Comment added to ${issueKey}`,
723
+ });
724
+ }
725
+
726
+ case 'jira_link_issues': {
727
+ const { inwardIssue, outwardIssue, linkType = 'Relates' } = args;
728
+ validateIssueKey(inwardIssue);
729
+ validateIssueKey(outwardIssue);
730
+
731
+ try {
732
+ await jiraApi.post('/issueLink', {
733
+ type: { name: linkType },
734
+ inwardIssue: { key: inwardIssue },
735
+ outwardIssue: { key: outwardIssue },
736
+ });
737
+
738
+ return createSuccessResponse({
739
+ success: true,
740
+ message: `Linked ${inwardIssue} to ${outwardIssue} with type "${linkType}"`,
741
+ });
742
+ } catch (linkError) {
743
+ if (linkError.response?.status === 400 &&
744
+ linkError.response?.data?.errorMessages?.includes('link already exists')) {
745
+ return createSuccessResponse({
746
+ success: true,
747
+ message: `Link between ${inwardIssue} and ${outwardIssue} already exists`,
748
+ alreadyLinked: true,
749
+ });
750
+ }
751
+ throw linkError;
752
+ }
753
+ }
754
+
755
+ case 'jira_get_project_info': {
756
+ const { projectKey = JIRA_PROJECT_KEY } = args;
757
+ const response = await jiraApi.get(`/project/${projectKey}`);
758
+
759
+ return createSuccessResponse({
760
+ key: response.data.key,
761
+ name: response.data.name,
762
+ description: response.data.description,
763
+ lead: response.data.lead?.displayName,
764
+ url: response.data.url,
765
+ });
766
+ }
767
+
768
+ case 'jira_delete_issue': {
769
+ const { issueKey } = args;
770
+ validateIssueKey(issueKey);
771
+ await jiraApi.delete(`/issue/${issueKey}`);
772
+
773
+ return createSuccessResponse({
774
+ success: true,
775
+ message: `Issue ${issueKey} deleted successfully`,
776
+ });
777
+ }
778
+
779
+ case 'jira_create_subtask': {
780
+ const { parentKey, summary, description, priority = 'Medium' } = args;
781
+ validateIssueKey(parentKey);
782
+
783
+ const issueData = {
784
+ fields: {
785
+ project: { key: JIRA_PROJECT_KEY },
786
+ summary: sanitizeString(summary, 500, 'summary'),
787
+ description: createADFDocument(description),
788
+ issuetype: { name: 'Subtask' },
789
+ priority: { name: priority },
790
+ parent: { key: parentKey },
791
+ },
792
+ };
793
+
794
+ const response = await jiraApi.post('/issue', issueData);
795
+
796
+ return createSuccessResponse({
797
+ success: true,
798
+ key: response.data.key,
799
+ id: response.data.id,
800
+ parent: parentKey,
801
+ url: createIssueUrl(response.data.key),
802
+ });
803
+ }
804
+
805
+ default:
806
+ throw new Error(`Unknown tool: ${name}`);
807
+ }
808
+ } catch (error) {
809
+ return handleError(error);
810
+ }
811
+ });
812
+
813
+ async function main() {
814
+ const transport = new StdioServerTransport();
815
+ await server.connect(transport);
816
+ console.error('Jira MCP Server running on stdio');
817
+ }
818
+
819
+ main().catch((error) => {
820
+ console.error('Fatal error in main():', error);
821
+ process.exit(1);
822
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@mcpio/jira",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol (MCP) server for Jira API integration with enhanced ADF formatting support and security hardening",
5
+ "author": {
6
+ "name": "Volodymyr Press",
7
+ "email": "volodymyr.press.gpt@gmail.com"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "main": "index.js",
12
+ "bin": {
13
+ "mcpio-jira": "index.js"
14
+ },
15
+ "scripts": {
16
+ "start": "node index.js"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "jira",
22
+ "atlassian",
23
+ "adf",
24
+ "jira-api",
25
+ "ai-tools",
26
+ "claude",
27
+ "automation"
28
+ ],
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.20.2",
31
+ "axios": "^1.13.1",
32
+ "dotenv": "^17.2.3"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/wince87/mcp_jira.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/wince87/mcp_jira/issues"
43
+ },
44
+ "homepage": "https://github.com/wince87/mcp_jira#readme"
45
+ }