@mcpio/jira 2.2.1 → 2.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.
- package/README.md +18 -3
- package/dist/index.js +749 -448
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Jira MCP Server v2.
|
|
1
|
+
# Jira MCP Server v2.3
|
|
2
2
|
|
|
3
3
|
Model Context Protocol (MCP) server for Jira API integration with automatic Markdown-to-ADF conversion.
|
|
4
4
|
|
|
@@ -8,11 +8,14 @@ Model Context Protocol (MCP) server for Jira API integration with automatic Mark
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- 32 Jira API tools via MCP protocol
|
|
12
12
|
- Automatic Markdown to ADF conversion (write Markdown, get proper Jira formatting)
|
|
13
13
|
- ADF to Markdown conversion when reading issues and comments
|
|
14
|
+
- Sprint and board management via Jira Agile API
|
|
15
|
+
- File attachment support
|
|
14
16
|
- Input validation, HTTPS enforcement, Jira error details in responses
|
|
15
17
|
- TypeScript source with full type definitions
|
|
18
|
+
- Zero runtime dependencies beyond MCP SDK and axios
|
|
16
19
|
|
|
17
20
|
## Installation
|
|
18
21
|
|
|
@@ -62,14 +65,25 @@ Automatically converted to Atlassian Document Format (ADF).
|
|
|
62
65
|
- `jira_search_issues` - Search with JQL
|
|
63
66
|
- `jira_update_issue` - Update issue fields and status
|
|
64
67
|
- `jira_delete_issue` - Delete issue
|
|
68
|
+
- `jira_clone_issue` - Clone an existing issue
|
|
65
69
|
- `jira_create_subtask` - Create subtask
|
|
70
|
+
- `jira_bulk_create_issues` - Create multiple issues at once
|
|
66
71
|
- `jira_assign_issue` - Assign/unassign user
|
|
67
72
|
- `jira_add_comment` - Add comment
|
|
73
|
+
- `jira_get_comments` - Get issue comments
|
|
68
74
|
- `jira_link_issues` - Link two issues
|
|
69
75
|
- `jira_list_transitions` - Get available status transitions
|
|
70
|
-
- `
|
|
76
|
+
- `jira_get_changelog` - Get issue change history
|
|
71
77
|
- `jira_add_worklog` - Add time tracking entry
|
|
72
78
|
- `jira_get_worklogs` - Get worklog entries
|
|
79
|
+
- `jira_get_attachments` - List attachments on an issue
|
|
80
|
+
- `jira_add_attachment` - Attach a local file to an issue
|
|
81
|
+
|
|
82
|
+
### Sprint & Board Management
|
|
83
|
+
- `jira_list_boards` - List all Scrum/Kanban boards
|
|
84
|
+
- `jira_list_sprints` - List sprints for a board
|
|
85
|
+
- `jira_get_sprint` - Get sprint details with all issues
|
|
86
|
+
- `jira_move_to_sprint` - Move issues to a sprint
|
|
73
87
|
|
|
74
88
|
### Project Management
|
|
75
89
|
- `jira_list_projects` - List all projects
|
|
@@ -83,6 +97,7 @@ Automatically converted to Atlassian Document Format (ADF).
|
|
|
83
97
|
- `jira_get_priorities` - Get available priorities
|
|
84
98
|
- `jira_get_link_types` - Get issue link types
|
|
85
99
|
- `jira_search_users` - Search users by name/email
|
|
100
|
+
- `jira_get_user_issues` - Get all issues assigned to a user
|
|
86
101
|
|
|
87
102
|
## Environment Variables
|
|
88
103
|
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import { readFileSync } from 'fs';
|
|
7
|
-
import { resolve } from 'path';
|
|
7
|
+
import { resolve, basename } from 'path';
|
|
8
8
|
try {
|
|
9
9
|
const envPath = resolve(process.cwd(), '.env');
|
|
10
10
|
const envContent = readFileSync(envPath, 'utf-8');
|
|
@@ -45,8 +45,8 @@ function validateProjectKey(key) {
|
|
|
45
45
|
if (!key || typeof key !== 'string') {
|
|
46
46
|
throw new Error('Invalid project key: must be a string');
|
|
47
47
|
}
|
|
48
|
-
if (!/^[A-Z][A-Z0-9_]{
|
|
49
|
-
throw new Error(`Invalid project key format: ${key}. Expected
|
|
48
|
+
if (!/^[A-Z][A-Z0-9_]{0,9}$/.test(key)) {
|
|
49
|
+
throw new Error(`Invalid project key format: ${key}. Expected 1-10 uppercase alphanumeric characters`);
|
|
50
50
|
}
|
|
51
51
|
return key;
|
|
52
52
|
}
|
|
@@ -69,16 +69,11 @@ function sanitizeString(str, maxLength = 1000, fieldName = 'input') {
|
|
|
69
69
|
return str.trim();
|
|
70
70
|
}
|
|
71
71
|
function validateSafeParam(str, fieldName, maxLength = 100) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
if (str.length > maxLength) {
|
|
76
|
-
throw new Error(`${fieldName} exceeds maximum length of ${maxLength} characters`);
|
|
77
|
-
}
|
|
78
|
-
if (/[\/\\]/.test(str)) {
|
|
72
|
+
const value = sanitizeString(str, maxLength, fieldName);
|
|
73
|
+
if (/[\/\\]/.test(value)) {
|
|
79
74
|
throw new Error(`Invalid ${fieldName}: contains unsafe characters`);
|
|
80
75
|
}
|
|
81
|
-
return
|
|
76
|
+
return value;
|
|
82
77
|
}
|
|
83
78
|
function validateMaxResults(maxResults) {
|
|
84
79
|
if (typeof maxResults !== 'number' || !Number.isInteger(maxResults) || maxResults < 1) {
|
|
@@ -106,6 +101,7 @@ function validateLabels(labels) {
|
|
|
106
101
|
return label;
|
|
107
102
|
});
|
|
108
103
|
}
|
|
104
|
+
const SERVER_VERSION = '2.3.0';
|
|
109
105
|
const JIRA_URL = getRequiredEnv('JIRA_HOST', process.env.JIRA_URL ?? null);
|
|
110
106
|
const JIRA_EMAIL = getRequiredEnv('JIRA_EMAIL');
|
|
111
107
|
const JIRA_API_TOKEN = getRequiredEnv('JIRA_API_TOKEN');
|
|
@@ -125,6 +121,9 @@ function createSuccessResponse(data) {
|
|
|
125
121
|
function createIssueUrl(issueKey) {
|
|
126
122
|
return `${JIRA_URL}/browse/${issueKey}`;
|
|
127
123
|
}
|
|
124
|
+
function resolveProjectKey(a) {
|
|
125
|
+
return a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
126
|
+
}
|
|
128
127
|
function handleError(error) {
|
|
129
128
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
130
129
|
const axiosError = error;
|
|
@@ -151,20 +150,26 @@ function handleError(error) {
|
|
|
151
150
|
isError: true,
|
|
152
151
|
};
|
|
153
152
|
}
|
|
154
|
-
const
|
|
155
|
-
baseURL: `${JIRA_URL}/rest/api/3`,
|
|
153
|
+
const axiosAuthConfig = {
|
|
156
154
|
auth: {
|
|
157
155
|
username: JIRA_EMAIL,
|
|
158
156
|
password: JIRA_API_TOKEN,
|
|
159
157
|
},
|
|
160
|
-
headers: {
|
|
161
|
-
'Content-Type': 'application/json',
|
|
162
|
-
},
|
|
163
158
|
timeout: 30000,
|
|
159
|
+
};
|
|
160
|
+
const jiraApi = axios.create({
|
|
161
|
+
baseURL: `${JIRA_URL}/rest/api/3`,
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
...axiosAuthConfig,
|
|
164
|
+
});
|
|
165
|
+
const agileApi = axios.create({
|
|
166
|
+
baseURL: `${JIRA_URL}/rest/agile/1.0`,
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
...axiosAuthConfig,
|
|
164
169
|
});
|
|
165
170
|
const server = new Server({
|
|
166
171
|
name: 'jira-mcp-server',
|
|
167
|
-
version:
|
|
172
|
+
version: SERVER_VERSION,
|
|
168
173
|
}, {
|
|
169
174
|
capabilities: {
|
|
170
175
|
tools: {},
|
|
@@ -205,34 +210,19 @@ function parseInlineContent(text) {
|
|
|
205
210
|
if (lastIndex < text.length) {
|
|
206
211
|
parts.push({ type: 'text', text: text.substring(lastIndex) });
|
|
207
212
|
}
|
|
208
|
-
|
|
209
|
-
return parts;
|
|
210
|
-
return text ? [{ type: 'text', text }] : [];
|
|
211
|
-
}
|
|
212
|
-
function addBulletItem(nodes, content) {
|
|
213
|
-
const listItem = {
|
|
214
|
-
type: 'listItem',
|
|
215
|
-
content: [{ type: 'paragraph', content }]
|
|
216
|
-
};
|
|
217
|
-
const lastNode = nodes[nodes.length - 1];
|
|
218
|
-
if (lastNode && lastNode.type === 'bulletList') {
|
|
219
|
-
lastNode.content.push(listItem);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
nodes.push({ type: 'bulletList', content: [listItem] });
|
|
223
|
-
}
|
|
213
|
+
return parts;
|
|
224
214
|
}
|
|
225
|
-
function
|
|
215
|
+
function addListItem(nodes, content, listType) {
|
|
226
216
|
const listItem = {
|
|
227
217
|
type: 'listItem',
|
|
228
218
|
content: [{ type: 'paragraph', content }]
|
|
229
219
|
};
|
|
230
220
|
const lastNode = nodes[nodes.length - 1];
|
|
231
|
-
if (lastNode && lastNode.type ===
|
|
221
|
+
if (lastNode && lastNode.type === listType) {
|
|
232
222
|
lastNode.content.push(listItem);
|
|
233
223
|
}
|
|
234
224
|
else {
|
|
235
|
-
nodes.push({ type:
|
|
225
|
+
nodes.push({ type: listType, content: [listItem] });
|
|
236
226
|
}
|
|
237
227
|
}
|
|
238
228
|
function createADFDocument(content) {
|
|
@@ -266,10 +256,10 @@ function createADFDocument(content) {
|
|
|
266
256
|
});
|
|
267
257
|
}
|
|
268
258
|
else if (line.startsWith('* ') || line.startsWith('- ')) {
|
|
269
|
-
|
|
259
|
+
addListItem(nodes, parseInlineContent(line.substring(2)), 'bulletList');
|
|
270
260
|
}
|
|
271
261
|
else if (/^\d+\.\s+/.test(line)) {
|
|
272
|
-
|
|
262
|
+
addListItem(nodes, parseInlineContent(line.replace(/^\d+\.\s+/, '')), 'orderedList');
|
|
273
263
|
}
|
|
274
264
|
else if (line.startsWith('> ')) {
|
|
275
265
|
const text = line.substring(2);
|
|
@@ -692,422 +682,733 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
692
682
|
required: ['query'],
|
|
693
683
|
},
|
|
694
684
|
},
|
|
685
|
+
{
|
|
686
|
+
name: 'jira_get_changelog',
|
|
687
|
+
description: 'Get the change history of a Jira issue (who changed what and when).',
|
|
688
|
+
inputSchema: {
|
|
689
|
+
type: 'object',
|
|
690
|
+
properties: {
|
|
691
|
+
issueKey: { type: 'string', description: 'Issue key (e.g., PROJ-123)' },
|
|
692
|
+
maxResults: { type: 'number', description: 'Maximum number of changelog entries (1-100)', default: 50 },
|
|
693
|
+
},
|
|
694
|
+
required: ['issueKey'],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: 'jira_get_user_issues',
|
|
699
|
+
description: 'Get all issues assigned to a specific user.',
|
|
700
|
+
inputSchema: {
|
|
701
|
+
type: 'object',
|
|
702
|
+
properties: {
|
|
703
|
+
accountId: { type: 'string', description: 'Atlassian account ID of the user' },
|
|
704
|
+
projectKey: { type: 'string', description: 'Filter by project key (defaults to configured JIRA_PROJECT_KEY)' },
|
|
705
|
+
maxResults: { type: 'number', description: 'Maximum number of results (1-100)', default: 50 },
|
|
706
|
+
status: { type: 'string', description: 'Filter by status (e.g., "In Progress")' },
|
|
707
|
+
},
|
|
708
|
+
required: ['accountId'],
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
name: 'jira_bulk_create_issues',
|
|
713
|
+
description: 'Create multiple Jira issues at once. Descriptions support Markdown, automatically converted to ADF.',
|
|
714
|
+
inputSchema: {
|
|
715
|
+
type: 'object',
|
|
716
|
+
properties: {
|
|
717
|
+
issues: {
|
|
718
|
+
type: 'array',
|
|
719
|
+
description: 'Array of issues to create',
|
|
720
|
+
items: {
|
|
721
|
+
type: 'object',
|
|
722
|
+
properties: {
|
|
723
|
+
summary: { type: 'string', description: 'Issue summary/title' },
|
|
724
|
+
description: { type: 'string', description: 'Issue description in Markdown' },
|
|
725
|
+
issueType: { type: 'string', description: 'Issue type (Story, Task, Bug, etc.)', default: 'Task' },
|
|
726
|
+
priority: { type: 'string', description: 'Priority (Highest, High, Medium, Low, Lowest)', default: 'Medium' },
|
|
727
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
728
|
+
storyPoints: { type: 'number' },
|
|
729
|
+
},
|
|
730
|
+
required: ['summary'],
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
projectKey: { type: 'string', description: 'Project key (defaults to configured JIRA_PROJECT_KEY)' },
|
|
734
|
+
},
|
|
735
|
+
required: ['issues'],
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
name: 'jira_clone_issue',
|
|
740
|
+
description: 'Clone an existing Jira issue with a new summary.',
|
|
741
|
+
inputSchema: {
|
|
742
|
+
type: 'object',
|
|
743
|
+
properties: {
|
|
744
|
+
issueKey: { type: 'string', description: 'Issue key to clone (e.g., PROJ-123)' },
|
|
745
|
+
summary: { type: 'string', description: 'Summary for the cloned issue (defaults to "Clone of <original>")' },
|
|
746
|
+
projectKey: { type: 'string', description: 'Target project key (defaults to same project as source)' },
|
|
747
|
+
},
|
|
748
|
+
required: ['issueKey'],
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'jira_list_boards',
|
|
753
|
+
description: 'List all Scrum/Kanban boards.',
|
|
754
|
+
inputSchema: {
|
|
755
|
+
type: 'object',
|
|
756
|
+
properties: {
|
|
757
|
+
projectKey: { type: 'string', description: 'Filter by project key' },
|
|
758
|
+
maxResults: { type: 'number', description: 'Maximum number of results (1-100)', default: 50 },
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: 'jira_list_sprints',
|
|
764
|
+
description: 'List sprints for a board.',
|
|
765
|
+
inputSchema: {
|
|
766
|
+
type: 'object',
|
|
767
|
+
properties: {
|
|
768
|
+
boardId: { type: 'number', description: 'Board ID (use jira_list_boards to find it)' },
|
|
769
|
+
state: { type: 'string', description: 'Filter by state: active, future, closed', default: 'active' },
|
|
770
|
+
maxResults: { type: 'number', description: 'Maximum number of results (1-100)', default: 50 },
|
|
771
|
+
},
|
|
772
|
+
required: ['boardId'],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: 'jira_get_sprint',
|
|
777
|
+
description: 'Get details of a sprint including all issues in it.',
|
|
778
|
+
inputSchema: {
|
|
779
|
+
type: 'object',
|
|
780
|
+
properties: {
|
|
781
|
+
sprintId: { type: 'number', description: 'Sprint ID (use jira_list_sprints to find it)' },
|
|
782
|
+
maxResults: { type: 'number', description: 'Maximum number of issues (1-100)', default: 50 },
|
|
783
|
+
},
|
|
784
|
+
required: ['sprintId'],
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
name: 'jira_move_to_sprint',
|
|
789
|
+
description: 'Move one or more issues to a sprint.',
|
|
790
|
+
inputSchema: {
|
|
791
|
+
type: 'object',
|
|
792
|
+
properties: {
|
|
793
|
+
sprintId: { type: 'number', description: 'Sprint ID (use jira_list_sprints to find it)' },
|
|
794
|
+
issueKeys: { type: 'array', items: { type: 'string' }, description: 'Array of issue keys to move (e.g., ["PROJ-1", "PROJ-2"])' },
|
|
795
|
+
},
|
|
796
|
+
required: ['sprintId', 'issueKeys'],
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: 'jira_get_attachments',
|
|
801
|
+
description: 'Get list of attachments on a Jira issue.',
|
|
802
|
+
inputSchema: {
|
|
803
|
+
type: 'object',
|
|
804
|
+
properties: {
|
|
805
|
+
issueKey: { type: 'string', description: 'Issue key (e.g., PROJ-123)' },
|
|
806
|
+
},
|
|
807
|
+
required: ['issueKey'],
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'jira_add_attachment',
|
|
812
|
+
description: 'Attach a local file to a Jira issue.',
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: 'object',
|
|
815
|
+
properties: {
|
|
816
|
+
issueKey: { type: 'string', description: 'Issue key (e.g., PROJ-123)' },
|
|
817
|
+
filePath: { type: 'string', description: 'Absolute path to the file to attach' },
|
|
818
|
+
},
|
|
819
|
+
required: ['issueKey', 'filePath'],
|
|
820
|
+
},
|
|
821
|
+
},
|
|
695
822
|
],
|
|
696
823
|
};
|
|
697
824
|
});
|
|
825
|
+
async function handleCreateIssue(a) {
|
|
826
|
+
const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = a;
|
|
827
|
+
const projectKey = resolveProjectKey(a);
|
|
828
|
+
validateSafeParam(issueType, 'issueType');
|
|
829
|
+
validateSafeParam(priority, 'priority');
|
|
830
|
+
const validatedLabels = validateLabels(labels);
|
|
831
|
+
const issueData = {
|
|
832
|
+
fields: {
|
|
833
|
+
project: { key: projectKey },
|
|
834
|
+
summary: sanitizeString(summary, 500, 'summary'),
|
|
835
|
+
description: createADFDocument(description),
|
|
836
|
+
issuetype: { name: issueType },
|
|
837
|
+
priority: { name: priority },
|
|
838
|
+
labels: validatedLabels,
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
if (storyPoints !== undefined && storyPoints !== null) {
|
|
842
|
+
issueData.fields[STORY_POINTS_FIELD] = validateStoryPoints(storyPoints);
|
|
843
|
+
}
|
|
844
|
+
const response = await jiraApi.post('/issue', issueData);
|
|
845
|
+
return createSuccessResponse({
|
|
846
|
+
success: true,
|
|
847
|
+
key: response.data.key,
|
|
848
|
+
id: response.data.id,
|
|
849
|
+
url: createIssueUrl(response.data.key),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
async function handleGetIssue(a) {
|
|
853
|
+
validateIssueKey(a.issueKey);
|
|
854
|
+
const response = await jiraApi.get(`/issue/${a.issueKey}`);
|
|
855
|
+
const f = response.data.fields;
|
|
856
|
+
return createSuccessResponse({
|
|
857
|
+
key: response.data.key,
|
|
858
|
+
summary: f.summary,
|
|
859
|
+
description: adfToText(f.description),
|
|
860
|
+
status: f.status?.name,
|
|
861
|
+
assignee: f.assignee ? { displayName: f.assignee.displayName, accountId: f.assignee.accountId } : null,
|
|
862
|
+
reporter: f.reporter?.displayName,
|
|
863
|
+
priority: f.priority?.name,
|
|
864
|
+
issueType: f.issuetype?.name,
|
|
865
|
+
labels: f.labels || [],
|
|
866
|
+
storyPoints: f[STORY_POINTS_FIELD],
|
|
867
|
+
parent: f.parent?.key,
|
|
868
|
+
created: f.created,
|
|
869
|
+
updated: f.updated,
|
|
870
|
+
url: createIssueUrl(response.data.key),
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
async function handleSearchIssues(a) {
|
|
874
|
+
const { jql, maxResults = 50 } = a;
|
|
875
|
+
validateJQL(jql);
|
|
876
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
877
|
+
const response = await jiraApi.get('/search/jql', {
|
|
878
|
+
params: {
|
|
879
|
+
jql,
|
|
880
|
+
maxResults: validatedMaxResults,
|
|
881
|
+
fields: 'summary,status,assignee,priority,created,updated,issuetype,parent,labels',
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
return createSuccessResponse({
|
|
885
|
+
total: response.data.total,
|
|
886
|
+
issues: response.data.issues.map((issue) => ({
|
|
887
|
+
key: issue.key,
|
|
888
|
+
summary: issue.fields.summary,
|
|
889
|
+
status: issue.fields.status?.name,
|
|
890
|
+
assignee: issue.fields.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null,
|
|
891
|
+
priority: issue.fields.priority?.name,
|
|
892
|
+
issueType: issue.fields.issuetype?.name,
|
|
893
|
+
labels: issue.fields.labels || [],
|
|
894
|
+
parent: issue.fields.parent?.key,
|
|
895
|
+
url: createIssueUrl(issue.key),
|
|
896
|
+
})),
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
async function handleUpdateIssue(a) {
|
|
900
|
+
const { issueKey, summary, description, status } = a;
|
|
901
|
+
validateIssueKey(issueKey);
|
|
902
|
+
const updateData = { fields: {} };
|
|
903
|
+
let hasFieldUpdates = false;
|
|
904
|
+
if (summary) {
|
|
905
|
+
updateData.fields.summary = sanitizeString(summary, 500, 'summary');
|
|
906
|
+
hasFieldUpdates = true;
|
|
907
|
+
}
|
|
908
|
+
if (description) {
|
|
909
|
+
updateData.fields.description = createADFDocument(description);
|
|
910
|
+
hasFieldUpdates = true;
|
|
911
|
+
}
|
|
912
|
+
if (hasFieldUpdates) {
|
|
913
|
+
await jiraApi.put(`/issue/${issueKey}`, updateData);
|
|
914
|
+
}
|
|
915
|
+
const warnings = [];
|
|
916
|
+
if (status) {
|
|
917
|
+
const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
918
|
+
const transition = transitions.data.transitions.find((t) => t.name === status);
|
|
919
|
+
if (transition) {
|
|
920
|
+
await jiraApi.post(`/issue/${issueKey}/transitions`, {
|
|
921
|
+
transition: { id: transition.id },
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
const available = transitions.data.transitions.map((t) => t.name).join(', ');
|
|
926
|
+
warnings.push(`Transition "${status}" not found. Available transitions: ${available}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (!hasFieldUpdates && !status) {
|
|
930
|
+
return createSuccessResponse({ success: false, message: `No updates provided for ${issueKey}` });
|
|
931
|
+
}
|
|
932
|
+
const result = {
|
|
933
|
+
success: warnings.length === 0,
|
|
934
|
+
message: `Issue ${issueKey} updated${warnings.length > 0 ? ' with warnings' : ' successfully'}`,
|
|
935
|
+
url: createIssueUrl(issueKey),
|
|
936
|
+
};
|
|
937
|
+
if (warnings.length > 0) {
|
|
938
|
+
result.warnings = warnings;
|
|
939
|
+
}
|
|
940
|
+
return createSuccessResponse(result);
|
|
941
|
+
}
|
|
942
|
+
async function handleAddComment(a) {
|
|
943
|
+
validateIssueKey(a.issueKey);
|
|
944
|
+
await jiraApi.post(`/issue/${a.issueKey}/comment`, { body: createADFDocument(a.comment) });
|
|
945
|
+
return createSuccessResponse({ success: true, message: `Comment added to ${a.issueKey}` });
|
|
946
|
+
}
|
|
947
|
+
async function handleLinkIssues(a) {
|
|
948
|
+
const { inwardIssue, outwardIssue, linkType = 'Relates' } = a;
|
|
949
|
+
validateIssueKey(inwardIssue);
|
|
950
|
+
validateIssueKey(outwardIssue);
|
|
951
|
+
validateSafeParam(linkType, 'linkType');
|
|
952
|
+
try {
|
|
953
|
+
await jiraApi.post('/issueLink', {
|
|
954
|
+
type: { name: linkType },
|
|
955
|
+
inwardIssue: { key: inwardIssue },
|
|
956
|
+
outwardIssue: { key: outwardIssue },
|
|
957
|
+
});
|
|
958
|
+
return createSuccessResponse({ success: true, message: `Linked ${inwardIssue} to ${outwardIssue} with type "${linkType}"` });
|
|
959
|
+
}
|
|
960
|
+
catch (linkError) {
|
|
961
|
+
const axiosErr = linkError;
|
|
962
|
+
if (axiosErr.response?.status === 400 && axiosErr.response?.data?.errorMessages?.includes('link already exists')) {
|
|
963
|
+
return createSuccessResponse({ success: true, message: `Link between ${inwardIssue} and ${outwardIssue} already exists`, alreadyLinked: true });
|
|
964
|
+
}
|
|
965
|
+
throw linkError;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async function handleGetProjectInfo(a) {
|
|
969
|
+
const projectKey = a.projectKey ?? JIRA_PROJECT_KEY;
|
|
970
|
+
validateProjectKey(projectKey);
|
|
971
|
+
const response = await jiraApi.get(`/project/${projectKey}`);
|
|
972
|
+
return createSuccessResponse({
|
|
973
|
+
key: response.data.key,
|
|
974
|
+
name: response.data.name,
|
|
975
|
+
description: response.data.description,
|
|
976
|
+
lead: response.data.lead?.displayName,
|
|
977
|
+
url: response.data.url,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
async function handleDeleteIssue(a) {
|
|
981
|
+
validateIssueKey(a.issueKey);
|
|
982
|
+
await jiraApi.delete(`/issue/${a.issueKey}`);
|
|
983
|
+
return createSuccessResponse({ success: true, message: `Issue ${a.issueKey} deleted successfully` });
|
|
984
|
+
}
|
|
985
|
+
async function handleCreateSubtask(a) {
|
|
986
|
+
const { parentKey, summary, description, priority = 'Medium' } = a;
|
|
987
|
+
validateIssueKey(parentKey);
|
|
988
|
+
validateSafeParam(priority, 'priority');
|
|
989
|
+
const projectKey = resolveProjectKey(a);
|
|
990
|
+
const response = await jiraApi.post('/issue', {
|
|
991
|
+
fields: {
|
|
992
|
+
project: { key: projectKey },
|
|
993
|
+
summary: sanitizeString(summary, 500, 'summary'),
|
|
994
|
+
description: createADFDocument(description),
|
|
995
|
+
issuetype: { name: 'Subtask' },
|
|
996
|
+
priority: { name: priority },
|
|
997
|
+
parent: { key: parentKey },
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
return createSuccessResponse({ success: true, key: response.data.key, id: response.data.id, parent: parentKey, url: createIssueUrl(response.data.key) });
|
|
1001
|
+
}
|
|
1002
|
+
async function handleAssignIssue(a) {
|
|
1003
|
+
validateIssueKey(a.issueKey);
|
|
1004
|
+
await jiraApi.put(`/issue/${a.issueKey}/assignee`, { accountId: a.accountId !== undefined ? a.accountId : null });
|
|
1005
|
+
return createSuccessResponse({
|
|
1006
|
+
success: true,
|
|
1007
|
+
message: a.accountId ? `Issue ${a.issueKey} assigned to ${a.accountId}` : `Issue ${a.issueKey} unassigned`,
|
|
1008
|
+
url: createIssueUrl(a.issueKey),
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
async function handleListTransitions(a) {
|
|
1012
|
+
validateIssueKey(a.issueKey);
|
|
1013
|
+
const response = await jiraApi.get(`/issue/${a.issueKey}/transitions`);
|
|
1014
|
+
return createSuccessResponse({
|
|
1015
|
+
issueKey: a.issueKey,
|
|
1016
|
+
transitions: response.data.transitions.map((t) => ({
|
|
1017
|
+
id: t.id,
|
|
1018
|
+
name: t.name,
|
|
1019
|
+
to: { id: t.to.id, name: t.to.name, category: t.to.statusCategory?.name },
|
|
1020
|
+
})),
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
async function handleAddWorklog(a) {
|
|
1024
|
+
const { issueKey, timeSpent, comment, started } = a;
|
|
1025
|
+
validateIssueKey(issueKey);
|
|
1026
|
+
sanitizeString(timeSpent, 50, 'timeSpent');
|
|
1027
|
+
const worklogData = { timeSpent };
|
|
1028
|
+
if (comment)
|
|
1029
|
+
worklogData.comment = createADFDocument(comment);
|
|
1030
|
+
if (started)
|
|
1031
|
+
worklogData.started = started;
|
|
1032
|
+
const response = await jiraApi.post(`/issue/${issueKey}/worklog`, worklogData);
|
|
1033
|
+
return createSuccessResponse({ success: true, id: response.data.id, issueKey, timeSpent: response.data.timeSpent, author: response.data.author?.displayName });
|
|
1034
|
+
}
|
|
1035
|
+
async function handleGetComments(a) {
|
|
1036
|
+
const { issueKey, maxResults = 50, orderBy = '-created' } = a;
|
|
1037
|
+
validateIssueKey(issueKey);
|
|
1038
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1039
|
+
const response = await jiraApi.get(`/issue/${issueKey}/comment`, { params: { maxResults: validatedMaxResults, orderBy } });
|
|
1040
|
+
return createSuccessResponse({
|
|
1041
|
+
issueKey,
|
|
1042
|
+
total: response.data.total,
|
|
1043
|
+
comments: response.data.comments.map((c) => ({
|
|
1044
|
+
id: c.id,
|
|
1045
|
+
author: c.author?.displayName,
|
|
1046
|
+
body: adfToText(c.body),
|
|
1047
|
+
created: c.created,
|
|
1048
|
+
updated: c.updated,
|
|
1049
|
+
})),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
async function handleGetWorklogs(a) {
|
|
1053
|
+
validateIssueKey(a.issueKey);
|
|
1054
|
+
const response = await jiraApi.get(`/issue/${a.issueKey}/worklog`);
|
|
1055
|
+
return createSuccessResponse({
|
|
1056
|
+
issueKey: a.issueKey,
|
|
1057
|
+
total: response.data.total,
|
|
1058
|
+
worklogs: response.data.worklogs.map((w) => ({
|
|
1059
|
+
id: w.id,
|
|
1060
|
+
author: w.author?.displayName,
|
|
1061
|
+
timeSpent: w.timeSpent,
|
|
1062
|
+
timeSpentSeconds: w.timeSpentSeconds,
|
|
1063
|
+
started: w.started,
|
|
1064
|
+
comment: adfToText(w.comment),
|
|
1065
|
+
})),
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
async function handleListProjects(a) {
|
|
1069
|
+
const { maxResults = 50, query } = a;
|
|
1070
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1071
|
+
const params = { maxResults: validatedMaxResults };
|
|
1072
|
+
if (query)
|
|
1073
|
+
params.query = sanitizeString(query, 200, 'query');
|
|
1074
|
+
const response = await jiraApi.get('/project/search', { params });
|
|
1075
|
+
return createSuccessResponse({
|
|
1076
|
+
total: response.data.total,
|
|
1077
|
+
projects: response.data.values.map((p) => ({ key: p.key, name: p.name, projectTypeKey: p.projectTypeKey, style: p.style, lead: p.lead?.displayName })),
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
async function handleGetProjectComponents(a) {
|
|
1081
|
+
const projectKey = resolveProjectKey(a);
|
|
1082
|
+
const response = await jiraApi.get(`/project/${projectKey}/components`);
|
|
1083
|
+
return createSuccessResponse({
|
|
1084
|
+
projectKey,
|
|
1085
|
+
components: response.data.map((c) => ({ id: c.id, name: c.name, description: c.description, lead: c.lead?.displayName, assigneeType: c.assigneeType })),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
async function handleGetProjectVersions(a) {
|
|
1089
|
+
const projectKey = resolveProjectKey(a);
|
|
1090
|
+
const response = await jiraApi.get(`/project/${projectKey}/versions`);
|
|
1091
|
+
return createSuccessResponse({
|
|
1092
|
+
projectKey,
|
|
1093
|
+
versions: response.data.map((v) => ({ id: v.id, name: v.name, description: v.description, released: v.released, archived: v.archived, releaseDate: v.releaseDate, startDate: v.startDate })),
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async function handleGetFields(_a) {
|
|
1097
|
+
const response = await jiraApi.get('/field');
|
|
1098
|
+
return createSuccessResponse({ fields: response.data.map((f) => ({ id: f.id, name: f.name, custom: f.custom, schema: f.schema })) });
|
|
1099
|
+
}
|
|
1100
|
+
async function handleGetIssueTypes(a) {
|
|
1101
|
+
const projectKey = resolveProjectKey(a);
|
|
1102
|
+
const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
1103
|
+
return createSuccessResponse({
|
|
1104
|
+
projectKey,
|
|
1105
|
+
issueTypes: response.data.issueTypes.map((t) => ({ id: t.id, name: t.name, subtask: t.subtask, description: t.description })),
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
async function handleGetPriorities(_a) {
|
|
1109
|
+
const response = await jiraApi.get('/priority/search');
|
|
1110
|
+
return createSuccessResponse({ priorities: response.data.values.map((p) => ({ id: p.id, name: p.name, description: p.description, iconUrl: p.iconUrl })) });
|
|
1111
|
+
}
|
|
1112
|
+
async function handleGetLinkTypes(_a) {
|
|
1113
|
+
const response = await jiraApi.get('/issueLinkType');
|
|
1114
|
+
return createSuccessResponse({ linkTypes: response.data.issueLinkTypes.map((lt) => ({ id: lt.id, name: lt.name, inward: lt.inward, outward: lt.outward })) });
|
|
1115
|
+
}
|
|
1116
|
+
async function handleSearchUsers(a) {
|
|
1117
|
+
const { query, maxResults = 10 } = a;
|
|
1118
|
+
sanitizeString(query, 200, 'query');
|
|
1119
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1120
|
+
const response = await jiraApi.get('/user/search', { params: { query, maxResults: validatedMaxResults } });
|
|
1121
|
+
return createSuccessResponse({
|
|
1122
|
+
users: response.data.map((u) => ({ accountId: u.accountId, displayName: u.displayName, emailAddress: u.emailAddress, active: u.active, accountType: u.accountType })),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
async function handleGetChangelog(a) {
|
|
1126
|
+
const { issueKey, maxResults = 50 } = a;
|
|
1127
|
+
validateIssueKey(issueKey);
|
|
1128
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1129
|
+
const response = await jiraApi.get(`/issue/${issueKey}/changelog`, {
|
|
1130
|
+
params: { maxResults: validatedMaxResults },
|
|
1131
|
+
});
|
|
1132
|
+
return createSuccessResponse({
|
|
1133
|
+
issueKey,
|
|
1134
|
+
total: response.data.total,
|
|
1135
|
+
histories: response.data.values.map((h) => ({
|
|
1136
|
+
id: h.id,
|
|
1137
|
+
author: h.author?.displayName,
|
|
1138
|
+
created: h.created,
|
|
1139
|
+
items: h.items.map((item) => ({
|
|
1140
|
+
field: item.field,
|
|
1141
|
+
from: item.fromString,
|
|
1142
|
+
to: item.toString,
|
|
1143
|
+
})),
|
|
1144
|
+
})),
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
async function handleGetUserIssues(a) {
|
|
1148
|
+
const { accountId, maxResults = 50, status } = a;
|
|
1149
|
+
sanitizeString(accountId, 100, 'accountId');
|
|
1150
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1151
|
+
const projectKey = resolveProjectKey(a);
|
|
1152
|
+
const escapedStatus = status ? sanitizeString(status, 100, 'status').replace(/"/g, '\\"') : null;
|
|
1153
|
+
let jql = `project = ${projectKey} AND assignee = "${accountId.replace(/"/g, '\\"')}"`;
|
|
1154
|
+
if (escapedStatus)
|
|
1155
|
+
jql += ` AND status = "${escapedStatus}"`;
|
|
1156
|
+
jql += ' ORDER BY updated DESC';
|
|
1157
|
+
const response = await jiraApi.get('/search/jql', {
|
|
1158
|
+
params: {
|
|
1159
|
+
jql,
|
|
1160
|
+
maxResults: validatedMaxResults,
|
|
1161
|
+
fields: 'summary,status,priority,created,updated,issuetype,labels',
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
return createSuccessResponse({
|
|
1165
|
+
total: response.data.total,
|
|
1166
|
+
issues: response.data.issues.map((issue) => ({
|
|
1167
|
+
key: issue.key,
|
|
1168
|
+
summary: issue.fields.summary,
|
|
1169
|
+
status: issue.fields.status?.name,
|
|
1170
|
+
priority: issue.fields.priority?.name,
|
|
1171
|
+
issueType: issue.fields.issuetype?.name,
|
|
1172
|
+
labels: issue.fields.labels || [],
|
|
1173
|
+
updated: issue.fields.updated,
|
|
1174
|
+
url: createIssueUrl(issue.key),
|
|
1175
|
+
})),
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
async function handleBulkCreateIssues(a) {
|
|
1179
|
+
const { issues } = a;
|
|
1180
|
+
const projectKey = resolveProjectKey(a);
|
|
1181
|
+
if (!Array.isArray(issues) || issues.length === 0) {
|
|
1182
|
+
throw new Error('issues must be a non-empty array');
|
|
1183
|
+
}
|
|
1184
|
+
if (issues.length > 50) {
|
|
1185
|
+
throw new Error('Maximum 50 issues per bulk create');
|
|
1186
|
+
}
|
|
1187
|
+
const issueList = issues.map((issue) => ({
|
|
1188
|
+
fields: {
|
|
1189
|
+
project: { key: projectKey },
|
|
1190
|
+
summary: sanitizeString(issue.summary, 500, 'summary'),
|
|
1191
|
+
description: createADFDocument(issue.description),
|
|
1192
|
+
issuetype: { name: issue.issueType || 'Task' },
|
|
1193
|
+
priority: { name: issue.priority || 'Medium' },
|
|
1194
|
+
labels: Array.isArray(issue.labels) ? validateLabels(issue.labels) : [],
|
|
1195
|
+
...(issue.storyPoints !== undefined && issue.storyPoints !== null
|
|
1196
|
+
? { [STORY_POINTS_FIELD]: validateStoryPoints(issue.storyPoints) }
|
|
1197
|
+
: {}),
|
|
1198
|
+
},
|
|
1199
|
+
}));
|
|
1200
|
+
const response = await jiraApi.post('/issue/bulk', { issueUpdates: issueList });
|
|
1201
|
+
return createSuccessResponse({
|
|
1202
|
+
created: response.data.issues.map((issue) => ({
|
|
1203
|
+
key: issue.key,
|
|
1204
|
+
id: issue.id,
|
|
1205
|
+
url: createIssueUrl(issue.key),
|
|
1206
|
+
})),
|
|
1207
|
+
errors: response.data.errors || [],
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
async function handleCloneIssue(a) {
|
|
1211
|
+
const { issueKey } = a;
|
|
1212
|
+
validateIssueKey(issueKey);
|
|
1213
|
+
const source = await jiraApi.get(`/issue/${issueKey}`);
|
|
1214
|
+
const f = source.data.fields;
|
|
1215
|
+
const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : f.project?.key ?? JIRA_PROJECT_KEY;
|
|
1216
|
+
const summary = a.summary ? sanitizeString(a.summary, 500, 'summary') : `Clone of ${f.summary}`;
|
|
1217
|
+
const issueData = {
|
|
1218
|
+
fields: {
|
|
1219
|
+
project: { key: projectKey },
|
|
1220
|
+
summary,
|
|
1221
|
+
description: f.description ?? createADFDocument(''),
|
|
1222
|
+
issuetype: { name: f.issuetype?.name ?? 'Task' },
|
|
1223
|
+
priority: { name: f.priority?.name ?? 'Medium' },
|
|
1224
|
+
labels: f.labels || [],
|
|
1225
|
+
},
|
|
1226
|
+
};
|
|
1227
|
+
if (f[STORY_POINTS_FIELD] !== undefined && f[STORY_POINTS_FIELD] !== null) {
|
|
1228
|
+
issueData.fields[STORY_POINTS_FIELD] = f[STORY_POINTS_FIELD];
|
|
1229
|
+
}
|
|
1230
|
+
const response = await jiraApi.post('/issue', issueData);
|
|
1231
|
+
return createSuccessResponse({
|
|
1232
|
+
success: true,
|
|
1233
|
+
key: response.data.key,
|
|
1234
|
+
id: response.data.id,
|
|
1235
|
+
clonedFrom: issueKey,
|
|
1236
|
+
url: createIssueUrl(response.data.key),
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
async function handleListBoards(a) {
|
|
1240
|
+
const { maxResults = 50 } = a;
|
|
1241
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1242
|
+
const params = { maxResults: validatedMaxResults };
|
|
1243
|
+
if (a.projectKey)
|
|
1244
|
+
params.projectKeyOrId = validateProjectKey(a.projectKey);
|
|
1245
|
+
const response = await agileApi.get('/board', { params });
|
|
1246
|
+
return createSuccessResponse({
|
|
1247
|
+
total: response.data.total,
|
|
1248
|
+
boards: response.data.values.map((b) => ({
|
|
1249
|
+
id: b.id,
|
|
1250
|
+
name: b.name,
|
|
1251
|
+
type: b.type,
|
|
1252
|
+
projectKey: b.location?.projectKey,
|
|
1253
|
+
projectName: b.location?.projectName,
|
|
1254
|
+
})),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
async function handleListSprints(a) {
|
|
1258
|
+
const { boardId, state = 'active', maxResults = 50 } = a;
|
|
1259
|
+
if (typeof boardId !== 'number')
|
|
1260
|
+
throw new Error('boardId must be a number');
|
|
1261
|
+
if (!['active', 'future', 'closed'].includes(state))
|
|
1262
|
+
throw new Error('state must be one of: active, future, closed');
|
|
1263
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1264
|
+
const response = await agileApi.get(`/board/${boardId}/sprint`, {
|
|
1265
|
+
params: { state, maxResults: validatedMaxResults },
|
|
1266
|
+
});
|
|
1267
|
+
return createSuccessResponse({
|
|
1268
|
+
total: response.data.total,
|
|
1269
|
+
sprints: response.data.values.map((s) => ({
|
|
1270
|
+
id: s.id,
|
|
1271
|
+
name: s.name,
|
|
1272
|
+
state: s.state,
|
|
1273
|
+
startDate: s.startDate,
|
|
1274
|
+
endDate: s.endDate,
|
|
1275
|
+
goal: s.goal,
|
|
1276
|
+
})),
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
async function handleGetSprint(a) {
|
|
1280
|
+
const { sprintId, maxResults = 50 } = a;
|
|
1281
|
+
if (typeof sprintId !== 'number')
|
|
1282
|
+
throw new Error('sprintId must be a number');
|
|
1283
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1284
|
+
const [sprintRes, issuesRes] = await Promise.all([
|
|
1285
|
+
agileApi.get(`/sprint/${sprintId}`),
|
|
1286
|
+
agileApi.get(`/sprint/${sprintId}/issue`, {
|
|
1287
|
+
params: {
|
|
1288
|
+
maxResults: validatedMaxResults,
|
|
1289
|
+
fields: 'summary,status,assignee,priority,issuetype,labels',
|
|
1290
|
+
},
|
|
1291
|
+
}),
|
|
1292
|
+
]);
|
|
1293
|
+
return createSuccessResponse({
|
|
1294
|
+
id: sprintRes.data.id,
|
|
1295
|
+
name: sprintRes.data.name,
|
|
1296
|
+
state: sprintRes.data.state,
|
|
1297
|
+
startDate: sprintRes.data.startDate,
|
|
1298
|
+
endDate: sprintRes.data.endDate,
|
|
1299
|
+
goal: sprintRes.data.goal,
|
|
1300
|
+
total: issuesRes.data.total,
|
|
1301
|
+
issues: issuesRes.data.issues.map((issue) => ({
|
|
1302
|
+
key: issue.key,
|
|
1303
|
+
summary: issue.fields.summary,
|
|
1304
|
+
status: issue.fields.status?.name,
|
|
1305
|
+
assignee: issue.fields.assignee?.displayName ?? null,
|
|
1306
|
+
priority: issue.fields.priority?.name,
|
|
1307
|
+
issueType: issue.fields.issuetype?.name,
|
|
1308
|
+
labels: issue.fields.labels || [],
|
|
1309
|
+
url: createIssueUrl(issue.key),
|
|
1310
|
+
})),
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
async function handleMoveToSprint(a) {
|
|
1314
|
+
const { sprintId, issueKeys } = a;
|
|
1315
|
+
if (typeof sprintId !== 'number')
|
|
1316
|
+
throw new Error('sprintId must be a number');
|
|
1317
|
+
if (!Array.isArray(issueKeys) || issueKeys.length === 0)
|
|
1318
|
+
throw new Error('issueKeys must be a non-empty array');
|
|
1319
|
+
const validatedKeys = issueKeys.map((k) => validateIssueKey(k));
|
|
1320
|
+
await agileApi.post(`/sprint/${sprintId}/issue`, { issues: validatedKeys });
|
|
1321
|
+
return createSuccessResponse({
|
|
1322
|
+
success: true,
|
|
1323
|
+
sprintId,
|
|
1324
|
+
moved: validatedKeys,
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
async function handleGetAttachments(a) {
|
|
1328
|
+
validateIssueKey(a.issueKey);
|
|
1329
|
+
const response = await jiraApi.get(`/issue/${a.issueKey}`, {
|
|
1330
|
+
params: { fields: 'attachment' },
|
|
1331
|
+
});
|
|
1332
|
+
const attachments = response.data.fields.attachment || [];
|
|
1333
|
+
return createSuccessResponse({
|
|
1334
|
+
issueKey: a.issueKey,
|
|
1335
|
+
total: attachments.length,
|
|
1336
|
+
attachments: attachments.map((att) => ({
|
|
1337
|
+
id: att.id,
|
|
1338
|
+
filename: att.filename,
|
|
1339
|
+
size: att.size,
|
|
1340
|
+
mimeType: att.mimeType,
|
|
1341
|
+
created: att.created,
|
|
1342
|
+
author: att.author?.displayName,
|
|
1343
|
+
url: att.content,
|
|
1344
|
+
})),
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
async function handleAddAttachment(a) {
|
|
1348
|
+
validateIssueKey(a.issueKey);
|
|
1349
|
+
const filePath = sanitizeString(a.filePath, 500, 'filePath');
|
|
1350
|
+
const absolutePath = resolve(filePath);
|
|
1351
|
+
if (!absolutePath.startsWith('/'))
|
|
1352
|
+
throw new Error('filePath must be an absolute path');
|
|
1353
|
+
const fileName = basename(absolutePath);
|
|
1354
|
+
const fileBuffer = readFileSync(absolutePath);
|
|
1355
|
+
const form = new FormData();
|
|
1356
|
+
form.append('file', new Blob([fileBuffer]), fileName);
|
|
1357
|
+
const response = await jiraApi.post(`/issue/${a.issueKey}/attachments`, form, {
|
|
1358
|
+
headers: { 'X-Atlassian-Token': 'no-check' },
|
|
1359
|
+
});
|
|
1360
|
+
return createSuccessResponse({
|
|
1361
|
+
success: true,
|
|
1362
|
+
attachments: response.data.map((att) => ({
|
|
1363
|
+
id: att.id,
|
|
1364
|
+
filename: att.filename,
|
|
1365
|
+
size: att.size,
|
|
1366
|
+
mimeType: att.mimeType,
|
|
1367
|
+
url: att.content,
|
|
1368
|
+
})),
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
const toolHandlers = {
|
|
1372
|
+
jira_create_issue: handleCreateIssue,
|
|
1373
|
+
jira_get_issue: handleGetIssue,
|
|
1374
|
+
jira_search_issues: handleSearchIssues,
|
|
1375
|
+
jira_update_issue: handleUpdateIssue,
|
|
1376
|
+
jira_add_comment: handleAddComment,
|
|
1377
|
+
jira_link_issues: handleLinkIssues,
|
|
1378
|
+
jira_get_project_info: handleGetProjectInfo,
|
|
1379
|
+
jira_delete_issue: handleDeleteIssue,
|
|
1380
|
+
jira_create_subtask: handleCreateSubtask,
|
|
1381
|
+
jira_assign_issue: handleAssignIssue,
|
|
1382
|
+
jira_list_transitions: handleListTransitions,
|
|
1383
|
+
jira_add_worklog: handleAddWorklog,
|
|
1384
|
+
jira_get_comments: handleGetComments,
|
|
1385
|
+
jira_get_worklogs: handleGetWorklogs,
|
|
1386
|
+
jira_list_projects: handleListProjects,
|
|
1387
|
+
jira_get_project_components: handleGetProjectComponents,
|
|
1388
|
+
jira_get_project_versions: handleGetProjectVersions,
|
|
1389
|
+
jira_get_fields: handleGetFields,
|
|
1390
|
+
jira_get_issue_types: handleGetIssueTypes,
|
|
1391
|
+
jira_get_priorities: handleGetPriorities,
|
|
1392
|
+
jira_get_link_types: handleGetLinkTypes,
|
|
1393
|
+
jira_search_users: handleSearchUsers,
|
|
1394
|
+
jira_get_changelog: handleGetChangelog,
|
|
1395
|
+
jira_get_user_issues: handleGetUserIssues,
|
|
1396
|
+
jira_bulk_create_issues: handleBulkCreateIssues,
|
|
1397
|
+
jira_clone_issue: handleCloneIssue,
|
|
1398
|
+
jira_list_boards: handleListBoards,
|
|
1399
|
+
jira_list_sprints: handleListSprints,
|
|
1400
|
+
jira_get_sprint: handleGetSprint,
|
|
1401
|
+
jira_move_to_sprint: handleMoveToSprint,
|
|
1402
|
+
jira_get_attachments: handleGetAttachments,
|
|
1403
|
+
jira_add_attachment: handleAddAttachment,
|
|
1404
|
+
};
|
|
698
1405
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
699
1406
|
const { name, arguments: args } = request.params;
|
|
700
|
-
const
|
|
1407
|
+
const handler = toolHandlers[name];
|
|
1408
|
+
if (!handler)
|
|
1409
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
701
1410
|
try {
|
|
702
|
-
|
|
703
|
-
case 'jira_create_issue': {
|
|
704
|
-
const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = a;
|
|
705
|
-
const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
706
|
-
validateSafeParam(issueType, 'issueType');
|
|
707
|
-
validateSafeParam(priority, 'priority');
|
|
708
|
-
const validatedLabels = validateLabels(labels);
|
|
709
|
-
const issueData = {
|
|
710
|
-
fields: {
|
|
711
|
-
project: { key: projectKey },
|
|
712
|
-
summary: sanitizeString(summary, 500, 'summary'),
|
|
713
|
-
description: createADFDocument(description),
|
|
714
|
-
issuetype: { name: issueType },
|
|
715
|
-
priority: { name: priority },
|
|
716
|
-
labels: validatedLabels,
|
|
717
|
-
},
|
|
718
|
-
};
|
|
719
|
-
if (storyPoints !== undefined && storyPoints !== null) {
|
|
720
|
-
issueData.fields[STORY_POINTS_FIELD] = validateStoryPoints(storyPoints);
|
|
721
|
-
}
|
|
722
|
-
const response = await jiraApi.post('/issue', issueData);
|
|
723
|
-
return createSuccessResponse({
|
|
724
|
-
success: true,
|
|
725
|
-
key: response.data.key,
|
|
726
|
-
id: response.data.id,
|
|
727
|
-
url: createIssueUrl(response.data.key),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
case 'jira_get_issue': {
|
|
731
|
-
const { issueKey } = a;
|
|
732
|
-
validateIssueKey(issueKey);
|
|
733
|
-
const response = await jiraApi.get(`/issue/${issueKey}`);
|
|
734
|
-
const f = response.data.fields;
|
|
735
|
-
return createSuccessResponse({
|
|
736
|
-
key: response.data.key,
|
|
737
|
-
summary: f.summary,
|
|
738
|
-
description: adfToText(f.description),
|
|
739
|
-
status: f.status?.name,
|
|
740
|
-
assignee: f.assignee ? { displayName: f.assignee.displayName, accountId: f.assignee.accountId } : null,
|
|
741
|
-
reporter: f.reporter?.displayName,
|
|
742
|
-
priority: f.priority?.name,
|
|
743
|
-
issueType: f.issuetype?.name,
|
|
744
|
-
labels: f.labels || [],
|
|
745
|
-
storyPoints: f[STORY_POINTS_FIELD],
|
|
746
|
-
parent: f.parent?.key,
|
|
747
|
-
created: f.created,
|
|
748
|
-
updated: f.updated,
|
|
749
|
-
url: createIssueUrl(response.data.key),
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
case 'jira_search_issues': {
|
|
753
|
-
const { jql, maxResults = 50 } = a;
|
|
754
|
-
validateJQL(jql);
|
|
755
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
756
|
-
const response = await jiraApi.get('/search/jql', {
|
|
757
|
-
params: {
|
|
758
|
-
jql,
|
|
759
|
-
maxResults: validatedMaxResults,
|
|
760
|
-
fields: 'summary,status,assignee,priority,created,updated,issuetype,parent,labels',
|
|
761
|
-
},
|
|
762
|
-
});
|
|
763
|
-
return createSuccessResponse({
|
|
764
|
-
total: response.data.total,
|
|
765
|
-
issues: response.data.issues.map((issue) => ({
|
|
766
|
-
key: issue.key,
|
|
767
|
-
summary: issue.fields.summary,
|
|
768
|
-
status: issue.fields.status?.name,
|
|
769
|
-
assignee: issue.fields.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null,
|
|
770
|
-
priority: issue.fields.priority?.name,
|
|
771
|
-
issueType: issue.fields.issuetype?.name,
|
|
772
|
-
labels: issue.fields.labels || [],
|
|
773
|
-
parent: issue.fields.parent?.key,
|
|
774
|
-
url: createIssueUrl(issue.key),
|
|
775
|
-
})),
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
case 'jira_update_issue': {
|
|
779
|
-
const { issueKey, summary, description, status } = a;
|
|
780
|
-
validateIssueKey(issueKey);
|
|
781
|
-
const updateData = { fields: {} };
|
|
782
|
-
let hasFieldUpdates = false;
|
|
783
|
-
if (summary) {
|
|
784
|
-
updateData.fields.summary = sanitizeString(summary, 500, 'summary');
|
|
785
|
-
hasFieldUpdates = true;
|
|
786
|
-
}
|
|
787
|
-
if (description) {
|
|
788
|
-
updateData.fields.description = createADFDocument(description);
|
|
789
|
-
hasFieldUpdates = true;
|
|
790
|
-
}
|
|
791
|
-
if (hasFieldUpdates) {
|
|
792
|
-
await jiraApi.put(`/issue/${issueKey}`, updateData);
|
|
793
|
-
}
|
|
794
|
-
const warnings = [];
|
|
795
|
-
if (status) {
|
|
796
|
-
const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
797
|
-
const transition = transitions.data.transitions.find((t) => t.name === status);
|
|
798
|
-
if (transition) {
|
|
799
|
-
await jiraApi.post(`/issue/${issueKey}/transitions`, {
|
|
800
|
-
transition: { id: transition.id },
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
const available = transitions.data.transitions.map((t) => t.name).join(', ');
|
|
805
|
-
warnings.push(`Transition "${status}" not found. Available transitions: ${available}`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
if (!hasFieldUpdates && !status) {
|
|
809
|
-
return createSuccessResponse({
|
|
810
|
-
success: false,
|
|
811
|
-
message: `No updates provided for ${issueKey}`,
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
const result = {
|
|
815
|
-
success: warnings.length === 0,
|
|
816
|
-
message: `Issue ${issueKey} updated${warnings.length > 0 ? ' with warnings' : ' successfully'}`,
|
|
817
|
-
url: createIssueUrl(issueKey),
|
|
818
|
-
};
|
|
819
|
-
if (warnings.length > 0) {
|
|
820
|
-
result.warnings = warnings;
|
|
821
|
-
}
|
|
822
|
-
return createSuccessResponse(result);
|
|
823
|
-
}
|
|
824
|
-
case 'jira_add_comment': {
|
|
825
|
-
const { issueKey, comment } = a;
|
|
826
|
-
validateIssueKey(issueKey);
|
|
827
|
-
await jiraApi.post(`/issue/${issueKey}/comment`, {
|
|
828
|
-
body: createADFDocument(comment),
|
|
829
|
-
});
|
|
830
|
-
return createSuccessResponse({
|
|
831
|
-
success: true,
|
|
832
|
-
message: `Comment added to ${issueKey}`,
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
case 'jira_link_issues': {
|
|
836
|
-
const { inwardIssue, outwardIssue, linkType = 'Relates' } = a;
|
|
837
|
-
validateIssueKey(inwardIssue);
|
|
838
|
-
validateIssueKey(outwardIssue);
|
|
839
|
-
validateSafeParam(linkType, 'linkType');
|
|
840
|
-
try {
|
|
841
|
-
await jiraApi.post('/issueLink', {
|
|
842
|
-
type: { name: linkType },
|
|
843
|
-
inwardIssue: { key: inwardIssue },
|
|
844
|
-
outwardIssue: { key: outwardIssue },
|
|
845
|
-
});
|
|
846
|
-
return createSuccessResponse({
|
|
847
|
-
success: true,
|
|
848
|
-
message: `Linked ${inwardIssue} to ${outwardIssue} with type "${linkType}"`,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
catch (linkError) {
|
|
852
|
-
const axiosErr = linkError;
|
|
853
|
-
if (axiosErr.response?.status === 400 &&
|
|
854
|
-
axiosErr.response?.data?.errorMessages?.includes('link already exists')) {
|
|
855
|
-
return createSuccessResponse({
|
|
856
|
-
success: true,
|
|
857
|
-
message: `Link between ${inwardIssue} and ${outwardIssue} already exists`,
|
|
858
|
-
alreadyLinked: true,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
throw linkError;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
case 'jira_get_project_info': {
|
|
865
|
-
const { projectKey = JIRA_PROJECT_KEY } = a;
|
|
866
|
-
validateProjectKey(projectKey);
|
|
867
|
-
const response = await jiraApi.get(`/project/${projectKey}`);
|
|
868
|
-
return createSuccessResponse({
|
|
869
|
-
key: response.data.key,
|
|
870
|
-
name: response.data.name,
|
|
871
|
-
description: response.data.description,
|
|
872
|
-
lead: response.data.lead?.displayName,
|
|
873
|
-
url: response.data.url,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
case 'jira_delete_issue': {
|
|
877
|
-
const { issueKey } = a;
|
|
878
|
-
validateIssueKey(issueKey);
|
|
879
|
-
await jiraApi.delete(`/issue/${issueKey}`);
|
|
880
|
-
return createSuccessResponse({
|
|
881
|
-
success: true,
|
|
882
|
-
message: `Issue ${issueKey} deleted successfully`,
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
case 'jira_create_subtask': {
|
|
886
|
-
const { parentKey, summary, description, priority = 'Medium' } = a;
|
|
887
|
-
validateIssueKey(parentKey);
|
|
888
|
-
validateSafeParam(priority, 'priority');
|
|
889
|
-
const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
890
|
-
const issueData = {
|
|
891
|
-
fields: {
|
|
892
|
-
project: { key: projectKey },
|
|
893
|
-
summary: sanitizeString(summary, 500, 'summary'),
|
|
894
|
-
description: createADFDocument(description),
|
|
895
|
-
issuetype: { name: 'Subtask' },
|
|
896
|
-
priority: { name: priority },
|
|
897
|
-
parent: { key: parentKey },
|
|
898
|
-
},
|
|
899
|
-
};
|
|
900
|
-
const response = await jiraApi.post('/issue', issueData);
|
|
901
|
-
return createSuccessResponse({
|
|
902
|
-
success: true,
|
|
903
|
-
key: response.data.key,
|
|
904
|
-
id: response.data.id,
|
|
905
|
-
parent: parentKey,
|
|
906
|
-
url: createIssueUrl(response.data.key),
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
case 'jira_assign_issue': {
|
|
910
|
-
const { issueKey, accountId } = a;
|
|
911
|
-
validateIssueKey(issueKey);
|
|
912
|
-
await jiraApi.put(`/issue/${issueKey}/assignee`, {
|
|
913
|
-
accountId: accountId !== undefined ? accountId : null,
|
|
914
|
-
});
|
|
915
|
-
return createSuccessResponse({
|
|
916
|
-
success: true,
|
|
917
|
-
message: accountId
|
|
918
|
-
? `Issue ${issueKey} assigned to ${accountId}`
|
|
919
|
-
: `Issue ${issueKey} unassigned`,
|
|
920
|
-
url: createIssueUrl(issueKey),
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
case 'jira_list_transitions': {
|
|
924
|
-
const { issueKey } = a;
|
|
925
|
-
validateIssueKey(issueKey);
|
|
926
|
-
const response = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
927
|
-
return createSuccessResponse({
|
|
928
|
-
issueKey,
|
|
929
|
-
transitions: response.data.transitions.map((t) => ({
|
|
930
|
-
id: t.id,
|
|
931
|
-
name: t.name,
|
|
932
|
-
to: {
|
|
933
|
-
id: t.to.id,
|
|
934
|
-
name: t.to.name,
|
|
935
|
-
category: t.to.statusCategory?.name,
|
|
936
|
-
},
|
|
937
|
-
})),
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
case 'jira_add_worklog': {
|
|
941
|
-
const { issueKey, timeSpent, comment, started } = a;
|
|
942
|
-
validateIssueKey(issueKey);
|
|
943
|
-
sanitizeString(timeSpent, 50, 'timeSpent');
|
|
944
|
-
const worklogData = { timeSpent };
|
|
945
|
-
if (comment) {
|
|
946
|
-
worklogData.comment = createADFDocument(comment);
|
|
947
|
-
}
|
|
948
|
-
if (started) {
|
|
949
|
-
worklogData.started = started;
|
|
950
|
-
}
|
|
951
|
-
const response = await jiraApi.post(`/issue/${issueKey}/worklog`, worklogData);
|
|
952
|
-
return createSuccessResponse({
|
|
953
|
-
success: true,
|
|
954
|
-
id: response.data.id,
|
|
955
|
-
issueKey,
|
|
956
|
-
timeSpent: response.data.timeSpent,
|
|
957
|
-
author: response.data.author?.displayName,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
case 'jira_get_comments': {
|
|
961
|
-
const { issueKey, maxResults = 50, orderBy = '-created' } = a;
|
|
962
|
-
validateIssueKey(issueKey);
|
|
963
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
964
|
-
const response = await jiraApi.get(`/issue/${issueKey}/comment`, {
|
|
965
|
-
params: { maxResults: validatedMaxResults, orderBy },
|
|
966
|
-
});
|
|
967
|
-
return createSuccessResponse({
|
|
968
|
-
issueKey,
|
|
969
|
-
total: response.data.total,
|
|
970
|
-
comments: response.data.comments.map((c) => ({
|
|
971
|
-
id: c.id,
|
|
972
|
-
author: c.author?.displayName,
|
|
973
|
-
body: adfToText(c.body),
|
|
974
|
-
created: c.created,
|
|
975
|
-
updated: c.updated,
|
|
976
|
-
})),
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
case 'jira_get_worklogs': {
|
|
980
|
-
const { issueKey } = a;
|
|
981
|
-
validateIssueKey(issueKey);
|
|
982
|
-
const response = await jiraApi.get(`/issue/${issueKey}/worklog`);
|
|
983
|
-
return createSuccessResponse({
|
|
984
|
-
issueKey,
|
|
985
|
-
total: response.data.total,
|
|
986
|
-
worklogs: response.data.worklogs.map((w) => ({
|
|
987
|
-
id: w.id,
|
|
988
|
-
author: w.author?.displayName,
|
|
989
|
-
timeSpent: w.timeSpent,
|
|
990
|
-
timeSpentSeconds: w.timeSpentSeconds,
|
|
991
|
-
started: w.started,
|
|
992
|
-
comment: adfToText(w.comment),
|
|
993
|
-
})),
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
case 'jira_list_projects': {
|
|
997
|
-
const { maxResults = 50, query } = a;
|
|
998
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
999
|
-
const params = { maxResults: validatedMaxResults };
|
|
1000
|
-
if (query) {
|
|
1001
|
-
params.query = sanitizeString(query, 200, 'query');
|
|
1002
|
-
}
|
|
1003
|
-
const response = await jiraApi.get('/project/search', { params });
|
|
1004
|
-
return createSuccessResponse({
|
|
1005
|
-
total: response.data.total,
|
|
1006
|
-
projects: response.data.values.map((p) => ({
|
|
1007
|
-
key: p.key,
|
|
1008
|
-
name: p.name,
|
|
1009
|
-
projectTypeKey: p.projectTypeKey,
|
|
1010
|
-
style: p.style,
|
|
1011
|
-
lead: p.lead?.displayName,
|
|
1012
|
-
})),
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
case 'jira_get_project_components': {
|
|
1016
|
-
const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
1017
|
-
const response = await jiraApi.get(`/project/${projectKey}/components`);
|
|
1018
|
-
return createSuccessResponse({
|
|
1019
|
-
projectKey,
|
|
1020
|
-
components: response.data.map((c) => ({
|
|
1021
|
-
id: c.id,
|
|
1022
|
-
name: c.name,
|
|
1023
|
-
description: c.description,
|
|
1024
|
-
lead: c.lead?.displayName,
|
|
1025
|
-
assigneeType: c.assigneeType,
|
|
1026
|
-
})),
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
case 'jira_get_project_versions': {
|
|
1030
|
-
const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
1031
|
-
const response = await jiraApi.get(`/project/${projectKey}/versions`);
|
|
1032
|
-
return createSuccessResponse({
|
|
1033
|
-
projectKey,
|
|
1034
|
-
versions: response.data.map((v) => ({
|
|
1035
|
-
id: v.id,
|
|
1036
|
-
name: v.name,
|
|
1037
|
-
description: v.description,
|
|
1038
|
-
released: v.released,
|
|
1039
|
-
archived: v.archived,
|
|
1040
|
-
releaseDate: v.releaseDate,
|
|
1041
|
-
startDate: v.startDate,
|
|
1042
|
-
})),
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
case 'jira_get_fields': {
|
|
1046
|
-
const response = await jiraApi.get('/field');
|
|
1047
|
-
return createSuccessResponse({
|
|
1048
|
-
fields: response.data.map((f) => ({
|
|
1049
|
-
id: f.id,
|
|
1050
|
-
name: f.name,
|
|
1051
|
-
custom: f.custom,
|
|
1052
|
-
schema: f.schema,
|
|
1053
|
-
})),
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
case 'jira_get_issue_types': {
|
|
1057
|
-
const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
1058
|
-
const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
1059
|
-
return createSuccessResponse({
|
|
1060
|
-
projectKey,
|
|
1061
|
-
issueTypes: response.data.issueTypes.map((t) => ({
|
|
1062
|
-
id: t.id,
|
|
1063
|
-
name: t.name,
|
|
1064
|
-
subtask: t.subtask,
|
|
1065
|
-
description: t.description,
|
|
1066
|
-
})),
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
case 'jira_get_priorities': {
|
|
1070
|
-
const response = await jiraApi.get('/priority/search');
|
|
1071
|
-
return createSuccessResponse({
|
|
1072
|
-
priorities: response.data.values.map((p) => ({
|
|
1073
|
-
id: p.id,
|
|
1074
|
-
name: p.name,
|
|
1075
|
-
description: p.description,
|
|
1076
|
-
iconUrl: p.iconUrl,
|
|
1077
|
-
})),
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
case 'jira_get_link_types': {
|
|
1081
|
-
const response = await jiraApi.get('/issueLinkType');
|
|
1082
|
-
return createSuccessResponse({
|
|
1083
|
-
linkTypes: response.data.issueLinkTypes.map((lt) => ({
|
|
1084
|
-
id: lt.id,
|
|
1085
|
-
name: lt.name,
|
|
1086
|
-
inward: lt.inward,
|
|
1087
|
-
outward: lt.outward,
|
|
1088
|
-
})),
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
case 'jira_search_users': {
|
|
1092
|
-
const { query, maxResults = 10 } = a;
|
|
1093
|
-
sanitizeString(query, 200, 'query');
|
|
1094
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1095
|
-
const response = await jiraApi.get('/user/search', {
|
|
1096
|
-
params: { query, maxResults: validatedMaxResults },
|
|
1097
|
-
});
|
|
1098
|
-
return createSuccessResponse({
|
|
1099
|
-
users: response.data.map((u) => ({
|
|
1100
|
-
accountId: u.accountId,
|
|
1101
|
-
displayName: u.displayName,
|
|
1102
|
-
emailAddress: u.emailAddress,
|
|
1103
|
-
active: u.active,
|
|
1104
|
-
accountType: u.accountType,
|
|
1105
|
-
})),
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
default:
|
|
1109
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1110
|
-
}
|
|
1411
|
+
return await handler(args);
|
|
1111
1412
|
}
|
|
1112
1413
|
catch (error) {
|
|
1113
1414
|
return handleError(error);
|