@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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/index.js +822 -0
- 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
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
|
+
}
|