@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.
Files changed (3) hide show
  1. package/README.md +18 -3
  2. package/dist/index.js +749 -448
  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');
@@ -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_]{1,9}$/.test(key)) {
49
- throw new Error(`Invalid project key format: ${key}. Expected 2-10 uppercase alphanumeric characters`);
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
- if (!str || typeof str !== 'string') {
73
- throw new Error(`Invalid ${fieldName}: must be a non-empty string`);
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 str.trim();
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 jiraApi = axios.create({
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: '2.0.0',
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
- if (parts.length > 0)
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 addOrderedItem(nodes, content) {
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 === 'orderedList') {
221
+ if (lastNode && lastNode.type === listType) {
232
222
  lastNode.content.push(listItem);
233
223
  }
234
224
  else {
235
- nodes.push({ type: 'orderedList', content: [listItem] });
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
- addBulletItem(nodes, parseInlineContent(line.substring(2)));
259
+ addListItem(nodes, parseInlineContent(line.substring(2)), 'bulletList');
270
260
  }
271
261
  else if (/^\d+\.\s+/.test(line)) {
272
- addOrderedItem(nodes, parseInlineContent(line.replace(/^\d+\.\s+/, '')));
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 a = args;
1407
+ const handler = toolHandlers[name];
1408
+ if (!handler)
1409
+ throw new Error(`Unknown tool: ${name}`);
701
1410
  try {
702
- switch (name) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpio/jira",
3
- "version": "2.2.1",
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",