@mcpio/jira 2.2.2 → 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 +415 -12
- 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');
|
|
@@ -101,6 +101,7 @@ function validateLabels(labels) {
|
|
|
101
101
|
return label;
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
+
const SERVER_VERSION = '2.3.0';
|
|
104
105
|
const JIRA_URL = getRequiredEnv('JIRA_HOST', process.env.JIRA_URL ?? null);
|
|
105
106
|
const JIRA_EMAIL = getRequiredEnv('JIRA_EMAIL');
|
|
106
107
|
const JIRA_API_TOKEN = getRequiredEnv('JIRA_API_TOKEN');
|
|
@@ -120,6 +121,9 @@ function createSuccessResponse(data) {
|
|
|
120
121
|
function createIssueUrl(issueKey) {
|
|
121
122
|
return `${JIRA_URL}/browse/${issueKey}`;
|
|
122
123
|
}
|
|
124
|
+
function resolveProjectKey(a) {
|
|
125
|
+
return a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
126
|
+
}
|
|
123
127
|
function handleError(error) {
|
|
124
128
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
125
129
|
const axiosError = error;
|
|
@@ -146,20 +150,26 @@ function handleError(error) {
|
|
|
146
150
|
isError: true,
|
|
147
151
|
};
|
|
148
152
|
}
|
|
149
|
-
const
|
|
150
|
-
baseURL: `${JIRA_URL}/rest/api/3`,
|
|
153
|
+
const axiosAuthConfig = {
|
|
151
154
|
auth: {
|
|
152
155
|
username: JIRA_EMAIL,
|
|
153
156
|
password: JIRA_API_TOKEN,
|
|
154
157
|
},
|
|
155
|
-
headers: {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
},
|
|
158
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,
|
|
159
169
|
});
|
|
160
170
|
const server = new Server({
|
|
161
171
|
name: 'jira-mcp-server',
|
|
162
|
-
version:
|
|
172
|
+
version: SERVER_VERSION,
|
|
163
173
|
}, {
|
|
164
174
|
capabilities: {
|
|
165
175
|
tools: {},
|
|
@@ -672,12 +682,149 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
672
682
|
required: ['query'],
|
|
673
683
|
},
|
|
674
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
|
+
},
|
|
675
822
|
],
|
|
676
823
|
};
|
|
677
824
|
});
|
|
678
825
|
async function handleCreateIssue(a) {
|
|
679
826
|
const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = a;
|
|
680
|
-
const projectKey =
|
|
827
|
+
const projectKey = resolveProjectKey(a);
|
|
681
828
|
validateSafeParam(issueType, 'issueType');
|
|
682
829
|
validateSafeParam(priority, 'priority');
|
|
683
830
|
const validatedLabels = validateLabels(labels);
|
|
@@ -839,7 +986,7 @@ async function handleCreateSubtask(a) {
|
|
|
839
986
|
const { parentKey, summary, description, priority = 'Medium' } = a;
|
|
840
987
|
validateIssueKey(parentKey);
|
|
841
988
|
validateSafeParam(priority, 'priority');
|
|
842
|
-
const projectKey =
|
|
989
|
+
const projectKey = resolveProjectKey(a);
|
|
843
990
|
const response = await jiraApi.post('/issue', {
|
|
844
991
|
fields: {
|
|
845
992
|
project: { key: projectKey },
|
|
@@ -931,7 +1078,7 @@ async function handleListProjects(a) {
|
|
|
931
1078
|
});
|
|
932
1079
|
}
|
|
933
1080
|
async function handleGetProjectComponents(a) {
|
|
934
|
-
const projectKey =
|
|
1081
|
+
const projectKey = resolveProjectKey(a);
|
|
935
1082
|
const response = await jiraApi.get(`/project/${projectKey}/components`);
|
|
936
1083
|
return createSuccessResponse({
|
|
937
1084
|
projectKey,
|
|
@@ -939,7 +1086,7 @@ async function handleGetProjectComponents(a) {
|
|
|
939
1086
|
});
|
|
940
1087
|
}
|
|
941
1088
|
async function handleGetProjectVersions(a) {
|
|
942
|
-
const projectKey =
|
|
1089
|
+
const projectKey = resolveProjectKey(a);
|
|
943
1090
|
const response = await jiraApi.get(`/project/${projectKey}/versions`);
|
|
944
1091
|
return createSuccessResponse({
|
|
945
1092
|
projectKey,
|
|
@@ -951,7 +1098,7 @@ async function handleGetFields(_a) {
|
|
|
951
1098
|
return createSuccessResponse({ fields: response.data.map((f) => ({ id: f.id, name: f.name, custom: f.custom, schema: f.schema })) });
|
|
952
1099
|
}
|
|
953
1100
|
async function handleGetIssueTypes(a) {
|
|
954
|
-
const projectKey =
|
|
1101
|
+
const projectKey = resolveProjectKey(a);
|
|
955
1102
|
const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
956
1103
|
return createSuccessResponse({
|
|
957
1104
|
projectKey,
|
|
@@ -975,6 +1122,252 @@ async function handleSearchUsers(a) {
|
|
|
975
1122
|
users: response.data.map((u) => ({ accountId: u.accountId, displayName: u.displayName, emailAddress: u.emailAddress, active: u.active, accountType: u.accountType })),
|
|
976
1123
|
});
|
|
977
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
|
+
}
|
|
978
1371
|
const toolHandlers = {
|
|
979
1372
|
jira_create_issue: handleCreateIssue,
|
|
980
1373
|
jira_get_issue: handleGetIssue,
|
|
@@ -998,6 +1391,16 @@ const toolHandlers = {
|
|
|
998
1391
|
jira_get_priorities: handleGetPriorities,
|
|
999
1392
|
jira_get_link_types: handleGetLinkTypes,
|
|
1000
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,
|
|
1001
1404
|
};
|
|
1002
1405
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1003
1406
|
const { name, arguments: args } = request.params;
|