@mcpio/jira 2.2.0 → 2.2.2

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 (2) hide show
  1. package/dist/index.js +358 -443
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -3,8 +3,25 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  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
- import * as dotenv from 'dotenv';
7
- dotenv.config();
6
+ import { readFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ try {
9
+ const envPath = resolve(process.cwd(), '.env');
10
+ const envContent = readFileSync(envPath, 'utf-8');
11
+ for (const line of envContent.split('\n')) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed || trimmed.startsWith('#'))
14
+ continue;
15
+ const eqIndex = trimmed.indexOf('=');
16
+ if (eqIndex === -1)
17
+ continue;
18
+ const key = trimmed.slice(0, eqIndex).trim();
19
+ const value = trimmed.slice(eqIndex + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
20
+ if (!process.env[key])
21
+ process.env[key] = value;
22
+ }
23
+ }
24
+ catch { }
8
25
  function getRequiredEnv(name, fallback = null) {
9
26
  const value = process.env[name];
10
27
  if (value !== undefined && value !== '') {
@@ -28,8 +45,8 @@ function validateProjectKey(key) {
28
45
  if (!key || typeof key !== 'string') {
29
46
  throw new Error('Invalid project key: must be a string');
30
47
  }
31
- if (!/^[A-Z][A-Z0-9_]{1,9}$/.test(key)) {
32
- 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`);
33
50
  }
34
51
  return key;
35
52
  }
@@ -52,16 +69,11 @@ function sanitizeString(str, maxLength = 1000, fieldName = 'input') {
52
69
  return str.trim();
53
70
  }
54
71
  function validateSafeParam(str, fieldName, maxLength = 100) {
55
- if (!str || typeof str !== 'string') {
56
- throw new Error(`Invalid ${fieldName}: must be a non-empty string`);
57
- }
58
- if (str.length > maxLength) {
59
- throw new Error(`${fieldName} exceeds maximum length of ${maxLength} characters`);
60
- }
61
- if (/[\/\\]/.test(str)) {
72
+ const value = sanitizeString(str, maxLength, fieldName);
73
+ if (/[\/\\]/.test(value)) {
62
74
  throw new Error(`Invalid ${fieldName}: contains unsafe characters`);
63
75
  }
64
- return str.trim();
76
+ return value;
65
77
  }
66
78
  function validateMaxResults(maxResults) {
67
79
  if (typeof maxResults !== 'number' || !Number.isInteger(maxResults) || maxResults < 1) {
@@ -188,34 +200,19 @@ function parseInlineContent(text) {
188
200
  if (lastIndex < text.length) {
189
201
  parts.push({ type: 'text', text: text.substring(lastIndex) });
190
202
  }
191
- if (parts.length > 0)
192
- return parts;
193
- return text ? [{ type: 'text', text }] : [];
194
- }
195
- function addBulletItem(nodes, content) {
196
- const listItem = {
197
- type: 'listItem',
198
- content: [{ type: 'paragraph', content }]
199
- };
200
- const lastNode = nodes[nodes.length - 1];
201
- if (lastNode && lastNode.type === 'bulletList') {
202
- lastNode.content.push(listItem);
203
- }
204
- else {
205
- nodes.push({ type: 'bulletList', content: [listItem] });
206
- }
203
+ return parts;
207
204
  }
208
- function addOrderedItem(nodes, content) {
205
+ function addListItem(nodes, content, listType) {
209
206
  const listItem = {
210
207
  type: 'listItem',
211
208
  content: [{ type: 'paragraph', content }]
212
209
  };
213
210
  const lastNode = nodes[nodes.length - 1];
214
- if (lastNode && lastNode.type === 'orderedList') {
211
+ if (lastNode && lastNode.type === listType) {
215
212
  lastNode.content.push(listItem);
216
213
  }
217
214
  else {
218
- nodes.push({ type: 'orderedList', content: [listItem] });
215
+ nodes.push({ type: listType, content: [listItem] });
219
216
  }
220
217
  }
221
218
  function createADFDocument(content) {
@@ -249,10 +246,10 @@ function createADFDocument(content) {
249
246
  });
250
247
  }
251
248
  else if (line.startsWith('* ') || line.startsWith('- ')) {
252
- addBulletItem(nodes, parseInlineContent(line.substring(2)));
249
+ addListItem(nodes, parseInlineContent(line.substring(2)), 'bulletList');
253
250
  }
254
251
  else if (/^\d+\.\s+/.test(line)) {
255
- addOrderedItem(nodes, parseInlineContent(line.replace(/^\d+\.\s+/, '')));
252
+ addListItem(nodes, parseInlineContent(line.replace(/^\d+\.\s+/, '')), 'orderedList');
256
253
  }
257
254
  else if (line.startsWith('> ')) {
258
255
  const text = line.substring(2);
@@ -678,419 +675,337 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
678
675
  ],
679
676
  };
680
677
  });
678
+ async function handleCreateIssue(a) {
679
+ const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = a;
680
+ const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
681
+ validateSafeParam(issueType, 'issueType');
682
+ validateSafeParam(priority, 'priority');
683
+ const validatedLabels = validateLabels(labels);
684
+ const issueData = {
685
+ fields: {
686
+ project: { key: projectKey },
687
+ summary: sanitizeString(summary, 500, 'summary'),
688
+ description: createADFDocument(description),
689
+ issuetype: { name: issueType },
690
+ priority: { name: priority },
691
+ labels: validatedLabels,
692
+ },
693
+ };
694
+ if (storyPoints !== undefined && storyPoints !== null) {
695
+ issueData.fields[STORY_POINTS_FIELD] = validateStoryPoints(storyPoints);
696
+ }
697
+ const response = await jiraApi.post('/issue', issueData);
698
+ return createSuccessResponse({
699
+ success: true,
700
+ key: response.data.key,
701
+ id: response.data.id,
702
+ url: createIssueUrl(response.data.key),
703
+ });
704
+ }
705
+ async function handleGetIssue(a) {
706
+ validateIssueKey(a.issueKey);
707
+ const response = await jiraApi.get(`/issue/${a.issueKey}`);
708
+ const f = response.data.fields;
709
+ return createSuccessResponse({
710
+ key: response.data.key,
711
+ summary: f.summary,
712
+ description: adfToText(f.description),
713
+ status: f.status?.name,
714
+ assignee: f.assignee ? { displayName: f.assignee.displayName, accountId: f.assignee.accountId } : null,
715
+ reporter: f.reporter?.displayName,
716
+ priority: f.priority?.name,
717
+ issueType: f.issuetype?.name,
718
+ labels: f.labels || [],
719
+ storyPoints: f[STORY_POINTS_FIELD],
720
+ parent: f.parent?.key,
721
+ created: f.created,
722
+ updated: f.updated,
723
+ url: createIssueUrl(response.data.key),
724
+ });
725
+ }
726
+ async function handleSearchIssues(a) {
727
+ const { jql, maxResults = 50 } = a;
728
+ validateJQL(jql);
729
+ const validatedMaxResults = validateMaxResults(maxResults);
730
+ const response = await jiraApi.get('/search/jql', {
731
+ params: {
732
+ jql,
733
+ maxResults: validatedMaxResults,
734
+ fields: 'summary,status,assignee,priority,created,updated,issuetype,parent,labels',
735
+ },
736
+ });
737
+ return createSuccessResponse({
738
+ total: response.data.total,
739
+ issues: response.data.issues.map((issue) => ({
740
+ key: issue.key,
741
+ summary: issue.fields.summary,
742
+ status: issue.fields.status?.name,
743
+ assignee: issue.fields.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null,
744
+ priority: issue.fields.priority?.name,
745
+ issueType: issue.fields.issuetype?.name,
746
+ labels: issue.fields.labels || [],
747
+ parent: issue.fields.parent?.key,
748
+ url: createIssueUrl(issue.key),
749
+ })),
750
+ });
751
+ }
752
+ async function handleUpdateIssue(a) {
753
+ const { issueKey, summary, description, status } = a;
754
+ validateIssueKey(issueKey);
755
+ const updateData = { fields: {} };
756
+ let hasFieldUpdates = false;
757
+ if (summary) {
758
+ updateData.fields.summary = sanitizeString(summary, 500, 'summary');
759
+ hasFieldUpdates = true;
760
+ }
761
+ if (description) {
762
+ updateData.fields.description = createADFDocument(description);
763
+ hasFieldUpdates = true;
764
+ }
765
+ if (hasFieldUpdates) {
766
+ await jiraApi.put(`/issue/${issueKey}`, updateData);
767
+ }
768
+ const warnings = [];
769
+ if (status) {
770
+ const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
771
+ const transition = transitions.data.transitions.find((t) => t.name === status);
772
+ if (transition) {
773
+ await jiraApi.post(`/issue/${issueKey}/transitions`, {
774
+ transition: { id: transition.id },
775
+ });
776
+ }
777
+ else {
778
+ const available = transitions.data.transitions.map((t) => t.name).join(', ');
779
+ warnings.push(`Transition "${status}" not found. Available transitions: ${available}`);
780
+ }
781
+ }
782
+ if (!hasFieldUpdates && !status) {
783
+ return createSuccessResponse({ success: false, message: `No updates provided for ${issueKey}` });
784
+ }
785
+ const result = {
786
+ success: warnings.length === 0,
787
+ message: `Issue ${issueKey} updated${warnings.length > 0 ? ' with warnings' : ' successfully'}`,
788
+ url: createIssueUrl(issueKey),
789
+ };
790
+ if (warnings.length > 0) {
791
+ result.warnings = warnings;
792
+ }
793
+ return createSuccessResponse(result);
794
+ }
795
+ async function handleAddComment(a) {
796
+ validateIssueKey(a.issueKey);
797
+ await jiraApi.post(`/issue/${a.issueKey}/comment`, { body: createADFDocument(a.comment) });
798
+ return createSuccessResponse({ success: true, message: `Comment added to ${a.issueKey}` });
799
+ }
800
+ async function handleLinkIssues(a) {
801
+ const { inwardIssue, outwardIssue, linkType = 'Relates' } = a;
802
+ validateIssueKey(inwardIssue);
803
+ validateIssueKey(outwardIssue);
804
+ validateSafeParam(linkType, 'linkType');
805
+ try {
806
+ await jiraApi.post('/issueLink', {
807
+ type: { name: linkType },
808
+ inwardIssue: { key: inwardIssue },
809
+ outwardIssue: { key: outwardIssue },
810
+ });
811
+ return createSuccessResponse({ success: true, message: `Linked ${inwardIssue} to ${outwardIssue} with type "${linkType}"` });
812
+ }
813
+ catch (linkError) {
814
+ const axiosErr = linkError;
815
+ if (axiosErr.response?.status === 400 && axiosErr.response?.data?.errorMessages?.includes('link already exists')) {
816
+ return createSuccessResponse({ success: true, message: `Link between ${inwardIssue} and ${outwardIssue} already exists`, alreadyLinked: true });
817
+ }
818
+ throw linkError;
819
+ }
820
+ }
821
+ async function handleGetProjectInfo(a) {
822
+ const projectKey = a.projectKey ?? JIRA_PROJECT_KEY;
823
+ validateProjectKey(projectKey);
824
+ const response = await jiraApi.get(`/project/${projectKey}`);
825
+ return createSuccessResponse({
826
+ key: response.data.key,
827
+ name: response.data.name,
828
+ description: response.data.description,
829
+ lead: response.data.lead?.displayName,
830
+ url: response.data.url,
831
+ });
832
+ }
833
+ async function handleDeleteIssue(a) {
834
+ validateIssueKey(a.issueKey);
835
+ await jiraApi.delete(`/issue/${a.issueKey}`);
836
+ return createSuccessResponse({ success: true, message: `Issue ${a.issueKey} deleted successfully` });
837
+ }
838
+ async function handleCreateSubtask(a) {
839
+ const { parentKey, summary, description, priority = 'Medium' } = a;
840
+ validateIssueKey(parentKey);
841
+ validateSafeParam(priority, 'priority');
842
+ const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
843
+ const response = await jiraApi.post('/issue', {
844
+ fields: {
845
+ project: { key: projectKey },
846
+ summary: sanitizeString(summary, 500, 'summary'),
847
+ description: createADFDocument(description),
848
+ issuetype: { name: 'Subtask' },
849
+ priority: { name: priority },
850
+ parent: { key: parentKey },
851
+ },
852
+ });
853
+ return createSuccessResponse({ success: true, key: response.data.key, id: response.data.id, parent: parentKey, url: createIssueUrl(response.data.key) });
854
+ }
855
+ async function handleAssignIssue(a) {
856
+ validateIssueKey(a.issueKey);
857
+ await jiraApi.put(`/issue/${a.issueKey}/assignee`, { accountId: a.accountId !== undefined ? a.accountId : null });
858
+ return createSuccessResponse({
859
+ success: true,
860
+ message: a.accountId ? `Issue ${a.issueKey} assigned to ${a.accountId}` : `Issue ${a.issueKey} unassigned`,
861
+ url: createIssueUrl(a.issueKey),
862
+ });
863
+ }
864
+ async function handleListTransitions(a) {
865
+ validateIssueKey(a.issueKey);
866
+ const response = await jiraApi.get(`/issue/${a.issueKey}/transitions`);
867
+ return createSuccessResponse({
868
+ issueKey: a.issueKey,
869
+ transitions: response.data.transitions.map((t) => ({
870
+ id: t.id,
871
+ name: t.name,
872
+ to: { id: t.to.id, name: t.to.name, category: t.to.statusCategory?.name },
873
+ })),
874
+ });
875
+ }
876
+ async function handleAddWorklog(a) {
877
+ const { issueKey, timeSpent, comment, started } = a;
878
+ validateIssueKey(issueKey);
879
+ sanitizeString(timeSpent, 50, 'timeSpent');
880
+ const worklogData = { timeSpent };
881
+ if (comment)
882
+ worklogData.comment = createADFDocument(comment);
883
+ if (started)
884
+ worklogData.started = started;
885
+ const response = await jiraApi.post(`/issue/${issueKey}/worklog`, worklogData);
886
+ return createSuccessResponse({ success: true, id: response.data.id, issueKey, timeSpent: response.data.timeSpent, author: response.data.author?.displayName });
887
+ }
888
+ async function handleGetComments(a) {
889
+ const { issueKey, maxResults = 50, orderBy = '-created' } = a;
890
+ validateIssueKey(issueKey);
891
+ const validatedMaxResults = validateMaxResults(maxResults);
892
+ const response = await jiraApi.get(`/issue/${issueKey}/comment`, { params: { maxResults: validatedMaxResults, orderBy } });
893
+ return createSuccessResponse({
894
+ issueKey,
895
+ total: response.data.total,
896
+ comments: response.data.comments.map((c) => ({
897
+ id: c.id,
898
+ author: c.author?.displayName,
899
+ body: adfToText(c.body),
900
+ created: c.created,
901
+ updated: c.updated,
902
+ })),
903
+ });
904
+ }
905
+ async function handleGetWorklogs(a) {
906
+ validateIssueKey(a.issueKey);
907
+ const response = await jiraApi.get(`/issue/${a.issueKey}/worklog`);
908
+ return createSuccessResponse({
909
+ issueKey: a.issueKey,
910
+ total: response.data.total,
911
+ worklogs: response.data.worklogs.map((w) => ({
912
+ id: w.id,
913
+ author: w.author?.displayName,
914
+ timeSpent: w.timeSpent,
915
+ timeSpentSeconds: w.timeSpentSeconds,
916
+ started: w.started,
917
+ comment: adfToText(w.comment),
918
+ })),
919
+ });
920
+ }
921
+ async function handleListProjects(a) {
922
+ const { maxResults = 50, query } = a;
923
+ const validatedMaxResults = validateMaxResults(maxResults);
924
+ const params = { maxResults: validatedMaxResults };
925
+ if (query)
926
+ params.query = sanitizeString(query, 200, 'query');
927
+ const response = await jiraApi.get('/project/search', { params });
928
+ return createSuccessResponse({
929
+ total: response.data.total,
930
+ projects: response.data.values.map((p) => ({ key: p.key, name: p.name, projectTypeKey: p.projectTypeKey, style: p.style, lead: p.lead?.displayName })),
931
+ });
932
+ }
933
+ async function handleGetProjectComponents(a) {
934
+ const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
935
+ const response = await jiraApi.get(`/project/${projectKey}/components`);
936
+ return createSuccessResponse({
937
+ projectKey,
938
+ components: response.data.map((c) => ({ id: c.id, name: c.name, description: c.description, lead: c.lead?.displayName, assigneeType: c.assigneeType })),
939
+ });
940
+ }
941
+ async function handleGetProjectVersions(a) {
942
+ const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
943
+ const response = await jiraApi.get(`/project/${projectKey}/versions`);
944
+ return createSuccessResponse({
945
+ projectKey,
946
+ 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 })),
947
+ });
948
+ }
949
+ async function handleGetFields(_a) {
950
+ const response = await jiraApi.get('/field');
951
+ return createSuccessResponse({ fields: response.data.map((f) => ({ id: f.id, name: f.name, custom: f.custom, schema: f.schema })) });
952
+ }
953
+ async function handleGetIssueTypes(a) {
954
+ const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
955
+ const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
956
+ return createSuccessResponse({
957
+ projectKey,
958
+ issueTypes: response.data.issueTypes.map((t) => ({ id: t.id, name: t.name, subtask: t.subtask, description: t.description })),
959
+ });
960
+ }
961
+ async function handleGetPriorities(_a) {
962
+ const response = await jiraApi.get('/priority/search');
963
+ return createSuccessResponse({ priorities: response.data.values.map((p) => ({ id: p.id, name: p.name, description: p.description, iconUrl: p.iconUrl })) });
964
+ }
965
+ async function handleGetLinkTypes(_a) {
966
+ const response = await jiraApi.get('/issueLinkType');
967
+ return createSuccessResponse({ linkTypes: response.data.issueLinkTypes.map((lt) => ({ id: lt.id, name: lt.name, inward: lt.inward, outward: lt.outward })) });
968
+ }
969
+ async function handleSearchUsers(a) {
970
+ const { query, maxResults = 10 } = a;
971
+ sanitizeString(query, 200, 'query');
972
+ const validatedMaxResults = validateMaxResults(maxResults);
973
+ const response = await jiraApi.get('/user/search', { params: { query, maxResults: validatedMaxResults } });
974
+ return createSuccessResponse({
975
+ users: response.data.map((u) => ({ accountId: u.accountId, displayName: u.displayName, emailAddress: u.emailAddress, active: u.active, accountType: u.accountType })),
976
+ });
977
+ }
978
+ const toolHandlers = {
979
+ jira_create_issue: handleCreateIssue,
980
+ jira_get_issue: handleGetIssue,
981
+ jira_search_issues: handleSearchIssues,
982
+ jira_update_issue: handleUpdateIssue,
983
+ jira_add_comment: handleAddComment,
984
+ jira_link_issues: handleLinkIssues,
985
+ jira_get_project_info: handleGetProjectInfo,
986
+ jira_delete_issue: handleDeleteIssue,
987
+ jira_create_subtask: handleCreateSubtask,
988
+ jira_assign_issue: handleAssignIssue,
989
+ jira_list_transitions: handleListTransitions,
990
+ jira_add_worklog: handleAddWorklog,
991
+ jira_get_comments: handleGetComments,
992
+ jira_get_worklogs: handleGetWorklogs,
993
+ jira_list_projects: handleListProjects,
994
+ jira_get_project_components: handleGetProjectComponents,
995
+ jira_get_project_versions: handleGetProjectVersions,
996
+ jira_get_fields: handleGetFields,
997
+ jira_get_issue_types: handleGetIssueTypes,
998
+ jira_get_priorities: handleGetPriorities,
999
+ jira_get_link_types: handleGetLinkTypes,
1000
+ jira_search_users: handleSearchUsers,
1001
+ };
681
1002
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
682
1003
  const { name, arguments: args } = request.params;
683
- const a = args;
1004
+ const handler = toolHandlers[name];
1005
+ if (!handler)
1006
+ throw new Error(`Unknown tool: ${name}`);
684
1007
  try {
685
- switch (name) {
686
- case 'jira_create_issue': {
687
- const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = a;
688
- const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
689
- validateSafeParam(issueType, 'issueType');
690
- validateSafeParam(priority, 'priority');
691
- const validatedLabels = validateLabels(labels);
692
- const issueData = {
693
- fields: {
694
- project: { key: projectKey },
695
- summary: sanitizeString(summary, 500, 'summary'),
696
- description: createADFDocument(description),
697
- issuetype: { name: issueType },
698
- priority: { name: priority },
699
- labels: validatedLabels,
700
- },
701
- };
702
- if (storyPoints !== undefined && storyPoints !== null) {
703
- issueData.fields[STORY_POINTS_FIELD] = validateStoryPoints(storyPoints);
704
- }
705
- const response = await jiraApi.post('/issue', issueData);
706
- return createSuccessResponse({
707
- success: true,
708
- key: response.data.key,
709
- id: response.data.id,
710
- url: createIssueUrl(response.data.key),
711
- });
712
- }
713
- case 'jira_get_issue': {
714
- const { issueKey } = a;
715
- validateIssueKey(issueKey);
716
- const response = await jiraApi.get(`/issue/${issueKey}`);
717
- const f = response.data.fields;
718
- return createSuccessResponse({
719
- key: response.data.key,
720
- summary: f.summary,
721
- description: adfToText(f.description),
722
- status: f.status?.name,
723
- assignee: f.assignee ? { displayName: f.assignee.displayName, accountId: f.assignee.accountId } : null,
724
- reporter: f.reporter?.displayName,
725
- priority: f.priority?.name,
726
- issueType: f.issuetype?.name,
727
- labels: f.labels || [],
728
- storyPoints: f[STORY_POINTS_FIELD],
729
- parent: f.parent?.key,
730
- created: f.created,
731
- updated: f.updated,
732
- url: createIssueUrl(response.data.key),
733
- });
734
- }
735
- case 'jira_search_issues': {
736
- const { jql, maxResults = 50 } = a;
737
- validateJQL(jql);
738
- const validatedMaxResults = validateMaxResults(maxResults);
739
- const response = await jiraApi.get('/search/jql', {
740
- params: {
741
- jql,
742
- maxResults: validatedMaxResults,
743
- fields: 'summary,status,assignee,priority,created,updated,issuetype,parent,labels',
744
- },
745
- });
746
- return createSuccessResponse({
747
- total: response.data.total,
748
- issues: response.data.issues.map((issue) => ({
749
- key: issue.key,
750
- summary: issue.fields.summary,
751
- status: issue.fields.status?.name,
752
- assignee: issue.fields.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null,
753
- priority: issue.fields.priority?.name,
754
- issueType: issue.fields.issuetype?.name,
755
- labels: issue.fields.labels || [],
756
- parent: issue.fields.parent?.key,
757
- url: createIssueUrl(issue.key),
758
- })),
759
- });
760
- }
761
- case 'jira_update_issue': {
762
- const { issueKey, summary, description, status } = a;
763
- validateIssueKey(issueKey);
764
- const updateData = { fields: {} };
765
- let hasFieldUpdates = false;
766
- if (summary) {
767
- updateData.fields.summary = sanitizeString(summary, 500, 'summary');
768
- hasFieldUpdates = true;
769
- }
770
- if (description) {
771
- updateData.fields.description = createADFDocument(description);
772
- hasFieldUpdates = true;
773
- }
774
- if (hasFieldUpdates) {
775
- await jiraApi.put(`/issue/${issueKey}`, updateData);
776
- }
777
- const warnings = [];
778
- if (status) {
779
- const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
780
- const transition = transitions.data.transitions.find((t) => t.name === status);
781
- if (transition) {
782
- await jiraApi.post(`/issue/${issueKey}/transitions`, {
783
- transition: { id: transition.id },
784
- });
785
- }
786
- else {
787
- const available = transitions.data.transitions.map((t) => t.name).join(', ');
788
- warnings.push(`Transition "${status}" not found. Available transitions: ${available}`);
789
- }
790
- }
791
- if (!hasFieldUpdates && !status) {
792
- return createSuccessResponse({
793
- success: false,
794
- message: `No updates provided for ${issueKey}`,
795
- });
796
- }
797
- const result = {
798
- success: warnings.length === 0,
799
- message: `Issue ${issueKey} updated${warnings.length > 0 ? ' with warnings' : ' successfully'}`,
800
- url: createIssueUrl(issueKey),
801
- };
802
- if (warnings.length > 0) {
803
- result.warnings = warnings;
804
- }
805
- return createSuccessResponse(result);
806
- }
807
- case 'jira_add_comment': {
808
- const { issueKey, comment } = a;
809
- validateIssueKey(issueKey);
810
- await jiraApi.post(`/issue/${issueKey}/comment`, {
811
- body: createADFDocument(comment),
812
- });
813
- return createSuccessResponse({
814
- success: true,
815
- message: `Comment added to ${issueKey}`,
816
- });
817
- }
818
- case 'jira_link_issues': {
819
- const { inwardIssue, outwardIssue, linkType = 'Relates' } = a;
820
- validateIssueKey(inwardIssue);
821
- validateIssueKey(outwardIssue);
822
- validateSafeParam(linkType, 'linkType');
823
- try {
824
- await jiraApi.post('/issueLink', {
825
- type: { name: linkType },
826
- inwardIssue: { key: inwardIssue },
827
- outwardIssue: { key: outwardIssue },
828
- });
829
- return createSuccessResponse({
830
- success: true,
831
- message: `Linked ${inwardIssue} to ${outwardIssue} with type "${linkType}"`,
832
- });
833
- }
834
- catch (linkError) {
835
- const axiosErr = linkError;
836
- if (axiosErr.response?.status === 400 &&
837
- axiosErr.response?.data?.errorMessages?.includes('link already exists')) {
838
- return createSuccessResponse({
839
- success: true,
840
- message: `Link between ${inwardIssue} and ${outwardIssue} already exists`,
841
- alreadyLinked: true,
842
- });
843
- }
844
- throw linkError;
845
- }
846
- }
847
- case 'jira_get_project_info': {
848
- const { projectKey = JIRA_PROJECT_KEY } = a;
849
- validateProjectKey(projectKey);
850
- const response = await jiraApi.get(`/project/${projectKey}`);
851
- return createSuccessResponse({
852
- key: response.data.key,
853
- name: response.data.name,
854
- description: response.data.description,
855
- lead: response.data.lead?.displayName,
856
- url: response.data.url,
857
- });
858
- }
859
- case 'jira_delete_issue': {
860
- const { issueKey } = a;
861
- validateIssueKey(issueKey);
862
- await jiraApi.delete(`/issue/${issueKey}`);
863
- return createSuccessResponse({
864
- success: true,
865
- message: `Issue ${issueKey} deleted successfully`,
866
- });
867
- }
868
- case 'jira_create_subtask': {
869
- const { parentKey, summary, description, priority = 'Medium' } = a;
870
- validateIssueKey(parentKey);
871
- validateSafeParam(priority, 'priority');
872
- const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
873
- const issueData = {
874
- fields: {
875
- project: { key: projectKey },
876
- summary: sanitizeString(summary, 500, 'summary'),
877
- description: createADFDocument(description),
878
- issuetype: { name: 'Subtask' },
879
- priority: { name: priority },
880
- parent: { key: parentKey },
881
- },
882
- };
883
- const response = await jiraApi.post('/issue', issueData);
884
- return createSuccessResponse({
885
- success: true,
886
- key: response.data.key,
887
- id: response.data.id,
888
- parent: parentKey,
889
- url: createIssueUrl(response.data.key),
890
- });
891
- }
892
- case 'jira_assign_issue': {
893
- const { issueKey, accountId } = a;
894
- validateIssueKey(issueKey);
895
- await jiraApi.put(`/issue/${issueKey}/assignee`, {
896
- accountId: accountId !== undefined ? accountId : null,
897
- });
898
- return createSuccessResponse({
899
- success: true,
900
- message: accountId
901
- ? `Issue ${issueKey} assigned to ${accountId}`
902
- : `Issue ${issueKey} unassigned`,
903
- url: createIssueUrl(issueKey),
904
- });
905
- }
906
- case 'jira_list_transitions': {
907
- const { issueKey } = a;
908
- validateIssueKey(issueKey);
909
- const response = await jiraApi.get(`/issue/${issueKey}/transitions`);
910
- return createSuccessResponse({
911
- issueKey,
912
- transitions: response.data.transitions.map((t) => ({
913
- id: t.id,
914
- name: t.name,
915
- to: {
916
- id: t.to.id,
917
- name: t.to.name,
918
- category: t.to.statusCategory?.name,
919
- },
920
- })),
921
- });
922
- }
923
- case 'jira_add_worklog': {
924
- const { issueKey, timeSpent, comment, started } = a;
925
- validateIssueKey(issueKey);
926
- sanitizeString(timeSpent, 50, 'timeSpent');
927
- const worklogData = { timeSpent };
928
- if (comment) {
929
- worklogData.comment = createADFDocument(comment);
930
- }
931
- if (started) {
932
- worklogData.started = started;
933
- }
934
- const response = await jiraApi.post(`/issue/${issueKey}/worklog`, worklogData);
935
- return createSuccessResponse({
936
- success: true,
937
- id: response.data.id,
938
- issueKey,
939
- timeSpent: response.data.timeSpent,
940
- author: response.data.author?.displayName,
941
- });
942
- }
943
- case 'jira_get_comments': {
944
- const { issueKey, maxResults = 50, orderBy = '-created' } = a;
945
- validateIssueKey(issueKey);
946
- const validatedMaxResults = validateMaxResults(maxResults);
947
- const response = await jiraApi.get(`/issue/${issueKey}/comment`, {
948
- params: { maxResults: validatedMaxResults, orderBy },
949
- });
950
- return createSuccessResponse({
951
- issueKey,
952
- total: response.data.total,
953
- comments: response.data.comments.map((c) => ({
954
- id: c.id,
955
- author: c.author?.displayName,
956
- body: adfToText(c.body),
957
- created: c.created,
958
- updated: c.updated,
959
- })),
960
- });
961
- }
962
- case 'jira_get_worklogs': {
963
- const { issueKey } = a;
964
- validateIssueKey(issueKey);
965
- const response = await jiraApi.get(`/issue/${issueKey}/worklog`);
966
- return createSuccessResponse({
967
- issueKey,
968
- total: response.data.total,
969
- worklogs: response.data.worklogs.map((w) => ({
970
- id: w.id,
971
- author: w.author?.displayName,
972
- timeSpent: w.timeSpent,
973
- timeSpentSeconds: w.timeSpentSeconds,
974
- started: w.started,
975
- comment: adfToText(w.comment),
976
- })),
977
- });
978
- }
979
- case 'jira_list_projects': {
980
- const { maxResults = 50, query } = a;
981
- const validatedMaxResults = validateMaxResults(maxResults);
982
- const params = { maxResults: validatedMaxResults };
983
- if (query) {
984
- params.query = sanitizeString(query, 200, 'query');
985
- }
986
- const response = await jiraApi.get('/project/search', { params });
987
- return createSuccessResponse({
988
- total: response.data.total,
989
- projects: response.data.values.map((p) => ({
990
- key: p.key,
991
- name: p.name,
992
- projectTypeKey: p.projectTypeKey,
993
- style: p.style,
994
- lead: p.lead?.displayName,
995
- })),
996
- });
997
- }
998
- case 'jira_get_project_components': {
999
- const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
1000
- const response = await jiraApi.get(`/project/${projectKey}/components`);
1001
- return createSuccessResponse({
1002
- projectKey,
1003
- components: response.data.map((c) => ({
1004
- id: c.id,
1005
- name: c.name,
1006
- description: c.description,
1007
- lead: c.lead?.displayName,
1008
- assigneeType: c.assigneeType,
1009
- })),
1010
- });
1011
- }
1012
- case 'jira_get_project_versions': {
1013
- const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
1014
- const response = await jiraApi.get(`/project/${projectKey}/versions`);
1015
- return createSuccessResponse({
1016
- projectKey,
1017
- versions: response.data.map((v) => ({
1018
- id: v.id,
1019
- name: v.name,
1020
- description: v.description,
1021
- released: v.released,
1022
- archived: v.archived,
1023
- releaseDate: v.releaseDate,
1024
- startDate: v.startDate,
1025
- })),
1026
- });
1027
- }
1028
- case 'jira_get_fields': {
1029
- const response = await jiraApi.get('/field');
1030
- return createSuccessResponse({
1031
- fields: response.data.map((f) => ({
1032
- id: f.id,
1033
- name: f.name,
1034
- custom: f.custom,
1035
- schema: f.schema,
1036
- })),
1037
- });
1038
- }
1039
- case 'jira_get_issue_types': {
1040
- const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
1041
- const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
1042
- return createSuccessResponse({
1043
- projectKey,
1044
- issueTypes: response.data.issueTypes.map((t) => ({
1045
- id: t.id,
1046
- name: t.name,
1047
- subtask: t.subtask,
1048
- description: t.description,
1049
- })),
1050
- });
1051
- }
1052
- case 'jira_get_priorities': {
1053
- const response = await jiraApi.get('/priority/search');
1054
- return createSuccessResponse({
1055
- priorities: response.data.values.map((p) => ({
1056
- id: p.id,
1057
- name: p.name,
1058
- description: p.description,
1059
- iconUrl: p.iconUrl,
1060
- })),
1061
- });
1062
- }
1063
- case 'jira_get_link_types': {
1064
- const response = await jiraApi.get('/issueLinkType');
1065
- return createSuccessResponse({
1066
- linkTypes: response.data.issueLinkTypes.map((lt) => ({
1067
- id: lt.id,
1068
- name: lt.name,
1069
- inward: lt.inward,
1070
- outward: lt.outward,
1071
- })),
1072
- });
1073
- }
1074
- case 'jira_search_users': {
1075
- const { query, maxResults = 10 } = a;
1076
- sanitizeString(query, 200, 'query');
1077
- const validatedMaxResults = validateMaxResults(maxResults);
1078
- const response = await jiraApi.get('/user/search', {
1079
- params: { query, maxResults: validatedMaxResults },
1080
- });
1081
- return createSuccessResponse({
1082
- users: response.data.map((u) => ({
1083
- accountId: u.accountId,
1084
- displayName: u.displayName,
1085
- emailAddress: u.emailAddress,
1086
- active: u.active,
1087
- accountType: u.accountType,
1088
- })),
1089
- });
1090
- }
1091
- default:
1092
- throw new Error(`Unknown tool: ${name}`);
1093
- }
1008
+ return await handler(args);
1094
1009
  }
1095
1010
  catch (error) {
1096
1011
  return handleError(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpio/jira",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",
@@ -30,8 +30,7 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.20.2",
33
- "axios": "^1.13.1",
34
- "dotenv": "^17.2.3"
33
+ "axios": "^1.13.1"
35
34
  },
36
35
  "engines": {
37
36
  "node": ">=18.0.0"