@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.
Files changed (3) hide show
  1. package/README.md +18 -3
  2. package/dist/index.js +415 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Jira MCP Server v2.0
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
- - 23 Jira API tools via MCP protocol
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
- - `jira_get_comments` - Get issue comments
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 jiraApi = axios.create({
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: '2.0.0',
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 = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
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 = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
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 = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
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 = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
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 = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpio/jira",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Model Context Protocol (MCP) server for Jira Cloud API v3 with automatic Markdown-to-ADF conversion",
5
5
  "author": {
6
6
  "name": "Volodymyr Press",