mcp-jira-cloud 2.0.2 → 2.0.4
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 +88 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2942
- package/package.json +5 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,2942 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
const AUTH_SERVICE = "jira-mcp";
|
|
6
|
-
const AUTH_ACCOUNT = "default";
|
|
7
|
-
const ACCEPTANCE_FIELD = (process.env.JIRA_ACCEPTANCE_CRITERIA_FIELD || "").trim();
|
|
8
|
-
// OAuth constants
|
|
9
|
-
const ATLASSIAN_AUTH_URL = "https://auth.atlassian.com/authorize";
|
|
10
|
-
const ATLASSIAN_TOKEN_URL = "https://auth.atlassian.com/oauth/token";
|
|
11
|
-
const ATLASSIAN_API_URL = "https://api.atlassian.com";
|
|
12
|
-
const ATLASSIAN_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources";
|
|
13
|
-
let inMemoryAuth = null;
|
|
14
|
-
let keytarModule = undefined;
|
|
15
|
-
async function getKeytar() {
|
|
16
|
-
if (keytarModule !== undefined) {
|
|
17
|
-
return keytarModule;
|
|
18
|
-
}
|
|
19
|
-
try {
|
|
20
|
-
keytarModule = await import("keytar");
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
keytarModule = null;
|
|
24
|
-
}
|
|
25
|
-
return keytarModule;
|
|
26
|
-
}
|
|
27
|
-
function normalizeBaseUrl(input) {
|
|
28
|
-
let parsed;
|
|
29
|
-
try {
|
|
30
|
-
parsed = new URL(input);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
throw new Error("baseUrl must be a valid URL like https://your-domain.atlassian.net");
|
|
34
|
-
}
|
|
35
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/, "");
|
|
36
|
-
return `${parsed.origin}${trimmedPath}`;
|
|
37
|
-
}
|
|
38
|
-
// ============ Basic Auth Functions ============
|
|
39
|
-
function basicAuthFromEnv() {
|
|
40
|
-
const baseUrl = process.env.JIRA_BASE_URL;
|
|
41
|
-
const email = process.env.JIRA_EMAIL;
|
|
42
|
-
const apiToken = process.env.JIRA_API_TOKEN;
|
|
43
|
-
if (!baseUrl || !email || !apiToken) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
type: "basic",
|
|
48
|
-
baseUrl: normalizeBaseUrl(baseUrl),
|
|
49
|
-
email,
|
|
50
|
-
apiToken,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
// ============ OAuth Functions ============
|
|
54
|
-
function oauthFromEnv() {
|
|
55
|
-
const clientId = process.env.JIRA_OAUTH_CLIENT_ID;
|
|
56
|
-
const clientSecret = process.env.JIRA_OAUTH_CLIENT_SECRET;
|
|
57
|
-
const accessToken = process.env.JIRA_OAUTH_ACCESS_TOKEN;
|
|
58
|
-
const refreshToken = process.env.JIRA_OAUTH_REFRESH_TOKEN;
|
|
59
|
-
const cloudId = process.env.JIRA_CLOUD_ID;
|
|
60
|
-
if (!clientId || !clientSecret || !accessToken || !cloudId) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
type: "oauth",
|
|
65
|
-
clientId,
|
|
66
|
-
clientSecret,
|
|
67
|
-
accessToken,
|
|
68
|
-
refreshToken,
|
|
69
|
-
cloudId,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function generateAuthorizationUrl(clientId, redirectUri, scopes, state) {
|
|
73
|
-
const params = new URLSearchParams({
|
|
74
|
-
audience: "api.atlassian.com",
|
|
75
|
-
client_id: clientId,
|
|
76
|
-
scope: scopes.join(" "),
|
|
77
|
-
redirect_uri: redirectUri,
|
|
78
|
-
state,
|
|
79
|
-
response_type: "code",
|
|
80
|
-
prompt: "consent",
|
|
81
|
-
});
|
|
82
|
-
return `${ATLASSIAN_AUTH_URL}?${params.toString()}`;
|
|
83
|
-
}
|
|
84
|
-
async function exchangeCodeForTokens(clientId, clientSecret, code, redirectUri) {
|
|
85
|
-
const response = await axios.post(ATLASSIAN_TOKEN_URL, {
|
|
86
|
-
grant_type: "authorization_code",
|
|
87
|
-
client_id: clientId,
|
|
88
|
-
client_secret: clientSecret,
|
|
89
|
-
code,
|
|
90
|
-
redirect_uri: redirectUri,
|
|
91
|
-
}, {
|
|
92
|
-
headers: { "Content-Type": "application/json" },
|
|
93
|
-
});
|
|
94
|
-
return {
|
|
95
|
-
accessToken: response.data.access_token,
|
|
96
|
-
refreshToken: response.data.refresh_token,
|
|
97
|
-
expiresIn: response.data.expires_in,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
async function refreshAccessToken(clientId, clientSecret, refreshToken) {
|
|
101
|
-
const response = await axios.post(ATLASSIAN_TOKEN_URL, {
|
|
102
|
-
grant_type: "refresh_token",
|
|
103
|
-
client_id: clientId,
|
|
104
|
-
client_secret: clientSecret,
|
|
105
|
-
refresh_token: refreshToken,
|
|
106
|
-
}, {
|
|
107
|
-
headers: { "Content-Type": "application/json" },
|
|
108
|
-
});
|
|
109
|
-
return {
|
|
110
|
-
accessToken: response.data.access_token,
|
|
111
|
-
refreshToken: response.data.refresh_token,
|
|
112
|
-
expiresIn: response.data.expires_in,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
async function getAccessibleResources(accessToken) {
|
|
116
|
-
const response = await axios.get(ATLASSIAN_RESOURCES_URL, {
|
|
117
|
-
headers: {
|
|
118
|
-
Authorization: `Bearer ${accessToken}`,
|
|
119
|
-
Accept: "application/json",
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
return response.data;
|
|
123
|
-
}
|
|
124
|
-
async function getCloudIdFromResources(accessToken, siteUrl) {
|
|
125
|
-
const resources = await getAccessibleResources(accessToken);
|
|
126
|
-
if (resources.length === 0) {
|
|
127
|
-
throw new Error("No accessible Jira sites found. Make sure your OAuth app has the correct scopes and you have granted access.");
|
|
128
|
-
}
|
|
129
|
-
// If siteUrl is provided, find matching resource
|
|
130
|
-
if (siteUrl) {
|
|
131
|
-
const normalizedSiteUrl = normalizeBaseUrl(siteUrl);
|
|
132
|
-
const resource = resources.find(r => normalizeBaseUrl(r.url) === normalizedSiteUrl);
|
|
133
|
-
if (resource) {
|
|
134
|
-
return { cloudId: resource.id, siteName: resource.name, siteUrl: resource.url };
|
|
135
|
-
}
|
|
136
|
-
throw new Error(`Site ${siteUrl} not found in accessible resources. Available sites: ${resources.map(r => r.url).join(", ")}`);
|
|
137
|
-
}
|
|
138
|
-
// Return first available resource
|
|
139
|
-
const resource = resources[0];
|
|
140
|
-
if (!resource) {
|
|
141
|
-
throw new Error("No accessible Jira resources found");
|
|
142
|
-
}
|
|
143
|
-
return { cloudId: resource.id, siteName: resource.name, siteUrl: resource.url };
|
|
144
|
-
}
|
|
145
|
-
// ============ Auth Management ============
|
|
146
|
-
async function authFromKeytar() {
|
|
147
|
-
const keytar = await getKeytar();
|
|
148
|
-
if (!keytar)
|
|
149
|
-
return null;
|
|
150
|
-
const stored = await keytar.getPassword(AUTH_SERVICE, AUTH_ACCOUNT);
|
|
151
|
-
if (!stored)
|
|
152
|
-
return null;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = JSON.parse(stored);
|
|
155
|
-
if (parsed.type === "basic") {
|
|
156
|
-
return {
|
|
157
|
-
...parsed,
|
|
158
|
-
baseUrl: normalizeBaseUrl(parsed.baseUrl),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
return parsed;
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
async function getAuthOrThrow() {
|
|
168
|
-
if (inMemoryAuth) {
|
|
169
|
-
// Check if OAuth token needs refresh
|
|
170
|
-
if (inMemoryAuth.type === "oauth" && inMemoryAuth.expiresAt && inMemoryAuth.refreshToken) {
|
|
171
|
-
const now = Date.now();
|
|
172
|
-
// Refresh if token expires in less than 5 minutes
|
|
173
|
-
if (now >= inMemoryAuth.expiresAt - 5 * 60 * 1000) {
|
|
174
|
-
try {
|
|
175
|
-
const tokens = await refreshAccessToken(inMemoryAuth.clientId, inMemoryAuth.clientSecret, inMemoryAuth.refreshToken);
|
|
176
|
-
inMemoryAuth = {
|
|
177
|
-
...inMemoryAuth,
|
|
178
|
-
accessToken: tokens.accessToken,
|
|
179
|
-
refreshToken: tokens.refreshToken || inMemoryAuth.refreshToken,
|
|
180
|
-
expiresAt: Date.now() + tokens.expiresIn * 1000,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
// If refresh fails, continue with existing token
|
|
185
|
-
console.error("Failed to refresh OAuth token:", error);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return inMemoryAuth;
|
|
190
|
-
}
|
|
191
|
-
// Try OAuth from env first
|
|
192
|
-
const oauthEnv = oauthFromEnv();
|
|
193
|
-
if (oauthEnv)
|
|
194
|
-
return oauthEnv;
|
|
195
|
-
// Try basic auth from env
|
|
196
|
-
const basicEnv = basicAuthFromEnv();
|
|
197
|
-
if (basicEnv)
|
|
198
|
-
return basicEnv;
|
|
199
|
-
// Try keytar
|
|
200
|
-
const keytarAuth = await authFromKeytar();
|
|
201
|
-
if (keytarAuth)
|
|
202
|
-
return keytarAuth;
|
|
203
|
-
throw new Error("MISSING_AUTH");
|
|
204
|
-
}
|
|
205
|
-
async function setAuth(auth, persist) {
|
|
206
|
-
inMemoryAuth = auth;
|
|
207
|
-
if (!persist)
|
|
208
|
-
return;
|
|
209
|
-
const keytar = await getKeytar();
|
|
210
|
-
if (!keytar) {
|
|
211
|
-
throw new Error("Keytar is not available to persist credentials.");
|
|
212
|
-
}
|
|
213
|
-
await keytar.setPassword(AUTH_SERVICE, AUTH_ACCOUNT, JSON.stringify(auth));
|
|
214
|
-
}
|
|
215
|
-
async function clearAuth() {
|
|
216
|
-
inMemoryAuth = null;
|
|
217
|
-
const keytar = await getKeytar();
|
|
218
|
-
if (!keytar)
|
|
219
|
-
return;
|
|
220
|
-
await keytar.deletePassword(AUTH_SERVICE, AUTH_ACCOUNT);
|
|
221
|
-
}
|
|
222
|
-
// ============ Client Creation ============
|
|
223
|
-
function createClient(auth) {
|
|
224
|
-
if (auth.type === "basic") {
|
|
225
|
-
return axios.create({
|
|
226
|
-
baseURL: auth.baseUrl,
|
|
227
|
-
auth: {
|
|
228
|
-
username: auth.email,
|
|
229
|
-
password: auth.apiToken,
|
|
230
|
-
},
|
|
231
|
-
headers: {
|
|
232
|
-
Accept: "application/json",
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
// OAuth client
|
|
237
|
-
return axios.create({
|
|
238
|
-
baseURL: `${ATLASSIAN_API_URL}/ex/jira/${auth.cloudId}`,
|
|
239
|
-
headers: {
|
|
240
|
-
Authorization: `Bearer ${auth.accessToken}`,
|
|
241
|
-
Accept: "application/json",
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
function adfToText(node) {
|
|
246
|
-
if (!node)
|
|
247
|
-
return "";
|
|
248
|
-
if (Array.isArray(node)) {
|
|
249
|
-
return node.map(adfToText).filter(Boolean).join("\n").trim();
|
|
250
|
-
}
|
|
251
|
-
if (typeof node.text === "string") {
|
|
252
|
-
return node.text;
|
|
253
|
-
}
|
|
254
|
-
if (Array.isArray(node.content)) {
|
|
255
|
-
const parts = node.content.map(adfToText).filter(Boolean);
|
|
256
|
-
return parts.join(node.type === "paragraph" ? "\n" : " ").trim();
|
|
257
|
-
}
|
|
258
|
-
return "";
|
|
259
|
-
}
|
|
260
|
-
function normalizeFieldText(value) {
|
|
261
|
-
if (typeof value === "string")
|
|
262
|
-
return value;
|
|
263
|
-
if (typeof value === "number")
|
|
264
|
-
return String(value);
|
|
265
|
-
if (value && typeof value === "object") {
|
|
266
|
-
const maybeAdf = value;
|
|
267
|
-
const text = adfToText(maybeAdf);
|
|
268
|
-
if (text)
|
|
269
|
-
return text;
|
|
270
|
-
}
|
|
271
|
-
return "";
|
|
272
|
-
}
|
|
273
|
-
function textToAdf(text) {
|
|
274
|
-
return {
|
|
275
|
-
type: "doc",
|
|
276
|
-
version: 1,
|
|
277
|
-
content: [
|
|
278
|
-
{
|
|
279
|
-
type: "paragraph",
|
|
280
|
-
content: [
|
|
281
|
-
{
|
|
282
|
-
type: "text",
|
|
283
|
-
text,
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
},
|
|
287
|
-
],
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
function pickIssueSummary(issue) {
|
|
291
|
-
const fields = issue?.fields || {};
|
|
292
|
-
const description = normalizeFieldText(fields.description);
|
|
293
|
-
const acceptanceCriteria = ACCEPTANCE_FIELD
|
|
294
|
-
? normalizeFieldText(fields[ACCEPTANCE_FIELD])
|
|
295
|
-
: "";
|
|
296
|
-
return {
|
|
297
|
-
key: issue?.key ?? "",
|
|
298
|
-
summary: fields.summary ?? "",
|
|
299
|
-
description,
|
|
300
|
-
acceptanceCriteria: acceptanceCriteria || null,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
function pickIssueSearchSummary(issue) {
|
|
304
|
-
const fields = issue?.fields || {};
|
|
305
|
-
return {
|
|
306
|
-
key: issue?.key ?? "",
|
|
307
|
-
summary: fields.summary ?? "",
|
|
308
|
-
status: fields.status?.name ?? "",
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
function defaultIssueFields() {
|
|
312
|
-
const base = ["summary", "description"];
|
|
313
|
-
if (ACCEPTANCE_FIELD)
|
|
314
|
-
base.push(ACCEPTANCE_FIELD);
|
|
315
|
-
return base;
|
|
316
|
-
}
|
|
317
|
-
function errorToMessage(error) {
|
|
318
|
-
if (error instanceof AxiosError) {
|
|
319
|
-
const status = error.response?.status;
|
|
320
|
-
const data = error.response?.data;
|
|
321
|
-
const detail = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
322
|
-
return `Jira API error${status ? ` (${status})` : ""}: ${detail || error.message}`;
|
|
323
|
-
}
|
|
324
|
-
if (error instanceof Error) {
|
|
325
|
-
return error.message;
|
|
326
|
-
}
|
|
327
|
-
return "Unknown error";
|
|
328
|
-
}
|
|
329
|
-
function errorToResult(error) {
|
|
330
|
-
if (error instanceof Error && error.message === "MISSING_AUTH") {
|
|
331
|
-
return {
|
|
332
|
-
error: "unauthorized",
|
|
333
|
-
message: "Jira credentials are missing. Provide credentials explicitly to authenticate.",
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (error instanceof AxiosError) {
|
|
337
|
-
const status = error.response?.status;
|
|
338
|
-
if (status === 401) {
|
|
339
|
-
return {
|
|
340
|
-
error: "unauthorized",
|
|
341
|
-
message: "Jira credentials are missing or invalid. If using OAuth, the token may have expired.",
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
if (status === 403) {
|
|
345
|
-
return {
|
|
346
|
-
error: "forbidden",
|
|
347
|
-
message: "You do not have permission to access this Jira resource.",
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
if (status === 404) {
|
|
351
|
-
return {
|
|
352
|
-
error: "not_found",
|
|
353
|
-
message: "The Jira resource does not exist or is not visible.",
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
if (status === 429) {
|
|
357
|
-
return {
|
|
358
|
-
error: "rate_limited",
|
|
359
|
-
message: "Jira rate limit exceeded. Please retry later.",
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
if (status && status >= 500) {
|
|
363
|
-
return {
|
|
364
|
-
error: "server_error",
|
|
365
|
-
message: "Jira server error. Please retry later.",
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
error: "jira_error",
|
|
370
|
-
message: errorToMessage(error),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
if (error instanceof Error) {
|
|
374
|
-
return {
|
|
375
|
-
error: "unknown",
|
|
376
|
-
message: error.message,
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
return {
|
|
380
|
-
error: "unknown",
|
|
381
|
-
message: "Unknown error",
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
function textResult(value) {
|
|
385
|
-
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
386
|
-
return {
|
|
387
|
-
content: [
|
|
388
|
-
{
|
|
389
|
-
type: "text",
|
|
390
|
-
text,
|
|
391
|
-
},
|
|
392
|
-
],
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
const server = new McpServer({
|
|
396
|
-
name: "jira-mcp",
|
|
397
|
-
version: "2.0.0",
|
|
398
|
-
});
|
|
399
|
-
// ============ Issue Field Builders ============
|
|
400
|
-
/**
|
|
401
|
-
* Builds the fields object for issue creation/update
|
|
402
|
-
* Handles proper formatting for different field types
|
|
403
|
-
*/
|
|
404
|
-
function buildIssueFields(params) {
|
|
405
|
-
const fields = {};
|
|
406
|
-
if (params.projectKey) {
|
|
407
|
-
fields.project = { key: params.projectKey };
|
|
408
|
-
}
|
|
409
|
-
if (params.issueType) {
|
|
410
|
-
// Support both name and ID
|
|
411
|
-
fields.issuetype = /^\d+$/.test(params.issueType)
|
|
412
|
-
? { id: params.issueType }
|
|
413
|
-
: { name: params.issueType };
|
|
414
|
-
}
|
|
415
|
-
if (params.summary !== undefined) {
|
|
416
|
-
fields.summary = params.summary;
|
|
417
|
-
}
|
|
418
|
-
if (params.description !== undefined) {
|
|
419
|
-
fields.description = params.description ? textToAdf(params.description) : null;
|
|
420
|
-
}
|
|
421
|
-
if (params.assignee !== undefined) {
|
|
422
|
-
fields.assignee = params.assignee === null ? null : { accountId: params.assignee };
|
|
423
|
-
}
|
|
424
|
-
if (params.reporter) {
|
|
425
|
-
fields.reporter = { accountId: params.reporter };
|
|
426
|
-
}
|
|
427
|
-
if (params.priority) {
|
|
428
|
-
// Support both name and ID
|
|
429
|
-
fields.priority = /^\d+$/.test(params.priority)
|
|
430
|
-
? { id: params.priority }
|
|
431
|
-
: { name: params.priority };
|
|
432
|
-
}
|
|
433
|
-
if (params.labels && params.labels.length > 0) {
|
|
434
|
-
fields.labels = params.labels;
|
|
435
|
-
}
|
|
436
|
-
if (params.components && params.components.length > 0) {
|
|
437
|
-
fields.components = params.components.map(c => /^\d+$/.test(c) ? { id: c } : { name: c });
|
|
438
|
-
}
|
|
439
|
-
if (params.fixVersions && params.fixVersions.length > 0) {
|
|
440
|
-
fields.fixVersions = params.fixVersions.map(v => /^\d+$/.test(v) ? { id: v } : { name: v });
|
|
441
|
-
}
|
|
442
|
-
if (params.affectsVersions && params.affectsVersions.length > 0) {
|
|
443
|
-
fields.versions = params.affectsVersions.map(v => /^\d+$/.test(v) ? { id: v } : { name: v });
|
|
444
|
-
}
|
|
445
|
-
if (params.dueDate !== undefined) {
|
|
446
|
-
fields.duedate = params.dueDate;
|
|
447
|
-
}
|
|
448
|
-
if (params.parentKey) {
|
|
449
|
-
fields.parent = { key: params.parentKey };
|
|
450
|
-
}
|
|
451
|
-
if (params.environment) {
|
|
452
|
-
fields.environment = textToAdf(params.environment);
|
|
453
|
-
}
|
|
454
|
-
if (params.originalEstimate || params.remainingEstimate) {
|
|
455
|
-
fields.timetracking = {};
|
|
456
|
-
if (params.originalEstimate) {
|
|
457
|
-
fields.timetracking.originalEstimate = params.originalEstimate;
|
|
458
|
-
}
|
|
459
|
-
if (params.remainingEstimate) {
|
|
460
|
-
fields.timetracking.remainingEstimate = params.remainingEstimate;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
// Add custom fields
|
|
464
|
-
if (params.customFields) {
|
|
465
|
-
for (const [key, value] of Object.entries(params.customFields)) {
|
|
466
|
-
const fieldKey = key.startsWith("customfield_") ? key : `customfield_${key}`;
|
|
467
|
-
// Handle string values that should be ADF
|
|
468
|
-
if (typeof value === "string" && value.length > 0) {
|
|
469
|
-
// Check if it looks like it needs ADF conversion (multi-line or rich text)
|
|
470
|
-
fields[fieldKey] = value;
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
fields[fieldKey] = value;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return fields;
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Builds update operations for issue modification
|
|
481
|
-
* Supports add, remove, set operations for array fields
|
|
482
|
-
*/
|
|
483
|
-
function buildUpdateOperations(params) {
|
|
484
|
-
const update = {};
|
|
485
|
-
// Labels use simple string values
|
|
486
|
-
if (params.labels) {
|
|
487
|
-
const labelsArr = [];
|
|
488
|
-
if (params.labels.add) {
|
|
489
|
-
params.labels.add.forEach(l => labelsArr.push({ add: l }));
|
|
490
|
-
}
|
|
491
|
-
if (params.labels.remove) {
|
|
492
|
-
params.labels.remove.forEach(l => labelsArr.push({ remove: l }));
|
|
493
|
-
}
|
|
494
|
-
if (params.labels.set) {
|
|
495
|
-
labelsArr.push({ set: params.labels.set });
|
|
496
|
-
}
|
|
497
|
-
update.labels = labelsArr;
|
|
498
|
-
}
|
|
499
|
-
// Components use object with id/name
|
|
500
|
-
if (params.components) {
|
|
501
|
-
const componentsArr = [];
|
|
502
|
-
if (params.components.add) {
|
|
503
|
-
params.components.add.forEach(c => componentsArr.push({ add: /^\d+$/.test(c) ? { id: c } : { name: c } }));
|
|
504
|
-
}
|
|
505
|
-
if (params.components.remove) {
|
|
506
|
-
params.components.remove.forEach(c => componentsArr.push({ remove: /^\d+$/.test(c) ? { id: c } : { name: c } }));
|
|
507
|
-
}
|
|
508
|
-
if (params.components.set) {
|
|
509
|
-
componentsArr.push({
|
|
510
|
-
set: params.components.set.map(c => (/^\d+$/.test(c) ? { id: c } : { name: c }))
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
update.components = componentsArr;
|
|
514
|
-
}
|
|
515
|
-
// Fix versions
|
|
516
|
-
if (params.fixVersions) {
|
|
517
|
-
const fixVersionsArr = [];
|
|
518
|
-
if (params.fixVersions.add) {
|
|
519
|
-
params.fixVersions.add.forEach(v => fixVersionsArr.push({ add: /^\d+$/.test(v) ? { id: v } : { name: v } }));
|
|
520
|
-
}
|
|
521
|
-
if (params.fixVersions.remove) {
|
|
522
|
-
params.fixVersions.remove.forEach(v => fixVersionsArr.push({ remove: /^\d+$/.test(v) ? { id: v } : { name: v } }));
|
|
523
|
-
}
|
|
524
|
-
if (params.fixVersions.set) {
|
|
525
|
-
fixVersionsArr.push({
|
|
526
|
-
set: params.fixVersions.set.map(v => (/^\d+$/.test(v) ? { id: v } : { name: v }))
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
update.fixVersions = fixVersionsArr;
|
|
530
|
-
}
|
|
531
|
-
// Affects versions
|
|
532
|
-
if (params.affectsVersions) {
|
|
533
|
-
const versionsArr = [];
|
|
534
|
-
if (params.affectsVersions.add) {
|
|
535
|
-
params.affectsVersions.add.forEach(v => versionsArr.push({ add: /^\d+$/.test(v) ? { id: v } : { name: v } }));
|
|
536
|
-
}
|
|
537
|
-
if (params.affectsVersions.remove) {
|
|
538
|
-
params.affectsVersions.remove.forEach(v => versionsArr.push({ remove: /^\d+$/.test(v) ? { id: v } : { name: v } }));
|
|
539
|
-
}
|
|
540
|
-
if (params.affectsVersions.set) {
|
|
541
|
-
versionsArr.push({
|
|
542
|
-
set: params.affectsVersions.set.map(v => (/^\d+$/.test(v) ? { id: v } : { name: v }))
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
update.versions = versionsArr;
|
|
546
|
-
}
|
|
547
|
-
return update;
|
|
548
|
-
}
|
|
549
|
-
// ============ Auth Tools ============
|
|
550
|
-
server.registerTool("_internal_jira_set_auth", {
|
|
551
|
-
title: "Set Jira Auth (Basic)",
|
|
552
|
-
description: "Use when the user wants to connect Jira using Basic Auth (email + API token). This tool should only be called when the user explicitly provides credentials.",
|
|
553
|
-
inputSchema: z.object({
|
|
554
|
-
baseUrl: z.string(),
|
|
555
|
-
email: z.string().email(),
|
|
556
|
-
apiToken: z.string().min(1),
|
|
557
|
-
persist: z.boolean().optional().default(false),
|
|
558
|
-
}),
|
|
559
|
-
}, async ({ baseUrl, email, apiToken, persist }) => {
|
|
560
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
561
|
-
await setAuth({ type: "basic", baseUrl: normalized, email, apiToken }, persist ?? false);
|
|
562
|
-
return textResult("Jira credentials loaded (Basic Auth).");
|
|
563
|
-
});
|
|
564
|
-
server.registerTool("jira_oauth_get_auth_url", {
|
|
565
|
-
title: "Get OAuth Authorization URL",
|
|
566
|
-
description: "Generate the OAuth 2.0 authorization URL that the user should visit to grant access. Returns the URL and required state parameter.",
|
|
567
|
-
inputSchema: z.object({
|
|
568
|
-
clientId: z.string().min(1).describe("OAuth Client ID from Atlassian Developer Console"),
|
|
569
|
-
redirectUri: z.string().url().describe("Callback URL configured in your OAuth app"),
|
|
570
|
-
scopes: z.array(z.string()).optional().default([
|
|
571
|
-
"read:jira-work",
|
|
572
|
-
"read:jira-user",
|
|
573
|
-
"write:jira-work",
|
|
574
|
-
"offline_access",
|
|
575
|
-
]).describe("OAuth scopes to request"),
|
|
576
|
-
}),
|
|
577
|
-
}, async ({ clientId, redirectUri, scopes }) => {
|
|
578
|
-
const state = Math.random().toString(36).substring(2, 15);
|
|
579
|
-
const authUrl = generateAuthorizationUrl(clientId, redirectUri, scopes, state);
|
|
580
|
-
return textResult({
|
|
581
|
-
authUrl,
|
|
582
|
-
state,
|
|
583
|
-
instructions: "1. Visit the authUrl in your browser\n2. Grant access to your Jira site\n3. Copy the 'code' parameter from the redirect URL\n4. Use jira_oauth_exchange_code to exchange it for tokens",
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
server.registerTool("jira_oauth_exchange_code", {
|
|
587
|
-
title: "Exchange OAuth Code for Tokens",
|
|
588
|
-
description: "Exchange the authorization code for access tokens after the user has completed the OAuth flow.",
|
|
589
|
-
inputSchema: z.object({
|
|
590
|
-
clientId: z.string().min(1),
|
|
591
|
-
clientSecret: z.string().min(1),
|
|
592
|
-
code: z.string().min(1).describe("Authorization code from the OAuth callback"),
|
|
593
|
-
redirectUri: z.string().url(),
|
|
594
|
-
siteUrl: z.string().url().optional().describe("Optional: specific Jira site URL (e.g., https://yoursite.atlassian.net)"),
|
|
595
|
-
persist: z.boolean().optional().default(false),
|
|
596
|
-
}),
|
|
597
|
-
}, async ({ clientId, clientSecret, code, redirectUri, siteUrl, persist }) => {
|
|
598
|
-
try {
|
|
599
|
-
// Exchange code for tokens
|
|
600
|
-
const tokens = await exchangeCodeForTokens(clientId, clientSecret, code, redirectUri);
|
|
601
|
-
// Get cloud ID
|
|
602
|
-
const { cloudId, siteName, siteUrl: actualSiteUrl } = await getCloudIdFromResources(tokens.accessToken, siteUrl);
|
|
603
|
-
const auth = {
|
|
604
|
-
type: "oauth",
|
|
605
|
-
clientId,
|
|
606
|
-
clientSecret,
|
|
607
|
-
accessToken: tokens.accessToken,
|
|
608
|
-
refreshToken: tokens.refreshToken,
|
|
609
|
-
cloudId,
|
|
610
|
-
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1000 : undefined,
|
|
611
|
-
};
|
|
612
|
-
await setAuth(auth, persist);
|
|
613
|
-
return textResult({
|
|
614
|
-
success: true,
|
|
615
|
-
message: `Successfully authenticated with OAuth to ${siteName}`,
|
|
616
|
-
site: {
|
|
617
|
-
name: siteName,
|
|
618
|
-
url: actualSiteUrl,
|
|
619
|
-
cloudId,
|
|
620
|
-
},
|
|
621
|
-
hasRefreshToken: !!tokens.refreshToken,
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
catch (error) {
|
|
625
|
-
return textResult(errorToResult(error));
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
server.registerTool("jira_oauth_set_tokens", {
|
|
629
|
-
title: "Set OAuth Tokens Directly",
|
|
630
|
-
description: "Set OAuth tokens directly if you already have them (e.g., from a previous session or external OAuth flow).",
|
|
631
|
-
inputSchema: z.object({
|
|
632
|
-
clientId: z.string().min(1),
|
|
633
|
-
clientSecret: z.string().min(1),
|
|
634
|
-
accessToken: z.string().min(1),
|
|
635
|
-
refreshToken: z.string().optional(),
|
|
636
|
-
cloudId: z.string().optional().describe("Cloud ID of the Jira site. If not provided, will be fetched automatically."),
|
|
637
|
-
siteUrl: z.string().url().optional().describe("Jira site URL to find the correct cloudId"),
|
|
638
|
-
persist: z.boolean().optional().default(false),
|
|
639
|
-
}),
|
|
640
|
-
}, async ({ clientId, clientSecret, accessToken, refreshToken, cloudId, siteUrl, persist }) => {
|
|
641
|
-
try {
|
|
642
|
-
let finalCloudId = cloudId;
|
|
643
|
-
let siteName = "";
|
|
644
|
-
let actualSiteUrl = siteUrl || "";
|
|
645
|
-
if (!finalCloudId) {
|
|
646
|
-
const resources = await getCloudIdFromResources(accessToken, siteUrl);
|
|
647
|
-
finalCloudId = resources.cloudId;
|
|
648
|
-
siteName = resources.siteName;
|
|
649
|
-
actualSiteUrl = resources.siteUrl;
|
|
650
|
-
}
|
|
651
|
-
const auth = {
|
|
652
|
-
type: "oauth",
|
|
653
|
-
clientId,
|
|
654
|
-
clientSecret,
|
|
655
|
-
accessToken,
|
|
656
|
-
refreshToken,
|
|
657
|
-
cloudId: finalCloudId,
|
|
658
|
-
};
|
|
659
|
-
await setAuth(auth, persist);
|
|
660
|
-
return textResult({
|
|
661
|
-
success: true,
|
|
662
|
-
message: siteName ? `OAuth tokens set for ${siteName}` : "OAuth tokens set successfully",
|
|
663
|
-
cloudId: finalCloudId,
|
|
664
|
-
siteUrl: actualSiteUrl,
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
catch (error) {
|
|
668
|
-
return textResult(errorToResult(error));
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
server.registerTool("jira_oauth_refresh", {
|
|
672
|
-
title: "Refresh OAuth Token",
|
|
673
|
-
description: "Manually refresh the OAuth access token using the refresh token.",
|
|
674
|
-
inputSchema: z.object({}),
|
|
675
|
-
}, async () => {
|
|
676
|
-
try {
|
|
677
|
-
const auth = await getAuthOrThrow();
|
|
678
|
-
if (auth.type !== "oauth") {
|
|
679
|
-
return textResult({
|
|
680
|
-
error: "invalid_auth_type",
|
|
681
|
-
message: "Current authentication is not OAuth. Use basic auth credentials directly.",
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
if (!auth.refreshToken) {
|
|
685
|
-
return textResult({
|
|
686
|
-
error: "no_refresh_token",
|
|
687
|
-
message: "No refresh token available. You need to re-authenticate with 'offline_access' scope.",
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
const tokens = await refreshAccessToken(auth.clientId, auth.clientSecret, auth.refreshToken);
|
|
691
|
-
const updatedAuth = {
|
|
692
|
-
...auth,
|
|
693
|
-
accessToken: tokens.accessToken,
|
|
694
|
-
refreshToken: tokens.refreshToken || auth.refreshToken,
|
|
695
|
-
expiresAt: Date.now() + tokens.expiresIn * 1000,
|
|
696
|
-
};
|
|
697
|
-
await setAuth(updatedAuth, false);
|
|
698
|
-
return textResult({
|
|
699
|
-
success: true,
|
|
700
|
-
message: "OAuth token refreshed successfully",
|
|
701
|
-
expiresIn: tokens.expiresIn,
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
catch (error) {
|
|
705
|
-
return textResult(errorToResult(error));
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
server.registerTool("jira_oauth_list_sites", {
|
|
709
|
-
title: "List Accessible Jira Sites",
|
|
710
|
-
description: "List all Jira sites accessible with the current OAuth token.",
|
|
711
|
-
inputSchema: z.object({}),
|
|
712
|
-
}, async () => {
|
|
713
|
-
try {
|
|
714
|
-
const auth = await getAuthOrThrow();
|
|
715
|
-
if (auth.type !== "oauth") {
|
|
716
|
-
return textResult({
|
|
717
|
-
error: "invalid_auth_type",
|
|
718
|
-
message: "This tool requires OAuth authentication. Current auth is basic auth.",
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
const resources = await getAccessibleResources(auth.accessToken);
|
|
722
|
-
return textResult({
|
|
723
|
-
currentCloudId: auth.cloudId,
|
|
724
|
-
sites: resources.map(r => ({
|
|
725
|
-
cloudId: r.id,
|
|
726
|
-
name: r.name,
|
|
727
|
-
url: r.url,
|
|
728
|
-
scopes: r.scopes,
|
|
729
|
-
})),
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
catch (error) {
|
|
733
|
-
return textResult(errorToResult(error));
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
server.registerTool("jira_clear_auth", {
|
|
737
|
-
title: "Clear Jira Auth",
|
|
738
|
-
description: "Use when the user asks to remove or reset stored Jira credentials.",
|
|
739
|
-
inputSchema: z.object({}),
|
|
740
|
-
}, async () => {
|
|
741
|
-
await clearAuth();
|
|
742
|
-
return textResult("Jira credentials cleared.");
|
|
743
|
-
});
|
|
744
|
-
server.registerTool("jira_auth_status", {
|
|
745
|
-
title: "Get Auth Status",
|
|
746
|
-
description: "Check the current authentication status and type.",
|
|
747
|
-
inputSchema: z.object({}),
|
|
748
|
-
}, async () => {
|
|
749
|
-
try {
|
|
750
|
-
const auth = await getAuthOrThrow();
|
|
751
|
-
if (auth.type === "basic") {
|
|
752
|
-
return textResult({
|
|
753
|
-
authenticated: true,
|
|
754
|
-
type: "basic",
|
|
755
|
-
baseUrl: auth.baseUrl,
|
|
756
|
-
email: auth.email,
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
return textResult({
|
|
760
|
-
authenticated: true,
|
|
761
|
-
type: "oauth",
|
|
762
|
-
cloudId: auth.cloudId,
|
|
763
|
-
hasRefreshToken: !!auth.refreshToken,
|
|
764
|
-
expiresAt: auth.expiresAt ? new Date(auth.expiresAt).toISOString() : null,
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
catch (error) {
|
|
768
|
-
if (error instanceof Error && error.message === "MISSING_AUTH") {
|
|
769
|
-
return textResult({
|
|
770
|
-
authenticated: false,
|
|
771
|
-
message: "No authentication configured. Use basic auth or OAuth to authenticate.",
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
return textResult(errorToResult(error));
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
// ============ Jira API Tools ============
|
|
778
|
-
server.registerTool("jira_whoami", {
|
|
779
|
-
title: "Get Jira Profile",
|
|
780
|
-
description: "Use when the user asks who they are in Jira or wants to verify the Jira account in use.",
|
|
781
|
-
}, async () => {
|
|
782
|
-
try {
|
|
783
|
-
const auth = await getAuthOrThrow();
|
|
784
|
-
const client = createClient(auth);
|
|
785
|
-
const response = await client.get("/rest/api/3/myself");
|
|
786
|
-
return textResult(response.data);
|
|
787
|
-
}
|
|
788
|
-
catch (error) {
|
|
789
|
-
return textResult(errorToResult(error));
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
server.registerTool("jira_get_issue", {
|
|
793
|
-
title: "Get Jira Issue",
|
|
794
|
-
description: "Get the full details of a Jira issue when the user mentions an issue key like PROJ-123 or asks about a specific ticket.",
|
|
795
|
-
inputSchema: z.object({
|
|
796
|
-
issueIdOrKey: z.string().min(1),
|
|
797
|
-
fields: z.array(z.string()).optional(),
|
|
798
|
-
expand: z.string().optional(),
|
|
799
|
-
}),
|
|
800
|
-
}, async ({ issueIdOrKey, fields, expand }) => {
|
|
801
|
-
try {
|
|
802
|
-
const auth = await getAuthOrThrow();
|
|
803
|
-
const client = createClient(auth);
|
|
804
|
-
const fieldParam = fields?.length ? fields : defaultIssueFields();
|
|
805
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`, {
|
|
806
|
-
params: {
|
|
807
|
-
fields: fieldParam.join(","),
|
|
808
|
-
expand,
|
|
809
|
-
},
|
|
810
|
-
});
|
|
811
|
-
return textResult(pickIssueSummary(response.data));
|
|
812
|
-
}
|
|
813
|
-
catch (error) {
|
|
814
|
-
return textResult(errorToResult(error));
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
server.registerTool("jira_search_issues", {
|
|
818
|
-
title: "Search Jira Issues",
|
|
819
|
-
description: "Use when the user asks to find issues matching criteria (JQL), like 'my open bugs' or 'tickets updated this week'.",
|
|
820
|
-
inputSchema: z.object({
|
|
821
|
-
jql: z.string().min(1),
|
|
822
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
823
|
-
maxResults: z.number().int().positive().max(200).optional(),
|
|
824
|
-
fields: z.array(z.string()).optional(),
|
|
825
|
-
expand: z.string().optional(),
|
|
826
|
-
nextPageToken: z.string().optional(),
|
|
827
|
-
reconcileIssues: z.boolean().optional(),
|
|
828
|
-
}),
|
|
829
|
-
}, async ({ jql, startAt, maxResults, fields, expand, nextPageToken, reconcileIssues }) => {
|
|
830
|
-
try {
|
|
831
|
-
const auth = await getAuthOrThrow();
|
|
832
|
-
const client = createClient(auth);
|
|
833
|
-
const fieldParam = fields?.length ? fields : defaultIssueFields();
|
|
834
|
-
const response = await client.get("/rest/api/3/search/jql", {
|
|
835
|
-
params: {
|
|
836
|
-
jql,
|
|
837
|
-
startAt,
|
|
838
|
-
maxResults,
|
|
839
|
-
fields: fieldParam.join(","),
|
|
840
|
-
expand,
|
|
841
|
-
nextPageToken,
|
|
842
|
-
reconcileIssues,
|
|
843
|
-
},
|
|
844
|
-
});
|
|
845
|
-
const issues = Array.isArray(response.data?.issues)
|
|
846
|
-
? response.data.issues.map(pickIssueSummary)
|
|
847
|
-
: [];
|
|
848
|
-
return textResult({
|
|
849
|
-
total: response.data?.total ?? issues.length,
|
|
850
|
-
issues,
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
return textResult(errorToResult(error));
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
server.registerTool("jira_search_issues_summary", {
|
|
858
|
-
title: "Search Jira Issues (Summary)",
|
|
859
|
-
description: "Use when the user wants the top results for a Jira search and only needs key, summary, and status.",
|
|
860
|
-
inputSchema: z.object({
|
|
861
|
-
jql: z.string().min(1),
|
|
862
|
-
maxResults: z.number().int().positive().max(50).optional(),
|
|
863
|
-
}),
|
|
864
|
-
}, async ({ jql, maxResults }) => {
|
|
865
|
-
try {
|
|
866
|
-
const auth = await getAuthOrThrow();
|
|
867
|
-
const client = createClient(auth);
|
|
868
|
-
const response = await client.get("/rest/api/3/search/jql", {
|
|
869
|
-
params: {
|
|
870
|
-
jql,
|
|
871
|
-
maxResults: maxResults ?? 10,
|
|
872
|
-
fields: ["summary", "status"].join(","),
|
|
873
|
-
},
|
|
874
|
-
});
|
|
875
|
-
const issues = Array.isArray(response.data?.issues)
|
|
876
|
-
? response.data.issues.map(pickIssueSearchSummary)
|
|
877
|
-
: [];
|
|
878
|
-
return textResult(issues);
|
|
879
|
-
}
|
|
880
|
-
catch (error) {
|
|
881
|
-
return textResult(errorToResult(error));
|
|
882
|
-
}
|
|
883
|
-
});
|
|
884
|
-
server.registerTool("jira_resolve", {
|
|
885
|
-
title: "Resolve Jira Intent",
|
|
886
|
-
description: "Primary routing tool. Use this tool first when the user intent is clear (get issue, search, or my issues) but the exact Jira tool to call is uncertain.",
|
|
887
|
-
inputSchema: z.object({
|
|
888
|
-
intent: z.enum(["get_issue", "search", "my_issues"]),
|
|
889
|
-
issueKey: z.string().optional(),
|
|
890
|
-
jql: z.string().optional(),
|
|
891
|
-
maxResults: z.number().int().positive().max(50).optional(),
|
|
892
|
-
}),
|
|
893
|
-
}, async ({ intent, issueKey, jql, maxResults }) => {
|
|
894
|
-
try {
|
|
895
|
-
const auth = await getAuthOrThrow();
|
|
896
|
-
const client = createClient(auth);
|
|
897
|
-
if (intent === "get_issue") {
|
|
898
|
-
if (!issueKey) {
|
|
899
|
-
return textResult({
|
|
900
|
-
error: "invalid_input",
|
|
901
|
-
message: "issueKey is required when intent is get_issue.",
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueKey)}`, {
|
|
905
|
-
params: {
|
|
906
|
-
fields: defaultIssueFields().join(","),
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
return textResult(pickIssueSummary(response.data));
|
|
910
|
-
}
|
|
911
|
-
if (intent === "search") {
|
|
912
|
-
if (!jql) {
|
|
913
|
-
return textResult({
|
|
914
|
-
error: "invalid_input",
|
|
915
|
-
message: "jql is required when intent is search.",
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
const response = await client.get("/rest/api/3/search/jql", {
|
|
919
|
-
params: {
|
|
920
|
-
jql,
|
|
921
|
-
maxResults: maxResults ?? 10,
|
|
922
|
-
fields: ["summary", "status"].join(","),
|
|
923
|
-
},
|
|
924
|
-
});
|
|
925
|
-
const issues = Array.isArray(response.data?.issues)
|
|
926
|
-
? response.data.issues.map(pickIssueSearchSummary)
|
|
927
|
-
: [];
|
|
928
|
-
return textResult(issues);
|
|
929
|
-
}
|
|
930
|
-
const response = await client.get("/rest/api/3/search/jql", {
|
|
931
|
-
params: {
|
|
932
|
-
jql: "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC",
|
|
933
|
-
maxResults: maxResults ?? 20,
|
|
934
|
-
fields: defaultIssueFields().join(","),
|
|
935
|
-
},
|
|
936
|
-
});
|
|
937
|
-
const issues = Array.isArray(response.data?.issues)
|
|
938
|
-
? response.data.issues.map(pickIssueSummary)
|
|
939
|
-
: [];
|
|
940
|
-
return textResult({
|
|
941
|
-
total: response.data?.total ?? issues.length,
|
|
942
|
-
issues,
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
catch (error) {
|
|
946
|
-
return textResult(errorToResult(error));
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
server.registerTool("jira_get_issue_summary", {
|
|
950
|
-
title: "Get Issue Summary",
|
|
951
|
-
description: "Use when the user wants the summary, description, and acceptance criteria for a specific issue key.",
|
|
952
|
-
inputSchema: z.object({
|
|
953
|
-
issueIdOrKey: z.string().min(1),
|
|
954
|
-
}),
|
|
955
|
-
}, async ({ issueIdOrKey }) => {
|
|
956
|
-
try {
|
|
957
|
-
const auth = await getAuthOrThrow();
|
|
958
|
-
const client = createClient(auth);
|
|
959
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`, {
|
|
960
|
-
params: {
|
|
961
|
-
fields: defaultIssueFields().join(","),
|
|
962
|
-
},
|
|
963
|
-
});
|
|
964
|
-
return textResult(pickIssueSummary(response.data));
|
|
965
|
-
}
|
|
966
|
-
catch (error) {
|
|
967
|
-
return textResult(errorToResult(error));
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
server.registerTool("jira_get_my_open_issues", {
|
|
971
|
-
title: "Get My Open Issues",
|
|
972
|
-
description: "Use when the user asks for their open tickets or what they should work on next.",
|
|
973
|
-
inputSchema: z.object({
|
|
974
|
-
maxResults: z.number().int().positive().max(50).optional(),
|
|
975
|
-
}),
|
|
976
|
-
}, async ({ maxResults }) => {
|
|
977
|
-
try {
|
|
978
|
-
const auth = await getAuthOrThrow();
|
|
979
|
-
const client = createClient(auth);
|
|
980
|
-
const response = await client.get("/rest/api/3/search/jql", {
|
|
981
|
-
params: {
|
|
982
|
-
jql: "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC",
|
|
983
|
-
maxResults: maxResults ?? 20,
|
|
984
|
-
fields: defaultIssueFields().join(","),
|
|
985
|
-
},
|
|
986
|
-
});
|
|
987
|
-
const issues = Array.isArray(response.data?.issues)
|
|
988
|
-
? response.data.issues.map(pickIssueSummary)
|
|
989
|
-
: [];
|
|
990
|
-
return textResult({
|
|
991
|
-
total: response.data?.total ?? issues.length,
|
|
992
|
-
issues,
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
catch (error) {
|
|
996
|
-
return textResult(errorToResult(error));
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
server.registerTool("jira_get_issue_comments", {
|
|
1000
|
-
title: "Get Issue Comments",
|
|
1001
|
-
description: "Use when the user asks for the discussion or comments on a specific ticket; returns a clean list.",
|
|
1002
|
-
inputSchema: z.object({
|
|
1003
|
-
issueIdOrKey: z.string().min(1),
|
|
1004
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
1005
|
-
maxResults: z.number().int().positive().max(100).optional(),
|
|
1006
|
-
}),
|
|
1007
|
-
}, async ({ issueIdOrKey, startAt, maxResults }) => {
|
|
1008
|
-
try {
|
|
1009
|
-
const auth = await getAuthOrThrow();
|
|
1010
|
-
const client = createClient(auth);
|
|
1011
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/comment`, {
|
|
1012
|
-
params: {
|
|
1013
|
-
startAt,
|
|
1014
|
-
maxResults,
|
|
1015
|
-
},
|
|
1016
|
-
});
|
|
1017
|
-
const comments = Array.isArray(response.data?.comments)
|
|
1018
|
-
? response.data.comments.map((comment) => ({
|
|
1019
|
-
author: comment?.author?.displayName ||
|
|
1020
|
-
comment?.author?.emailAddress ||
|
|
1021
|
-
comment?.author?.accountId ||
|
|
1022
|
-
"",
|
|
1023
|
-
created: comment?.created ?? "",
|
|
1024
|
-
body: normalizeFieldText(comment?.body),
|
|
1025
|
-
}))
|
|
1026
|
-
: [];
|
|
1027
|
-
return textResult(comments);
|
|
1028
|
-
}
|
|
1029
|
-
catch (error) {
|
|
1030
|
-
return textResult(errorToResult(error));
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
server.registerTool("jira_add_comment", {
|
|
1034
|
-
title: "Add Jira Comment",
|
|
1035
|
-
description: "Use when the user asks to add a comment to a specific ticket; confirm intent before posting.",
|
|
1036
|
-
inputSchema: z.object({
|
|
1037
|
-
issueIdOrKey: z.string().min(1),
|
|
1038
|
-
body: z.string().min(1),
|
|
1039
|
-
}),
|
|
1040
|
-
}, async ({ issueIdOrKey, body }) => {
|
|
1041
|
-
try {
|
|
1042
|
-
const auth = await getAuthOrThrow();
|
|
1043
|
-
const client = createClient(auth);
|
|
1044
|
-
const response = await client.post(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/comment`, {
|
|
1045
|
-
body: textToAdf(body),
|
|
1046
|
-
});
|
|
1047
|
-
return textResult({
|
|
1048
|
-
id: response.data?.id ?? "",
|
|
1049
|
-
created: response.data?.created ?? "",
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
catch (error) {
|
|
1053
|
-
return textResult(errorToResult(error));
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
server.registerTool("jira_add_worklog", {
|
|
1057
|
-
title: "Add Work Log",
|
|
1058
|
-
description: "Use when the user wants to log time/work on a specific Jira ticket. Allows specifying time spent, start date/time, and an optional description.",
|
|
1059
|
-
inputSchema: z.object({
|
|
1060
|
-
issueIdOrKey: z.string().min(1).describe("The issue key (e.g., PROJ-123) to log work against"),
|
|
1061
|
-
timeSpent: z.string().min(1).describe("Time spent in Jira format (e.g., '1h', '30m', '1h 30m', '1d')"),
|
|
1062
|
-
started: z.string().optional().describe("When the work started in ISO 8601 format (e.g., '2026-02-13T14:00:00.000+0000'). Defaults to now if not provided."),
|
|
1063
|
-
comment: z.string().optional().describe("Optional description of the work performed"),
|
|
1064
|
-
}),
|
|
1065
|
-
}, async ({ issueIdOrKey, timeSpent, started, comment }) => {
|
|
1066
|
-
try {
|
|
1067
|
-
const auth = await getAuthOrThrow();
|
|
1068
|
-
const client = createClient(auth);
|
|
1069
|
-
const worklogData = {
|
|
1070
|
-
timeSpent,
|
|
1071
|
-
};
|
|
1072
|
-
if (started) {
|
|
1073
|
-
worklogData.started = started;
|
|
1074
|
-
}
|
|
1075
|
-
if (comment) {
|
|
1076
|
-
worklogData.comment = textToAdf(comment);
|
|
1077
|
-
}
|
|
1078
|
-
const response = await client.post(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/worklog`, worklogData);
|
|
1079
|
-
return textResult({
|
|
1080
|
-
id: response.data?.id ?? "",
|
|
1081
|
-
issueId: response.data?.issueId ?? "",
|
|
1082
|
-
timeSpent: response.data?.timeSpent ?? "",
|
|
1083
|
-
started: response.data?.started ?? "",
|
|
1084
|
-
author: response.data?.author?.displayName ?? response.data?.author?.emailAddress ?? "",
|
|
1085
|
-
created: response.data?.created ?? "",
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
catch (error) {
|
|
1089
|
-
return textResult(errorToResult(error));
|
|
1090
|
-
}
|
|
1091
|
-
});
|
|
1092
|
-
server.registerTool("jira_get_worklogs", {
|
|
1093
|
-
title: "Get Work Logs",
|
|
1094
|
-
description: "Use when the user wants to see work logs recorded on a specific Jira ticket.",
|
|
1095
|
-
inputSchema: z.object({
|
|
1096
|
-
issueIdOrKey: z.string().min(1).describe("The issue key (e.g., PROJ-123) to get work logs for"),
|
|
1097
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
1098
|
-
maxResults: z.number().int().positive().max(100).optional(),
|
|
1099
|
-
}),
|
|
1100
|
-
}, async ({ issueIdOrKey, startAt, maxResults }) => {
|
|
1101
|
-
try {
|
|
1102
|
-
const auth = await getAuthOrThrow();
|
|
1103
|
-
const client = createClient(auth);
|
|
1104
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/worklog`, {
|
|
1105
|
-
params: {
|
|
1106
|
-
startAt,
|
|
1107
|
-
maxResults,
|
|
1108
|
-
},
|
|
1109
|
-
});
|
|
1110
|
-
const worklogs = Array.isArray(response.data?.worklogs)
|
|
1111
|
-
? response.data.worklogs.map((worklog) => ({
|
|
1112
|
-
id: worklog?.id ?? "",
|
|
1113
|
-
author: worklog?.author?.displayName || worklog?.author?.emailAddress || "",
|
|
1114
|
-
timeSpent: worklog?.timeSpent ?? "",
|
|
1115
|
-
timeSpentSeconds: worklog?.timeSpentSeconds ?? 0,
|
|
1116
|
-
started: worklog?.started ?? "",
|
|
1117
|
-
created: worklog?.created ?? "",
|
|
1118
|
-
comment: normalizeFieldText(worklog?.comment),
|
|
1119
|
-
}))
|
|
1120
|
-
: [];
|
|
1121
|
-
return textResult({
|
|
1122
|
-
total: response.data?.total ?? worklogs.length,
|
|
1123
|
-
worklogs,
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
catch (error) {
|
|
1127
|
-
return textResult(errorToResult(error));
|
|
1128
|
-
}
|
|
1129
|
-
});
|
|
1130
|
-
server.registerTool("jira_list_projects", {
|
|
1131
|
-
title: "List Jira Projects",
|
|
1132
|
-
description: "Use when the user asks which Jira projects they can access or wants a list of projects.",
|
|
1133
|
-
inputSchema: z.object({
|
|
1134
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
1135
|
-
maxResults: z.number().int().positive().max(50).optional(),
|
|
1136
|
-
}),
|
|
1137
|
-
}, async ({ startAt, maxResults }) => {
|
|
1138
|
-
try {
|
|
1139
|
-
const auth = await getAuthOrThrow();
|
|
1140
|
-
const client = createClient(auth);
|
|
1141
|
-
const response = await client.get("/rest/api/3/project/search", {
|
|
1142
|
-
params: {
|
|
1143
|
-
startAt,
|
|
1144
|
-
maxResults,
|
|
1145
|
-
},
|
|
1146
|
-
});
|
|
1147
|
-
return textResult(response.data);
|
|
1148
|
-
}
|
|
1149
|
-
catch (error) {
|
|
1150
|
-
return textResult(errorToResult(error));
|
|
1151
|
-
}
|
|
1152
|
-
});
|
|
1153
|
-
server.registerTool("jira_get_project", {
|
|
1154
|
-
title: "Get Jira Project",
|
|
1155
|
-
description: "Use when the user mentions a project key and asks for project details or metadata.",
|
|
1156
|
-
inputSchema: z.object({
|
|
1157
|
-
projectIdOrKey: z.string().min(1),
|
|
1158
|
-
}),
|
|
1159
|
-
}, async ({ projectIdOrKey }) => {
|
|
1160
|
-
try {
|
|
1161
|
-
const auth = await getAuthOrThrow();
|
|
1162
|
-
const client = createClient(auth);
|
|
1163
|
-
const response = await client.get(`/rest/api/3/project/${encodeURIComponent(projectIdOrKey)}`);
|
|
1164
|
-
return textResult(response.data);
|
|
1165
|
-
}
|
|
1166
|
-
catch (error) {
|
|
1167
|
-
return textResult(errorToResult(error));
|
|
1168
|
-
}
|
|
1169
|
-
});
|
|
1170
|
-
// ============ Phase 1.1: Create Issue ============
|
|
1171
|
-
server.registerTool("jira_create_issue", {
|
|
1172
|
-
title: "Create Jira Issue",
|
|
1173
|
-
description: "Create a new Jira issue. Requires project key, issue type, and summary at minimum.",
|
|
1174
|
-
inputSchema: z.object({
|
|
1175
|
-
projectKey: z.string().min(1).describe("Project key (e.g., 'MXTS')"),
|
|
1176
|
-
issueType: z.string().min(1).describe("Issue type name or ID (e.g., 'Bug', 'Task', 'Story')"),
|
|
1177
|
-
summary: z.string().min(1).describe("Issue title/summary"),
|
|
1178
|
-
description: z.string().optional().describe("Issue description (plain text, will be converted to ADF)"),
|
|
1179
|
-
assignee: z.string().optional().describe("Assignee account ID. Use '-1' for automatic assignment."),
|
|
1180
|
-
reporter: z.string().optional().describe("Reporter account ID"),
|
|
1181
|
-
priority: z.string().optional().describe("Priority name or ID (e.g., 'High', 'Medium', 'Low')"),
|
|
1182
|
-
labels: z.array(z.string()).optional().describe("Array of label strings"),
|
|
1183
|
-
components: z.array(z.string()).optional().describe("Array of component names or IDs"),
|
|
1184
|
-
fixVersions: z.array(z.string()).optional().describe("Array of fix version names or IDs"),
|
|
1185
|
-
affectsVersions: z.array(z.string()).optional().describe("Array of affected version names or IDs"),
|
|
1186
|
-
dueDate: z.string().optional().describe("Due date in YYYY-MM-DD format"),
|
|
1187
|
-
parentKey: z.string().optional().describe("Parent issue key for subtasks"),
|
|
1188
|
-
environment: z.string().optional().describe("Environment description"),
|
|
1189
|
-
originalEstimate: z.string().optional().describe("Original time estimate (e.g., '2h', '1d')"),
|
|
1190
|
-
customFields: z.record(z.string(), z.unknown()).optional().describe("Custom field values as key-value pairs"),
|
|
1191
|
-
}),
|
|
1192
|
-
}, async (params) => {
|
|
1193
|
-
try {
|
|
1194
|
-
const auth = await getAuthOrThrow();
|
|
1195
|
-
const client = createClient(auth);
|
|
1196
|
-
const fields = buildIssueFields({
|
|
1197
|
-
projectKey: params.projectKey,
|
|
1198
|
-
issueType: params.issueType,
|
|
1199
|
-
summary: params.summary,
|
|
1200
|
-
description: params.description,
|
|
1201
|
-
assignee: params.assignee,
|
|
1202
|
-
reporter: params.reporter,
|
|
1203
|
-
priority: params.priority,
|
|
1204
|
-
labels: params.labels,
|
|
1205
|
-
components: params.components,
|
|
1206
|
-
fixVersions: params.fixVersions,
|
|
1207
|
-
affectsVersions: params.affectsVersions,
|
|
1208
|
-
dueDate: params.dueDate,
|
|
1209
|
-
parentKey: params.parentKey,
|
|
1210
|
-
environment: params.environment,
|
|
1211
|
-
originalEstimate: params.originalEstimate,
|
|
1212
|
-
customFields: params.customFields,
|
|
1213
|
-
});
|
|
1214
|
-
const response = await client.post("/rest/api/3/issue", { fields });
|
|
1215
|
-
return textResult({
|
|
1216
|
-
success: true,
|
|
1217
|
-
id: response.data?.id ?? "",
|
|
1218
|
-
key: response.data?.key ?? "",
|
|
1219
|
-
self: response.data?.self ?? "",
|
|
1220
|
-
message: `Issue ${response.data?.key} created successfully`,
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
catch (error) {
|
|
1224
|
-
return textResult(errorToResult(error));
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
// ============ Phase 1.2: Update Issue ============
|
|
1228
|
-
server.registerTool("jira_update_issue", {
|
|
1229
|
-
title: "Update Jira Issue",
|
|
1230
|
-
description: "Update an existing Jira issue. Only provided fields will be modified.",
|
|
1231
|
-
inputSchema: z.object({
|
|
1232
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID (e.g., 'MXTS-123')"),
|
|
1233
|
-
summary: z.string().optional().describe("New summary/title"),
|
|
1234
|
-
description: z.string().optional().describe("New description (plain text)"),
|
|
1235
|
-
assignee: z.string().nullable().optional().describe("Assignee account ID. Use null to unassign."),
|
|
1236
|
-
priority: z.string().optional().describe("Priority name or ID"),
|
|
1237
|
-
dueDate: z.string().nullable().optional().describe("Due date (YYYY-MM-DD) or null to clear"),
|
|
1238
|
-
labels: z.object({
|
|
1239
|
-
add: z.array(z.string()).optional(),
|
|
1240
|
-
remove: z.array(z.string()).optional(),
|
|
1241
|
-
set: z.array(z.string()).optional(),
|
|
1242
|
-
}).optional().describe("Label operations: add, remove, or set"),
|
|
1243
|
-
components: z.object({
|
|
1244
|
-
add: z.array(z.string()).optional(),
|
|
1245
|
-
remove: z.array(z.string()).optional(),
|
|
1246
|
-
set: z.array(z.string()).optional(),
|
|
1247
|
-
}).optional().describe("Component operations: add, remove, or set"),
|
|
1248
|
-
fixVersions: z.object({
|
|
1249
|
-
add: z.array(z.string()).optional(),
|
|
1250
|
-
remove: z.array(z.string()).optional(),
|
|
1251
|
-
set: z.array(z.string()).optional(),
|
|
1252
|
-
}).optional().describe("Fix version operations: add, remove, or set"),
|
|
1253
|
-
customFields: z.record(z.string(), z.unknown()).optional().describe("Custom field values"),
|
|
1254
|
-
notifyUsers: z.boolean().optional().default(true).describe("Send notifications to watchers"),
|
|
1255
|
-
}),
|
|
1256
|
-
}, async (params) => {
|
|
1257
|
-
try {
|
|
1258
|
-
const auth = await getAuthOrThrow();
|
|
1259
|
-
const client = createClient(auth);
|
|
1260
|
-
const payload = {};
|
|
1261
|
-
// Build fields object for direct field updates
|
|
1262
|
-
const fields = {};
|
|
1263
|
-
if (params.summary !== undefined) {
|
|
1264
|
-
fields.summary = params.summary;
|
|
1265
|
-
}
|
|
1266
|
-
if (params.description !== undefined) {
|
|
1267
|
-
fields.description = params.description ? textToAdf(params.description) : null;
|
|
1268
|
-
}
|
|
1269
|
-
if (params.assignee !== undefined) {
|
|
1270
|
-
fields.assignee = params.assignee === null ? null : { accountId: params.assignee };
|
|
1271
|
-
}
|
|
1272
|
-
if (params.priority !== undefined) {
|
|
1273
|
-
fields.priority = /^\d+$/.test(params.priority)
|
|
1274
|
-
? { id: params.priority }
|
|
1275
|
-
: { name: params.priority };
|
|
1276
|
-
}
|
|
1277
|
-
if (params.dueDate !== undefined) {
|
|
1278
|
-
fields.duedate = params.dueDate;
|
|
1279
|
-
}
|
|
1280
|
-
// Add custom fields
|
|
1281
|
-
if (params.customFields) {
|
|
1282
|
-
for (const [key, value] of Object.entries(params.customFields)) {
|
|
1283
|
-
const fieldKey = key.startsWith("customfield_") ? key : `customfield_${key}`;
|
|
1284
|
-
fields[fieldKey] = value;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
if (Object.keys(fields).length > 0) {
|
|
1288
|
-
payload.fields = fields;
|
|
1289
|
-
}
|
|
1290
|
-
// Build update operations for array fields
|
|
1291
|
-
const update = buildUpdateOperations({
|
|
1292
|
-
labels: params.labels,
|
|
1293
|
-
components: params.components,
|
|
1294
|
-
fixVersions: params.fixVersions,
|
|
1295
|
-
});
|
|
1296
|
-
if (Object.keys(update).length > 0) {
|
|
1297
|
-
payload.update = update;
|
|
1298
|
-
}
|
|
1299
|
-
if (Object.keys(payload).length === 0) {
|
|
1300
|
-
return textResult({
|
|
1301
|
-
error: "no_changes",
|
|
1302
|
-
message: "No fields provided to update",
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
await client.put(`/rest/api/3/issue/${encodeURIComponent(params.issueIdOrKey)}`, payload, {
|
|
1306
|
-
params: {
|
|
1307
|
-
notifyUsers: params.notifyUsers ?? true,
|
|
1308
|
-
},
|
|
1309
|
-
});
|
|
1310
|
-
return textResult({
|
|
1311
|
-
success: true,
|
|
1312
|
-
key: params.issueIdOrKey,
|
|
1313
|
-
message: `Issue ${params.issueIdOrKey} updated successfully`,
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
catch (error) {
|
|
1317
|
-
return textResult(errorToResult(error));
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
// ============ Phase 1.3: Delete Issue ============
|
|
1321
|
-
server.registerTool("jira_delete_issue", {
|
|
1322
|
-
title: "Delete Jira Issue",
|
|
1323
|
-
description: "Delete a Jira issue. Requires explicit confirmation. Use with caution - this action cannot be undone.",
|
|
1324
|
-
inputSchema: z.object({
|
|
1325
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID to delete"),
|
|
1326
|
-
deleteSubtasks: z.boolean().optional().default(false).describe("Also delete subtasks"),
|
|
1327
|
-
confirmDelete: z.boolean().describe("Must be true to confirm deletion"),
|
|
1328
|
-
}),
|
|
1329
|
-
}, async ({ issueIdOrKey, deleteSubtasks, confirmDelete }) => {
|
|
1330
|
-
try {
|
|
1331
|
-
if (!confirmDelete) {
|
|
1332
|
-
return textResult({
|
|
1333
|
-
error: "confirmation_required",
|
|
1334
|
-
message: "Deletion not confirmed. Set confirmDelete: true to proceed. This action cannot be undone.",
|
|
1335
|
-
issueKey: issueIdOrKey,
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
const auth = await getAuthOrThrow();
|
|
1339
|
-
const client = createClient(auth);
|
|
1340
|
-
await client.delete(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`, {
|
|
1341
|
-
params: {
|
|
1342
|
-
deleteSubtasks: deleteSubtasks ?? false,
|
|
1343
|
-
},
|
|
1344
|
-
});
|
|
1345
|
-
return textResult({
|
|
1346
|
-
success: true,
|
|
1347
|
-
message: `Issue ${issueIdOrKey} deleted successfully${deleteSubtasks ? " (including subtasks)" : ""}`,
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
|
-
catch (error) {
|
|
1351
|
-
return textResult(errorToResult(error));
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
// ============ Phase 1.4: Assign Issue ============
|
|
1355
|
-
server.registerTool("jira_assign_issue", {
|
|
1356
|
-
title: "Assign Jira Issue",
|
|
1357
|
-
description: "Assign or unassign a Jira issue to a user.",
|
|
1358
|
-
inputSchema: z.object({
|
|
1359
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
1360
|
-
accountId: z.string().nullable().describe("User account ID to assign, '-1' for automatic, or null to unassign"),
|
|
1361
|
-
}),
|
|
1362
|
-
}, async ({ issueIdOrKey, accountId }) => {
|
|
1363
|
-
try {
|
|
1364
|
-
const auth = await getAuthOrThrow();
|
|
1365
|
-
const client = createClient(auth);
|
|
1366
|
-
await client.put(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/assignee`, {
|
|
1367
|
-
accountId: accountId,
|
|
1368
|
-
});
|
|
1369
|
-
const action = accountId === null ? "unassigned" : "assigned";
|
|
1370
|
-
return textResult({
|
|
1371
|
-
success: true,
|
|
1372
|
-
key: issueIdOrKey,
|
|
1373
|
-
message: `Issue ${issueIdOrKey} ${action} successfully`,
|
|
1374
|
-
assignee: accountId,
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
catch (error) {
|
|
1378
|
-
return textResult(errorToResult(error));
|
|
1379
|
-
}
|
|
1380
|
-
});
|
|
1381
|
-
// ============ Phase 1.5: Get Transitions ============
|
|
1382
|
-
server.registerTool("jira_get_transitions", {
|
|
1383
|
-
title: "Get Issue Transitions",
|
|
1384
|
-
description: "Get available workflow transitions for an issue. Use before transitioning to see valid options.",
|
|
1385
|
-
inputSchema: z.object({
|
|
1386
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
1387
|
-
expand: z.string().optional().describe("Expand options: 'transitions.fields' to include required fields"),
|
|
1388
|
-
}),
|
|
1389
|
-
}, async ({ issueIdOrKey, expand }) => {
|
|
1390
|
-
try {
|
|
1391
|
-
const auth = await getAuthOrThrow();
|
|
1392
|
-
const client = createClient(auth);
|
|
1393
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/transitions`, {
|
|
1394
|
-
params: { expand },
|
|
1395
|
-
});
|
|
1396
|
-
const transitions = Array.isArray(response.data?.transitions)
|
|
1397
|
-
? response.data.transitions.map((t) => ({
|
|
1398
|
-
id: t.id,
|
|
1399
|
-
name: t.name,
|
|
1400
|
-
to: {
|
|
1401
|
-
id: t.to?.id,
|
|
1402
|
-
name: t.to?.name,
|
|
1403
|
-
statusCategory: t.to?.statusCategory?.name,
|
|
1404
|
-
},
|
|
1405
|
-
hasScreen: t.hasScreen ?? false,
|
|
1406
|
-
isGlobal: t.isGlobal ?? false,
|
|
1407
|
-
isInitial: t.isInitial ?? false,
|
|
1408
|
-
isConditional: t.isConditional ?? false,
|
|
1409
|
-
fields: t.fields ? Object.keys(t.fields) : [],
|
|
1410
|
-
}))
|
|
1411
|
-
: [];
|
|
1412
|
-
return textResult({
|
|
1413
|
-
issueKey: issueIdOrKey,
|
|
1414
|
-
transitions,
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
catch (error) {
|
|
1418
|
-
return textResult(errorToResult(error));
|
|
1419
|
-
}
|
|
1420
|
-
});
|
|
1421
|
-
// ============ Phase 1.6: Transition Issue ============
|
|
1422
|
-
server.registerTool("jira_transition_issue", {
|
|
1423
|
-
title: "Transition Jira Issue",
|
|
1424
|
-
description: "Move a Jira issue to a different status by executing a workflow transition.",
|
|
1425
|
-
inputSchema: z.object({
|
|
1426
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
1427
|
-
transitionId: z.string().min(1).describe("Transition ID (get from jira_get_transitions)"),
|
|
1428
|
-
comment: z.string().optional().describe("Comment to add during transition"),
|
|
1429
|
-
resolution: z.string().optional().describe("Resolution name for closing transitions (e.g., 'Done', 'Fixed')"),
|
|
1430
|
-
fields: z.record(z.string(), z.unknown()).optional().describe("Additional fields required by the transition"),
|
|
1431
|
-
}),
|
|
1432
|
-
}, async ({ issueIdOrKey, transitionId, comment, resolution, fields }) => {
|
|
1433
|
-
try {
|
|
1434
|
-
const auth = await getAuthOrThrow();
|
|
1435
|
-
const client = createClient(auth);
|
|
1436
|
-
const payload = {
|
|
1437
|
-
transition: { id: transitionId },
|
|
1438
|
-
};
|
|
1439
|
-
// Add fields if provided
|
|
1440
|
-
if (fields || resolution) {
|
|
1441
|
-
const transitionFields = { ...fields };
|
|
1442
|
-
if (resolution) {
|
|
1443
|
-
transitionFields.resolution = { name: resolution };
|
|
1444
|
-
}
|
|
1445
|
-
payload.fields = transitionFields;
|
|
1446
|
-
}
|
|
1447
|
-
// Add comment if provided
|
|
1448
|
-
if (comment) {
|
|
1449
|
-
payload.update = {
|
|
1450
|
-
comment: [
|
|
1451
|
-
{
|
|
1452
|
-
add: {
|
|
1453
|
-
body: textToAdf(comment),
|
|
1454
|
-
},
|
|
1455
|
-
},
|
|
1456
|
-
],
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
await client.post(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/transitions`, payload);
|
|
1460
|
-
return textResult({
|
|
1461
|
-
success: true,
|
|
1462
|
-
key: issueIdOrKey,
|
|
1463
|
-
transitionId,
|
|
1464
|
-
message: `Issue ${issueIdOrKey} transitioned successfully`,
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
catch (error) {
|
|
1468
|
-
return textResult(errorToResult(error));
|
|
1469
|
-
}
|
|
1470
|
-
});
|
|
1471
|
-
// ============ Phase 1.7: Helper Tools ============
|
|
1472
|
-
server.registerTool("jira_get_issue_types", {
|
|
1473
|
-
title: "Get Issue Types",
|
|
1474
|
-
description: "Get available issue types, optionally filtered by project.",
|
|
1475
|
-
inputSchema: z.object({
|
|
1476
|
-
projectKey: z.string().optional().describe("Filter issue types for a specific project"),
|
|
1477
|
-
}),
|
|
1478
|
-
}, async ({ projectKey }) => {
|
|
1479
|
-
try {
|
|
1480
|
-
const auth = await getAuthOrThrow();
|
|
1481
|
-
const client = createClient(auth);
|
|
1482
|
-
let issueTypes;
|
|
1483
|
-
if (projectKey) {
|
|
1484
|
-
// Get project-specific issue types
|
|
1485
|
-
const response = await client.get(`/rest/api/3/project/${encodeURIComponent(projectKey)}`);
|
|
1486
|
-
issueTypes = response.data?.issueTypes || [];
|
|
1487
|
-
}
|
|
1488
|
-
else {
|
|
1489
|
-
// Get all issue types
|
|
1490
|
-
const response = await client.get("/rest/api/3/issuetype");
|
|
1491
|
-
issueTypes = response.data || [];
|
|
1492
|
-
}
|
|
1493
|
-
return textResult(issueTypes.map((it) => ({
|
|
1494
|
-
id: it.id,
|
|
1495
|
-
name: it.name,
|
|
1496
|
-
description: it.description || "",
|
|
1497
|
-
subtask: it.subtask ?? false,
|
|
1498
|
-
hierarchyLevel: it.hierarchyLevel,
|
|
1499
|
-
})));
|
|
1500
|
-
}
|
|
1501
|
-
catch (error) {
|
|
1502
|
-
return textResult(errorToResult(error));
|
|
1503
|
-
}
|
|
1504
|
-
});
|
|
1505
|
-
server.registerTool("jira_get_priorities", {
|
|
1506
|
-
title: "Get Priorities",
|
|
1507
|
-
description: "Get available priority levels for issues.",
|
|
1508
|
-
inputSchema: z.object({}),
|
|
1509
|
-
}, async () => {
|
|
1510
|
-
try {
|
|
1511
|
-
const auth = await getAuthOrThrow();
|
|
1512
|
-
const client = createClient(auth);
|
|
1513
|
-
const response = await client.get("/rest/api/3/priority");
|
|
1514
|
-
return textResult((response.data || []).map((p) => ({
|
|
1515
|
-
id: p.id,
|
|
1516
|
-
name: p.name,
|
|
1517
|
-
description: p.description || "",
|
|
1518
|
-
iconUrl: p.iconUrl,
|
|
1519
|
-
})));
|
|
1520
|
-
}
|
|
1521
|
-
catch (error) {
|
|
1522
|
-
return textResult(errorToResult(error));
|
|
1523
|
-
}
|
|
1524
|
-
});
|
|
1525
|
-
server.registerTool("jira_get_statuses", {
|
|
1526
|
-
title: "Get Statuses",
|
|
1527
|
-
description: "Get available statuses, optionally filtered by project.",
|
|
1528
|
-
inputSchema: z.object({
|
|
1529
|
-
projectKey: z.string().optional().describe("Filter statuses for a specific project"),
|
|
1530
|
-
}),
|
|
1531
|
-
}, async ({ projectKey }) => {
|
|
1532
|
-
try {
|
|
1533
|
-
const auth = await getAuthOrThrow();
|
|
1534
|
-
const client = createClient(auth);
|
|
1535
|
-
if (projectKey) {
|
|
1536
|
-
// Get project-specific statuses
|
|
1537
|
-
const response = await client.get(`/rest/api/3/project/${encodeURIComponent(projectKey)}/statuses`);
|
|
1538
|
-
return textResult(response.data || []);
|
|
1539
|
-
}
|
|
1540
|
-
else {
|
|
1541
|
-
// Get all statuses
|
|
1542
|
-
const response = await client.get("/rest/api/3/status");
|
|
1543
|
-
return textResult((response.data || []).map((s) => ({
|
|
1544
|
-
id: s.id,
|
|
1545
|
-
name: s.name,
|
|
1546
|
-
description: s.description || "",
|
|
1547
|
-
statusCategory: s.statusCategory?.name,
|
|
1548
|
-
})));
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
catch (error) {
|
|
1552
|
-
return textResult(errorToResult(error));
|
|
1553
|
-
}
|
|
1554
|
-
});
|
|
1555
|
-
server.registerTool("jira_get_components", {
|
|
1556
|
-
title: "Get Project Components",
|
|
1557
|
-
description: "Get components for a specific project.",
|
|
1558
|
-
inputSchema: z.object({
|
|
1559
|
-
projectKey: z.string().min(1).describe("Project key"),
|
|
1560
|
-
}),
|
|
1561
|
-
}, async ({ projectKey }) => {
|
|
1562
|
-
try {
|
|
1563
|
-
const auth = await getAuthOrThrow();
|
|
1564
|
-
const client = createClient(auth);
|
|
1565
|
-
const response = await client.get(`/rest/api/3/project/${encodeURIComponent(projectKey)}/components`);
|
|
1566
|
-
return textResult((response.data || []).map((c) => ({
|
|
1567
|
-
id: c.id,
|
|
1568
|
-
name: c.name,
|
|
1569
|
-
description: c.description || "",
|
|
1570
|
-
lead: c.lead?.displayName,
|
|
1571
|
-
assigneeType: c.assigneeType,
|
|
1572
|
-
})));
|
|
1573
|
-
}
|
|
1574
|
-
catch (error) {
|
|
1575
|
-
return textResult(errorToResult(error));
|
|
1576
|
-
}
|
|
1577
|
-
});
|
|
1578
|
-
server.registerTool("jira_get_versions", {
|
|
1579
|
-
title: "Get Project Versions",
|
|
1580
|
-
description: "Get versions for a specific project.",
|
|
1581
|
-
inputSchema: z.object({
|
|
1582
|
-
projectKey: z.string().min(1).describe("Project key"),
|
|
1583
|
-
released: z.boolean().optional().describe("Filter by released status"),
|
|
1584
|
-
}),
|
|
1585
|
-
}, async ({ projectKey, released }) => {
|
|
1586
|
-
try {
|
|
1587
|
-
const auth = await getAuthOrThrow();
|
|
1588
|
-
const client = createClient(auth);
|
|
1589
|
-
const response = await client.get(`/rest/api/3/project/${encodeURIComponent(projectKey)}/versions`);
|
|
1590
|
-
let versions = response.data || [];
|
|
1591
|
-
if (released !== undefined) {
|
|
1592
|
-
versions = versions.filter((v) => v.released === released);
|
|
1593
|
-
}
|
|
1594
|
-
return textResult(versions.map((v) => ({
|
|
1595
|
-
id: v.id,
|
|
1596
|
-
name: v.name,
|
|
1597
|
-
description: v.description || "",
|
|
1598
|
-
released: v.released ?? false,
|
|
1599
|
-
archived: v.archived ?? false,
|
|
1600
|
-
releaseDate: v.releaseDate,
|
|
1601
|
-
startDate: v.startDate,
|
|
1602
|
-
})));
|
|
1603
|
-
}
|
|
1604
|
-
catch (error) {
|
|
1605
|
-
return textResult(errorToResult(error));
|
|
1606
|
-
}
|
|
1607
|
-
});
|
|
1608
|
-
server.registerTool("jira_search_users", {
|
|
1609
|
-
title: "Search Jira Users",
|
|
1610
|
-
description: "Search for Jira users by name, email, or username.",
|
|
1611
|
-
inputSchema: z.object({
|
|
1612
|
-
query: z.string().min(1).describe("Search query (name, email, or username)"),
|
|
1613
|
-
projectKey: z.string().optional().describe("Filter users with access to this project"),
|
|
1614
|
-
maxResults: z.number().int().positive().max(50).optional().default(10),
|
|
1615
|
-
}),
|
|
1616
|
-
}, async ({ query, projectKey, maxResults }) => {
|
|
1617
|
-
try {
|
|
1618
|
-
const auth = await getAuthOrThrow();
|
|
1619
|
-
const client = createClient(auth);
|
|
1620
|
-
const response = await client.get("/rest/api/3/user/search", {
|
|
1621
|
-
params: {
|
|
1622
|
-
query,
|
|
1623
|
-
maxResults: maxResults ?? 10,
|
|
1624
|
-
},
|
|
1625
|
-
});
|
|
1626
|
-
let users = response.data || [];
|
|
1627
|
-
// If projectKey provided, filter by assignable users (secondary call)
|
|
1628
|
-
if (projectKey && users.length > 0) {
|
|
1629
|
-
try {
|
|
1630
|
-
const assignableResponse = await client.get("/rest/api/3/user/assignable/search", {
|
|
1631
|
-
params: {
|
|
1632
|
-
query,
|
|
1633
|
-
project: projectKey,
|
|
1634
|
-
maxResults: maxResults ?? 10,
|
|
1635
|
-
},
|
|
1636
|
-
});
|
|
1637
|
-
users = assignableResponse.data || [];
|
|
1638
|
-
}
|
|
1639
|
-
catch {
|
|
1640
|
-
// Fall back to original search if assignable search fails
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
return textResult(users.map((u) => ({
|
|
1644
|
-
accountId: u.accountId,
|
|
1645
|
-
displayName: u.displayName,
|
|
1646
|
-
emailAddress: u.emailAddress,
|
|
1647
|
-
active: u.active ?? true,
|
|
1648
|
-
avatarUrl: u.avatarUrls?.["48x48"],
|
|
1649
|
-
})));
|
|
1650
|
-
}
|
|
1651
|
-
catch (error) {
|
|
1652
|
-
return textResult(errorToResult(error));
|
|
1653
|
-
}
|
|
1654
|
-
});
|
|
1655
|
-
server.registerTool("jira_get_changelog", {
|
|
1656
|
-
title: "Get Issue Changelog",
|
|
1657
|
-
description: "Get the history of changes for an issue.",
|
|
1658
|
-
inputSchema: z.object({
|
|
1659
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
1660
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
1661
|
-
maxResults: z.number().int().positive().max(100).optional().default(20),
|
|
1662
|
-
}),
|
|
1663
|
-
}, async ({ issueIdOrKey, startAt, maxResults }) => {
|
|
1664
|
-
try {
|
|
1665
|
-
const auth = await getAuthOrThrow();
|
|
1666
|
-
const client = createClient(auth);
|
|
1667
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/changelog`, {
|
|
1668
|
-
params: {
|
|
1669
|
-
startAt,
|
|
1670
|
-
maxResults: maxResults ?? 20,
|
|
1671
|
-
},
|
|
1672
|
-
});
|
|
1673
|
-
const changes = Array.isArray(response.data?.values)
|
|
1674
|
-
? response.data.values.map((change) => ({
|
|
1675
|
-
id: change.id,
|
|
1676
|
-
author: change.author?.displayName || change.author?.emailAddress || "",
|
|
1677
|
-
created: change.created,
|
|
1678
|
-
items: (change.items || []).map((item) => ({
|
|
1679
|
-
field: item.field,
|
|
1680
|
-
fieldtype: item.fieldtype,
|
|
1681
|
-
from: item.fromString || item.from,
|
|
1682
|
-
to: item.toString || item.to,
|
|
1683
|
-
})),
|
|
1684
|
-
}))
|
|
1685
|
-
: [];
|
|
1686
|
-
return textResult({
|
|
1687
|
-
total: response.data?.total ?? changes.length,
|
|
1688
|
-
startAt: response.data?.startAt ?? 0,
|
|
1689
|
-
changes,
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
catch (error) {
|
|
1693
|
-
return textResult(errorToResult(error));
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
// ============ Phase 2: Agile Tools ============
|
|
1697
|
-
server.registerTool("jira_get_boards", {
|
|
1698
|
-
title: "Get Jira Boards",
|
|
1699
|
-
description: "Get all Scrum and Kanban boards, optionally filtered by project or type.",
|
|
1700
|
-
inputSchema: z.object({
|
|
1701
|
-
projectKeyOrId: z.string().optional().describe("Filter boards by project"),
|
|
1702
|
-
type: z.enum(["scrum", "kanban", "simple"]).optional().describe("Filter by board type"),
|
|
1703
|
-
name: z.string().optional().describe("Filter boards by name (contains)"),
|
|
1704
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
1705
|
-
maxResults: z.number().int().positive().max(50).optional().default(50),
|
|
1706
|
-
}),
|
|
1707
|
-
}, async ({ projectKeyOrId, type, name, startAt, maxResults }) => {
|
|
1708
|
-
try {
|
|
1709
|
-
const auth = await getAuthOrThrow();
|
|
1710
|
-
const client = createClient(auth);
|
|
1711
|
-
const response = await client.get("/rest/agile/1.0/board", {
|
|
1712
|
-
params: {
|
|
1713
|
-
projectKeyOrId,
|
|
1714
|
-
type,
|
|
1715
|
-
name,
|
|
1716
|
-
startAt,
|
|
1717
|
-
maxResults: maxResults ?? 50,
|
|
1718
|
-
},
|
|
1719
|
-
});
|
|
1720
|
-
const boards = Array.isArray(response.data?.values)
|
|
1721
|
-
? response.data.values.map((b) => ({
|
|
1722
|
-
id: b.id,
|
|
1723
|
-
name: b.name,
|
|
1724
|
-
type: b.type,
|
|
1725
|
-
projectKey: b.location?.projectKey,
|
|
1726
|
-
projectName: b.location?.displayName,
|
|
1727
|
-
}))
|
|
1728
|
-
: [];
|
|
1729
|
-
return textResult({
|
|
1730
|
-
total: response.data?.total ?? boards.length,
|
|
1731
|
-
startAt: response.data?.startAt ?? 0,
|
|
1732
|
-
boards,
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
catch (error) {
|
|
1736
|
-
return textResult(errorToResult(error));
|
|
1737
|
-
}
|
|
1738
|
-
});
|
|
1739
|
-
server.registerTool("jira_get_board", {
|
|
1740
|
-
title: "Get Board Details",
|
|
1741
|
-
description: "Get details of a specific board including configuration.",
|
|
1742
|
-
inputSchema: z.object({
|
|
1743
|
-
boardId: z.number().int().positive().describe("Board ID"),
|
|
1744
|
-
}),
|
|
1745
|
-
}, async ({ boardId }) => {
|
|
1746
|
-
try {
|
|
1747
|
-
const auth = await getAuthOrThrow();
|
|
1748
|
-
const client = createClient(auth);
|
|
1749
|
-
const response = await client.get(`/rest/agile/1.0/board/${boardId}`);
|
|
1750
|
-
return textResult({
|
|
1751
|
-
id: response.data?.id,
|
|
1752
|
-
name: response.data?.name,
|
|
1753
|
-
type: response.data?.type,
|
|
1754
|
-
self: response.data?.self,
|
|
1755
|
-
location: response.data?.location,
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
catch (error) {
|
|
1759
|
-
return textResult(errorToResult(error));
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
server.registerTool("jira_get_board_configuration", {
|
|
1763
|
-
title: "Get Board Configuration",
|
|
1764
|
-
description: "Get the configuration of a board including columns, estimation, and ranking.",
|
|
1765
|
-
inputSchema: z.object({
|
|
1766
|
-
boardId: z.number().int().positive().describe("Board ID"),
|
|
1767
|
-
}),
|
|
1768
|
-
}, async ({ boardId }) => {
|
|
1769
|
-
try {
|
|
1770
|
-
const auth = await getAuthOrThrow();
|
|
1771
|
-
const client = createClient(auth);
|
|
1772
|
-
const response = await client.get(`/rest/agile/1.0/board/${boardId}/configuration`);
|
|
1773
|
-
return textResult({
|
|
1774
|
-
id: response.data?.id,
|
|
1775
|
-
name: response.data?.name,
|
|
1776
|
-
type: response.data?.type,
|
|
1777
|
-
filter: response.data?.filter,
|
|
1778
|
-
columnConfig: response.data?.columnConfig,
|
|
1779
|
-
estimation: response.data?.estimation,
|
|
1780
|
-
ranking: response.data?.ranking,
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
catch (error) {
|
|
1784
|
-
return textResult(errorToResult(error));
|
|
1785
|
-
}
|
|
1786
|
-
});
|
|
1787
|
-
server.registerTool("jira_get_sprints", {
|
|
1788
|
-
title: "Get Sprints",
|
|
1789
|
-
description: "Get sprints for a board, optionally filtered by state.",
|
|
1790
|
-
inputSchema: z.object({
|
|
1791
|
-
boardId: z.number().int().positive().describe("Board ID"),
|
|
1792
|
-
state: z.enum(["future", "active", "closed"]).optional().describe("Filter by sprint state"),
|
|
1793
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
1794
|
-
maxResults: z.number().int().positive().max(50).optional().default(50),
|
|
1795
|
-
}),
|
|
1796
|
-
}, async ({ boardId, state, startAt, maxResults }) => {
|
|
1797
|
-
try {
|
|
1798
|
-
const auth = await getAuthOrThrow();
|
|
1799
|
-
const client = createClient(auth);
|
|
1800
|
-
const response = await client.get(`/rest/agile/1.0/board/${boardId}/sprint`, {
|
|
1801
|
-
params: {
|
|
1802
|
-
state,
|
|
1803
|
-
startAt,
|
|
1804
|
-
maxResults: maxResults ?? 50,
|
|
1805
|
-
},
|
|
1806
|
-
});
|
|
1807
|
-
const sprints = Array.isArray(response.data?.values)
|
|
1808
|
-
? response.data.values.map((s) => ({
|
|
1809
|
-
id: s.id,
|
|
1810
|
-
name: s.name,
|
|
1811
|
-
state: s.state,
|
|
1812
|
-
startDate: s.startDate,
|
|
1813
|
-
endDate: s.endDate,
|
|
1814
|
-
completeDate: s.completeDate,
|
|
1815
|
-
originBoardId: s.originBoardId,
|
|
1816
|
-
goal: s.goal,
|
|
1817
|
-
}))
|
|
1818
|
-
: [];
|
|
1819
|
-
return textResult({
|
|
1820
|
-
total: response.data?.total ?? sprints.length,
|
|
1821
|
-
startAt: response.data?.startAt ?? 0,
|
|
1822
|
-
sprints,
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
catch (error) {
|
|
1826
|
-
return textResult(errorToResult(error));
|
|
1827
|
-
}
|
|
1828
|
-
});
|
|
1829
|
-
server.registerTool("jira_get_sprint", {
|
|
1830
|
-
title: "Get Sprint Details",
|
|
1831
|
-
description: "Get details of a specific sprint.",
|
|
1832
|
-
inputSchema: z.object({
|
|
1833
|
-
sprintId: z.number().int().positive().describe("Sprint ID"),
|
|
1834
|
-
}),
|
|
1835
|
-
}, async ({ sprintId }) => {
|
|
1836
|
-
try {
|
|
1837
|
-
const auth = await getAuthOrThrow();
|
|
1838
|
-
const client = createClient(auth);
|
|
1839
|
-
const response = await client.get(`/rest/agile/1.0/sprint/${sprintId}`);
|
|
1840
|
-
return textResult({
|
|
1841
|
-
id: response.data?.id,
|
|
1842
|
-
name: response.data?.name,
|
|
1843
|
-
state: response.data?.state,
|
|
1844
|
-
startDate: response.data?.startDate,
|
|
1845
|
-
endDate: response.data?.endDate,
|
|
1846
|
-
completeDate: response.data?.completeDate,
|
|
1847
|
-
originBoardId: response.data?.originBoardId,
|
|
1848
|
-
goal: response.data?.goal,
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
catch (error) {
|
|
1852
|
-
return textResult(errorToResult(error));
|
|
1853
|
-
}
|
|
1854
|
-
});
|
|
1855
|
-
server.registerTool("jira_create_sprint", {
|
|
1856
|
-
title: "Create Sprint",
|
|
1857
|
-
description: "Create a new sprint on a board.",
|
|
1858
|
-
inputSchema: z.object({
|
|
1859
|
-
boardId: z.number().int().positive().describe("Board ID"),
|
|
1860
|
-
name: z.string().min(1).describe("Sprint name"),
|
|
1861
|
-
startDate: z.string().optional().describe("Start date (ISO 8601)"),
|
|
1862
|
-
endDate: z.string().optional().describe("End date (ISO 8601)"),
|
|
1863
|
-
goal: z.string().optional().describe("Sprint goal"),
|
|
1864
|
-
}),
|
|
1865
|
-
}, async ({ boardId, name, startDate, endDate, goal }) => {
|
|
1866
|
-
try {
|
|
1867
|
-
const auth = await getAuthOrThrow();
|
|
1868
|
-
const client = createClient(auth);
|
|
1869
|
-
const response = await client.post("/rest/agile/1.0/sprint", {
|
|
1870
|
-
originBoardId: boardId,
|
|
1871
|
-
name,
|
|
1872
|
-
startDate,
|
|
1873
|
-
endDate,
|
|
1874
|
-
goal,
|
|
1875
|
-
});
|
|
1876
|
-
return textResult({
|
|
1877
|
-
success: true,
|
|
1878
|
-
id: response.data?.id,
|
|
1879
|
-
name: response.data?.name,
|
|
1880
|
-
state: response.data?.state,
|
|
1881
|
-
message: `Sprint "${name}" created successfully`,
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
catch (error) {
|
|
1885
|
-
return textResult(errorToResult(error));
|
|
1886
|
-
}
|
|
1887
|
-
});
|
|
1888
|
-
server.registerTool("jira_update_sprint", {
|
|
1889
|
-
title: "Update Sprint",
|
|
1890
|
-
description: "Update sprint details including name, dates, and goal.",
|
|
1891
|
-
inputSchema: z.object({
|
|
1892
|
-
sprintId: z.number().int().positive().describe("Sprint ID"),
|
|
1893
|
-
name: z.string().optional().describe("New sprint name"),
|
|
1894
|
-
state: z.enum(["future", "active", "closed"]).optional().describe("Sprint state"),
|
|
1895
|
-
startDate: z.string().optional().describe("Start date (ISO 8601)"),
|
|
1896
|
-
endDate: z.string().optional().describe("End date (ISO 8601)"),
|
|
1897
|
-
goal: z.string().optional().describe("Sprint goal"),
|
|
1898
|
-
}),
|
|
1899
|
-
}, async ({ sprintId, name, state, startDate, endDate, goal }) => {
|
|
1900
|
-
try {
|
|
1901
|
-
const auth = await getAuthOrThrow();
|
|
1902
|
-
const client = createClient(auth);
|
|
1903
|
-
const payload = {};
|
|
1904
|
-
if (name !== undefined)
|
|
1905
|
-
payload.name = name;
|
|
1906
|
-
if (state !== undefined)
|
|
1907
|
-
payload.state = state;
|
|
1908
|
-
if (startDate !== undefined)
|
|
1909
|
-
payload.startDate = startDate;
|
|
1910
|
-
if (endDate !== undefined)
|
|
1911
|
-
payload.endDate = endDate;
|
|
1912
|
-
if (goal !== undefined)
|
|
1913
|
-
payload.goal = goal;
|
|
1914
|
-
if (Object.keys(payload).length === 0) {
|
|
1915
|
-
return textResult({
|
|
1916
|
-
error: "no_changes",
|
|
1917
|
-
message: "No fields provided to update",
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
const response = await client.put(`/rest/agile/1.0/sprint/${sprintId}`, payload);
|
|
1921
|
-
return textResult({
|
|
1922
|
-
success: true,
|
|
1923
|
-
id: response.data?.id ?? sprintId,
|
|
1924
|
-
name: response.data?.name,
|
|
1925
|
-
state: response.data?.state,
|
|
1926
|
-
message: `Sprint updated successfully`,
|
|
1927
|
-
});
|
|
1928
|
-
}
|
|
1929
|
-
catch (error) {
|
|
1930
|
-
return textResult(errorToResult(error));
|
|
1931
|
-
}
|
|
1932
|
-
});
|
|
1933
|
-
server.registerTool("jira_start_sprint", {
|
|
1934
|
-
title: "Start Sprint",
|
|
1935
|
-
description: "Start a sprint that is in 'future' state.",
|
|
1936
|
-
inputSchema: z.object({
|
|
1937
|
-
sprintId: z.number().int().positive().describe("Sprint ID"),
|
|
1938
|
-
startDate: z.string().optional().describe("Start date (defaults to now)"),
|
|
1939
|
-
endDate: z.string().describe("End date (required for starting a sprint)"),
|
|
1940
|
-
}),
|
|
1941
|
-
}, async ({ sprintId, startDate, endDate }) => {
|
|
1942
|
-
try {
|
|
1943
|
-
const auth = await getAuthOrThrow();
|
|
1944
|
-
const client = createClient(auth);
|
|
1945
|
-
const response = await client.post(`/rest/agile/1.0/sprint/${sprintId}`, {
|
|
1946
|
-
state: "active",
|
|
1947
|
-
startDate: startDate || new Date().toISOString(),
|
|
1948
|
-
endDate,
|
|
1949
|
-
});
|
|
1950
|
-
return textResult({
|
|
1951
|
-
success: true,
|
|
1952
|
-
id: response.data?.id ?? sprintId,
|
|
1953
|
-
state: "active",
|
|
1954
|
-
message: `Sprint started successfully`,
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
catch (error) {
|
|
1958
|
-
return textResult(errorToResult(error));
|
|
1959
|
-
}
|
|
1960
|
-
});
|
|
1961
|
-
server.registerTool("jira_complete_sprint", {
|
|
1962
|
-
title: "Complete Sprint",
|
|
1963
|
-
description: "Complete an active sprint. Optionally move incomplete issues to another sprint or backlog.",
|
|
1964
|
-
inputSchema: z.object({
|
|
1965
|
-
sprintId: z.number().int().positive().describe("Sprint ID to complete"),
|
|
1966
|
-
moveIncompleteIssuesTo: z.number().int().positive().optional().describe("Sprint ID to move incomplete issues to (omit to move to backlog)"),
|
|
1967
|
-
}),
|
|
1968
|
-
}, async ({ sprintId, moveIncompleteIssuesTo }) => {
|
|
1969
|
-
try {
|
|
1970
|
-
const auth = await getAuthOrThrow();
|
|
1971
|
-
const client = createClient(auth);
|
|
1972
|
-
// Complete the sprint
|
|
1973
|
-
await client.post(`/rest/agile/1.0/sprint/${sprintId}`, {
|
|
1974
|
-
state: "closed",
|
|
1975
|
-
});
|
|
1976
|
-
return textResult({
|
|
1977
|
-
success: true,
|
|
1978
|
-
id: sprintId,
|
|
1979
|
-
state: "closed",
|
|
1980
|
-
message: `Sprint completed successfully`,
|
|
1981
|
-
incompleteIssuesMovedTo: moveIncompleteIssuesTo || "backlog",
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
catch (error) {
|
|
1985
|
-
return textResult(errorToResult(error));
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
server.registerTool("jira_delete_sprint", {
|
|
1989
|
-
title: "Delete Sprint",
|
|
1990
|
-
description: "Delete a sprint. Use with caution - cannot be undone.",
|
|
1991
|
-
inputSchema: z.object({
|
|
1992
|
-
sprintId: z.number().int().positive().describe("Sprint ID to delete"),
|
|
1993
|
-
confirmDelete: z.boolean().describe("Must be true to confirm deletion"),
|
|
1994
|
-
}),
|
|
1995
|
-
}, async ({ sprintId, confirmDelete }) => {
|
|
1996
|
-
try {
|
|
1997
|
-
if (!confirmDelete) {
|
|
1998
|
-
return textResult({
|
|
1999
|
-
error: "confirmation_required",
|
|
2000
|
-
message: "Deletion not confirmed. Set confirmDelete: true to proceed.",
|
|
2001
|
-
sprintId,
|
|
2002
|
-
});
|
|
2003
|
-
}
|
|
2004
|
-
const auth = await getAuthOrThrow();
|
|
2005
|
-
const client = createClient(auth);
|
|
2006
|
-
await client.delete(`/rest/agile/1.0/sprint/${sprintId}`);
|
|
2007
|
-
return textResult({
|
|
2008
|
-
success: true,
|
|
2009
|
-
message: `Sprint ${sprintId} deleted successfully`,
|
|
2010
|
-
});
|
|
2011
|
-
}
|
|
2012
|
-
catch (error) {
|
|
2013
|
-
return textResult(errorToResult(error));
|
|
2014
|
-
}
|
|
2015
|
-
});
|
|
2016
|
-
server.registerTool("jira_get_sprint_issues", {
|
|
2017
|
-
title: "Get Sprint Issues",
|
|
2018
|
-
description: "Get all issues in a sprint.",
|
|
2019
|
-
inputSchema: z.object({
|
|
2020
|
-
sprintId: z.number().int().positive().describe("Sprint ID"),
|
|
2021
|
-
jql: z.string().optional().describe("Additional JQL filter"),
|
|
2022
|
-
fields: z.array(z.string()).optional().describe("Fields to return"),
|
|
2023
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
2024
|
-
maxResults: z.number().int().positive().max(100).optional().default(50),
|
|
2025
|
-
}),
|
|
2026
|
-
}, async ({ sprintId, jql, fields, startAt, maxResults }) => {
|
|
2027
|
-
try {
|
|
2028
|
-
const auth = await getAuthOrThrow();
|
|
2029
|
-
const client = createClient(auth);
|
|
2030
|
-
const response = await client.get(`/rest/agile/1.0/sprint/${sprintId}/issue`, {
|
|
2031
|
-
params: {
|
|
2032
|
-
jql,
|
|
2033
|
-
fields: fields?.join(","),
|
|
2034
|
-
startAt,
|
|
2035
|
-
maxResults: maxResults ?? 50,
|
|
2036
|
-
},
|
|
2037
|
-
});
|
|
2038
|
-
const issues = Array.isArray(response.data?.issues)
|
|
2039
|
-
? response.data.issues.map((issue) => ({
|
|
2040
|
-
key: issue.key,
|
|
2041
|
-
summary: issue.fields?.summary,
|
|
2042
|
-
status: issue.fields?.status?.name,
|
|
2043
|
-
assignee: issue.fields?.assignee?.displayName,
|
|
2044
|
-
issueType: issue.fields?.issuetype?.name,
|
|
2045
|
-
priority: issue.fields?.priority?.name,
|
|
2046
|
-
storyPoints: issue.fields?.customfield_10016,
|
|
2047
|
-
}))
|
|
2048
|
-
: [];
|
|
2049
|
-
return textResult({
|
|
2050
|
-
total: response.data?.total ?? issues.length,
|
|
2051
|
-
startAt: response.data?.startAt ?? 0,
|
|
2052
|
-
sprintId,
|
|
2053
|
-
issues,
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
catch (error) {
|
|
2057
|
-
return textResult(errorToResult(error));
|
|
2058
|
-
}
|
|
2059
|
-
});
|
|
2060
|
-
server.registerTool("jira_move_issues_to_sprint", {
|
|
2061
|
-
title: "Move Issues to Sprint",
|
|
2062
|
-
description: "Move issues to a sprint.",
|
|
2063
|
-
inputSchema: z.object({
|
|
2064
|
-
sprintId: z.number().int().positive().describe("Target sprint ID"),
|
|
2065
|
-
issueKeys: z.array(z.string().min(1)).min(1).describe("Issue keys to move"),
|
|
2066
|
-
}),
|
|
2067
|
-
}, async ({ sprintId, issueKeys }) => {
|
|
2068
|
-
try {
|
|
2069
|
-
const auth = await getAuthOrThrow();
|
|
2070
|
-
const client = createClient(auth);
|
|
2071
|
-
await client.post(`/rest/agile/1.0/sprint/${sprintId}/issue`, {
|
|
2072
|
-
issues: issueKeys,
|
|
2073
|
-
});
|
|
2074
|
-
return textResult({
|
|
2075
|
-
success: true,
|
|
2076
|
-
sprintId,
|
|
2077
|
-
issuesMoved: issueKeys,
|
|
2078
|
-
message: `${issueKeys.length} issue(s) moved to sprint ${sprintId}`,
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
catch (error) {
|
|
2082
|
-
return textResult(errorToResult(error));
|
|
2083
|
-
}
|
|
2084
|
-
});
|
|
2085
|
-
server.registerTool("jira_get_backlog_issues", {
|
|
2086
|
-
title: "Get Backlog Issues",
|
|
2087
|
-
description: "Get issues in the backlog (not in any active sprint) for a board.",
|
|
2088
|
-
inputSchema: z.object({
|
|
2089
|
-
boardId: z.number().int().positive().describe("Board ID"),
|
|
2090
|
-
jql: z.string().optional().describe("Additional JQL filter"),
|
|
2091
|
-
fields: z.array(z.string()).optional().describe("Fields to return"),
|
|
2092
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
2093
|
-
maxResults: z.number().int().positive().max(100).optional().default(50),
|
|
2094
|
-
}),
|
|
2095
|
-
}, async ({ boardId, jql, fields, startAt, maxResults }) => {
|
|
2096
|
-
try {
|
|
2097
|
-
const auth = await getAuthOrThrow();
|
|
2098
|
-
const client = createClient(auth);
|
|
2099
|
-
const response = await client.get(`/rest/agile/1.0/board/${boardId}/backlog`, {
|
|
2100
|
-
params: {
|
|
2101
|
-
jql,
|
|
2102
|
-
fields: fields?.join(","),
|
|
2103
|
-
startAt,
|
|
2104
|
-
maxResults: maxResults ?? 50,
|
|
2105
|
-
},
|
|
2106
|
-
});
|
|
2107
|
-
const issues = Array.isArray(response.data?.issues)
|
|
2108
|
-
? response.data.issues.map((issue) => ({
|
|
2109
|
-
key: issue.key,
|
|
2110
|
-
summary: issue.fields?.summary,
|
|
2111
|
-
status: issue.fields?.status?.name,
|
|
2112
|
-
assignee: issue.fields?.assignee?.displayName,
|
|
2113
|
-
issueType: issue.fields?.issuetype?.name,
|
|
2114
|
-
priority: issue.fields?.priority?.name,
|
|
2115
|
-
}))
|
|
2116
|
-
: [];
|
|
2117
|
-
return textResult({
|
|
2118
|
-
total: response.data?.total ?? issues.length,
|
|
2119
|
-
startAt: response.data?.startAt ?? 0,
|
|
2120
|
-
boardId,
|
|
2121
|
-
issues,
|
|
2122
|
-
});
|
|
2123
|
-
}
|
|
2124
|
-
catch (error) {
|
|
2125
|
-
return textResult(errorToResult(error));
|
|
2126
|
-
}
|
|
2127
|
-
});
|
|
2128
|
-
server.registerTool("jira_move_issues_to_backlog", {
|
|
2129
|
-
title: "Move Issues to Backlog",
|
|
2130
|
-
description: "Move issues from a sprint back to the backlog.",
|
|
2131
|
-
inputSchema: z.object({
|
|
2132
|
-
issueKeys: z.array(z.string().min(1)).min(1).describe("Issue keys to move to backlog"),
|
|
2133
|
-
}),
|
|
2134
|
-
}, async ({ issueKeys }) => {
|
|
2135
|
-
try {
|
|
2136
|
-
const auth = await getAuthOrThrow();
|
|
2137
|
-
const client = createClient(auth);
|
|
2138
|
-
await client.post("/rest/agile/1.0/backlog/issue", {
|
|
2139
|
-
issues: issueKeys,
|
|
2140
|
-
});
|
|
2141
|
-
return textResult({
|
|
2142
|
-
success: true,
|
|
2143
|
-
issuesMoved: issueKeys,
|
|
2144
|
-
message: `${issueKeys.length} issue(s) moved to backlog`,
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
catch (error) {
|
|
2148
|
-
return textResult(errorToResult(error));
|
|
2149
|
-
}
|
|
2150
|
-
});
|
|
2151
|
-
server.registerTool("jira_rank_issues", {
|
|
2152
|
-
title: "Rank Issues",
|
|
2153
|
-
description: "Change the rank of issues on a board by placing them before or after another issue.",
|
|
2154
|
-
inputSchema: z.object({
|
|
2155
|
-
issueKeys: z.array(z.string().min(1)).min(1).describe("Issue keys to rank"),
|
|
2156
|
-
rankBeforeIssue: z.string().optional().describe("Issue key to rank before"),
|
|
2157
|
-
rankAfterIssue: z.string().optional().describe("Issue key to rank after"),
|
|
2158
|
-
}),
|
|
2159
|
-
}, async ({ issueKeys, rankBeforeIssue, rankAfterIssue }) => {
|
|
2160
|
-
try {
|
|
2161
|
-
if (!rankBeforeIssue && !rankAfterIssue) {
|
|
2162
|
-
return textResult({
|
|
2163
|
-
error: "invalid_parameters",
|
|
2164
|
-
message: "Either rankBeforeIssue or rankAfterIssue must be provided",
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
const auth = await getAuthOrThrow();
|
|
2168
|
-
const client = createClient(auth);
|
|
2169
|
-
const payload = {
|
|
2170
|
-
issues: issueKeys,
|
|
2171
|
-
};
|
|
2172
|
-
if (rankBeforeIssue) {
|
|
2173
|
-
payload.rankBeforeIssue = rankBeforeIssue;
|
|
2174
|
-
}
|
|
2175
|
-
else if (rankAfterIssue) {
|
|
2176
|
-
payload.rankAfterIssue = rankAfterIssue;
|
|
2177
|
-
}
|
|
2178
|
-
await client.put("/rest/agile/1.0/issue/rank", payload);
|
|
2179
|
-
return textResult({
|
|
2180
|
-
success: true,
|
|
2181
|
-
issuesRanked: issueKeys,
|
|
2182
|
-
message: `${issueKeys.length} issue(s) ranked successfully`,
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
catch (error) {
|
|
2186
|
-
return textResult(errorToResult(error));
|
|
2187
|
-
}
|
|
2188
|
-
});
|
|
2189
|
-
// ============ Phase 3: Issue Relationships ============
|
|
2190
|
-
server.registerTool("jira_get_issue_links", {
|
|
2191
|
-
title: "Get Issue Links",
|
|
2192
|
-
description: "Get all linked issues for a specific issue.",
|
|
2193
|
-
inputSchema: z.object({
|
|
2194
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2195
|
-
}),
|
|
2196
|
-
}, async ({ issueIdOrKey }) => {
|
|
2197
|
-
try {
|
|
2198
|
-
const auth = await getAuthOrThrow();
|
|
2199
|
-
const client = createClient(auth);
|
|
2200
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`, {
|
|
2201
|
-
params: {
|
|
2202
|
-
fields: "issuelinks",
|
|
2203
|
-
},
|
|
2204
|
-
});
|
|
2205
|
-
const links = Array.isArray(response.data?.fields?.issuelinks)
|
|
2206
|
-
? response.data.fields.issuelinks.map((link) => {
|
|
2207
|
-
const isInward = !!link.inwardIssue;
|
|
2208
|
-
const linkedIssue = isInward ? link.inwardIssue : link.outwardIssue;
|
|
2209
|
-
return {
|
|
2210
|
-
id: link.id,
|
|
2211
|
-
type: link.type?.name,
|
|
2212
|
-
direction: isInward ? "inward" : "outward",
|
|
2213
|
-
description: isInward
|
|
2214
|
-
? link.type?.inward
|
|
2215
|
-
: link.type?.outward,
|
|
2216
|
-
linkedIssue: {
|
|
2217
|
-
key: linkedIssue?.key,
|
|
2218
|
-
summary: linkedIssue?.fields?.summary,
|
|
2219
|
-
status: linkedIssue?.fields?.status?.name,
|
|
2220
|
-
issueType: linkedIssue?.fields?.issuetype?.name,
|
|
2221
|
-
},
|
|
2222
|
-
};
|
|
2223
|
-
})
|
|
2224
|
-
: [];
|
|
2225
|
-
return textResult({
|
|
2226
|
-
issueKey: issueIdOrKey,
|
|
2227
|
-
links,
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
|
-
catch (error) {
|
|
2231
|
-
return textResult(errorToResult(error));
|
|
2232
|
-
}
|
|
2233
|
-
});
|
|
2234
|
-
server.registerTool("jira_create_issue_link", {
|
|
2235
|
-
title: "Link Issues",
|
|
2236
|
-
description: "Create a link between two issues.",
|
|
2237
|
-
inputSchema: z.object({
|
|
2238
|
-
inwardIssue: z.string().min(1).describe("Inward issue key (the 'from' issue)"),
|
|
2239
|
-
outwardIssue: z.string().min(1).describe("Outward issue key (the 'to' issue)"),
|
|
2240
|
-
linkType: z.string().min(1).describe("Link type name (e.g., 'Blocks', 'Relates', 'Duplicates')"),
|
|
2241
|
-
comment: z.string().optional().describe("Comment to add with the link"),
|
|
2242
|
-
}),
|
|
2243
|
-
}, async ({ inwardIssue, outwardIssue, linkType, comment }) => {
|
|
2244
|
-
try {
|
|
2245
|
-
const auth = await getAuthOrThrow();
|
|
2246
|
-
const client = createClient(auth);
|
|
2247
|
-
const payload = {
|
|
2248
|
-
type: { name: linkType },
|
|
2249
|
-
inwardIssue: { key: inwardIssue },
|
|
2250
|
-
outwardIssue: { key: outwardIssue },
|
|
2251
|
-
};
|
|
2252
|
-
if (comment) {
|
|
2253
|
-
payload.comment = {
|
|
2254
|
-
body: textToAdf(comment),
|
|
2255
|
-
};
|
|
2256
|
-
}
|
|
2257
|
-
await client.post("/rest/api/3/issueLink", payload);
|
|
2258
|
-
return textResult({
|
|
2259
|
-
success: true,
|
|
2260
|
-
message: `Link created: ${inwardIssue} ${linkType} ${outwardIssue}`,
|
|
2261
|
-
inwardIssue,
|
|
2262
|
-
outwardIssue,
|
|
2263
|
-
linkType,
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
catch (error) {
|
|
2267
|
-
return textResult(errorToResult(error));
|
|
2268
|
-
}
|
|
2269
|
-
});
|
|
2270
|
-
server.registerTool("jira_delete_issue_link", {
|
|
2271
|
-
title: "Delete Issue Link",
|
|
2272
|
-
description: "Remove a link between issues.",
|
|
2273
|
-
inputSchema: z.object({
|
|
2274
|
-
linkId: z.string().min(1).describe("Link ID to delete (get from jira_get_issue_links)"),
|
|
2275
|
-
}),
|
|
2276
|
-
}, async ({ linkId }) => {
|
|
2277
|
-
try {
|
|
2278
|
-
const auth = await getAuthOrThrow();
|
|
2279
|
-
const client = createClient(auth);
|
|
2280
|
-
await client.delete(`/rest/api/3/issueLink/${linkId}`);
|
|
2281
|
-
return textResult({
|
|
2282
|
-
success: true,
|
|
2283
|
-
message: `Link ${linkId} deleted successfully`,
|
|
2284
|
-
});
|
|
2285
|
-
}
|
|
2286
|
-
catch (error) {
|
|
2287
|
-
return textResult(errorToResult(error));
|
|
2288
|
-
}
|
|
2289
|
-
});
|
|
2290
|
-
server.registerTool("jira_get_link_types", {
|
|
2291
|
-
title: "Get Issue Link Types",
|
|
2292
|
-
description: "Get available link types for linking issues.",
|
|
2293
|
-
inputSchema: z.object({}),
|
|
2294
|
-
}, async () => {
|
|
2295
|
-
try {
|
|
2296
|
-
const auth = await getAuthOrThrow();
|
|
2297
|
-
const client = createClient(auth);
|
|
2298
|
-
const response = await client.get("/rest/api/3/issueLinkType");
|
|
2299
|
-
return textResult((response.data?.issueLinkTypes || []).map((lt) => ({
|
|
2300
|
-
id: lt.id,
|
|
2301
|
-
name: lt.name,
|
|
2302
|
-
inward: lt.inward,
|
|
2303
|
-
outward: lt.outward,
|
|
2304
|
-
})));
|
|
2305
|
-
}
|
|
2306
|
-
catch (error) {
|
|
2307
|
-
return textResult(errorToResult(error));
|
|
2308
|
-
}
|
|
2309
|
-
});
|
|
2310
|
-
server.registerTool("jira_get_watchers", {
|
|
2311
|
-
title: "Get Issue Watchers",
|
|
2312
|
-
description: "Get the list of users watching an issue.",
|
|
2313
|
-
inputSchema: z.object({
|
|
2314
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2315
|
-
}),
|
|
2316
|
-
}, async ({ issueIdOrKey }) => {
|
|
2317
|
-
try {
|
|
2318
|
-
const auth = await getAuthOrThrow();
|
|
2319
|
-
const client = createClient(auth);
|
|
2320
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/watchers`);
|
|
2321
|
-
const watchers = Array.isArray(response.data?.watchers)
|
|
2322
|
-
? response.data.watchers.map((w) => ({
|
|
2323
|
-
accountId: w.accountId,
|
|
2324
|
-
displayName: w.displayName,
|
|
2325
|
-
emailAddress: w.emailAddress,
|
|
2326
|
-
}))
|
|
2327
|
-
: [];
|
|
2328
|
-
return textResult({
|
|
2329
|
-
issueKey: issueIdOrKey,
|
|
2330
|
-
watchCount: response.data?.watchCount ?? watchers.length,
|
|
2331
|
-
isWatching: response.data?.isWatching ?? false,
|
|
2332
|
-
watchers,
|
|
2333
|
-
});
|
|
2334
|
-
}
|
|
2335
|
-
catch (error) {
|
|
2336
|
-
return textResult(errorToResult(error));
|
|
2337
|
-
}
|
|
2338
|
-
});
|
|
2339
|
-
server.registerTool("jira_add_watcher", {
|
|
2340
|
-
title: "Add Issue Watcher",
|
|
2341
|
-
description: "Add a user to watch an issue.",
|
|
2342
|
-
inputSchema: z.object({
|
|
2343
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2344
|
-
accountId: z.string().min(1).describe("User account ID to add as watcher"),
|
|
2345
|
-
}),
|
|
2346
|
-
}, async ({ issueIdOrKey, accountId }) => {
|
|
2347
|
-
try {
|
|
2348
|
-
const auth = await getAuthOrThrow();
|
|
2349
|
-
const client = createClient(auth);
|
|
2350
|
-
await client.post(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/watchers`, JSON.stringify(accountId), {
|
|
2351
|
-
headers: {
|
|
2352
|
-
"Content-Type": "application/json",
|
|
2353
|
-
},
|
|
2354
|
-
});
|
|
2355
|
-
return textResult({
|
|
2356
|
-
success: true,
|
|
2357
|
-
issueKey: issueIdOrKey,
|
|
2358
|
-
accountId,
|
|
2359
|
-
message: `User added as watcher`,
|
|
2360
|
-
});
|
|
2361
|
-
}
|
|
2362
|
-
catch (error) {
|
|
2363
|
-
return textResult(errorToResult(error));
|
|
2364
|
-
}
|
|
2365
|
-
});
|
|
2366
|
-
server.registerTool("jira_remove_watcher", {
|
|
2367
|
-
title: "Remove Issue Watcher",
|
|
2368
|
-
description: "Remove a user from watching an issue.",
|
|
2369
|
-
inputSchema: z.object({
|
|
2370
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2371
|
-
accountId: z.string().min(1).describe("User account ID to remove"),
|
|
2372
|
-
}),
|
|
2373
|
-
}, async ({ issueIdOrKey, accountId }) => {
|
|
2374
|
-
try {
|
|
2375
|
-
const auth = await getAuthOrThrow();
|
|
2376
|
-
const client = createClient(auth);
|
|
2377
|
-
await client.delete(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/watchers`, {
|
|
2378
|
-
params: { accountId },
|
|
2379
|
-
});
|
|
2380
|
-
return textResult({
|
|
2381
|
-
success: true,
|
|
2382
|
-
issueKey: issueIdOrKey,
|
|
2383
|
-
accountId,
|
|
2384
|
-
message: `User removed from watchers`,
|
|
2385
|
-
});
|
|
2386
|
-
}
|
|
2387
|
-
catch (error) {
|
|
2388
|
-
return textResult(errorToResult(error));
|
|
2389
|
-
}
|
|
2390
|
-
});
|
|
2391
|
-
server.registerTool("jira_get_votes", {
|
|
2392
|
-
title: "Get Issue Votes",
|
|
2393
|
-
description: "Get the vote count and voters for an issue.",
|
|
2394
|
-
inputSchema: z.object({
|
|
2395
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2396
|
-
}),
|
|
2397
|
-
}, async ({ issueIdOrKey }) => {
|
|
2398
|
-
try {
|
|
2399
|
-
const auth = await getAuthOrThrow();
|
|
2400
|
-
const client = createClient(auth);
|
|
2401
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/votes`);
|
|
2402
|
-
const voters = Array.isArray(response.data?.voters)
|
|
2403
|
-
? response.data.voters.map((v) => ({
|
|
2404
|
-
accountId: v.accountId,
|
|
2405
|
-
displayName: v.displayName,
|
|
2406
|
-
}))
|
|
2407
|
-
: [];
|
|
2408
|
-
return textResult({
|
|
2409
|
-
issueKey: issueIdOrKey,
|
|
2410
|
-
votes: response.data?.votes ?? 0,
|
|
2411
|
-
hasVoted: response.data?.hasVoted ?? false,
|
|
2412
|
-
voters,
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
catch (error) {
|
|
2416
|
-
return textResult(errorToResult(error));
|
|
2417
|
-
}
|
|
2418
|
-
});
|
|
2419
|
-
server.registerTool("jira_add_vote", {
|
|
2420
|
-
title: "Vote for Issue",
|
|
2421
|
-
description: "Add your vote to an issue.",
|
|
2422
|
-
inputSchema: z.object({
|
|
2423
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2424
|
-
}),
|
|
2425
|
-
}, async ({ issueIdOrKey }) => {
|
|
2426
|
-
try {
|
|
2427
|
-
const auth = await getAuthOrThrow();
|
|
2428
|
-
const client = createClient(auth);
|
|
2429
|
-
await client.post(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/votes`);
|
|
2430
|
-
return textResult({
|
|
2431
|
-
success: true,
|
|
2432
|
-
issueKey: issueIdOrKey,
|
|
2433
|
-
message: `Vote added successfully`,
|
|
2434
|
-
});
|
|
2435
|
-
}
|
|
2436
|
-
catch (error) {
|
|
2437
|
-
return textResult(errorToResult(error));
|
|
2438
|
-
}
|
|
2439
|
-
});
|
|
2440
|
-
server.registerTool("jira_remove_vote", {
|
|
2441
|
-
title: "Remove Vote",
|
|
2442
|
-
description: "Remove your vote from an issue.",
|
|
2443
|
-
inputSchema: z.object({
|
|
2444
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2445
|
-
}),
|
|
2446
|
-
}, async ({ issueIdOrKey }) => {
|
|
2447
|
-
try {
|
|
2448
|
-
const auth = await getAuthOrThrow();
|
|
2449
|
-
const client = createClient(auth);
|
|
2450
|
-
await client.delete(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/votes`);
|
|
2451
|
-
return textResult({
|
|
2452
|
-
success: true,
|
|
2453
|
-
issueKey: issueIdOrKey,
|
|
2454
|
-
message: `Vote removed successfully`,
|
|
2455
|
-
});
|
|
2456
|
-
}
|
|
2457
|
-
catch (error) {
|
|
2458
|
-
return textResult(errorToResult(error));
|
|
2459
|
-
}
|
|
2460
|
-
});
|
|
2461
|
-
// ============ Phase 4: Attachments ============
|
|
2462
|
-
server.registerTool("jira_get_attachments", {
|
|
2463
|
-
title: "Get Issue Attachments",
|
|
2464
|
-
description: "Get all attachments for an issue.",
|
|
2465
|
-
inputSchema: z.object({
|
|
2466
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2467
|
-
}),
|
|
2468
|
-
}, async ({ issueIdOrKey }) => {
|
|
2469
|
-
try {
|
|
2470
|
-
const auth = await getAuthOrThrow();
|
|
2471
|
-
const client = createClient(auth);
|
|
2472
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`, {
|
|
2473
|
-
params: { fields: "attachment" },
|
|
2474
|
-
});
|
|
2475
|
-
const attachments = Array.isArray(response.data?.fields?.attachment)
|
|
2476
|
-
? response.data.fields.attachment.map((a) => ({
|
|
2477
|
-
id: a.id,
|
|
2478
|
-
filename: a.filename,
|
|
2479
|
-
size: a.size,
|
|
2480
|
-
mimeType: a.mimeType,
|
|
2481
|
-
content: a.content,
|
|
2482
|
-
thumbnail: a.thumbnail,
|
|
2483
|
-
author: a.author?.displayName,
|
|
2484
|
-
created: a.created,
|
|
2485
|
-
}))
|
|
2486
|
-
: [];
|
|
2487
|
-
return textResult({
|
|
2488
|
-
issueKey: issueIdOrKey,
|
|
2489
|
-
attachments,
|
|
2490
|
-
});
|
|
2491
|
-
}
|
|
2492
|
-
catch (error) {
|
|
2493
|
-
return textResult(errorToResult(error));
|
|
2494
|
-
}
|
|
2495
|
-
});
|
|
2496
|
-
server.registerTool("jira_delete_attachment", {
|
|
2497
|
-
title: "Delete Attachment",
|
|
2498
|
-
description: "Delete an attachment from an issue.",
|
|
2499
|
-
inputSchema: z.object({
|
|
2500
|
-
attachmentId: z.string().min(1).describe("Attachment ID to delete"),
|
|
2501
|
-
}),
|
|
2502
|
-
}, async ({ attachmentId }) => {
|
|
2503
|
-
try {
|
|
2504
|
-
const auth = await getAuthOrThrow();
|
|
2505
|
-
const client = createClient(auth);
|
|
2506
|
-
await client.delete(`/rest/api/3/attachment/${attachmentId}`);
|
|
2507
|
-
return textResult({
|
|
2508
|
-
success: true,
|
|
2509
|
-
message: `Attachment ${attachmentId} deleted successfully`,
|
|
2510
|
-
});
|
|
2511
|
-
}
|
|
2512
|
-
catch (error) {
|
|
2513
|
-
return textResult(errorToResult(error));
|
|
2514
|
-
}
|
|
2515
|
-
});
|
|
2516
|
-
// ============ Phase 5: Epic Management ============
|
|
2517
|
-
server.registerTool("jira_get_epics", {
|
|
2518
|
-
title: "Get Epics",
|
|
2519
|
-
description: "Get epics for a board.",
|
|
2520
|
-
inputSchema: z.object({
|
|
2521
|
-
boardId: z.number().int().positive().describe("Board ID"),
|
|
2522
|
-
done: z.enum(["true", "false"]).optional().describe("Filter by completion status"),
|
|
2523
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
2524
|
-
maxResults: z.number().int().positive().max(50).optional().default(50),
|
|
2525
|
-
}),
|
|
2526
|
-
}, async ({ boardId, done, startAt, maxResults }) => {
|
|
2527
|
-
try {
|
|
2528
|
-
const auth = await getAuthOrThrow();
|
|
2529
|
-
const client = createClient(auth);
|
|
2530
|
-
const response = await client.get(`/rest/agile/1.0/board/${boardId}/epic`, {
|
|
2531
|
-
params: {
|
|
2532
|
-
done,
|
|
2533
|
-
startAt,
|
|
2534
|
-
maxResults: maxResults ?? 50,
|
|
2535
|
-
},
|
|
2536
|
-
});
|
|
2537
|
-
const epics = Array.isArray(response.data?.values)
|
|
2538
|
-
? response.data.values.map((e) => ({
|
|
2539
|
-
id: e.id,
|
|
2540
|
-
key: e.key,
|
|
2541
|
-
name: e.name,
|
|
2542
|
-
summary: e.summary,
|
|
2543
|
-
done: e.done ?? false,
|
|
2544
|
-
}))
|
|
2545
|
-
: [];
|
|
2546
|
-
return textResult({
|
|
2547
|
-
total: response.data?.total ?? epics.length,
|
|
2548
|
-
startAt: response.data?.startAt ?? 0,
|
|
2549
|
-
boardId,
|
|
2550
|
-
epics,
|
|
2551
|
-
});
|
|
2552
|
-
}
|
|
2553
|
-
catch (error) {
|
|
2554
|
-
return textResult(errorToResult(error));
|
|
2555
|
-
}
|
|
2556
|
-
});
|
|
2557
|
-
server.registerTool("jira_get_epic_issues", {
|
|
2558
|
-
title: "Get Epic Issues",
|
|
2559
|
-
description: "Get all issues belonging to an epic.",
|
|
2560
|
-
inputSchema: z.object({
|
|
2561
|
-
epicIdOrKey: z.string().min(1).describe("Epic ID or key"),
|
|
2562
|
-
jql: z.string().optional().describe("Additional JQL filter"),
|
|
2563
|
-
fields: z.array(z.string()).optional().describe("Fields to return"),
|
|
2564
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
2565
|
-
maxResults: z.number().int().positive().max(100).optional().default(50),
|
|
2566
|
-
}),
|
|
2567
|
-
}, async ({ epicIdOrKey, jql, fields, startAt, maxResults }) => {
|
|
2568
|
-
try {
|
|
2569
|
-
const auth = await getAuthOrThrow();
|
|
2570
|
-
const client = createClient(auth);
|
|
2571
|
-
const response = await client.get(`/rest/agile/1.0/epic/${epicIdOrKey}/issue`, {
|
|
2572
|
-
params: {
|
|
2573
|
-
jql,
|
|
2574
|
-
fields: fields?.join(","),
|
|
2575
|
-
startAt,
|
|
2576
|
-
maxResults: maxResults ?? 50,
|
|
2577
|
-
},
|
|
2578
|
-
});
|
|
2579
|
-
const issues = Array.isArray(response.data?.issues)
|
|
2580
|
-
? response.data.issues.map((issue) => ({
|
|
2581
|
-
key: issue.key,
|
|
2582
|
-
summary: issue.fields?.summary,
|
|
2583
|
-
status: issue.fields?.status?.name,
|
|
2584
|
-
assignee: issue.fields?.assignee?.displayName,
|
|
2585
|
-
issueType: issue.fields?.issuetype?.name,
|
|
2586
|
-
}))
|
|
2587
|
-
: [];
|
|
2588
|
-
return textResult({
|
|
2589
|
-
total: response.data?.total ?? issues.length,
|
|
2590
|
-
startAt: response.data?.startAt ?? 0,
|
|
2591
|
-
epicKey: epicIdOrKey,
|
|
2592
|
-
issues,
|
|
2593
|
-
});
|
|
2594
|
-
}
|
|
2595
|
-
catch (error) {
|
|
2596
|
-
return textResult(errorToResult(error));
|
|
2597
|
-
}
|
|
2598
|
-
});
|
|
2599
|
-
server.registerTool("jira_move_issues_to_epic", {
|
|
2600
|
-
title: "Move Issues to Epic",
|
|
2601
|
-
description: "Move issues to an epic.",
|
|
2602
|
-
inputSchema: z.object({
|
|
2603
|
-
epicIdOrKey: z.string().min(1).describe("Epic ID or key"),
|
|
2604
|
-
issueKeys: z.array(z.string().min(1)).min(1).describe("Issue keys to move"),
|
|
2605
|
-
}),
|
|
2606
|
-
}, async ({ epicIdOrKey, issueKeys }) => {
|
|
2607
|
-
try {
|
|
2608
|
-
const auth = await getAuthOrThrow();
|
|
2609
|
-
const client = createClient(auth);
|
|
2610
|
-
await client.post(`/rest/agile/1.0/epic/${epicIdOrKey}/issue`, {
|
|
2611
|
-
issues: issueKeys,
|
|
2612
|
-
});
|
|
2613
|
-
return textResult({
|
|
2614
|
-
success: true,
|
|
2615
|
-
epicKey: epicIdOrKey,
|
|
2616
|
-
issuesMoved: issueKeys,
|
|
2617
|
-
message: `${issueKeys.length} issue(s) moved to epic ${epicIdOrKey}`,
|
|
2618
|
-
});
|
|
2619
|
-
}
|
|
2620
|
-
catch (error) {
|
|
2621
|
-
return textResult(errorToResult(error));
|
|
2622
|
-
}
|
|
2623
|
-
});
|
|
2624
|
-
server.registerTool("jira_remove_issues_from_epic", {
|
|
2625
|
-
title: "Remove Issues from Epic",
|
|
2626
|
-
description: "Remove issues from their epic (move to no epic).",
|
|
2627
|
-
inputSchema: z.object({
|
|
2628
|
-
issueKeys: z.array(z.string().min(1)).min(1).describe("Issue keys to remove from epic"),
|
|
2629
|
-
}),
|
|
2630
|
-
}, async ({ issueKeys }) => {
|
|
2631
|
-
try {
|
|
2632
|
-
const auth = await getAuthOrThrow();
|
|
2633
|
-
const client = createClient(auth);
|
|
2634
|
-
await client.post("/rest/agile/1.0/epic/none/issue", {
|
|
2635
|
-
issues: issueKeys,
|
|
2636
|
-
});
|
|
2637
|
-
return textResult({
|
|
2638
|
-
success: true,
|
|
2639
|
-
issuesRemoved: issueKeys,
|
|
2640
|
-
message: `${issueKeys.length} issue(s) removed from epic`,
|
|
2641
|
-
});
|
|
2642
|
-
}
|
|
2643
|
-
catch (error) {
|
|
2644
|
-
return textResult(errorToResult(error));
|
|
2645
|
-
}
|
|
2646
|
-
});
|
|
2647
|
-
// ============ Phase 6: Fields and Metadata ============
|
|
2648
|
-
server.registerTool("jira_get_fields", {
|
|
2649
|
-
title: "Get All Fields",
|
|
2650
|
-
description: "Get all available fields including custom fields.",
|
|
2651
|
-
inputSchema: z.object({}),
|
|
2652
|
-
}, async () => {
|
|
2653
|
-
try {
|
|
2654
|
-
const auth = await getAuthOrThrow();
|
|
2655
|
-
const client = createClient(auth);
|
|
2656
|
-
const response = await client.get("/rest/api/3/field");
|
|
2657
|
-
return textResult((response.data || []).map((f) => ({
|
|
2658
|
-
id: f.id,
|
|
2659
|
-
key: f.key,
|
|
2660
|
-
name: f.name,
|
|
2661
|
-
custom: f.custom ?? false,
|
|
2662
|
-
orderable: f.orderable ?? false,
|
|
2663
|
-
navigable: f.navigable ?? false,
|
|
2664
|
-
searchable: f.searchable ?? false,
|
|
2665
|
-
clauseNames: f.clauseNames || [],
|
|
2666
|
-
schema: f.schema,
|
|
2667
|
-
})));
|
|
2668
|
-
}
|
|
2669
|
-
catch (error) {
|
|
2670
|
-
return textResult(errorToResult(error));
|
|
2671
|
-
}
|
|
2672
|
-
});
|
|
2673
|
-
server.registerTool("jira_get_create_metadata", {
|
|
2674
|
-
title: "Get Create Issue Metadata",
|
|
2675
|
-
description: "Get metadata for creating issues in a project, including required fields.",
|
|
2676
|
-
inputSchema: z.object({
|
|
2677
|
-
projectKeys: z.array(z.string()).optional().describe("Project keys to get metadata for"),
|
|
2678
|
-
projectIds: z.array(z.string()).optional().describe("Project IDs to get metadata for"),
|
|
2679
|
-
issuetypeNames: z.array(z.string()).optional().describe("Issue type names to filter"),
|
|
2680
|
-
expand: z.string().optional().describe("Expand options"),
|
|
2681
|
-
}),
|
|
2682
|
-
}, async ({ projectKeys, projectIds, issuetypeNames, expand }) => {
|
|
2683
|
-
try {
|
|
2684
|
-
const auth = await getAuthOrThrow();
|
|
2685
|
-
const client = createClient(auth);
|
|
2686
|
-
const response = await client.get("/rest/api/3/issue/createmeta", {
|
|
2687
|
-
params: {
|
|
2688
|
-
projectKeys: projectKeys?.join(","),
|
|
2689
|
-
projectIds: projectIds?.join(","),
|
|
2690
|
-
issuetypeNames: issuetypeNames?.join(","),
|
|
2691
|
-
expand: expand || "projects.issuetypes.fields",
|
|
2692
|
-
},
|
|
2693
|
-
});
|
|
2694
|
-
return textResult(response.data);
|
|
2695
|
-
}
|
|
2696
|
-
catch (error) {
|
|
2697
|
-
return textResult(errorToResult(error));
|
|
2698
|
-
}
|
|
2699
|
-
});
|
|
2700
|
-
server.registerTool("jira_get_edit_metadata", {
|
|
2701
|
-
title: "Get Edit Issue Metadata",
|
|
2702
|
-
description: "Get metadata for editing a specific issue, including editable fields.",
|
|
2703
|
-
inputSchema: z.object({
|
|
2704
|
-
issueIdOrKey: z.string().min(1).describe("Issue key or ID"),
|
|
2705
|
-
}),
|
|
2706
|
-
}, async ({ issueIdOrKey }) => {
|
|
2707
|
-
try {
|
|
2708
|
-
const auth = await getAuthOrThrow();
|
|
2709
|
-
const client = createClient(auth);
|
|
2710
|
-
const response = await client.get(`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/editmeta`);
|
|
2711
|
-
return textResult(response.data);
|
|
2712
|
-
}
|
|
2713
|
-
catch (error) {
|
|
2714
|
-
return textResult(errorToResult(error));
|
|
2715
|
-
}
|
|
2716
|
-
});
|
|
2717
|
-
// ============ Phase 7: Filters and Dashboards ============
|
|
2718
|
-
server.registerTool("jira_get_filters", {
|
|
2719
|
-
title: "Get Filters",
|
|
2720
|
-
description: "Get saved filters, optionally filtered by name.",
|
|
2721
|
-
inputSchema: z.object({
|
|
2722
|
-
filterName: z.string().optional().describe("Filter by name (contains)"),
|
|
2723
|
-
owner: z.string().optional().describe("Filter by owner account ID"),
|
|
2724
|
-
expand: z.string().optional().describe("Expand options: description, owner, jql, viewUrl, searchUrl, favourite, favouritedCount, sharePermissions"),
|
|
2725
|
-
startAt: z.number().int().nonnegative().optional(),
|
|
2726
|
-
maxResults: z.number().int().positive().max(50).optional().default(50),
|
|
2727
|
-
}),
|
|
2728
|
-
}, async ({ filterName, owner, expand, startAt, maxResults }) => {
|
|
2729
|
-
try {
|
|
2730
|
-
const auth = await getAuthOrThrow();
|
|
2731
|
-
const client = createClient(auth);
|
|
2732
|
-
const response = await client.get("/rest/api/3/filter/search", {
|
|
2733
|
-
params: {
|
|
2734
|
-
filterName,
|
|
2735
|
-
owner,
|
|
2736
|
-
expand,
|
|
2737
|
-
startAt,
|
|
2738
|
-
maxResults: maxResults ?? 50,
|
|
2739
|
-
},
|
|
2740
|
-
});
|
|
2741
|
-
const filters = Array.isArray(response.data?.values)
|
|
2742
|
-
? response.data.values.map((f) => ({
|
|
2743
|
-
id: f.id,
|
|
2744
|
-
name: f.name,
|
|
2745
|
-
description: f.description,
|
|
2746
|
-
owner: f.owner?.displayName,
|
|
2747
|
-
jql: f.jql,
|
|
2748
|
-
favourite: f.favourite ?? false,
|
|
2749
|
-
favouritedCount: f.favouritedCount ?? 0,
|
|
2750
|
-
}))
|
|
2751
|
-
: [];
|
|
2752
|
-
return textResult({
|
|
2753
|
-
total: response.data?.total ?? filters.length,
|
|
2754
|
-
startAt: response.data?.startAt ?? 0,
|
|
2755
|
-
filters,
|
|
2756
|
-
});
|
|
2757
|
-
}
|
|
2758
|
-
catch (error) {
|
|
2759
|
-
return textResult(errorToResult(error));
|
|
2760
|
-
}
|
|
2761
|
-
});
|
|
2762
|
-
server.registerTool("jira_get_filter", {
|
|
2763
|
-
title: "Get Filter Details",
|
|
2764
|
-
description: "Get details of a specific filter.",
|
|
2765
|
-
inputSchema: z.object({
|
|
2766
|
-
filterId: z.string().min(1).describe("Filter ID"),
|
|
2767
|
-
expand: z.string().optional().describe("Expand options"),
|
|
2768
|
-
}),
|
|
2769
|
-
}, async ({ filterId, expand }) => {
|
|
2770
|
-
try {
|
|
2771
|
-
const auth = await getAuthOrThrow();
|
|
2772
|
-
const client = createClient(auth);
|
|
2773
|
-
const response = await client.get(`/rest/api/3/filter/${filterId}`, {
|
|
2774
|
-
params: { expand },
|
|
2775
|
-
});
|
|
2776
|
-
return textResult({
|
|
2777
|
-
id: response.data?.id,
|
|
2778
|
-
name: response.data?.name,
|
|
2779
|
-
description: response.data?.description,
|
|
2780
|
-
owner: response.data?.owner?.displayName,
|
|
2781
|
-
jql: response.data?.jql,
|
|
2782
|
-
favourite: response.data?.favourite ?? false,
|
|
2783
|
-
sharePermissions: response.data?.sharePermissions,
|
|
2784
|
-
});
|
|
2785
|
-
}
|
|
2786
|
-
catch (error) {
|
|
2787
|
-
return textResult(errorToResult(error));
|
|
2788
|
-
}
|
|
2789
|
-
});
|
|
2790
|
-
server.registerTool("jira_create_filter", {
|
|
2791
|
-
title: "Create Filter",
|
|
2792
|
-
description: "Create a new saved filter.",
|
|
2793
|
-
inputSchema: z.object({
|
|
2794
|
-
name: z.string().min(1).describe("Filter name"),
|
|
2795
|
-
jql: z.string().min(1).describe("JQL query"),
|
|
2796
|
-
description: z.string().optional().describe("Filter description"),
|
|
2797
|
-
favourite: z.boolean().optional().describe("Mark as favourite"),
|
|
2798
|
-
}),
|
|
2799
|
-
}, async ({ name, jql, description, favourite }) => {
|
|
2800
|
-
try {
|
|
2801
|
-
const auth = await getAuthOrThrow();
|
|
2802
|
-
const client = createClient(auth);
|
|
2803
|
-
const response = await client.post("/rest/api/3/filter", {
|
|
2804
|
-
name,
|
|
2805
|
-
jql,
|
|
2806
|
-
description,
|
|
2807
|
-
favourite,
|
|
2808
|
-
});
|
|
2809
|
-
return textResult({
|
|
2810
|
-
success: true,
|
|
2811
|
-
id: response.data?.id,
|
|
2812
|
-
name: response.data?.name,
|
|
2813
|
-
jql: response.data?.jql,
|
|
2814
|
-
message: `Filter "${name}" created successfully`,
|
|
2815
|
-
});
|
|
2816
|
-
}
|
|
2817
|
-
catch (error) {
|
|
2818
|
-
return textResult(errorToResult(error));
|
|
2819
|
-
}
|
|
2820
|
-
});
|
|
2821
|
-
server.registerTool("jira_update_filter", {
|
|
2822
|
-
title: "Update Filter",
|
|
2823
|
-
description: "Update an existing filter.",
|
|
2824
|
-
inputSchema: z.object({
|
|
2825
|
-
filterId: z.string().min(1).describe("Filter ID"),
|
|
2826
|
-
name: z.string().optional().describe("New filter name"),
|
|
2827
|
-
jql: z.string().optional().describe("New JQL query"),
|
|
2828
|
-
description: z.string().optional().describe("New description"),
|
|
2829
|
-
favourite: z.boolean().optional().describe("Favourite status"),
|
|
2830
|
-
}),
|
|
2831
|
-
}, async ({ filterId, name, jql, description, favourite }) => {
|
|
2832
|
-
try {
|
|
2833
|
-
const auth = await getAuthOrThrow();
|
|
2834
|
-
const client = createClient(auth);
|
|
2835
|
-
const payload = {};
|
|
2836
|
-
if (name !== undefined)
|
|
2837
|
-
payload.name = name;
|
|
2838
|
-
if (jql !== undefined)
|
|
2839
|
-
payload.jql = jql;
|
|
2840
|
-
if (description !== undefined)
|
|
2841
|
-
payload.description = description;
|
|
2842
|
-
if (favourite !== undefined)
|
|
2843
|
-
payload.favourite = favourite;
|
|
2844
|
-
if (Object.keys(payload).length === 0) {
|
|
2845
|
-
return textResult({
|
|
2846
|
-
error: "no_changes",
|
|
2847
|
-
message: "No fields provided to update",
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
const response = await client.put(`/rest/api/3/filter/${filterId}`, payload);
|
|
2851
|
-
return textResult({
|
|
2852
|
-
success: true,
|
|
2853
|
-
id: response.data?.id ?? filterId,
|
|
2854
|
-
name: response.data?.name,
|
|
2855
|
-
message: `Filter updated successfully`,
|
|
2856
|
-
});
|
|
2857
|
-
}
|
|
2858
|
-
catch (error) {
|
|
2859
|
-
return textResult(errorToResult(error));
|
|
2860
|
-
}
|
|
2861
|
-
});
|
|
2862
|
-
server.registerTool("jira_delete_filter", {
|
|
2863
|
-
title: "Delete Filter",
|
|
2864
|
-
description: "Delete a saved filter.",
|
|
2865
|
-
inputSchema: z.object({
|
|
2866
|
-
filterId: z.string().min(1).describe("Filter ID to delete"),
|
|
2867
|
-
confirmDelete: z.boolean().describe("Must be true to confirm deletion"),
|
|
2868
|
-
}),
|
|
2869
|
-
}, async ({ filterId, confirmDelete }) => {
|
|
2870
|
-
try {
|
|
2871
|
-
if (!confirmDelete) {
|
|
2872
|
-
return textResult({
|
|
2873
|
-
error: "confirmation_required",
|
|
2874
|
-
message: "Deletion not confirmed. Set confirmDelete: true to proceed.",
|
|
2875
|
-
filterId,
|
|
2876
|
-
});
|
|
2877
|
-
}
|
|
2878
|
-
const auth = await getAuthOrThrow();
|
|
2879
|
-
const client = createClient(auth);
|
|
2880
|
-
await client.delete(`/rest/api/3/filter/${filterId}`);
|
|
2881
|
-
return textResult({
|
|
2882
|
-
success: true,
|
|
2883
|
-
message: `Filter ${filterId} deleted successfully`,
|
|
2884
|
-
});
|
|
2885
|
-
}
|
|
2886
|
-
catch (error) {
|
|
2887
|
-
return textResult(errorToResult(error));
|
|
2888
|
-
}
|
|
2889
|
-
});
|
|
2890
|
-
server.registerTool("jira_get_my_filters", {
|
|
2891
|
-
title: "Get My Filters",
|
|
2892
|
-
description: "Get filters owned by the current user.",
|
|
2893
|
-
inputSchema: z.object({
|
|
2894
|
-
expand: z.string().optional().describe("Expand options"),
|
|
2895
|
-
}),
|
|
2896
|
-
}, async ({ expand }) => {
|
|
2897
|
-
try {
|
|
2898
|
-
const auth = await getAuthOrThrow();
|
|
2899
|
-
const client = createClient(auth);
|
|
2900
|
-
const response = await client.get("/rest/api/3/filter/my", {
|
|
2901
|
-
params: { expand },
|
|
2902
|
-
});
|
|
2903
|
-
return textResult((response.data || []).map((f) => ({
|
|
2904
|
-
id: f.id,
|
|
2905
|
-
name: f.name,
|
|
2906
|
-
description: f.description,
|
|
2907
|
-
jql: f.jql,
|
|
2908
|
-
favourite: f.favourite ?? false,
|
|
2909
|
-
})));
|
|
2910
|
-
}
|
|
2911
|
-
catch (error) {
|
|
2912
|
-
return textResult(errorToResult(error));
|
|
2913
|
-
}
|
|
2914
|
-
});
|
|
2915
|
-
server.registerTool("jira_get_favourite_filters", {
|
|
2916
|
-
title: "Get Favourite Filters",
|
|
2917
|
-
description: "Get filters marked as favourite by the current user.",
|
|
2918
|
-
inputSchema: z.object({
|
|
2919
|
-
expand: z.string().optional().describe("Expand options"),
|
|
2920
|
-
}),
|
|
2921
|
-
}, async ({ expand }) => {
|
|
2922
|
-
try {
|
|
2923
|
-
const auth = await getAuthOrThrow();
|
|
2924
|
-
const client = createClient(auth);
|
|
2925
|
-
const response = await client.get("/rest/api/3/filter/favourite", {
|
|
2926
|
-
params: { expand },
|
|
2927
|
-
});
|
|
2928
|
-
return textResult((response.data || []).map((f) => ({
|
|
2929
|
-
id: f.id,
|
|
2930
|
-
name: f.name,
|
|
2931
|
-
description: f.description,
|
|
2932
|
-
owner: f.owner?.displayName,
|
|
2933
|
-
jql: f.jql,
|
|
2934
|
-
})));
|
|
2935
|
-
}
|
|
2936
|
-
catch (error) {
|
|
2937
|
-
return textResult(errorToResult(error));
|
|
2938
|
-
}
|
|
2939
|
-
});
|
|
2940
|
-
const transport = new StdioServerTransport();
|
|
2941
|
-
await server.connect(transport);
|
|
2942
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import e,{AxiosError as t}from"axios";import{McpServer as s}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as r}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";const i="jira-mcp",n="default",o=(process.env.JIRA_ACCEPTANCE_CRITERIA_FIELD||"").trim(),c="https://auth.atlassian.com/oauth/token";let u,d=null;async function l(){if(void 0!==u)return u;try{u=await import("keytar")}catch{u=null}return u}function p(e){let t;try{t=new URL(e)}catch{throw new Error("baseUrl must be a valid URL like https://your-domain.atlassian.net")}const s=t.pathname.replace(/\/+$/,"");return`${t.origin}${s}`}async function m(t,s,r){const a=await e.post(c,{t:"refresh_token",i:t,o:s,u:r},{headers:{"Content-Type":"application/json"}});return{accessToken:a.data.l,refreshToken:a.data.u,expiresIn:a.data.p}}async function y(t){return(await e.get("https://api.atlassian.com/oauth/token/accessible-resources",{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}})).data}async function h(e,t){const s=await y(e);if(0===s.length)throw new Error("No accessible Jira sites found. Make sure your OAuth app has the correct scopes and you have granted access.");if(t){const e=p(t),r=s.find(t=>p(t.url)===e);if(r)return{cloudId:r.id,siteName:r.name,siteUrl:r.url};throw new Error(`Site ${t} not found in accessible resources. Available sites: ${s.map(e=>e.url).join(", ")}`)}const r=s[0];if(!r)throw new Error("No accessible Jira resources found");return{cloudId:r.id,siteName:r.name,siteUrl:r.url}}async function f(){if(d){if("oauth"===d.type&&d.expiresAt&&d.refreshToken){if(Date.now()>=d.expiresAt-3e5)try{const e=await m(d.clientId,d.clientSecret,d.refreshToken);d={...d,accessToken:e.accessToken,refreshToken:e.refreshToken||d.refreshToken,expiresAt:Date.now()+1e3*e.expiresIn}}catch(e){console.error("Failed to refresh OAuth token:",e)}}return d}const e=function(){const e=process.env.JIRA_OAUTH_CLIENT_ID,t=process.env.JIRA_OAUTH_CLIENT_SECRET,s=process.env.JIRA_OAUTH_ACCESS_TOKEN,r=process.env.JIRA_OAUTH_REFRESH_TOKEN,a=process.env.JIRA_CLOUD_ID;return e&&t&&s&&a?{type:"oauth",clientId:e,clientSecret:t,accessToken:s,refreshToken:r,cloudId:a}:null}();if(e)return e;const t=function(){const e=process.env.JIRA_BASE_URL,t=process.env.JIRA_EMAIL,s=process.env.JIRA_API_TOKEN;return e&&t&&s?{type:"basic",baseUrl:p(e),email:t,apiToken:s}:null}();if(t)return t;const s=await async function(){const e=await l();if(!e)return null;const t=await e.getPassword(i,n);if(!t)return null;try{const e=JSON.parse(t);return"basic"===e.type?{...e,baseUrl:p(e.baseUrl)}:e}catch{return null}}();if(s)return s;throw new Error("MISSING_AUTH")}async function I(e,t){if(d=e,!t)return;const s=await l();if(!s)throw new Error("Keytar is not available to persist credentials.");await s.setPassword(i,n,JSON.stringify(e))}function w(t){return"basic"===t.type?e.create({baseURL:t.baseUrl,auth:{username:t.email,password:t.apiToken},headers:{Accept:"application/json"}}):e.create({baseURL:`https://api.atlassian.com/ex/jira/${t.cloudId}`,headers:{Authorization:`Bearer ${t.accessToken}`,Accept:"application/json"}})}function g(e){if(!e)return"";if(Array.isArray(e))return e.map(g).filter(Boolean).join("\n").trim();if("string"==typeof e.text)return e.text;if(Array.isArray(e.content)){return e.content.map(g).filter(Boolean).join("paragraph"===e.type?"\n":" ").trim()}return""}function _(e){if("string"==typeof e)return e;if("number"==typeof e)return String(e);if(e&&"object"==typeof e){const t=g(e);if(t)return t}return""}function k(e){return{type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:e}]}]}}function j(e){const t=e?.fields||{},s=_(t.description),r=o?_(t[o]):"";return{key:e?.key??"",summary:t.summary??"",description:s,acceptanceCriteria:r||null}}function v(e){const t=e?.fields||{};return{key:e?.key??"",summary:t.summary??"",status:t.status?.name??""}}function S(){const e=["summary","description"];return o&&e.push(o),e}function b(e){if(e instanceof t){const t=e.response?.status,s=e.response?.data;return`Jira API error${t?` (${t})`:""}: ${("string"==typeof s?s:JSON.stringify(s,null,2))||e.message}`}return e instanceof Error?e.message:"Unknown error"}function A(e){if(e instanceof Error&&"MISSING_AUTH"===e.message)return{error:"unauthorized",message:"Jira credentials are missing. Provide credentials explicitly to authenticate."};if(e instanceof t){const t=e.response?.status;return 401===t?{error:"unauthorized",message:"Jira credentials are missing or invalid. If using OAuth, the token may have expired."}:403===t?{error:"forbidden",message:"You do not have permission to access this Jira resource."}:404===t?{error:"not_found",message:"The Jira resource does not exist or is not visible."}:429===t?{error:"rate_limited",message:"Jira rate limit exceeded. Please retry later."}:t&&t>=500?{error:"server_error",message:"Jira server error. Please retry later."}:{error:"jira_error",message:b(e)}}return e instanceof Error?{error:"unknown",message:e.message}:{error:"unknown",message:"Unknown error"}}function D(e){return{content:[{type:"text",text:"string"==typeof e?e:JSON.stringify(e,null,2)}]}}const x=new s({name:"jira-mcp",version:"2.0.0"});x.registerTool("_internal_jira_set_auth",{title:"Set Jira Auth (Basic)",description:"Use when the user wants to connect Jira using Basic Auth (email + API token). This tool should only be called when the user explicitly provides credentials.",inputSchema:a.object({baseUrl:a.string(),email:a.string().email(),apiToken:a.string().min(1),persist:a.boolean().optional().default(!1)})},async({baseUrl:e,email:t,apiToken:s,persist:r})=>{const a=p(e);return await I({type:"basic",baseUrl:a,email:t,apiToken:s},r??!1),D("Jira credentials loaded (Basic Auth).")}),x.registerTool("jira_oauth_get_auth_url",{title:"Get OAuth Authorization URL",description:"Generate the OAuth 2.0 authorization URL that the user should visit to grant access. Returns the URL and required state parameter.",inputSchema:a.object({clientId:a.string().min(1).describe("OAuth Client ID from Atlassian Developer Console"),redirectUri:a.string().url().describe("Callback URL configured in your OAuth app"),scopes:a.array(a.string()).optional().default(["read:jira-work","read:jira-user","write:jira-work","offline_access"]).describe("OAuth scopes to request")})},async({clientId:e,redirectUri:t,scopes:s})=>{const r=Math.random().toString(36).substring(2,15),a=function(e,t,s,r){return`https://auth.atlassian.com/authorize?${new URLSearchParams({audience:"api.atlassian.com",i:e,scope:s.join(" "),m:t,state:r,h:"code",prompt:"consent"}).toString()}`}(e,t,s,r);return D({authUrl:a,state:r,instructions:"1. Visit the authUrl in your browser\n2. Grant access to your Jira site\n3. Copy the 'code' parameter from the redirect URL\n4. Use jira_oauth_exchange_code to exchange it for tokens"})}),x.registerTool("jira_oauth_exchange_code",{title:"Exchange OAuth Code for Tokens",description:"Exchange the authorization code for access tokens after the user has completed the OAuth flow.",inputSchema:a.object({clientId:a.string().min(1),clientSecret:a.string().min(1),code:a.string().min(1).describe("Authorization code from the OAuth callback"),redirectUri:a.string().url(),siteUrl:a.string().url().optional().describe("Optional: specific Jira site URL (e.g., https://yoursite.atlassian.net)"),persist:a.boolean().optional().default(!1)})},async({clientId:t,clientSecret:s,code:r,redirectUri:a,siteUrl:i,persist:n})=>{try{const o=await async function(t,s,r,a){const i=await e.post(c,{t:"authorization_code",i:t,o:s,code:r,m:a},{headers:{"Content-Type":"application/json"}});return{accessToken:i.data.l,refreshToken:i.data.u,expiresIn:i.data.p}}(t,s,r,a),{cloudId:u,siteName:d,siteUrl:l}=await h(o.accessToken,i),p={type:"oauth",clientId:t,clientSecret:s,accessToken:o.accessToken,refreshToken:o.refreshToken,cloudId:u,expiresAt:o.expiresIn?Date.now()+1e3*o.expiresIn:void 0};return await I(p,n),D({success:!0,message:`Successfully authenticated with OAuth to ${d}`,site:{name:d,url:l,cloudId:u},hasRefreshToken:!!o.refreshToken})}catch(e){return D(A(e))}}),x.registerTool("jira_oauth_set_tokens",{title:"Set OAuth Tokens Directly",description:"Set OAuth tokens directly if you already have them (e.g., from a previous session or external OAuth flow).",inputSchema:a.object({clientId:a.string().min(1),clientSecret:a.string().min(1),accessToken:a.string().min(1),refreshToken:a.string().optional(),cloudId:a.string().optional().describe("Cloud ID of the Jira site. If not provided, will be fetched automatically."),siteUrl:a.string().url().optional().describe("Jira site URL to find the correct cloudId"),persist:a.boolean().optional().default(!1)})},async({clientId:e,clientSecret:t,accessToken:s,refreshToken:r,cloudId:a,siteUrl:i,persist:n})=>{try{let o=a,c="",u=i||"";if(!o){const e=await h(s,i);o=e.cloudId,c=e.siteName,u=e.siteUrl}const d={type:"oauth",clientId:e,clientSecret:t,accessToken:s,refreshToken:r,cloudId:o};return await I(d,n),D({success:!0,message:c?`OAuth tokens set for ${c}`:"OAuth tokens set successfully",cloudId:o,siteUrl:u})}catch(e){return D(A(e))}}),x.registerTool("jira_oauth_refresh",{title:"Refresh OAuth Token",description:"Manually refresh the OAuth access token using the refresh token.",inputSchema:a.object({})},async()=>{try{const e=await f();if("oauth"!==e.type)return D({error:"invalid_auth_type",message:"Current authentication is not OAuth. Use basic auth credentials directly."});if(!e.refreshToken)return D({error:"no_refresh_token",message:"No refresh token available. You need to re-authenticate with 'offline_access' scope."});const t=await m(e.clientId,e.clientSecret,e.refreshToken),s={...e,accessToken:t.accessToken,refreshToken:t.refreshToken||e.refreshToken,expiresAt:Date.now()+1e3*t.expiresIn};return await I(s,!1),D({success:!0,message:"OAuth token refreshed successfully",expiresIn:t.expiresIn})}catch(e){return D(A(e))}}),x.registerTool("jira_oauth_list_sites",{title:"List Accessible Jira Sites",description:"List all Jira sites accessible with the current OAuth token.",inputSchema:a.object({})},async()=>{try{const e=await f();if("oauth"!==e.type)return D({error:"invalid_auth_type",message:"This tool requires OAuth authentication. Current auth is basic auth."});const t=await y(e.accessToken);return D({currentCloudId:e.cloudId,sites:t.map(e=>({cloudId:e.id,name:e.name,url:e.url,scopes:e.scopes}))})}catch(e){return D(A(e))}}),x.registerTool("jira_clear_auth",{title:"Clear Jira Auth",description:"Use when the user asks to remove or reset stored Jira credentials.",inputSchema:a.object({})},async()=>(await async function(){d=null;const e=await l();e&&await e.deletePassword(i,n)}(),D("Jira credentials cleared."))),x.registerTool("jira_auth_status",{title:"Get Auth Status",description:"Check the current authentication status and type.",inputSchema:a.object({})},async()=>{try{const e=await f();return"basic"===e.type?D({authenticated:!0,type:"basic",baseUrl:e.baseUrl,email:e.email}):D({authenticated:!0,type:"oauth",cloudId:e.cloudId,hasRefreshToken:!!e.refreshToken,expiresAt:e.expiresAt?new Date(e.expiresAt).toISOString():null})}catch(e){return e instanceof Error&&"MISSING_AUTH"===e.message?D({authenticated:!1,message:"No authentication configured. Use basic auth or OAuth to authenticate."}):D(A(e))}}),x.registerTool("jira_whoami",{title:"Get Jira Profile",description:"Use when the user asks who they are in Jira or wants to verify the Jira account in use."},async()=>{try{const e=w(await f());return D((await e.get("/rest/api/3/myself")).data)}catch(e){return D(A(e))}}),x.registerTool("jira_get_issue",{title:"Get Jira Issue",description:"Get the full details of a Jira issue when the user mentions an issue key like PROJ-123 or asks about a specific ticket.",inputSchema:a.object({issueIdOrKey:a.string().min(1),fields:a.array(a.string()).optional(),expand:a.string().optional()})},async({issueIdOrKey:e,fields:t,expand:s})=>{try{const r=w(await f()),a=t?.length?t:S();return D(j((await r.get(`/rest/api/3/issue/${encodeURIComponent(e)}`,{params:{fields:a.join(","),expand:s}})).data))}catch(e){return D(A(e))}}),x.registerTool("jira_search_issues",{title:"Search Jira Issues",description:"Use when the user asks to find issues matching criteria (JQL), like 'my open bugs' or 'tickets updated this week'.",inputSchema:a.object({jql:a.string().min(1),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(200).optional(),fields:a.array(a.string()).optional(),expand:a.string().optional(),nextPageToken:a.string().optional(),reconcileIssues:a.boolean().optional()})},async({jql:e,startAt:t,maxResults:s,fields:r,expand:a,nextPageToken:i,reconcileIssues:n})=>{try{const o=w(await f()),c=r?.length?r:S(),u=await o.get("/rest/api/3/search/jql",{params:{jql:e,startAt:t,maxResults:s,fields:c.join(","),expand:a,nextPageToken:i,reconcileIssues:n}}),d=Array.isArray(u.data?.issues)?u.data.issues.map(j):[];return D({total:u.data?.total??d.length,issues:d})}catch(e){return D(A(e))}}),x.registerTool("jira_search_issues_summary",{title:"Search Jira Issues (Summary)",description:"Use when the user wants the top results for a Jira search and only needs key, summary, and status.",inputSchema:a.object({jql:a.string().min(1),maxResults:a.number().int().positive().max(50).optional()})},async({jql:e,maxResults:t})=>{try{const s=w(await f()),r=await s.get("/rest/api/3/search/jql",{params:{jql:e,maxResults:t??10,fields:["summary","status"].join(",")}});return D(Array.isArray(r.data?.issues)?r.data.issues.map(v):[])}catch(e){return D(A(e))}}),x.registerTool("jira_resolve",{title:"Resolve Jira Intent",description:"Primary routing tool. Use this tool first when the user intent is clear (get issue, search, or my issues) but the exact Jira tool to call is uncertain.",inputSchema:a.object({intent:a.enum(["get_issue","search","my_issues"]),issueKey:a.string().optional(),jql:a.string().optional(),maxResults:a.number().int().positive().max(50).optional()})},async({intent:e,issueKey:t,jql:s,maxResults:r})=>{try{const a=w(await f());if("get_issue"===e){if(!t)return D({error:"invalid_input",message:"issueKey is required when intent is get_issue."});return D(j((await a.get(`/rest/api/3/issue/${encodeURIComponent(t)}`,{params:{fields:S().join(",")}})).data))}if("search"===e){if(!s)return D({error:"invalid_input",message:"jql is required when intent is search."});const e=await a.get("/rest/api/3/search/jql",{params:{jql:s,maxResults:r??10,fields:["summary","status"].join(",")}});return D(Array.isArray(e.data?.issues)?e.data.issues.map(v):[])}const i=await a.get("/rest/api/3/search/jql",{params:{jql:"assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC",maxResults:r??20,fields:S().join(",")}}),n=Array.isArray(i.data?.issues)?i.data.issues.map(j):[];return D({total:i.data?.total??n.length,issues:n})}catch(e){return D(A(e))}}),x.registerTool("jira_get_issue_summary",{title:"Get Issue Summary",description:"Use when the user wants the summary, description, and acceptance criteria for a specific issue key.",inputSchema:a.object({issueIdOrKey:a.string().min(1)})},async({issueIdOrKey:e})=>{try{const t=w(await f());return D(j((await t.get(`/rest/api/3/issue/${encodeURIComponent(e)}`,{params:{fields:S().join(",")}})).data))}catch(e){return D(A(e))}}),x.registerTool("jira_get_my_open_issues",{title:"Get My Open Issues",description:"Use when the user asks for their open tickets or what they should work on next.",inputSchema:a.object({maxResults:a.number().int().positive().max(50).optional()})},async({maxResults:e})=>{try{const t=w(await f()),s=await t.get("/rest/api/3/search/jql",{params:{jql:"assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC",maxResults:e??20,fields:S().join(",")}}),r=Array.isArray(s.data?.issues)?s.data.issues.map(j):[];return D({total:s.data?.total??r.length,issues:r})}catch(e){return D(A(e))}}),x.registerTool("jira_get_issue_comments",{title:"Get Issue Comments",description:"Use when the user asks for the discussion or comments on a specific ticket; returns a clean list.",inputSchema:a.object({issueIdOrKey:a.string().min(1),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(100).optional()})},async({issueIdOrKey:e,startAt:t,maxResults:s})=>{try{const r=w(await f()),a=await r.get(`/rest/api/3/issue/${encodeURIComponent(e)}/comment`,{params:{startAt:t,maxResults:s}});return D(Array.isArray(a.data?.comments)?a.data.comments.map(e=>({author:e?.author?.displayName||e?.author?.emailAddress||e?.author?.accountId||"",created:e?.created??"",body:_(e?.body)})):[])}catch(e){return D(A(e))}}),x.registerTool("jira_add_comment",{title:"Add Jira Comment",description:"Use when the user asks to add a comment to a specific ticket; confirm intent before posting.",inputSchema:a.object({issueIdOrKey:a.string().min(1),body:a.string().min(1)})},async({issueIdOrKey:e,body:t})=>{try{const s=w(await f()),r=await s.post(`/rest/api/3/issue/${encodeURIComponent(e)}/comment`,{body:k(t)});return D({id:r.data?.id??"",created:r.data?.created??""})}catch(e){return D(A(e))}}),x.registerTool("jira_add_worklog",{title:"Add Work Log",description:"Use when the user wants to log time/work on a specific Jira ticket. Allows specifying time spent, start date/time, and an optional description.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("The issue key (e.g., PROJ-123) to log work against"),timeSpent:a.string().min(1).describe("Time spent in Jira format (e.g., '1h', '30m', '1h 30m', '1d')"),started:a.string().optional().describe("When the work started in ISO 8601 format (e.g., '2026-02-13T14:00:00.000+0000'). Defaults to now if not provided."),comment:a.string().optional().describe("Optional description of the work performed")})},async({issueIdOrKey:e,timeSpent:t,started:s,comment:r})=>{try{const a=w(await f()),i={timeSpent:t};s&&(i.started=s),r&&(i.comment=k(r));const n=await a.post(`/rest/api/3/issue/${encodeURIComponent(e)}/worklog`,i);return D({id:n.data?.id??"",issueId:n.data?.issueId??"",timeSpent:n.data?.timeSpent??"",started:n.data?.started??"",author:n.data?.author?.displayName??n.data?.author?.emailAddress??"",created:n.data?.created??""})}catch(e){return D(A(e))}}),x.registerTool("jira_get_worklogs",{title:"Get Work Logs",description:"Use when the user wants to see work logs recorded on a specific Jira ticket.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("The issue key (e.g., PROJ-123) to get work logs for"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(100).optional()})},async({issueIdOrKey:e,startAt:t,maxResults:s})=>{try{const r=w(await f()),a=await r.get(`/rest/api/3/issue/${encodeURIComponent(e)}/worklog`,{params:{startAt:t,maxResults:s}}),i=Array.isArray(a.data?.worklogs)?a.data.worklogs.map(e=>({id:e?.id??"",author:e?.author?.displayName||e?.author?.emailAddress||"",timeSpent:e?.timeSpent??"",timeSpentSeconds:e?.timeSpentSeconds??0,started:e?.started??"",created:e?.created??"",comment:_(e?.comment)})):[];return D({total:a.data?.total??i.length,worklogs:i})}catch(e){return D(A(e))}}),x.registerTool("jira_list_projects",{title:"List Jira Projects",description:"Use when the user asks which Jira projects they can access or wants a list of projects.",inputSchema:a.object({startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(50).optional()})},async({startAt:e,maxResults:t})=>{try{const s=w(await f());return D((await s.get("/rest/api/3/project/search",{params:{startAt:e,maxResults:t}})).data)}catch(e){return D(A(e))}}),x.registerTool("jira_get_project",{title:"Get Jira Project",description:"Use when the user mentions a project key and asks for project details or metadata.",inputSchema:a.object({projectIdOrKey:a.string().min(1)})},async({projectIdOrKey:e})=>{try{const t=w(await f());return D((await t.get(`/rest/api/3/project/${encodeURIComponent(e)}`)).data)}catch(e){return D(A(e))}}),x.registerTool("jira_create_issue",{title:"Create Jira Issue",description:"Create a new Jira issue. Requires project key, issue type, and summary at minimum.",inputSchema:a.object({projectKey:a.string().min(1).describe("Project key (e.g., 'MXTS')"),issueType:a.string().min(1).describe("Issue type name or ID (e.g., 'Bug', 'Task', 'Story')"),summary:a.string().min(1).describe("Issue title/summary"),description:a.string().optional().describe("Issue description (plain text, will be converted to ADF)"),assignee:a.string().optional().describe("Assignee account ID. Use '-1' for automatic assignment."),reporter:a.string().optional().describe("Reporter account ID"),priority:a.string().optional().describe("Priority name or ID (e.g., 'High', 'Medium', 'Low')"),labels:a.array(a.string()).optional().describe("Array of label strings"),components:a.array(a.string()).optional().describe("Array of component names or IDs"),fixVersions:a.array(a.string()).optional().describe("Array of fix version names or IDs"),affectsVersions:a.array(a.string()).optional().describe("Array of affected version names or IDs"),dueDate:a.string().optional().describe("Due date in YYYY-MM-DD format"),parentKey:a.string().optional().describe("Parent issue key for subtasks"),environment:a.string().optional().describe("Environment description"),originalEstimate:a.string().optional().describe("Original time estimate (e.g., '2h', '1d')"),customFields:a.record(a.string(),a.unknown()).optional().describe("Custom field values as key-value pairs")})},async e=>{try{const t=w(await f()),s=function(e){const t={};if(e.projectKey&&(t.project={key:e.projectKey}),e.issueType&&(t.issuetype=/^\d+$/.test(e.issueType)?{id:e.issueType}:{name:e.issueType}),void 0!==e.summary&&(t.summary=e.summary),void 0!==e.description&&(t.description=e.description?k(e.description):null),void 0!==e.assignee&&(t.assignee=null===e.assignee?null:{accountId:e.assignee}),e.reporter&&(t.reporter={accountId:e.reporter}),e.priority&&(t.priority=/^\d+$/.test(e.priority)?{id:e.priority}:{name:e.priority}),e.labels&&e.labels.length>0&&(t.labels=e.labels),e.components&&e.components.length>0&&(t.components=e.components.map(e=>/^\d+$/.test(e)?{id:e}:{name:e})),e.fixVersions&&e.fixVersions.length>0&&(t.fixVersions=e.fixVersions.map(e=>/^\d+$/.test(e)?{id:e}:{name:e})),e.affectsVersions&&e.affectsVersions.length>0&&(t.versions=e.affectsVersions.map(e=>/^\d+$/.test(e)?{id:e}:{name:e})),void 0!==e.dueDate&&(t.duedate=e.dueDate),e.parentKey&&(t.parent={key:e.parentKey}),e.environment&&(t.environment=k(e.environment)),(e.originalEstimate||e.remainingEstimate)&&(t.timetracking={},e.originalEstimate&&(t.timetracking.originalEstimate=e.originalEstimate),e.remainingEstimate&&(t.timetracking.remainingEstimate=e.remainingEstimate)),e.customFields)for(const[s,r]of Object.entries(e.customFields)){const e=s.startsWith("customfield_")?s:`customfield_${s}`;"string"==typeof r&&r.length,t[e]=r}return t}({projectKey:e.projectKey,issueType:e.issueType,summary:e.summary,description:e.description,assignee:e.assignee,reporter:e.reporter,priority:e.priority,labels:e.labels,components:e.components,fixVersions:e.fixVersions,affectsVersions:e.affectsVersions,dueDate:e.dueDate,parentKey:e.parentKey,environment:e.environment,originalEstimate:e.originalEstimate,customFields:e.customFields}),r=await t.post("/rest/api/3/issue",{fields:s});return D({success:!0,id:r.data?.id??"",key:r.data?.key??"",self:r.data?.self??"",message:`Issue ${r.data?.key} created successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_update_issue",{title:"Update Jira Issue",description:"Update an existing Jira issue. Only provided fields will be modified.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID (e.g., 'MXTS-123')"),summary:a.string().optional().describe("New summary/title"),description:a.string().optional().describe("New description (plain text)"),assignee:a.string().nullable().optional().describe("Assignee account ID. Use null to unassign."),priority:a.string().optional().describe("Priority name or ID"),dueDate:a.string().nullable().optional().describe("Due date (YYYY-MM-DD) or null to clear"),labels:a.object({add:a.array(a.string()).optional(),remove:a.array(a.string()).optional(),set:a.array(a.string()).optional()}).optional().describe("Label operations: add, remove, or set"),components:a.object({add:a.array(a.string()).optional(),remove:a.array(a.string()).optional(),set:a.array(a.string()).optional()}).optional().describe("Component operations: add, remove, or set"),fixVersions:a.object({add:a.array(a.string()).optional(),remove:a.array(a.string()).optional(),set:a.array(a.string()).optional()}).optional().describe("Fix version operations: add, remove, or set"),customFields:a.record(a.string(),a.unknown()).optional().describe("Custom field values"),notifyUsers:a.boolean().optional().default(!0).describe("Send notifications to watchers")})},async e=>{try{const t=w(await f()),s={},r={};if(void 0!==e.summary&&(r.summary=e.summary),void 0!==e.description&&(r.description=e.description?k(e.description):null),void 0!==e.assignee&&(r.assignee=null===e.assignee?null:{accountId:e.assignee}),void 0!==e.priority&&(r.priority=/^\d+$/.test(e.priority)?{id:e.priority}:{name:e.priority}),void 0!==e.dueDate&&(r.duedate=e.dueDate),e.customFields)for(const[t,s]of Object.entries(e.customFields)){r[t.startsWith("customfield_")?t:`customfield_${t}`]=s}Object.keys(r).length>0&&(s.fields=r);const a=function(e){const t={};if(e.labels){const s=[];e.labels.add&&e.labels.add.forEach(e=>s.push({add:e})),e.labels.remove&&e.labels.remove.forEach(e=>s.push({remove:e})),e.labels.set&&s.push({set:e.labels.set}),t.labels=s}if(e.components){const s=[];e.components.add&&e.components.add.forEach(e=>s.push({add:/^\d+$/.test(e)?{id:e}:{name:e}})),e.components.remove&&e.components.remove.forEach(e=>s.push({remove:/^\d+$/.test(e)?{id:e}:{name:e}})),e.components.set&&s.push({set:e.components.set.map(e=>/^\d+$/.test(e)?{id:e}:{name:e})}),t.components=s}if(e.fixVersions){const s=[];e.fixVersions.add&&e.fixVersions.add.forEach(e=>s.push({add:/^\d+$/.test(e)?{id:e}:{name:e}})),e.fixVersions.remove&&e.fixVersions.remove.forEach(e=>s.push({remove:/^\d+$/.test(e)?{id:e}:{name:e}})),e.fixVersions.set&&s.push({set:e.fixVersions.set.map(e=>/^\d+$/.test(e)?{id:e}:{name:e})}),t.fixVersions=s}if(e.affectsVersions){const s=[];e.affectsVersions.add&&e.affectsVersions.add.forEach(e=>s.push({add:/^\d+$/.test(e)?{id:e}:{name:e}})),e.affectsVersions.remove&&e.affectsVersions.remove.forEach(e=>s.push({remove:/^\d+$/.test(e)?{id:e}:{name:e}})),e.affectsVersions.set&&s.push({set:e.affectsVersions.set.map(e=>/^\d+$/.test(e)?{id:e}:{name:e})}),t.versions=s}return t}({labels:e.labels,components:e.components,fixVersions:e.fixVersions});return Object.keys(a).length>0&&(s.update=a),0===Object.keys(s).length?D({error:"no_changes",message:"No fields provided to update"}):(await t.put(`/rest/api/3/issue/${encodeURIComponent(e.issueIdOrKey)}`,s,{params:{notifyUsers:e.notifyUsers??!0}}),D({success:!0,key:e.issueIdOrKey,message:`Issue ${e.issueIdOrKey} updated successfully`}))}catch(e){return D(A(e))}}),x.registerTool("jira_delete_issue",{title:"Delete Jira Issue",description:"Delete a Jira issue. Requires explicit confirmation. Use with caution - this action cannot be undone.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID to delete"),deleteSubtasks:a.boolean().optional().default(!1).describe("Also delete subtasks"),confirmDelete:a.boolean().describe("Must be true to confirm deletion")})},async({issueIdOrKey:e,deleteSubtasks:t,confirmDelete:s})=>{try{if(!s)return D({error:"confirmation_required",message:"Deletion not confirmed. Set confirmDelete: true to proceed. This action cannot be undone.",issueKey:e});const r=w(await f());return await r.delete(`/rest/api/3/issue/${encodeURIComponent(e)}`,{params:{deleteSubtasks:t??!1}}),D({success:!0,message:`Issue ${e} deleted successfully${t?" (including subtasks)":""}`})}catch(e){return D(A(e))}}),x.registerTool("jira_assign_issue",{title:"Assign Jira Issue",description:"Assign or unassign a Jira issue to a user.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID"),accountId:a.string().nullable().describe("User account ID to assign, '-1' for automatic, or null to unassign")})},async({issueIdOrKey:e,accountId:t})=>{try{const s=w(await f());await s.put(`/rest/api/3/issue/${encodeURIComponent(e)}/assignee`,{accountId:t});return D({success:!0,key:e,message:`Issue ${e} ${null===t?"unassigned":"assigned"} successfully`,assignee:t})}catch(e){return D(A(e))}}),x.registerTool("jira_get_transitions",{title:"Get Issue Transitions",description:"Get available workflow transitions for an issue. Use before transitioning to see valid options.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID"),expand:a.string().optional().describe("Expand options: 'transitions.fields' to include required fields")})},async({issueIdOrKey:e,expand:t})=>{try{const s=w(await f()),r=await s.get(`/rest/api/3/issue/${encodeURIComponent(e)}/transitions`,{params:{expand:t}});return D({issueKey:e,transitions:Array.isArray(r.data?.transitions)?r.data.transitions.map(e=>({id:e.id,name:e.name,to:{id:e.to?.id,name:e.to?.name,statusCategory:e.to?.statusCategory?.name},hasScreen:e.hasScreen??!1,isGlobal:e.isGlobal??!1,isInitial:e.isInitial??!1,isConditional:e.isConditional??!1,fields:e.fields?Object.keys(e.fields):[]})):[]})}catch(e){return D(A(e))}}),x.registerTool("jira_transition_issue",{title:"Transition Jira Issue",description:"Move a Jira issue to a different status by executing a workflow transition.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID"),transitionId:a.string().min(1).describe("Transition ID (get from jira_get_transitions)"),comment:a.string().optional().describe("Comment to add during transition"),resolution:a.string().optional().describe("Resolution name for closing transitions (e.g., 'Done', 'Fixed')"),fields:a.record(a.string(),a.unknown()).optional().describe("Additional fields required by the transition")})},async({issueIdOrKey:e,transitionId:t,comment:s,resolution:r,fields:a})=>{try{const i=w(await f()),n={transition:{id:t}};if(a||r){const e={...a};r&&(e.resolution={name:r}),n.fields=e}return s&&(n.update={comment:[{add:{body:k(s)}}]}),await i.post(`/rest/api/3/issue/${encodeURIComponent(e)}/transitions`,n),D({success:!0,key:e,transitionId:t,message:`Issue ${e} transitioned successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_issue_types",{title:"Get Issue Types",description:"Get available issue types, optionally filtered by project.",inputSchema:a.object({projectKey:a.string().optional().describe("Filter issue types for a specific project")})},async({projectKey:e})=>{try{const t=w(await f());let s;if(e){const r=await t.get(`/rest/api/3/project/${encodeURIComponent(e)}`);s=r.data?.issueTypes||[]}else{s=(await t.get("/rest/api/3/issuetype")).data||[]}return D(s.map(e=>({id:e.id,name:e.name,description:e.description||"",subtask:e.subtask??!1,hierarchyLevel:e.hierarchyLevel})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_priorities",{title:"Get Priorities",description:"Get available priority levels for issues.",inputSchema:a.object({})},async()=>{try{const e=w(await f());return D(((await e.get("/rest/api/3/priority")).data||[]).map(e=>({id:e.id,name:e.name,description:e.description||"",iconUrl:e.iconUrl})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_statuses",{title:"Get Statuses",description:"Get available statuses, optionally filtered by project.",inputSchema:a.object({projectKey:a.string().optional().describe("Filter statuses for a specific project")})},async({projectKey:e})=>{try{const t=w(await f());if(e){return D((await t.get(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`)).data||[])}return D(((await t.get("/rest/api/3/status")).data||[]).map(e=>({id:e.id,name:e.name,description:e.description||"",statusCategory:e.statusCategory?.name})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_components",{title:"Get Project Components",description:"Get components for a specific project.",inputSchema:a.object({projectKey:a.string().min(1).describe("Project key")})},async({projectKey:e})=>{try{const t=w(await f());return D(((await t.get(`/rest/api/3/project/${encodeURIComponent(e)}/components`)).data||[]).map(e=>({id:e.id,name:e.name,description:e.description||"",lead:e.lead?.displayName,assigneeType:e.assigneeType})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_versions",{title:"Get Project Versions",description:"Get versions for a specific project.",inputSchema:a.object({projectKey:a.string().min(1).describe("Project key"),released:a.boolean().optional().describe("Filter by released status")})},async({projectKey:e,released:t})=>{try{const s=w(await f());let r=(await s.get(`/rest/api/3/project/${encodeURIComponent(e)}/versions`)).data||[];return void 0!==t&&(r=r.filter(e=>e.released===t)),D(r.map(e=>({id:e.id,name:e.name,description:e.description||"",released:e.released??!1,archived:e.archived??!1,releaseDate:e.releaseDate,startDate:e.startDate})))}catch(e){return D(A(e))}}),x.registerTool("jira_search_users",{title:"Search Jira Users",description:"Search for Jira users by name, email, or username.",inputSchema:a.object({query:a.string().min(1).describe("Search query (name, email, or username)"),projectKey:a.string().optional().describe("Filter users with access to this project"),maxResults:a.number().int().positive().max(50).optional().default(10)})},async({query:e,projectKey:t,maxResults:s})=>{try{const r=w(await f());let a=(await r.get("/rest/api/3/user/search",{params:{query:e,maxResults:s??10}})).data||[];if(t&&a.length>0)try{a=(await r.get("/rest/api/3/user/assignable/search",{params:{query:e,project:t,maxResults:s??10}})).data||[]}catch{}return D(a.map(e=>({accountId:e.accountId,displayName:e.displayName,emailAddress:e.emailAddress,active:e.active??!0,avatarUrl:e.avatarUrls?.["48x48"]})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_changelog",{title:"Get Issue Changelog",description:"Get the history of changes for an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(100).optional().default(20)})},async({issueIdOrKey:e,startAt:t,maxResults:s})=>{try{const r=w(await f()),a=await r.get(`/rest/api/3/issue/${encodeURIComponent(e)}/changelog`,{params:{startAt:t,maxResults:s??20}}),i=Array.isArray(a.data?.values)?a.data.values.map(e=>({id:e.id,author:e.author?.displayName||e.author?.emailAddress||"",created:e.created,items:(e.items||[]).map(e=>({field:e.field,fieldtype:e.fieldtype,from:e.fromString||e.from,to:e.toString||e.to}))})):[];return D({total:a.data?.total??i.length,startAt:a.data?.startAt??0,changes:i})}catch(e){return D(A(e))}}),x.registerTool("jira_get_boards",{title:"Get Jira Boards",description:"Get all Scrum and Kanban boards, optionally filtered by project or type.",inputSchema:a.object({projectKeyOrId:a.string().optional().describe("Filter boards by project"),type:a.enum(["scrum","kanban","simple"]).optional().describe("Filter by board type"),name:a.string().optional().describe("Filter boards by name (contains)"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(50).optional().default(50)})},async({projectKeyOrId:e,type:t,name:s,startAt:r,maxResults:a})=>{try{const i=w(await f()),n=await i.get("/rest/agile/1.0/board",{params:{projectKeyOrId:e,type:t,name:s,startAt:r,maxResults:a??50}}),o=Array.isArray(n.data?.values)?n.data.values.map(e=>({id:e.id,name:e.name,type:e.type,projectKey:e.location?.projectKey,projectName:e.location?.displayName})):[];return D({total:n.data?.total??o.length,startAt:n.data?.startAt??0,boards:o})}catch(e){return D(A(e))}}),x.registerTool("jira_get_board",{title:"Get Board Details",description:"Get details of a specific board including configuration.",inputSchema:a.object({boardId:a.number().int().positive().describe("Board ID")})},async({boardId:e})=>{try{const t=w(await f()),s=await t.get(`/rest/agile/1.0/board/${e}`);return D({id:s.data?.id,name:s.data?.name,type:s.data?.type,self:s.data?.self,location:s.data?.location})}catch(e){return D(A(e))}}),x.registerTool("jira_get_board_configuration",{title:"Get Board Configuration",description:"Get the configuration of a board including columns, estimation, and ranking.",inputSchema:a.object({boardId:a.number().int().positive().describe("Board ID")})},async({boardId:e})=>{try{const t=w(await f()),s=await t.get(`/rest/agile/1.0/board/${e}/configuration`);return D({id:s.data?.id,name:s.data?.name,type:s.data?.type,filter:s.data?.filter,columnConfig:s.data?.columnConfig,estimation:s.data?.estimation,ranking:s.data?.ranking})}catch(e){return D(A(e))}}),x.registerTool("jira_get_sprints",{title:"Get Sprints",description:"Get sprints for a board, optionally filtered by state.",inputSchema:a.object({boardId:a.number().int().positive().describe("Board ID"),state:a.enum(["future","active","closed"]).optional().describe("Filter by sprint state"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(50).optional().default(50)})},async({boardId:e,state:t,startAt:s,maxResults:r})=>{try{const a=w(await f()),i=await a.get(`/rest/agile/1.0/board/${e}/sprint`,{params:{state:t,startAt:s,maxResults:r??50}}),n=Array.isArray(i.data?.values)?i.data.values.map(e=>({id:e.id,name:e.name,state:e.state,startDate:e.startDate,endDate:e.endDate,completeDate:e.completeDate,originBoardId:e.originBoardId,goal:e.goal})):[];return D({total:i.data?.total??n.length,startAt:i.data?.startAt??0,sprints:n})}catch(e){return D(A(e))}}),x.registerTool("jira_get_sprint",{title:"Get Sprint Details",description:"Get details of a specific sprint.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Sprint ID")})},async({sprintId:e})=>{try{const t=w(await f()),s=await t.get(`/rest/agile/1.0/sprint/${e}`);return D({id:s.data?.id,name:s.data?.name,state:s.data?.state,startDate:s.data?.startDate,endDate:s.data?.endDate,completeDate:s.data?.completeDate,originBoardId:s.data?.originBoardId,goal:s.data?.goal})}catch(e){return D(A(e))}}),x.registerTool("jira_create_sprint",{title:"Create Sprint",description:"Create a new sprint on a board.",inputSchema:a.object({boardId:a.number().int().positive().describe("Board ID"),name:a.string().min(1).describe("Sprint name"),startDate:a.string().optional().describe("Start date (ISO 8601)"),endDate:a.string().optional().describe("End date (ISO 8601)"),goal:a.string().optional().describe("Sprint goal")})},async({boardId:e,name:t,startDate:s,endDate:r,goal:a})=>{try{const i=w(await f()),n=await i.post("/rest/agile/1.0/sprint",{originBoardId:e,name:t,startDate:s,endDate:r,goal:a});return D({success:!0,id:n.data?.id,name:n.data?.name,state:n.data?.state,message:`Sprint "${t}" created successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_update_sprint",{title:"Update Sprint",description:"Update sprint details including name, dates, and goal.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Sprint ID"),name:a.string().optional().describe("New sprint name"),state:a.enum(["future","active","closed"]).optional().describe("Sprint state"),startDate:a.string().optional().describe("Start date (ISO 8601)"),endDate:a.string().optional().describe("End date (ISO 8601)"),goal:a.string().optional().describe("Sprint goal")})},async({sprintId:e,name:t,state:s,startDate:r,endDate:a,goal:i})=>{try{const n=w(await f()),o={};if(void 0!==t&&(o.name=t),void 0!==s&&(o.state=s),void 0!==r&&(o.startDate=r),void 0!==a&&(o.endDate=a),void 0!==i&&(o.goal=i),0===Object.keys(o).length)return D({error:"no_changes",message:"No fields provided to update"});const c=await n.put(`/rest/agile/1.0/sprint/${e}`,o);return D({success:!0,id:c.data?.id??e,name:c.data?.name,state:c.data?.state,message:"Sprint updated successfully"})}catch(e){return D(A(e))}}),x.registerTool("jira_start_sprint",{title:"Start Sprint",description:"Start a sprint that is in 'future' state.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Sprint ID"),startDate:a.string().optional().describe("Start date (defaults to now)"),endDate:a.string().describe("End date (required for starting a sprint)")})},async({sprintId:e,startDate:t,endDate:s})=>{try{const r=w(await f()),a=await r.post(`/rest/agile/1.0/sprint/${e}`,{state:"active",startDate:t||(new Date).toISOString(),endDate:s});return D({success:!0,id:a.data?.id??e,state:"active",message:"Sprint started successfully"})}catch(e){return D(A(e))}}),x.registerTool("jira_complete_sprint",{title:"Complete Sprint",description:"Complete an active sprint. Optionally move incomplete issues to another sprint or backlog.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Sprint ID to complete"),moveIncompleteIssuesTo:a.number().int().positive().optional().describe("Sprint ID to move incomplete issues to (omit to move to backlog)")})},async({sprintId:e,moveIncompleteIssuesTo:t})=>{try{const s=w(await f());return await s.post(`/rest/agile/1.0/sprint/${e}`,{state:"closed"}),D({success:!0,id:e,state:"closed",message:"Sprint completed successfully",incompleteIssuesMovedTo:t||"backlog"})}catch(e){return D(A(e))}}),x.registerTool("jira_delete_sprint",{title:"Delete Sprint",description:"Delete a sprint. Use with caution - cannot be undone.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Sprint ID to delete"),confirmDelete:a.boolean().describe("Must be true to confirm deletion")})},async({sprintId:e,confirmDelete:t})=>{try{if(!t)return D({error:"confirmation_required",message:"Deletion not confirmed. Set confirmDelete: true to proceed.",sprintId:e});const s=w(await f());return await s.delete(`/rest/agile/1.0/sprint/${e}`),D({success:!0,message:`Sprint ${e} deleted successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_sprint_issues",{title:"Get Sprint Issues",description:"Get all issues in a sprint.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Sprint ID"),jql:a.string().optional().describe("Additional JQL filter"),fields:a.array(a.string()).optional().describe("Fields to return"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(100).optional().default(50)})},async({sprintId:e,jql:t,fields:s,startAt:r,maxResults:a})=>{try{const i=w(await f()),n=await i.get(`/rest/agile/1.0/sprint/${e}/issue`,{params:{jql:t,fields:s?.join(","),startAt:r,maxResults:a??50}}),o=Array.isArray(n.data?.issues)?n.data.issues.map(e=>({key:e.key,summary:e.fields?.summary,status:e.fields?.status?.name,assignee:e.fields?.assignee?.displayName,issueType:e.fields?.issuetype?.name,priority:e.fields?.priority?.name,storyPoints:e.fields?.I})):[];return D({total:n.data?.total??o.length,startAt:n.data?.startAt??0,sprintId:e,issues:o})}catch(e){return D(A(e))}}),x.registerTool("jira_move_issues_to_sprint",{title:"Move Issues to Sprint",description:"Move issues to a sprint.",inputSchema:a.object({sprintId:a.number().int().positive().describe("Target sprint ID"),issueKeys:a.array(a.string().min(1)).min(1).describe("Issue keys to move")})},async({sprintId:e,issueKeys:t})=>{try{const s=w(await f());return await s.post(`/rest/agile/1.0/sprint/${e}/issue`,{issues:t}),D({success:!0,sprintId:e,issuesMoved:t,message:`${t.length} issue(s) moved to sprint ${e}`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_backlog_issues",{title:"Get Backlog Issues",description:"Get issues in the backlog (not in any active sprint) for a board.",inputSchema:a.object({boardId:a.number().int().positive().describe("Board ID"),jql:a.string().optional().describe("Additional JQL filter"),fields:a.array(a.string()).optional().describe("Fields to return"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(100).optional().default(50)})},async({boardId:e,jql:t,fields:s,startAt:r,maxResults:a})=>{try{const i=w(await f()),n=await i.get(`/rest/agile/1.0/board/${e}/backlog`,{params:{jql:t,fields:s?.join(","),startAt:r,maxResults:a??50}}),o=Array.isArray(n.data?.issues)?n.data.issues.map(e=>({key:e.key,summary:e.fields?.summary,status:e.fields?.status?.name,assignee:e.fields?.assignee?.displayName,issueType:e.fields?.issuetype?.name,priority:e.fields?.priority?.name})):[];return D({total:n.data?.total??o.length,startAt:n.data?.startAt??0,boardId:e,issues:o})}catch(e){return D(A(e))}}),x.registerTool("jira_move_issues_to_backlog",{title:"Move Issues to Backlog",description:"Move issues from a sprint back to the backlog.",inputSchema:a.object({issueKeys:a.array(a.string().min(1)).min(1).describe("Issue keys to move to backlog")})},async({issueKeys:e})=>{try{const t=w(await f());return await t.post("/rest/agile/1.0/backlog/issue",{issues:e}),D({success:!0,issuesMoved:e,message:`${e.length} issue(s) moved to backlog`})}catch(e){return D(A(e))}}),x.registerTool("jira_rank_issues",{title:"Rank Issues",description:"Change the rank of issues on a board by placing them before or after another issue.",inputSchema:a.object({issueKeys:a.array(a.string().min(1)).min(1).describe("Issue keys to rank"),rankBeforeIssue:a.string().optional().describe("Issue key to rank before"),rankAfterIssue:a.string().optional().describe("Issue key to rank after")})},async({issueKeys:e,rankBeforeIssue:t,rankAfterIssue:s})=>{try{if(!t&&!s)return D({error:"invalid_parameters",message:"Either rankBeforeIssue or rankAfterIssue must be provided"});const r=w(await f()),a={issues:e};return t?a.rankBeforeIssue=t:s&&(a.rankAfterIssue=s),await r.put("/rest/agile/1.0/issue/rank",a),D({success:!0,issuesRanked:e,message:`${e.length} issue(s) ranked successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_issue_links",{title:"Get Issue Links",description:"Get all linked issues for a specific issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f()),s=await t.get(`/rest/api/3/issue/${encodeURIComponent(e)}`,{params:{fields:"issuelinks"}});return D({issueKey:e,links:Array.isArray(s.data?.fields?.issuelinks)?s.data.fields.issuelinks.map(e=>{const t=!!e.inwardIssue,s=t?e.inwardIssue:e.outwardIssue;return{id:e.id,type:e.type?.name,direction:t?"inward":"outward",description:t?e.type?.inward:e.type?.outward,linkedIssue:{key:s?.key,summary:s?.fields?.summary,status:s?.fields?.status?.name,issueType:s?.fields?.issuetype?.name}}}):[]})}catch(e){return D(A(e))}}),x.registerTool("jira_create_issue_link",{title:"Link Issues",description:"Create a link between two issues.",inputSchema:a.object({inwardIssue:a.string().min(1).describe("Inward issue key (the 'from' issue)"),outwardIssue:a.string().min(1).describe("Outward issue key (the 'to' issue)"),linkType:a.string().min(1).describe("Link type name (e.g., 'Blocks', 'Relates', 'Duplicates')"),comment:a.string().optional().describe("Comment to add with the link")})},async({inwardIssue:e,outwardIssue:t,linkType:s,comment:r})=>{try{const a=w(await f()),i={type:{name:s},inwardIssue:{key:e},outwardIssue:{key:t}};return r&&(i.comment={body:k(r)}),await a.post("/rest/api/3/issueLink",i),D({success:!0,message:`Link created: ${e} ${s} ${t}`,inwardIssue:e,outwardIssue:t,linkType:s})}catch(e){return D(A(e))}}),x.registerTool("jira_delete_issue_link",{title:"Delete Issue Link",description:"Remove a link between issues.",inputSchema:a.object({linkId:a.string().min(1).describe("Link ID to delete (get from jira_get_issue_links)")})},async({linkId:e})=>{try{const t=w(await f());return await t.delete(`/rest/api/3/issueLink/${e}`),D({success:!0,message:`Link ${e} deleted successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_link_types",{title:"Get Issue Link Types",description:"Get available link types for linking issues.",inputSchema:a.object({})},async()=>{try{const e=w(await f()),t=await e.get("/rest/api/3/issueLinkType");return D((t.data?.issueLinkTypes||[]).map(e=>({id:e.id,name:e.name,inward:e.inward,outward:e.outward})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_watchers",{title:"Get Issue Watchers",description:"Get the list of users watching an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f()),s=await t.get(`/rest/api/3/issue/${encodeURIComponent(e)}/watchers`),r=Array.isArray(s.data?.watchers)?s.data.watchers.map(e=>({accountId:e.accountId,displayName:e.displayName,emailAddress:e.emailAddress})):[];return D({issueKey:e,watchCount:s.data?.watchCount??r.length,isWatching:s.data?.isWatching??!1,watchers:r})}catch(e){return D(A(e))}}),x.registerTool("jira_add_watcher",{title:"Add Issue Watcher",description:"Add a user to watch an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID"),accountId:a.string().min(1).describe("User account ID to add as watcher")})},async({issueIdOrKey:e,accountId:t})=>{try{const s=w(await f());return await s.post(`/rest/api/3/issue/${encodeURIComponent(e)}/watchers`,JSON.stringify(t),{headers:{"Content-Type":"application/json"}}),D({success:!0,issueKey:e,accountId:t,message:"User added as watcher"})}catch(e){return D(A(e))}}),x.registerTool("jira_remove_watcher",{title:"Remove Issue Watcher",description:"Remove a user from watching an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID"),accountId:a.string().min(1).describe("User account ID to remove")})},async({issueIdOrKey:e,accountId:t})=>{try{const s=w(await f());return await s.delete(`/rest/api/3/issue/${encodeURIComponent(e)}/watchers`,{params:{accountId:t}}),D({success:!0,issueKey:e,accountId:t,message:"User removed from watchers"})}catch(e){return D(A(e))}}),x.registerTool("jira_get_votes",{title:"Get Issue Votes",description:"Get the vote count and voters for an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f()),s=await t.get(`/rest/api/3/issue/${encodeURIComponent(e)}/votes`),r=Array.isArray(s.data?.voters)?s.data.voters.map(e=>({accountId:e.accountId,displayName:e.displayName})):[];return D({issueKey:e,votes:s.data?.votes??0,hasVoted:s.data?.hasVoted??!1,voters:r})}catch(e){return D(A(e))}}),x.registerTool("jira_add_vote",{title:"Vote for Issue",description:"Add your vote to an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f());return await t.post(`/rest/api/3/issue/${encodeURIComponent(e)}/votes`),D({success:!0,issueKey:e,message:"Vote added successfully"})}catch(e){return D(A(e))}}),x.registerTool("jira_remove_vote",{title:"Remove Vote",description:"Remove your vote from an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f());return await t.delete(`/rest/api/3/issue/${encodeURIComponent(e)}/votes`),D({success:!0,issueKey:e,message:"Vote removed successfully"})}catch(e){return D(A(e))}}),x.registerTool("jira_get_attachments",{title:"Get Issue Attachments",description:"Get all attachments for an issue.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f()),s=await t.get(`/rest/api/3/issue/${encodeURIComponent(e)}`,{params:{fields:"attachment"}});return D({issueKey:e,attachments:Array.isArray(s.data?.fields?.attachment)?s.data.fields.attachment.map(e=>({id:e.id,filename:e.filename,size:e.size,mimeType:e.mimeType,content:e.content,thumbnail:e.thumbnail,author:e.author?.displayName,created:e.created})):[]})}catch(e){return D(A(e))}}),x.registerTool("jira_delete_attachment",{title:"Delete Attachment",description:"Delete an attachment from an issue.",inputSchema:a.object({attachmentId:a.string().min(1).describe("Attachment ID to delete")})},async({attachmentId:e})=>{try{const t=w(await f());return await t.delete(`/rest/api/3/attachment/${e}`),D({success:!0,message:`Attachment ${e} deleted successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_epics",{title:"Get Epics",description:"Get epics for a board.",inputSchema:a.object({boardId:a.number().int().positive().describe("Board ID"),done:a.enum(["true","false"]).optional().describe("Filter by completion status"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(50).optional().default(50)})},async({boardId:e,done:t,startAt:s,maxResults:r})=>{try{const a=w(await f()),i=await a.get(`/rest/agile/1.0/board/${e}/epic`,{params:{done:t,startAt:s,maxResults:r??50}}),n=Array.isArray(i.data?.values)?i.data.values.map(e=>({id:e.id,key:e.key,name:e.name,summary:e.summary,done:e.done??!1})):[];return D({total:i.data?.total??n.length,startAt:i.data?.startAt??0,boardId:e,epics:n})}catch(e){return D(A(e))}}),x.registerTool("jira_get_epic_issues",{title:"Get Epic Issues",description:"Get all issues belonging to an epic.",inputSchema:a.object({epicIdOrKey:a.string().min(1).describe("Epic ID or key"),jql:a.string().optional().describe("Additional JQL filter"),fields:a.array(a.string()).optional().describe("Fields to return"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(100).optional().default(50)})},async({epicIdOrKey:e,jql:t,fields:s,startAt:r,maxResults:a})=>{try{const i=w(await f()),n=await i.get(`/rest/agile/1.0/epic/${e}/issue`,{params:{jql:t,fields:s?.join(","),startAt:r,maxResults:a??50}}),o=Array.isArray(n.data?.issues)?n.data.issues.map(e=>({key:e.key,summary:e.fields?.summary,status:e.fields?.status?.name,assignee:e.fields?.assignee?.displayName,issueType:e.fields?.issuetype?.name})):[];return D({total:n.data?.total??o.length,startAt:n.data?.startAt??0,epicKey:e,issues:o})}catch(e){return D(A(e))}}),x.registerTool("jira_move_issues_to_epic",{title:"Move Issues to Epic",description:"Move issues to an epic.",inputSchema:a.object({epicIdOrKey:a.string().min(1).describe("Epic ID or key"),issueKeys:a.array(a.string().min(1)).min(1).describe("Issue keys to move")})},async({epicIdOrKey:e,issueKeys:t})=>{try{const s=w(await f());return await s.post(`/rest/agile/1.0/epic/${e}/issue`,{issues:t}),D({success:!0,epicKey:e,issuesMoved:t,message:`${t.length} issue(s) moved to epic ${e}`})}catch(e){return D(A(e))}}),x.registerTool("jira_remove_issues_from_epic",{title:"Remove Issues from Epic",description:"Remove issues from their epic (move to no epic).",inputSchema:a.object({issueKeys:a.array(a.string().min(1)).min(1).describe("Issue keys to remove from epic")})},async({issueKeys:e})=>{try{const t=w(await f());return await t.post("/rest/agile/1.0/epic/none/issue",{issues:e}),D({success:!0,issuesRemoved:e,message:`${e.length} issue(s) removed from epic`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_fields",{title:"Get All Fields",description:"Get all available fields including custom fields.",inputSchema:a.object({})},async()=>{try{const e=w(await f());return D(((await e.get("/rest/api/3/field")).data||[]).map(e=>({id:e.id,key:e.key,name:e.name,custom:e.custom??!1,orderable:e.orderable??!1,navigable:e.navigable??!1,searchable:e.searchable??!1,clauseNames:e.clauseNames||[],schema:e.schema})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_create_metadata",{title:"Get Create Issue Metadata",description:"Get metadata for creating issues in a project, including required fields.",inputSchema:a.object({projectKeys:a.array(a.string()).optional().describe("Project keys to get metadata for"),projectIds:a.array(a.string()).optional().describe("Project IDs to get metadata for"),issuetypeNames:a.array(a.string()).optional().describe("Issue type names to filter"),expand:a.string().optional().describe("Expand options")})},async({projectKeys:e,projectIds:t,issuetypeNames:s,expand:r})=>{try{const a=w(await f());return D((await a.get("/rest/api/3/issue/createmeta",{params:{projectKeys:e?.join(","),projectIds:t?.join(","),issuetypeNames:s?.join(","),expand:r||"projects.issuetypes.fields"}})).data)}catch(e){return D(A(e))}}),x.registerTool("jira_get_edit_metadata",{title:"Get Edit Issue Metadata",description:"Get metadata for editing a specific issue, including editable fields.",inputSchema:a.object({issueIdOrKey:a.string().min(1).describe("Issue key or ID")})},async({issueIdOrKey:e})=>{try{const t=w(await f());return D((await t.get(`/rest/api/3/issue/${encodeURIComponent(e)}/editmeta`)).data)}catch(e){return D(A(e))}}),x.registerTool("jira_get_filters",{title:"Get Filters",description:"Get saved filters, optionally filtered by name.",inputSchema:a.object({filterName:a.string().optional().describe("Filter by name (contains)"),owner:a.string().optional().describe("Filter by owner account ID"),expand:a.string().optional().describe("Expand options: description, owner, jql, viewUrl, searchUrl, favourite, favouritedCount, sharePermissions"),startAt:a.number().int().nonnegative().optional(),maxResults:a.number().int().positive().max(50).optional().default(50)})},async({filterName:e,owner:t,expand:s,startAt:r,maxResults:a})=>{try{const i=w(await f()),n=await i.get("/rest/api/3/filter/search",{params:{filterName:e,owner:t,expand:s,startAt:r,maxResults:a??50}}),o=Array.isArray(n.data?.values)?n.data.values.map(e=>({id:e.id,name:e.name,description:e.description,owner:e.owner?.displayName,jql:e.jql,favourite:e.favourite??!1,favouritedCount:e.favouritedCount??0})):[];return D({total:n.data?.total??o.length,startAt:n.data?.startAt??0,filters:o})}catch(e){return D(A(e))}}),x.registerTool("jira_get_filter",{title:"Get Filter Details",description:"Get details of a specific filter.",inputSchema:a.object({filterId:a.string().min(1).describe("Filter ID"),expand:a.string().optional().describe("Expand options")})},async({filterId:e,expand:t})=>{try{const s=w(await f()),r=await s.get(`/rest/api/3/filter/${e}`,{params:{expand:t}});return D({id:r.data?.id,name:r.data?.name,description:r.data?.description,owner:r.data?.owner?.displayName,jql:r.data?.jql,favourite:r.data?.favourite??!1,sharePermissions:r.data?.sharePermissions})}catch(e){return D(A(e))}}),x.registerTool("jira_create_filter",{title:"Create Filter",description:"Create a new saved filter.",inputSchema:a.object({name:a.string().min(1).describe("Filter name"),jql:a.string().min(1).describe("JQL query"),description:a.string().optional().describe("Filter description"),favourite:a.boolean().optional().describe("Mark as favourite")})},async({name:e,jql:t,description:s,favourite:r})=>{try{const a=w(await f()),i=await a.post("/rest/api/3/filter",{name:e,jql:t,description:s,favourite:r});return D({success:!0,id:i.data?.id,name:i.data?.name,jql:i.data?.jql,message:`Filter "${e}" created successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_update_filter",{title:"Update Filter",description:"Update an existing filter.",inputSchema:a.object({filterId:a.string().min(1).describe("Filter ID"),name:a.string().optional().describe("New filter name"),jql:a.string().optional().describe("New JQL query"),description:a.string().optional().describe("New description"),favourite:a.boolean().optional().describe("Favourite status")})},async({filterId:e,name:t,jql:s,description:r,favourite:a})=>{try{const i=w(await f()),n={};if(void 0!==t&&(n.name=t),void 0!==s&&(n.jql=s),void 0!==r&&(n.description=r),void 0!==a&&(n.favourite=a),0===Object.keys(n).length)return D({error:"no_changes",message:"No fields provided to update"});const o=await i.put(`/rest/api/3/filter/${e}`,n);return D({success:!0,id:o.data?.id??e,name:o.data?.name,message:"Filter updated successfully"})}catch(e){return D(A(e))}}),x.registerTool("jira_delete_filter",{title:"Delete Filter",description:"Delete a saved filter.",inputSchema:a.object({filterId:a.string().min(1).describe("Filter ID to delete"),confirmDelete:a.boolean().describe("Must be true to confirm deletion")})},async({filterId:e,confirmDelete:t})=>{try{if(!t)return D({error:"confirmation_required",message:"Deletion not confirmed. Set confirmDelete: true to proceed.",filterId:e});const s=w(await f());return await s.delete(`/rest/api/3/filter/${e}`),D({success:!0,message:`Filter ${e} deleted successfully`})}catch(e){return D(A(e))}}),x.registerTool("jira_get_my_filters",{title:"Get My Filters",description:"Get filters owned by the current user.",inputSchema:a.object({expand:a.string().optional().describe("Expand options")})},async({expand:e})=>{try{const t=w(await f());return D(((await t.get("/rest/api/3/filter/my",{params:{expand:e}})).data||[]).map(e=>({id:e.id,name:e.name,description:e.description,jql:e.jql,favourite:e.favourite??!1})))}catch(e){return D(A(e))}}),x.registerTool("jira_get_favourite_filters",{title:"Get Favourite Filters",description:"Get filters marked as favourite by the current user.",inputSchema:a.object({expand:a.string().optional().describe("Expand options")})},async({expand:e})=>{try{const t=w(await f());return D(((await t.get("/rest/api/3/filter/favourite",{params:{expand:e}})).data||[]).map(e=>({id:e.id,name:e.name,description:e.description,owner:e.owner?.displayName,jql:e.jql})))}catch(e){return D(A(e))}});const R=new r;await x.connect(R);
|