jira-pilot 2.0.4 → 2.1.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.
- package/README.md +216 -173
- package/bin/{jira.js → jira.ts} +10 -1
- package/dist/bin/jira.js +64 -0
- package/package.json +21 -15
- package/src/commands/ai-actions/{plan.js → plan.ts} +9 -9
- package/src/commands/ai-actions/{review.js → review.ts} +2 -2
- package/src/commands/ai-actions/{standup.js → standup.ts} +4 -4
- package/src/commands/{ai.js → ai.ts} +11 -11
- package/src/commands/{board.js → board.ts} +11 -11
- package/src/commands/bulk.ts +230 -0
- package/src/commands/{config.js → config.ts} +57 -8
- package/src/commands/dashboard.ts +222 -0
- package/src/commands/filter.ts +84 -0
- package/src/commands/{git.js → git.ts} +4 -4
- package/src/commands/issue-attach.ts +44 -0
- package/src/commands/issue-pr.ts +87 -0
- package/src/commands/issue-worklog.ts +90 -0
- package/src/commands/{issue.js → issue.ts} +359 -68
- package/src/commands/{mcp.js → mcp.ts} +2 -2
- package/src/commands/{project.js → project.ts} +11 -11
- package/src/commands/sprint.ts +269 -0
- package/src/server/{mcp-server.js → mcp-server.ts} +235 -8
- package/src/services/{ai-service.js → ai-service.ts} +16 -16
- package/src/services/{api-service.js → api-service.ts} +33 -9
- package/src/services/config-service.ts +21 -0
- package/src/types.ts +68 -0
- package/src/utils/{adf-parser.js → adf-parser.ts} +12 -12
- package/src/utils/config-store.ts +109 -0
- package/src/utils/{config.js → config.ts} +14 -41
- package/src/utils/{error-handler.js → error-handler.ts} +2 -1
- package/src/utils/{text-to-adf.js → text-to-adf.ts} +1 -1
- package/src/utils/{validators.js → validators.ts} +4 -4
- package/src/commands/bulk.js +0 -108
- package/src/commands/dashboard.js +0 -89
- package/src/commands/sprint.js +0 -153
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { startServer } from '../server/mcp-server.js';
|
|
4
4
|
|
|
5
|
-
export function registerMcpCommand(program) {
|
|
5
|
+
export function registerMcpCommand(program: Command) {
|
|
6
6
|
const mcpCmd = new Command('mcp')
|
|
7
7
|
.description('Start MCP Agent Server (Stdio)')
|
|
8
8
|
.action(async () => {
|
|
@@ -17,7 +17,7 @@ export function registerMcpCommand(program) {
|
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
19
|
await startServer();
|
|
20
|
-
} catch (e) {
|
|
20
|
+
} catch (e: any) {
|
|
21
21
|
console.error('MCP Server Error:', e);
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { handleCommandError } from '../utils/error-handler.js';
|
|
7
7
|
|
|
8
|
-
export function registerProjectCommand(program) {
|
|
8
|
+
export function registerProjectCommand(program: Command) {
|
|
9
9
|
const projectCmd = new Command('project')
|
|
10
10
|
.description('Manage Jira projects')
|
|
11
11
|
.addHelpText('after', `
|
|
@@ -17,7 +17,7 @@ Common Actions:
|
|
|
17
17
|
.command('list')
|
|
18
18
|
.description('List accessible projects')
|
|
19
19
|
.option('-o, --output <format>', 'Output format (json)')
|
|
20
|
-
.action(async (options) => {
|
|
20
|
+
.action(async (options: any) => {
|
|
21
21
|
const spinner = ora('Fetching projects...').start();
|
|
22
22
|
try {
|
|
23
23
|
const data = await api.get('/project/search');
|
|
@@ -29,19 +29,19 @@ Common Actions:
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (options.output === 'json') {
|
|
32
|
-
console.log(JSON.stringify(data.values.map(p => ({
|
|
32
|
+
console.log(JSON.stringify(data.values.map((p: any) => ({
|
|
33
33
|
key: p.key, name: p.name,
|
|
34
34
|
lead: p.lead?.displayName || null, style: p.style
|
|
35
35
|
})), null, 2));
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
[chalk.bold('Key'), chalk.bold('Name'), chalk.bold('Leader'), chalk.bold('Style')]
|
|
41
|
-
|
|
39
|
+
const table = new Table({
|
|
40
|
+
head: [chalk.bold('Key'), chalk.bold('Name'), chalk.bold('Leader'), chalk.bold('Style')]
|
|
41
|
+
});
|
|
42
42
|
|
|
43
|
-
data.values.forEach(p => {
|
|
44
|
-
|
|
43
|
+
data.values.forEach((p: any) => {
|
|
44
|
+
table.push([
|
|
45
45
|
chalk.cyan(p.key),
|
|
46
46
|
p.name,
|
|
47
47
|
p.lead ? p.lead.displayName : 'N/A',
|
|
@@ -49,8 +49,8 @@ Common Actions:
|
|
|
49
49
|
]);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
console.log(table(
|
|
53
|
-
} catch (e) {
|
|
52
|
+
console.log(table.toString());
|
|
53
|
+
} catch (e: any) {
|
|
54
54
|
handleCommandError(spinner, e, 'Failed to list projects');
|
|
55
55
|
}
|
|
56
56
|
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import { api } from '../services/api-service.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import enquirer from 'enquirer';
|
|
7
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
8
|
+
|
|
9
|
+
export function registerSprintCommand(program: Command) {
|
|
10
|
+
const sprintCmd = new Command('sprint')
|
|
11
|
+
.description('Manage Sprints')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Common Actions:
|
|
14
|
+
$ jira sprint list --board <ID|Name> # List sprints for a board
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
sprintCmd
|
|
18
|
+
.command('list')
|
|
19
|
+
.description('List sprints for a board')
|
|
20
|
+
.requiredOption('-b, --board <id>', 'Board ID or name')
|
|
21
|
+
.option('-s, --state <state>', 'State (active, future, closed)', 'active,future')
|
|
22
|
+
.action(async (options: any) => {
|
|
23
|
+
const spinner = ora(`Fetching sprints for board ${options.board}...`).start();
|
|
24
|
+
try {
|
|
25
|
+
let boardId = options.board;
|
|
26
|
+
|
|
27
|
+
// If board option is not a number, look it up by name
|
|
28
|
+
if (isNaN(boardId)) {
|
|
29
|
+
spinner.text = `Looking up board "${options.board}"...`;
|
|
30
|
+
const boardData = await api.agileGet(`/board?name=${encodeURIComponent(options.board)}`);
|
|
31
|
+
|
|
32
|
+
if (!boardData.values || boardData.values.length === 0) {
|
|
33
|
+
throw new Error(`Board with name "${options.board}" not found. Please provide the numeric Board ID.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (boardData.values.length > 1) {
|
|
37
|
+
const exact = boardData.values.find((b: any) => b.name.toLowerCase() === options.board.toLowerCase());
|
|
38
|
+
if (exact) {
|
|
39
|
+
boardId = exact.id;
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.yellow(`\nMultiple boards found for "${options.board}". Using "${boardData.values[0].name}" (ID: ${boardData.values[0].id}).`));
|
|
42
|
+
boardId = boardData.values[0].id;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
boardId = boardData.values[0].id;
|
|
46
|
+
}
|
|
47
|
+
spinner.text = `Fetching sprints for board ${options.board} (ID: ${boardId})...`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await api.agileGet(`/board/${boardId}/sprint?state=${options.state}`);
|
|
51
|
+
spinner.stop();
|
|
52
|
+
|
|
53
|
+
if (!data.values || data.values.length === 0) {
|
|
54
|
+
console.log(chalk.yellow('No sprints found.'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const table = new Table({
|
|
59
|
+
head: [chalk.bold('ID'), chalk.bold('Name'), chalk.bold('State'), chalk.bold('Dates')]
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
data.values.forEach((s: any) => {
|
|
63
|
+
table.push([
|
|
64
|
+
s.id,
|
|
65
|
+
s.name,
|
|
66
|
+
s.state === 'active' ? chalk.green(s.state) : s.state,
|
|
67
|
+
`${s.startDate ? s.startDate.split('T')[0] : ''} -> ${s.endDate ? s.endDate.split('T')[0] : ''}`
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(table.toString());
|
|
72
|
+
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
handleCommandError(spinner, e, 'Failed to list sprints');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── SPRINT ISSUES ────────────────────────────────────────────────
|
|
79
|
+
sprintCmd
|
|
80
|
+
.command('issues')
|
|
81
|
+
.description('List issues in the active sprint')
|
|
82
|
+
.requiredOption('-b, --board <id>', 'Board ID or name')
|
|
83
|
+
.option('-o, --output <format>', 'Output format (json)')
|
|
84
|
+
.addHelpText('after', `
|
|
85
|
+
Examples:
|
|
86
|
+
$ jira sprint issues --board 5
|
|
87
|
+
$ jira sprint issues --board "My Board" --output json
|
|
88
|
+
`)
|
|
89
|
+
.action(async (options: any) => {
|
|
90
|
+
const spinner = ora('Fetching active sprint...').start();
|
|
91
|
+
try {
|
|
92
|
+
let boardId = options.board;
|
|
93
|
+
|
|
94
|
+
if (isNaN(boardId)) {
|
|
95
|
+
spinner.text = `Looking up board "${options.board}"...`;
|
|
96
|
+
const boardData = await api.agileGet(`/board?name=${encodeURIComponent(options.board)}`);
|
|
97
|
+
if (!boardData.values || boardData.values.length === 0) {
|
|
98
|
+
throw new Error(`Board "${options.board}" not found.`);
|
|
99
|
+
}
|
|
100
|
+
boardId = boardData.values[0].id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get active sprint
|
|
104
|
+
const sprints = await api.agileGet(`/board/${boardId}/sprint?state=active`);
|
|
105
|
+
if (!sprints.values || sprints.values.length === 0) {
|
|
106
|
+
spinner.stop();
|
|
107
|
+
console.log(chalk.yellow('No active sprint found.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const activeSprint = sprints.values[0];
|
|
112
|
+
spinner.text = `Fetching issues for sprint "${activeSprint.name}"...`;
|
|
113
|
+
|
|
114
|
+
const issues = await api.agileGet(`/sprint/${activeSprint.id}/issue?maxResults=50&fields=summary,status,assignee,priority`);
|
|
115
|
+
spinner.stop();
|
|
116
|
+
|
|
117
|
+
if (!issues.issues || issues.issues.length === 0) {
|
|
118
|
+
console.log(chalk.yellow('No issues in active sprint.'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(chalk.bold(`\n🏃 Sprint: ${activeSprint.name}\n`));
|
|
123
|
+
|
|
124
|
+
if (options.output === 'json') {
|
|
125
|
+
console.log(JSON.stringify(issues.issues.map((i: any) => ({
|
|
126
|
+
key: i.key, summary: i.fields.summary,
|
|
127
|
+
status: i.fields.status?.name, assignee: i.fields.assignee?.displayName || null,
|
|
128
|
+
priority: i.fields.priority?.name
|
|
129
|
+
})), null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const table = new Table({
|
|
134
|
+
head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Priority')]
|
|
135
|
+
});
|
|
136
|
+
issues.issues.forEach((i: any) => {
|
|
137
|
+
table.push([
|
|
138
|
+
chalk.cyan(i.key),
|
|
139
|
+
i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
|
|
140
|
+
i.fields.status?.name || '',
|
|
141
|
+
i.fields.assignee?.displayName || 'Unassigned',
|
|
142
|
+
i.fields.priority?.name || ''
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
console.log(table.toString());
|
|
146
|
+
console.log(chalk.grey(`${issues.issues.length} issue(s) in sprint`));
|
|
147
|
+
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
handleCommandError(spinner, e, 'Failed to list sprint issues');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
// ── START SPRINT ──────────────────────────────────────────────────
|
|
156
|
+
sprintCmd
|
|
157
|
+
.command('start')
|
|
158
|
+
.description('Start a future sprint')
|
|
159
|
+
.argument('<sprintId>', 'Sprint ID')
|
|
160
|
+
.option('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
|
161
|
+
.option('--end-date <date>', 'End date (YYYY-MM-DD)')
|
|
162
|
+
.action(async (sprintId, options) => {
|
|
163
|
+
const spinner = ora(`Fetching sprint ${sprintId}...`).start();
|
|
164
|
+
try {
|
|
165
|
+
const sprint = await api.agileGet(`/sprint/${sprintId}`);
|
|
166
|
+
|
|
167
|
+
if (sprint.state === 'active') {
|
|
168
|
+
spinner.fail(`Sprint "${sprint.name}" is already active.`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (sprint.state === 'closed') {
|
|
172
|
+
spinner.fail(`Sprint "${sprint.name}" is closed.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
spinner.stop();
|
|
176
|
+
|
|
177
|
+
let startDate = options.startDate;
|
|
178
|
+
let endDate = options.endDate;
|
|
179
|
+
|
|
180
|
+
if (!startDate || !endDate) {
|
|
181
|
+
console.log(chalk.bold(`\nStarting Sprint: ${sprint.name}`));
|
|
182
|
+
|
|
183
|
+
const now = new Date();
|
|
184
|
+
const twoWeeks = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
|
|
185
|
+
|
|
186
|
+
const answers = await enquirer.prompt([
|
|
187
|
+
{
|
|
188
|
+
type: 'input',
|
|
189
|
+
name: 'startDate',
|
|
190
|
+
message: 'Start Date (YYYY-MM-DD):',
|
|
191
|
+
initial: now.toISOString().split('T')[0],
|
|
192
|
+
skip: !!startDate
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: 'input',
|
|
196
|
+
name: 'endDate',
|
|
197
|
+
message: 'End Date (YYYY-MM-DD):',
|
|
198
|
+
initial: twoWeeks.toISOString().split('T')[0],
|
|
199
|
+
skip: !!endDate
|
|
200
|
+
}
|
|
201
|
+
]) as any;
|
|
202
|
+
|
|
203
|
+
if (!startDate) startDate = answers.startDate;
|
|
204
|
+
if (!endDate) endDate = answers.endDate;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Append time to dates if missing (Jira requires ISO with time)
|
|
208
|
+
const formatISO = (dateStr: string) => {
|
|
209
|
+
return dateStr.includes('T') ? dateStr : `${dateStr}T10:00:00.000+0000`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const updateSpinner = ora('Starting sprint...').start();
|
|
213
|
+
await api.agilePost(`/sprint/${sprintId}`, {
|
|
214
|
+
state: 'active',
|
|
215
|
+
startDate: formatISO(startDate),
|
|
216
|
+
endDate: formatISO(endDate)
|
|
217
|
+
});
|
|
218
|
+
updateSpinner.succeed(chalk.green(`Sprint "${chalk.bold(sprint.name)}" is now ACTIVE.`));
|
|
219
|
+
|
|
220
|
+
} catch (e: any) {
|
|
221
|
+
handleCommandError(spinner, e, 'Failed to start sprint');
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ── COMPLETE SPRINT ───────────────────────────────────────────────
|
|
226
|
+
sprintCmd
|
|
227
|
+
.command('complete')
|
|
228
|
+
.description('Complete (close) an active sprint')
|
|
229
|
+
.argument('<sprintId>', 'Sprint ID')
|
|
230
|
+
.action(async (sprintId) => {
|
|
231
|
+
const spinner = ora(`Fetching sprint ${sprintId}...`).start();
|
|
232
|
+
try {
|
|
233
|
+
const sprint = await api.agileGet(`/sprint/${sprintId}`);
|
|
234
|
+
|
|
235
|
+
if (sprint.state !== 'active') {
|
|
236
|
+
spinner.fail(`Sprint "${sprint.name}" is not active (State: ${sprint.state}).`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
spinner.stop();
|
|
240
|
+
|
|
241
|
+
const { confirmed } = await enquirer.prompt({
|
|
242
|
+
type: 'confirm',
|
|
243
|
+
name: 'confirmed',
|
|
244
|
+
message: `Are you sure you want to complete sprint "${chalk.cyan(sprint.name)}"?`,
|
|
245
|
+
initial: false
|
|
246
|
+
}) as any;
|
|
247
|
+
|
|
248
|
+
if (!confirmed) return;
|
|
249
|
+
|
|
250
|
+
const closeSpinner = ora('Completing sprint...').start();
|
|
251
|
+
// Note: If there are incomplete issues, Jira API might error or require specific handling (swap).
|
|
252
|
+
// For simplified flow, we try basic close. If it fails, we inform user.
|
|
253
|
+
await api.agilePost(`/sprint/${sprintId}`, {
|
|
254
|
+
state: 'closed'
|
|
255
|
+
});
|
|
256
|
+
closeSpinner.succeed(chalk.green(`Sprint "${chalk.bold(sprint.name)}" completed.`));
|
|
257
|
+
|
|
258
|
+
} catch (e: any) {
|
|
259
|
+
if (e.response && e.response.status === 400) {
|
|
260
|
+
// Check if it mentions incomplete issues
|
|
261
|
+
handleCommandError(spinner, e, 'Failed to complete sprint (Check if there are incomplete issues that need moving)');
|
|
262
|
+
} else {
|
|
263
|
+
handleCommandError(spinner, e, 'Failed to complete sprint');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
program.addCommand(sprintCmd);
|
|
269
|
+
}
|
|
@@ -60,6 +60,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
60
60
|
required: ["projectKey", "summary"]
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: "jira_update_issue",
|
|
65
|
+
description: "Update an existing Jira issue. Supports updating summary, description, priority, and assignee.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
|
|
70
|
+
summary: { type: "string", description: "New summary" },
|
|
71
|
+
description: { type: "string", description: "New description (plain text)" },
|
|
72
|
+
priority: { type: "string", description: "New priority name" },
|
|
73
|
+
assigneeId: { type: "string", description: "New assignee account ID (or 'me', 'none')" }
|
|
74
|
+
},
|
|
75
|
+
required: ["issueKey"]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
63
78
|
{
|
|
64
79
|
name: "jira_transition_issue",
|
|
65
80
|
description: "Transition a Jira issue to a new status. First call with only issueKey to see available transitions, then call again with the transitionId.",
|
|
@@ -97,6 +112,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
97
112
|
}
|
|
98
113
|
},
|
|
99
114
|
|
|
115
|
+
// ── Users ───────────────────────────────────────
|
|
116
|
+
{
|
|
117
|
+
name: "jira_search_users",
|
|
118
|
+
description: "Search for Jira users by name or email. Returns accountId and displayName.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
query: { type: "string", description: "Name, email, or part of it" }
|
|
123
|
+
},
|
|
124
|
+
required: ["query"]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "jira_myself",
|
|
129
|
+
description: "Get details about the current authenticated user.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
100
136
|
// ── Projects & Sprints ──────────────────────────
|
|
101
137
|
{
|
|
102
138
|
name: "jira_list_projects",
|
|
@@ -119,6 +155,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
119
155
|
},
|
|
120
156
|
required: ["boardId"]
|
|
121
157
|
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "jira_add_worklog",
|
|
161
|
+
description: "Log work to a Jira issue.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
issueKey: { type: "string", description: "Issue Key" },
|
|
166
|
+
timeSpent: { type: "string", description: "Time spent (e.g., '2h 30m', '1d')" },
|
|
167
|
+
comment: { type: "string", description: "Worklog comment" }
|
|
168
|
+
},
|
|
169
|
+
required: ["issueKey", "timeSpent"]
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "jira_create_subtask",
|
|
174
|
+
description: "Create a subtask for a parent issue.",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
parentKey: { type: "string", description: "Parent Issue Key" },
|
|
179
|
+
summary: { type: "string", description: "Subtask summary" },
|
|
180
|
+
description: { type: "string", description: "Subtask description" },
|
|
181
|
+
priority: { type: "string", description: "Priority name" },
|
|
182
|
+
assigneeId: { type: "string", description: "Assignee Account ID" }
|
|
183
|
+
},
|
|
184
|
+
required: ["parentKey", "summary"]
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "jira_add_attachment",
|
|
189
|
+
description: "Attach a file to a Jira issue.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
issueKey: { type: "string", description: "Issue Key" },
|
|
194
|
+
filePath: { type: "string", description: "Absolute path to the file to extract/upload" }
|
|
195
|
+
},
|
|
196
|
+
required: ["issueKey", "filePath"]
|
|
197
|
+
}
|
|
122
198
|
}
|
|
123
199
|
]
|
|
124
200
|
};
|
|
@@ -126,7 +202,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
126
202
|
|
|
127
203
|
// ── Tool Handlers ────────────────────────────────────────────────────
|
|
128
204
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
129
|
-
const { name, arguments: args } = request.params;
|
|
205
|
+
const { name, arguments: args } = request.params as { name: string; arguments: any };
|
|
130
206
|
|
|
131
207
|
try {
|
|
132
208
|
// ── jira_list_issues ────────────────────────────────
|
|
@@ -140,7 +216,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
140
216
|
});
|
|
141
217
|
|
|
142
218
|
// Return a cleaner format for LLM consumption
|
|
143
|
-
const issues = (data.issues || []).map(i => ({
|
|
219
|
+
const issues = (data.issues || []).map((i: any) => ({
|
|
144
220
|
key: i.key,
|
|
145
221
|
summary: i.fields.summary,
|
|
146
222
|
status: i.fields.status?.name,
|
|
@@ -173,7 +249,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
173
249
|
updated: data.fields.updated,
|
|
174
250
|
description: data.fields.description,
|
|
175
251
|
labels: data.fields.labels,
|
|
176
|
-
comments: data.fields.comment?.comments?.map(c => ({
|
|
252
|
+
comments: data.fields.comment?.comments?.map((c: any) => ({
|
|
177
253
|
author: c.author.displayName,
|
|
178
254
|
body: c.body,
|
|
179
255
|
created: c.created
|
|
@@ -187,7 +263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
187
263
|
|
|
188
264
|
// ── jira_create_issue ───────────────────────────────
|
|
189
265
|
if (name === "jira_create_issue") {
|
|
190
|
-
const body = {
|
|
266
|
+
const body: any = {
|
|
191
267
|
fields: {
|
|
192
268
|
project: { key: args.projectKey },
|
|
193
269
|
summary: args.summary,
|
|
@@ -225,7 +301,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
225
301
|
issueKey: args.issueKey,
|
|
226
302
|
summary: issue.fields.summary,
|
|
227
303
|
currentStatus: issue.fields.status?.name,
|
|
228
|
-
availableTransitions: (transData.transitions || []).map(t => ({
|
|
304
|
+
availableTransitions: (transData.transitions || []).map((t: any) => ({
|
|
229
305
|
id: t.id,
|
|
230
306
|
name: t.name,
|
|
231
307
|
toStatus: t.to.name
|
|
@@ -277,12 +353,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
277
353
|
};
|
|
278
354
|
}
|
|
279
355
|
|
|
356
|
+
// ── jira_update_issue ───────────────────────────────
|
|
357
|
+
if (name === "jira_update_issue") {
|
|
358
|
+
const updateBody: any = { fields: {} };
|
|
359
|
+
|
|
360
|
+
if (args.summary) updateBody.fields.summary = args.summary;
|
|
361
|
+
if (args.description) updateBody.fields.description = textToADF(args.description);
|
|
362
|
+
if (args.priority) updateBody.fields.priority = { name: args.priority };
|
|
363
|
+
|
|
364
|
+
if (args.assigneeId) {
|
|
365
|
+
let accId = args.assigneeId;
|
|
366
|
+
if (accId === 'me') {
|
|
367
|
+
const myself = await api.get('/myself');
|
|
368
|
+
accId = myself.accountId;
|
|
369
|
+
} else if (accId === 'none') {
|
|
370
|
+
accId = null;
|
|
371
|
+
}
|
|
372
|
+
updateBody.fields.assignee = { accountId: accId };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (Object.keys(updateBody.fields).length === 0) {
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: "No fields to update provided." }],
|
|
378
|
+
isError: true
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await api.put(`/issue/${args.issueKey}`, updateBody);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey }) }]
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── jira_search_users ───────────────────────────────
|
|
390
|
+
if (name === "jira_search_users") {
|
|
391
|
+
const users = await api.get(`/user/search?query=${encodeURIComponent(args.query)}`);
|
|
392
|
+
|
|
393
|
+
const results = (users || []).map((u: any) => ({
|
|
394
|
+
accountId: u.accountId,
|
|
395
|
+
displayName: u.displayName,
|
|
396
|
+
email: u.emailAddress,
|
|
397
|
+
active: u.active
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── jira_myself ─────────────────────────────────────
|
|
406
|
+
if (name === "jira_myself") {
|
|
407
|
+
const myself = await api.get('/myself');
|
|
408
|
+
const result = {
|
|
409
|
+
accountId: myself.accountId,
|
|
410
|
+
displayName: myself.displayName,
|
|
411
|
+
email: myself.emailAddress,
|
|
412
|
+
active: myself.active,
|
|
413
|
+
timeZone: myself.timeZone
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
280
421
|
// ── jira_list_projects ──────────────────────────────
|
|
281
422
|
if (name === "jira_list_projects") {
|
|
282
423
|
const limit = args.limit || 50;
|
|
283
424
|
const data = await api.get(`/project/search?maxResults=${limit}`);
|
|
284
425
|
|
|
285
|
-
const projects = (data.values || []).map(p => ({
|
|
426
|
+
const projects = (data.values || []).map((p: any) => ({
|
|
286
427
|
key: p.key,
|
|
287
428
|
name: p.name,
|
|
288
429
|
lead: p.lead?.displayName || 'N/A',
|
|
@@ -300,7 +441,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
300
441
|
const state = args.state || 'active,future';
|
|
301
442
|
const data = await api.agileGet(`/board/${args.boardId}/sprint?state=${state}`);
|
|
302
443
|
|
|
303
|
-
const sprints = (data.values || []).map(s => ({
|
|
444
|
+
const sprints = (data.values || []).map((s: any) => ({
|
|
304
445
|
id: s.id,
|
|
305
446
|
name: s.name,
|
|
306
447
|
state: s.state,
|
|
@@ -314,9 +455,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
314
455
|
};
|
|
315
456
|
}
|
|
316
457
|
|
|
458
|
+
// ── jira_add_worklog ────────────────────────────────
|
|
459
|
+
if (name === "jira_add_worklog") {
|
|
460
|
+
const body: any = {
|
|
461
|
+
timeSpent: args.timeSpent
|
|
462
|
+
};
|
|
463
|
+
if (args.comment) {
|
|
464
|
+
body.comment = textToADF(args.comment);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await api.post(`/issue/${args.issueKey}/worklog`, body);
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, timeSpent: args.timeSpent }) }]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── jira_create_subtask ─────────────────────────────
|
|
475
|
+
if (name === "jira_create_subtask") {
|
|
476
|
+
// 1. Fetch parent to get project
|
|
477
|
+
const parent = await api.get(`/issue/${args.parentKey}?fields=project`);
|
|
478
|
+
const projectKey = parent.fields.project.key;
|
|
479
|
+
|
|
480
|
+
// 2. Find subtask issue type
|
|
481
|
+
const meta = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
482
|
+
const allTypes = meta.issueTypes || meta.values || [];
|
|
483
|
+
const subtaskTypes = allTypes.filter((t: any) => t.subtask);
|
|
484
|
+
|
|
485
|
+
if (subtaskTypes.length === 0) {
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text", text: `Error: No subtask types found in project ${projectKey}` }],
|
|
488
|
+
isError: true
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const subtaskId = subtaskTypes[0].id; // Default to first available
|
|
492
|
+
|
|
493
|
+
const body: any = {
|
|
494
|
+
fields: {
|
|
495
|
+
project: { key: projectKey },
|
|
496
|
+
parent: { key: args.parentKey },
|
|
497
|
+
issuetype: { id: subtaskId },
|
|
498
|
+
summary: args.summary
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
if (args.description) body.fields.description = textToADF(args.description);
|
|
503
|
+
if (args.priority) body.fields.priority = { name: args.priority };
|
|
504
|
+
if (args.assigneeId) {
|
|
505
|
+
let accId = args.assigneeId;
|
|
506
|
+
if (accId === 'me') {
|
|
507
|
+
const myself = await api.get('/myself');
|
|
508
|
+
accId = myself.accountId;
|
|
509
|
+
}
|
|
510
|
+
body.fields.assignee = { accountId: accId };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const data = await api.post('/issue', body);
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: JSON.stringify({ key: data.key, self: data.self }, null, 2) }]
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ── jira_add_attachment ─────────────────────────────
|
|
520
|
+
if (name === "jira_add_attachment") {
|
|
521
|
+
try {
|
|
522
|
+
// Dynamically import fs/path to avoid top-level node dependencies if this runs in browser-like env (unlikely but safe)
|
|
523
|
+
const { openAsBlob } = await import('node:fs');
|
|
524
|
+
const path = await import('node:path');
|
|
525
|
+
|
|
526
|
+
const filePath = args.filePath;
|
|
527
|
+
const file = await openAsBlob(filePath);
|
|
528
|
+
const formData = new FormData();
|
|
529
|
+
formData.append('file', file, path.default.basename(filePath));
|
|
530
|
+
|
|
531
|
+
const result = await api.upload(`/issue/${args.issueKey}/attachments`, formData);
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
535
|
+
};
|
|
536
|
+
} catch (e: any) {
|
|
537
|
+
return {
|
|
538
|
+
content: [{ type: "text", text: `Error attaching file: ${e.message}` }],
|
|
539
|
+
isError: true
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
317
544
|
throw new Error(`Unknown tool: ${name}`);
|
|
318
545
|
|
|
319
|
-
} catch (e) {
|
|
546
|
+
} catch (e: any) {
|
|
320
547
|
const errorMessage = e.response?.data ? JSON.stringify(e.response.data) : e.message;
|
|
321
548
|
return {
|
|
322
549
|
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|