@aaronsb/jira-cloud-mcp 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -30
- package/build/client/field-discovery.js +346 -0
- package/build/client/field-type-map.js +106 -0
- package/build/client/jira-client.js +119 -88
- package/build/docs/tool-documentation.js +481 -0
- package/build/handlers/board-handlers.js +10 -153
- package/build/handlers/filter-handlers.js +7 -98
- package/build/handlers/issue-handlers.js +148 -75
- package/build/handlers/project-handlers.js +11 -154
- package/build/handlers/queue-handler.js +252 -0
- package/build/handlers/resource-handlers.js +94 -19
- package/build/handlers/sprint-handlers.js +9 -94
- package/build/handlers/tool-resource-handlers.js +16 -1165
- package/build/index.js +59 -51
- package/build/mcp/markdown-renderer.js +11 -0
- package/build/schemas/tool-schemas.js +98 -197
- package/build/utils/bulk-operation-guard.js +74 -0
- package/build/utils/next-steps.js +124 -0
- package/build/utils/normalize-args.js +12 -0
- package/build/utils/text-processing.js +5 -1
- package/package.json +6 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates contextual next-step suggestions for LLM callers.
|
|
3
|
+
* Appended to handler responses to guide multi-step workflows.
|
|
4
|
+
*/
|
|
5
|
+
function formatSteps(steps) {
|
|
6
|
+
const lines = ['\n---\n**Next steps:**'];
|
|
7
|
+
for (const step of steps) {
|
|
8
|
+
const tool = step.tool ? `\`${step.tool}\`` : '';
|
|
9
|
+
const example = step.example ? ` — \`${JSON.stringify(step.example)}\`` : '';
|
|
10
|
+
lines.push(`- ${step.description}${tool ? ` using ${tool}` : ''}${example}`);
|
|
11
|
+
}
|
|
12
|
+
return lines.join('\n');
|
|
13
|
+
}
|
|
14
|
+
export function issueNextSteps(operation, issueKey) {
|
|
15
|
+
const steps = [];
|
|
16
|
+
switch (operation) {
|
|
17
|
+
case 'create':
|
|
18
|
+
steps.push({ description: 'Transition to a new status', tool: 'manage_jira_issue', example: { operation: 'transition', issueKey, expand: ['transitions'] } }, { description: 'Add to a sprint', tool: 'manage_jira_sprint', example: { operation: 'manage_issues', sprintId: '<id>', add: [issueKey] } }, { description: 'Link to a related issue', tool: 'manage_jira_issue', example: { operation: 'link', issueKey, linkedIssueKey: '<key>', linkType: 'relates to' } }, { description: 'Read jira://custom-fields to discover available custom fields for this instance' });
|
|
19
|
+
break;
|
|
20
|
+
case 'get':
|
|
21
|
+
steps.push({ description: 'Update fields', tool: 'manage_jira_issue', example: { operation: 'update', issueKey } }, { description: 'Add a comment', tool: 'manage_jira_issue', example: { operation: 'comment', issueKey, comment: '<text>' } }, { description: 'View available transitions', tool: 'manage_jira_issue', example: { operation: 'get', issueKey, expand: ['transitions'] } });
|
|
22
|
+
break;
|
|
23
|
+
case 'update':
|
|
24
|
+
steps.push({ description: 'View the updated issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey } }, { description: 'Transition to a new status', tool: 'manage_jira_issue', example: { operation: 'get', issueKey, expand: ['transitions'] } }, { description: 'Read jira://custom-fields to discover available custom fields for this instance' });
|
|
25
|
+
break;
|
|
26
|
+
case 'transition':
|
|
27
|
+
steps.push({ description: 'Add a comment about the status change', tool: 'manage_jira_issue', example: { operation: 'comment', issueKey, comment: '<text>' } }, { description: 'View available transitions', tool: 'manage_jira_issue', example: { operation: 'get', issueKey, expand: ['transitions'] } });
|
|
28
|
+
break;
|
|
29
|
+
case 'comment':
|
|
30
|
+
steps.push({ description: 'Transition the issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey, expand: ['transitions'] } }, { description: 'Update issue fields', tool: 'manage_jira_issue', example: { operation: 'update', issueKey } });
|
|
31
|
+
break;
|
|
32
|
+
case 'delete':
|
|
33
|
+
steps.push({ description: 'Search for related issues', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `issue in linkedIssues("${issueKey}")` } });
|
|
34
|
+
break;
|
|
35
|
+
case 'move':
|
|
36
|
+
steps.push({ description: 'View the moved issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey } }, { description: 'Update fields for the new project context', tool: 'manage_jira_issue', example: { operation: 'update', issueKey } });
|
|
37
|
+
break;
|
|
38
|
+
case 'link':
|
|
39
|
+
steps.push({ description: 'View the linked issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey } }, { description: 'Read available link types from jira://issue-link-types resource' });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return steps.length > 0 ? formatSteps(steps) : '';
|
|
43
|
+
}
|
|
44
|
+
export function filterNextSteps(operation, filterId, jql) {
|
|
45
|
+
const steps = [];
|
|
46
|
+
switch (operation) {
|
|
47
|
+
case 'execute_jql':
|
|
48
|
+
case 'execute_filter':
|
|
49
|
+
steps.push({ description: 'Get details on a specific issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey: '<key>', expand: ['transitions'] } }, { description: 'Refine the search with additional JQL clauses', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: '<refined query>' } });
|
|
50
|
+
if (operation === 'execute_jql' && jql) {
|
|
51
|
+
steps.push({ description: 'Save this query as a filter', tool: 'manage_jira_filter', example: { operation: 'create', name: '<name>', jql } });
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case 'get':
|
|
55
|
+
steps.push({ description: 'Run this filter', tool: 'manage_jira_filter', example: { operation: 'execute_filter', filterId } }, { description: 'Update the filter', tool: 'manage_jira_filter', example: { operation: 'update', filterId } });
|
|
56
|
+
break;
|
|
57
|
+
case 'list':
|
|
58
|
+
steps.push({ description: 'Run a filter', tool: 'manage_jira_filter', example: { operation: 'execute_filter', filterId: '<id>' } }, { description: 'Search with JQL directly', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: '<query>' } });
|
|
59
|
+
break;
|
|
60
|
+
case 'create':
|
|
61
|
+
steps.push({ description: 'Execute the new filter', tool: 'manage_jira_filter', example: { operation: 'execute_filter', filterId } });
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
return steps.length > 0 ? formatSteps(steps) : '';
|
|
65
|
+
}
|
|
66
|
+
export function sprintNextSteps(operation, sprintId, boardId, state) {
|
|
67
|
+
const steps = [];
|
|
68
|
+
switch (operation) {
|
|
69
|
+
case 'create':
|
|
70
|
+
steps.push({ description: 'Add issues to the sprint', tool: 'manage_jira_sprint', example: { operation: 'manage_issues', sprintId, add: ['<issueKey>'] } }, { description: 'Start the sprint', tool: 'manage_jira_sprint', example: { operation: 'update', sprintId, state: 'active' } });
|
|
71
|
+
break;
|
|
72
|
+
case 'list':
|
|
73
|
+
steps.push({ description: 'Get sprint details', tool: 'manage_jira_sprint', example: { operation: 'get', sprintId: '<id>', expand: ['issues'] } }, { description: 'Create a new sprint', tool: 'manage_jira_sprint', example: { operation: 'create', boardId, name: '<name>' } });
|
|
74
|
+
break;
|
|
75
|
+
case 'get':
|
|
76
|
+
steps.push({ description: 'Add or remove issues', tool: 'manage_jira_sprint', example: { operation: 'manage_issues', sprintId, add: ['<issueKey>'] } });
|
|
77
|
+
if (state === 'future') {
|
|
78
|
+
steps.push({ description: 'Start the sprint', tool: 'manage_jira_sprint', example: { operation: 'update', sprintId, state: 'active' } });
|
|
79
|
+
}
|
|
80
|
+
else if (state === 'active') {
|
|
81
|
+
steps.push({ description: 'Close the sprint', tool: 'manage_jira_sprint', example: { operation: 'update', sprintId, state: 'closed' } });
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'manage_issues':
|
|
85
|
+
steps.push({ description: 'View sprint issues', tool: 'manage_jira_sprint', example: { operation: 'get', sprintId, expand: ['issues'] } });
|
|
86
|
+
if (state === 'future') {
|
|
87
|
+
steps.push({ description: 'Start the sprint', tool: 'manage_jira_sprint', example: { operation: 'update', sprintId, state: 'active' } });
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case 'update':
|
|
91
|
+
if (state === 'active') {
|
|
92
|
+
steps.push({ description: 'View sprint issues', tool: 'manage_jira_sprint', example: { operation: 'get', sprintId, expand: ['issues'] } }, { description: 'Close the sprint when done', tool: 'manage_jira_sprint', example: { operation: 'update', sprintId, state: 'closed' } });
|
|
93
|
+
}
|
|
94
|
+
else if (state === 'closed') {
|
|
95
|
+
steps.push({ description: 'Create the next sprint', tool: 'manage_jira_sprint', example: { operation: 'create', boardId, name: '<name>' } });
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return steps.length > 0 ? formatSteps(steps) : '';
|
|
100
|
+
}
|
|
101
|
+
export function projectNextSteps(operation, projectKey) {
|
|
102
|
+
const steps = [];
|
|
103
|
+
switch (operation) {
|
|
104
|
+
case 'list':
|
|
105
|
+
steps.push({ description: 'Get project details', tool: 'manage_jira_project', example: { operation: 'get', projectKey: '<key>' } }, { description: 'Search issues in a project', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `project = <key>` } });
|
|
106
|
+
break;
|
|
107
|
+
case 'get':
|
|
108
|
+
steps.push({ description: 'Search issues in this project', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `project = ${projectKey}` } }, { description: 'View project boards', tool: 'manage_jira_board', example: { operation: 'list' } }, { description: `Read jira://projects/${projectKey}/overview for additional context` });
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
return steps.length > 0 ? formatSteps(steps) : '';
|
|
112
|
+
}
|
|
113
|
+
export function boardNextSteps(operation, boardId) {
|
|
114
|
+
const steps = [];
|
|
115
|
+
switch (operation) {
|
|
116
|
+
case 'list':
|
|
117
|
+
steps.push({ description: 'Get board details', tool: 'manage_jira_board', example: { operation: 'get', boardId: '<id>', expand: ['sprints'] } });
|
|
118
|
+
break;
|
|
119
|
+
case 'get':
|
|
120
|
+
steps.push({ description: 'View board sprints', tool: 'manage_jira_sprint', example: { operation: 'list', boardId } }, { description: `Read board overview resource at jira://boards/${boardId}/overview` });
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
return steps.length > 0 ? formatSteps(steps) : '';
|
|
124
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts snake_case keys to camelCase.
|
|
3
|
+
* Used by all handlers to accept both naming conventions from LLM callers.
|
|
4
|
+
*/
|
|
5
|
+
export function normalizeArgs(args) {
|
|
6
|
+
const normalized = {};
|
|
7
|
+
for (const [key, value] of Object.entries(args)) {
|
|
8
|
+
const camelKey = key.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
|
9
|
+
normalized[camelKey] = value;
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
@@ -2,7 +2,11 @@ import MarkdownIt from 'markdown-it';
|
|
|
2
2
|
export class TextProcessor {
|
|
3
3
|
static md = new MarkdownIt();
|
|
4
4
|
static markdownToAdf(markdown) {
|
|
5
|
-
|
|
5
|
+
// Replace literal \n sequences with actual newlines so markdown-it
|
|
6
|
+
// correctly splits paragraphs. MCP JSON transport may deliver these
|
|
7
|
+
// as escaped two-character sequences rather than real newline chars.
|
|
8
|
+
const normalized = markdown.replace(/\\n/g, '\n');
|
|
9
|
+
const tokens = TextProcessor.md.parse(normalized, {});
|
|
6
10
|
const content = [];
|
|
7
11
|
let currentListItems = [];
|
|
8
12
|
let isInList = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaronsb/jira-cloud-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"mcpName": "io.github.aaronsb/jira-cloud",
|
|
4
5
|
"description": "Model Context Protocol (MCP) server for Jira Cloud - enables AI assistants to interact with Jira",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"build": "tsc",
|
|
37
38
|
"postbuild": "node --eval \"import('fs').then(fs => fs.chmodSync('build/index.js', '755'))\"",
|
|
38
39
|
"prepare": "npm run build",
|
|
39
|
-
"test": "
|
|
40
|
+
"test": "vitest run",
|
|
40
41
|
"lint": "eslint --ext .ts src/",
|
|
41
42
|
"lint:fix": "eslint --ext .ts src/ --fix",
|
|
42
43
|
"watch": "tsc --watch",
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
49
|
-
"jira.js": "5.
|
|
50
|
+
"jira.js": "5.3.1",
|
|
50
51
|
"jsdom": "^27.3.0",
|
|
51
52
|
"markdown-it": "^14.1.0"
|
|
52
53
|
},
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"eslint": "^8.57.0",
|
|
61
62
|
"eslint-plugin-import": "^2.32.0",
|
|
62
63
|
"tsx": "^4.21.0",
|
|
63
|
-
"typescript": "^5.9.3"
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.0.18"
|
|
64
66
|
}
|
|
65
67
|
}
|