@mcpio/jira 2.2.1 → 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.
- package/dist/index.js +339 -441
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,8 +45,8 @@ function validateProjectKey(key) {
|
|
|
45
45
|
if (!key || typeof key !== 'string') {
|
|
46
46
|
throw new Error('Invalid project key: must be a string');
|
|
47
47
|
}
|
|
48
|
-
if (!/^[A-Z][A-Z0-9_]{
|
|
49
|
-
throw new Error(`Invalid project key format: ${key}. Expected
|
|
48
|
+
if (!/^[A-Z][A-Z0-9_]{0,9}$/.test(key)) {
|
|
49
|
+
throw new Error(`Invalid project key format: ${key}. Expected 1-10 uppercase alphanumeric characters`);
|
|
50
50
|
}
|
|
51
51
|
return key;
|
|
52
52
|
}
|
|
@@ -69,16 +69,11 @@ function sanitizeString(str, maxLength = 1000, fieldName = 'input') {
|
|
|
69
69
|
return str.trim();
|
|
70
70
|
}
|
|
71
71
|
function validateSafeParam(str, fieldName, maxLength = 100) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
if (str.length > maxLength) {
|
|
76
|
-
throw new Error(`${fieldName} exceeds maximum length of ${maxLength} characters`);
|
|
77
|
-
}
|
|
78
|
-
if (/[\/\\]/.test(str)) {
|
|
72
|
+
const value = sanitizeString(str, maxLength, fieldName);
|
|
73
|
+
if (/[\/\\]/.test(value)) {
|
|
79
74
|
throw new Error(`Invalid ${fieldName}: contains unsafe characters`);
|
|
80
75
|
}
|
|
81
|
-
return
|
|
76
|
+
return value;
|
|
82
77
|
}
|
|
83
78
|
function validateMaxResults(maxResults) {
|
|
84
79
|
if (typeof maxResults !== 'number' || !Number.isInteger(maxResults) || maxResults < 1) {
|
|
@@ -205,34 +200,19 @@ function parseInlineContent(text) {
|
|
|
205
200
|
if (lastIndex < text.length) {
|
|
206
201
|
parts.push({ type: 'text', text: text.substring(lastIndex) });
|
|
207
202
|
}
|
|
208
|
-
|
|
209
|
-
return parts;
|
|
210
|
-
return text ? [{ type: 'text', text }] : [];
|
|
203
|
+
return parts;
|
|
211
204
|
}
|
|
212
|
-
function
|
|
205
|
+
function addListItem(nodes, content, listType) {
|
|
213
206
|
const listItem = {
|
|
214
207
|
type: 'listItem',
|
|
215
208
|
content: [{ type: 'paragraph', content }]
|
|
216
209
|
};
|
|
217
210
|
const lastNode = nodes[nodes.length - 1];
|
|
218
|
-
if (lastNode && lastNode.type ===
|
|
211
|
+
if (lastNode && lastNode.type === listType) {
|
|
219
212
|
lastNode.content.push(listItem);
|
|
220
213
|
}
|
|
221
214
|
else {
|
|
222
|
-
nodes.push({ type:
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function addOrderedItem(nodes, content) {
|
|
226
|
-
const listItem = {
|
|
227
|
-
type: 'listItem',
|
|
228
|
-
content: [{ type: 'paragraph', content }]
|
|
229
|
-
};
|
|
230
|
-
const lastNode = nodes[nodes.length - 1];
|
|
231
|
-
if (lastNode && lastNode.type === 'orderedList') {
|
|
232
|
-
lastNode.content.push(listItem);
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
nodes.push({ type: 'orderedList', content: [listItem] });
|
|
215
|
+
nodes.push({ type: listType, content: [listItem] });
|
|
236
216
|
}
|
|
237
217
|
}
|
|
238
218
|
function createADFDocument(content) {
|
|
@@ -266,10 +246,10 @@ function createADFDocument(content) {
|
|
|
266
246
|
});
|
|
267
247
|
}
|
|
268
248
|
else if (line.startsWith('* ') || line.startsWith('- ')) {
|
|
269
|
-
|
|
249
|
+
addListItem(nodes, parseInlineContent(line.substring(2)), 'bulletList');
|
|
270
250
|
}
|
|
271
251
|
else if (/^\d+\.\s+/.test(line)) {
|
|
272
|
-
|
|
252
|
+
addListItem(nodes, parseInlineContent(line.replace(/^\d+\.\s+/, '')), 'orderedList');
|
|
273
253
|
}
|
|
274
254
|
else if (line.startsWith('> ')) {
|
|
275
255
|
const text = line.substring(2);
|
|
@@ -695,419 +675,337 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
695
675
|
],
|
|
696
676
|
};
|
|
697
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
|
+
};
|
|
698
1002
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
699
1003
|
const { name, arguments: args } = request.params;
|
|
700
|
-
const
|
|
1004
|
+
const handler = toolHandlers[name];
|
|
1005
|
+
if (!handler)
|
|
1006
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
701
1007
|
try {
|
|
702
|
-
|
|
703
|
-
case 'jira_create_issue': {
|
|
704
|
-
const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = a;
|
|
705
|
-
const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
706
|
-
validateSafeParam(issueType, 'issueType');
|
|
707
|
-
validateSafeParam(priority, 'priority');
|
|
708
|
-
const validatedLabels = validateLabels(labels);
|
|
709
|
-
const issueData = {
|
|
710
|
-
fields: {
|
|
711
|
-
project: { key: projectKey },
|
|
712
|
-
summary: sanitizeString(summary, 500, 'summary'),
|
|
713
|
-
description: createADFDocument(description),
|
|
714
|
-
issuetype: { name: issueType },
|
|
715
|
-
priority: { name: priority },
|
|
716
|
-
labels: validatedLabels,
|
|
717
|
-
},
|
|
718
|
-
};
|
|
719
|
-
if (storyPoints !== undefined && storyPoints !== null) {
|
|
720
|
-
issueData.fields[STORY_POINTS_FIELD] = validateStoryPoints(storyPoints);
|
|
721
|
-
}
|
|
722
|
-
const response = await jiraApi.post('/issue', issueData);
|
|
723
|
-
return createSuccessResponse({
|
|
724
|
-
success: true,
|
|
725
|
-
key: response.data.key,
|
|
726
|
-
id: response.data.id,
|
|
727
|
-
url: createIssueUrl(response.data.key),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
case 'jira_get_issue': {
|
|
731
|
-
const { issueKey } = a;
|
|
732
|
-
validateIssueKey(issueKey);
|
|
733
|
-
const response = await jiraApi.get(`/issue/${issueKey}`);
|
|
734
|
-
const f = response.data.fields;
|
|
735
|
-
return createSuccessResponse({
|
|
736
|
-
key: response.data.key,
|
|
737
|
-
summary: f.summary,
|
|
738
|
-
description: adfToText(f.description),
|
|
739
|
-
status: f.status?.name,
|
|
740
|
-
assignee: f.assignee ? { displayName: f.assignee.displayName, accountId: f.assignee.accountId } : null,
|
|
741
|
-
reporter: f.reporter?.displayName,
|
|
742
|
-
priority: f.priority?.name,
|
|
743
|
-
issueType: f.issuetype?.name,
|
|
744
|
-
labels: f.labels || [],
|
|
745
|
-
storyPoints: f[STORY_POINTS_FIELD],
|
|
746
|
-
parent: f.parent?.key,
|
|
747
|
-
created: f.created,
|
|
748
|
-
updated: f.updated,
|
|
749
|
-
url: createIssueUrl(response.data.key),
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
case 'jira_search_issues': {
|
|
753
|
-
const { jql, maxResults = 50 } = a;
|
|
754
|
-
validateJQL(jql);
|
|
755
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
756
|
-
const response = await jiraApi.get('/search/jql', {
|
|
757
|
-
params: {
|
|
758
|
-
jql,
|
|
759
|
-
maxResults: validatedMaxResults,
|
|
760
|
-
fields: 'summary,status,assignee,priority,created,updated,issuetype,parent,labels',
|
|
761
|
-
},
|
|
762
|
-
});
|
|
763
|
-
return createSuccessResponse({
|
|
764
|
-
total: response.data.total,
|
|
765
|
-
issues: response.data.issues.map((issue) => ({
|
|
766
|
-
key: issue.key,
|
|
767
|
-
summary: issue.fields.summary,
|
|
768
|
-
status: issue.fields.status?.name,
|
|
769
|
-
assignee: issue.fields.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null,
|
|
770
|
-
priority: issue.fields.priority?.name,
|
|
771
|
-
issueType: issue.fields.issuetype?.name,
|
|
772
|
-
labels: issue.fields.labels || [],
|
|
773
|
-
parent: issue.fields.parent?.key,
|
|
774
|
-
url: createIssueUrl(issue.key),
|
|
775
|
-
})),
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
case 'jira_update_issue': {
|
|
779
|
-
const { issueKey, summary, description, status } = a;
|
|
780
|
-
validateIssueKey(issueKey);
|
|
781
|
-
const updateData = { fields: {} };
|
|
782
|
-
let hasFieldUpdates = false;
|
|
783
|
-
if (summary) {
|
|
784
|
-
updateData.fields.summary = sanitizeString(summary, 500, 'summary');
|
|
785
|
-
hasFieldUpdates = true;
|
|
786
|
-
}
|
|
787
|
-
if (description) {
|
|
788
|
-
updateData.fields.description = createADFDocument(description);
|
|
789
|
-
hasFieldUpdates = true;
|
|
790
|
-
}
|
|
791
|
-
if (hasFieldUpdates) {
|
|
792
|
-
await jiraApi.put(`/issue/${issueKey}`, updateData);
|
|
793
|
-
}
|
|
794
|
-
const warnings = [];
|
|
795
|
-
if (status) {
|
|
796
|
-
const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
797
|
-
const transition = transitions.data.transitions.find((t) => t.name === status);
|
|
798
|
-
if (transition) {
|
|
799
|
-
await jiraApi.post(`/issue/${issueKey}/transitions`, {
|
|
800
|
-
transition: { id: transition.id },
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
const available = transitions.data.transitions.map((t) => t.name).join(', ');
|
|
805
|
-
warnings.push(`Transition "${status}" not found. Available transitions: ${available}`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
if (!hasFieldUpdates && !status) {
|
|
809
|
-
return createSuccessResponse({
|
|
810
|
-
success: false,
|
|
811
|
-
message: `No updates provided for ${issueKey}`,
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
const result = {
|
|
815
|
-
success: warnings.length === 0,
|
|
816
|
-
message: `Issue ${issueKey} updated${warnings.length > 0 ? ' with warnings' : ' successfully'}`,
|
|
817
|
-
url: createIssueUrl(issueKey),
|
|
818
|
-
};
|
|
819
|
-
if (warnings.length > 0) {
|
|
820
|
-
result.warnings = warnings;
|
|
821
|
-
}
|
|
822
|
-
return createSuccessResponse(result);
|
|
823
|
-
}
|
|
824
|
-
case 'jira_add_comment': {
|
|
825
|
-
const { issueKey, comment } = a;
|
|
826
|
-
validateIssueKey(issueKey);
|
|
827
|
-
await jiraApi.post(`/issue/${issueKey}/comment`, {
|
|
828
|
-
body: createADFDocument(comment),
|
|
829
|
-
});
|
|
830
|
-
return createSuccessResponse({
|
|
831
|
-
success: true,
|
|
832
|
-
message: `Comment added to ${issueKey}`,
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
case 'jira_link_issues': {
|
|
836
|
-
const { inwardIssue, outwardIssue, linkType = 'Relates' } = a;
|
|
837
|
-
validateIssueKey(inwardIssue);
|
|
838
|
-
validateIssueKey(outwardIssue);
|
|
839
|
-
validateSafeParam(linkType, 'linkType');
|
|
840
|
-
try {
|
|
841
|
-
await jiraApi.post('/issueLink', {
|
|
842
|
-
type: { name: linkType },
|
|
843
|
-
inwardIssue: { key: inwardIssue },
|
|
844
|
-
outwardIssue: { key: outwardIssue },
|
|
845
|
-
});
|
|
846
|
-
return createSuccessResponse({
|
|
847
|
-
success: true,
|
|
848
|
-
message: `Linked ${inwardIssue} to ${outwardIssue} with type "${linkType}"`,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
catch (linkError) {
|
|
852
|
-
const axiosErr = linkError;
|
|
853
|
-
if (axiosErr.response?.status === 400 &&
|
|
854
|
-
axiosErr.response?.data?.errorMessages?.includes('link already exists')) {
|
|
855
|
-
return createSuccessResponse({
|
|
856
|
-
success: true,
|
|
857
|
-
message: `Link between ${inwardIssue} and ${outwardIssue} already exists`,
|
|
858
|
-
alreadyLinked: true,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
throw linkError;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
case 'jira_get_project_info': {
|
|
865
|
-
const { projectKey = JIRA_PROJECT_KEY } = a;
|
|
866
|
-
validateProjectKey(projectKey);
|
|
867
|
-
const response = await jiraApi.get(`/project/${projectKey}`);
|
|
868
|
-
return createSuccessResponse({
|
|
869
|
-
key: response.data.key,
|
|
870
|
-
name: response.data.name,
|
|
871
|
-
description: response.data.description,
|
|
872
|
-
lead: response.data.lead?.displayName,
|
|
873
|
-
url: response.data.url,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
case 'jira_delete_issue': {
|
|
877
|
-
const { issueKey } = a;
|
|
878
|
-
validateIssueKey(issueKey);
|
|
879
|
-
await jiraApi.delete(`/issue/${issueKey}`);
|
|
880
|
-
return createSuccessResponse({
|
|
881
|
-
success: true,
|
|
882
|
-
message: `Issue ${issueKey} deleted successfully`,
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
case 'jira_create_subtask': {
|
|
886
|
-
const { parentKey, summary, description, priority = 'Medium' } = a;
|
|
887
|
-
validateIssueKey(parentKey);
|
|
888
|
-
validateSafeParam(priority, 'priority');
|
|
889
|
-
const projectKey = a.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
890
|
-
const issueData = {
|
|
891
|
-
fields: {
|
|
892
|
-
project: { key: projectKey },
|
|
893
|
-
summary: sanitizeString(summary, 500, 'summary'),
|
|
894
|
-
description: createADFDocument(description),
|
|
895
|
-
issuetype: { name: 'Subtask' },
|
|
896
|
-
priority: { name: priority },
|
|
897
|
-
parent: { key: parentKey },
|
|
898
|
-
},
|
|
899
|
-
};
|
|
900
|
-
const response = await jiraApi.post('/issue', issueData);
|
|
901
|
-
return createSuccessResponse({
|
|
902
|
-
success: true,
|
|
903
|
-
key: response.data.key,
|
|
904
|
-
id: response.data.id,
|
|
905
|
-
parent: parentKey,
|
|
906
|
-
url: createIssueUrl(response.data.key),
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
case 'jira_assign_issue': {
|
|
910
|
-
const { issueKey, accountId } = a;
|
|
911
|
-
validateIssueKey(issueKey);
|
|
912
|
-
await jiraApi.put(`/issue/${issueKey}/assignee`, {
|
|
913
|
-
accountId: accountId !== undefined ? accountId : null,
|
|
914
|
-
});
|
|
915
|
-
return createSuccessResponse({
|
|
916
|
-
success: true,
|
|
917
|
-
message: accountId
|
|
918
|
-
? `Issue ${issueKey} assigned to ${accountId}`
|
|
919
|
-
: `Issue ${issueKey} unassigned`,
|
|
920
|
-
url: createIssueUrl(issueKey),
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
case 'jira_list_transitions': {
|
|
924
|
-
const { issueKey } = a;
|
|
925
|
-
validateIssueKey(issueKey);
|
|
926
|
-
const response = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
927
|
-
return createSuccessResponse({
|
|
928
|
-
issueKey,
|
|
929
|
-
transitions: response.data.transitions.map((t) => ({
|
|
930
|
-
id: t.id,
|
|
931
|
-
name: t.name,
|
|
932
|
-
to: {
|
|
933
|
-
id: t.to.id,
|
|
934
|
-
name: t.to.name,
|
|
935
|
-
category: t.to.statusCategory?.name,
|
|
936
|
-
},
|
|
937
|
-
})),
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
case 'jira_add_worklog': {
|
|
941
|
-
const { issueKey, timeSpent, comment, started } = a;
|
|
942
|
-
validateIssueKey(issueKey);
|
|
943
|
-
sanitizeString(timeSpent, 50, 'timeSpent');
|
|
944
|
-
const worklogData = { timeSpent };
|
|
945
|
-
if (comment) {
|
|
946
|
-
worklogData.comment = createADFDocument(comment);
|
|
947
|
-
}
|
|
948
|
-
if (started) {
|
|
949
|
-
worklogData.started = started;
|
|
950
|
-
}
|
|
951
|
-
const response = await jiraApi.post(`/issue/${issueKey}/worklog`, worklogData);
|
|
952
|
-
return createSuccessResponse({
|
|
953
|
-
success: true,
|
|
954
|
-
id: response.data.id,
|
|
955
|
-
issueKey,
|
|
956
|
-
timeSpent: response.data.timeSpent,
|
|
957
|
-
author: response.data.author?.displayName,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
case 'jira_get_comments': {
|
|
961
|
-
const { issueKey, maxResults = 50, orderBy = '-created' } = a;
|
|
962
|
-
validateIssueKey(issueKey);
|
|
963
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
964
|
-
const response = await jiraApi.get(`/issue/${issueKey}/comment`, {
|
|
965
|
-
params: { maxResults: validatedMaxResults, orderBy },
|
|
966
|
-
});
|
|
967
|
-
return createSuccessResponse({
|
|
968
|
-
issueKey,
|
|
969
|
-
total: response.data.total,
|
|
970
|
-
comments: response.data.comments.map((c) => ({
|
|
971
|
-
id: c.id,
|
|
972
|
-
author: c.author?.displayName,
|
|
973
|
-
body: adfToText(c.body),
|
|
974
|
-
created: c.created,
|
|
975
|
-
updated: c.updated,
|
|
976
|
-
})),
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
case 'jira_get_worklogs': {
|
|
980
|
-
const { issueKey } = a;
|
|
981
|
-
validateIssueKey(issueKey);
|
|
982
|
-
const response = await jiraApi.get(`/issue/${issueKey}/worklog`);
|
|
983
|
-
return createSuccessResponse({
|
|
984
|
-
issueKey,
|
|
985
|
-
total: response.data.total,
|
|
986
|
-
worklogs: response.data.worklogs.map((w) => ({
|
|
987
|
-
id: w.id,
|
|
988
|
-
author: w.author?.displayName,
|
|
989
|
-
timeSpent: w.timeSpent,
|
|
990
|
-
timeSpentSeconds: w.timeSpentSeconds,
|
|
991
|
-
started: w.started,
|
|
992
|
-
comment: adfToText(w.comment),
|
|
993
|
-
})),
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
case 'jira_list_projects': {
|
|
997
|
-
const { maxResults = 50, query } = a;
|
|
998
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
999
|
-
const params = { maxResults: validatedMaxResults };
|
|
1000
|
-
if (query) {
|
|
1001
|
-
params.query = sanitizeString(query, 200, 'query');
|
|
1002
|
-
}
|
|
1003
|
-
const response = await jiraApi.get('/project/search', { params });
|
|
1004
|
-
return createSuccessResponse({
|
|
1005
|
-
total: response.data.total,
|
|
1006
|
-
projects: response.data.values.map((p) => ({
|
|
1007
|
-
key: p.key,
|
|
1008
|
-
name: p.name,
|
|
1009
|
-
projectTypeKey: p.projectTypeKey,
|
|
1010
|
-
style: p.style,
|
|
1011
|
-
lead: p.lead?.displayName,
|
|
1012
|
-
})),
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
case 'jira_get_project_components': {
|
|
1016
|
-
const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
1017
|
-
const response = await jiraApi.get(`/project/${projectKey}/components`);
|
|
1018
|
-
return createSuccessResponse({
|
|
1019
|
-
projectKey,
|
|
1020
|
-
components: response.data.map((c) => ({
|
|
1021
|
-
id: c.id,
|
|
1022
|
-
name: c.name,
|
|
1023
|
-
description: c.description,
|
|
1024
|
-
lead: c.lead?.displayName,
|
|
1025
|
-
assigneeType: c.assigneeType,
|
|
1026
|
-
})),
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
case 'jira_get_project_versions': {
|
|
1030
|
-
const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
1031
|
-
const response = await jiraApi.get(`/project/${projectKey}/versions`);
|
|
1032
|
-
return createSuccessResponse({
|
|
1033
|
-
projectKey,
|
|
1034
|
-
versions: response.data.map((v) => ({
|
|
1035
|
-
id: v.id,
|
|
1036
|
-
name: v.name,
|
|
1037
|
-
description: v.description,
|
|
1038
|
-
released: v.released,
|
|
1039
|
-
archived: v.archived,
|
|
1040
|
-
releaseDate: v.releaseDate,
|
|
1041
|
-
startDate: v.startDate,
|
|
1042
|
-
})),
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
case 'jira_get_fields': {
|
|
1046
|
-
const response = await jiraApi.get('/field');
|
|
1047
|
-
return createSuccessResponse({
|
|
1048
|
-
fields: response.data.map((f) => ({
|
|
1049
|
-
id: f.id,
|
|
1050
|
-
name: f.name,
|
|
1051
|
-
custom: f.custom,
|
|
1052
|
-
schema: f.schema,
|
|
1053
|
-
})),
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
case 'jira_get_issue_types': {
|
|
1057
|
-
const projectKey = a?.projectKey ? validateProjectKey(a.projectKey) : JIRA_PROJECT_KEY;
|
|
1058
|
-
const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
1059
|
-
return createSuccessResponse({
|
|
1060
|
-
projectKey,
|
|
1061
|
-
issueTypes: response.data.issueTypes.map((t) => ({
|
|
1062
|
-
id: t.id,
|
|
1063
|
-
name: t.name,
|
|
1064
|
-
subtask: t.subtask,
|
|
1065
|
-
description: t.description,
|
|
1066
|
-
})),
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
case 'jira_get_priorities': {
|
|
1070
|
-
const response = await jiraApi.get('/priority/search');
|
|
1071
|
-
return createSuccessResponse({
|
|
1072
|
-
priorities: response.data.values.map((p) => ({
|
|
1073
|
-
id: p.id,
|
|
1074
|
-
name: p.name,
|
|
1075
|
-
description: p.description,
|
|
1076
|
-
iconUrl: p.iconUrl,
|
|
1077
|
-
})),
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
case 'jira_get_link_types': {
|
|
1081
|
-
const response = await jiraApi.get('/issueLinkType');
|
|
1082
|
-
return createSuccessResponse({
|
|
1083
|
-
linkTypes: response.data.issueLinkTypes.map((lt) => ({
|
|
1084
|
-
id: lt.id,
|
|
1085
|
-
name: lt.name,
|
|
1086
|
-
inward: lt.inward,
|
|
1087
|
-
outward: lt.outward,
|
|
1088
|
-
})),
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
case 'jira_search_users': {
|
|
1092
|
-
const { query, maxResults = 10 } = a;
|
|
1093
|
-
sanitizeString(query, 200, 'query');
|
|
1094
|
-
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1095
|
-
const response = await jiraApi.get('/user/search', {
|
|
1096
|
-
params: { query, maxResults: validatedMaxResults },
|
|
1097
|
-
});
|
|
1098
|
-
return createSuccessResponse({
|
|
1099
|
-
users: response.data.map((u) => ({
|
|
1100
|
-
accountId: u.accountId,
|
|
1101
|
-
displayName: u.displayName,
|
|
1102
|
-
emailAddress: u.emailAddress,
|
|
1103
|
-
active: u.active,
|
|
1104
|
-
accountType: u.accountType,
|
|
1105
|
-
})),
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
default:
|
|
1109
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1110
|
-
}
|
|
1008
|
+
return await handler(args);
|
|
1111
1009
|
}
|
|
1112
1010
|
catch (error) {
|
|
1113
1011
|
return handleError(error);
|