issues-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1671 @@
1
+ // src/config.ts
2
+ var ENV_SPECS = [
3
+ {
4
+ kind: "jira",
5
+ required: ["JIRA_BASE_URL", "JIRA_API_TOKEN", "JIRA_ACCOUNT_EMAIL"],
6
+ secretVars: ["JIRA_API_TOKEN"],
7
+ build: (env) => ({
8
+ baseUrl: env.JIRA_BASE_URL ?? "",
9
+ credentials: {
10
+ type: "apiKey",
11
+ apiKey: env.JIRA_API_TOKEN ?? "",
12
+ accountId: env.JIRA_ACCOUNT_EMAIL ?? ""
13
+ }
14
+ })
15
+ },
16
+ {
17
+ kind: "redmine",
18
+ required: ["REDMINE_BASE_URL", "REDMINE_API_KEY"],
19
+ secretVars: ["REDMINE_API_KEY"],
20
+ build: (env) => ({
21
+ baseUrl: env.REDMINE_BASE_URL ?? "",
22
+ credentials: { type: "apiKey", apiKey: env.REDMINE_API_KEY ?? "" }
23
+ })
24
+ }
25
+ ];
26
+ function loadConfig(env = process.env) {
27
+ const trackers = [];
28
+ const secrets = [];
29
+ const warnings = [];
30
+ for (const spec of ENV_SPECS) {
31
+ const present = spec.required.filter((name) => hasValue(env[name]));
32
+ if (present.length === 0) continue;
33
+ if (present.length < spec.required.length) {
34
+ const missing = spec.required.filter((name) => !hasValue(env[name]));
35
+ warnings.push(`${spec.kind}: skipped \u2014 missing required env var(s): ${missing.join(", ")}`);
36
+ continue;
37
+ }
38
+ trackers.push({ kind: spec.kind, connection: spec.build(env) });
39
+ for (const name of spec.secretVars) {
40
+ const value = env[name];
41
+ if (hasValue(value)) secrets.push(value);
42
+ }
43
+ }
44
+ return { trackers, secrets, warnings };
45
+ }
46
+ function hasValue(value) {
47
+ return typeof value === "string" && value.trim().length > 0;
48
+ }
49
+ function makeRedactor(secrets) {
50
+ const real = [...new Set(secrets)].filter((s) => s.length > 0);
51
+ real.sort((a, b) => b.length - a.length);
52
+ return (text) => real.reduce((acc, s) => acc.split(s).join("***"), text);
53
+ }
54
+
55
+ // src/server.ts
56
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
57
+
58
+ // ../core/dist/errors.js
59
+ var IssueTrackerError = class extends Error {
60
+ constructor(message, options) {
61
+ super(message, options);
62
+ this.name = new.target.name;
63
+ }
64
+ };
65
+ var NotFoundError = class extends IssueTrackerError {
66
+ };
67
+ var AuthError = class extends IssueTrackerError {
68
+ };
69
+ var ValidationError = class extends IssueTrackerError {
70
+ fields;
71
+ constructor(message, options) {
72
+ super(message, options);
73
+ this.fields = options?.fields;
74
+ }
75
+ };
76
+ var RateLimitError = class extends IssueTrackerError {
77
+ retryAfterSeconds;
78
+ constructor(message, options) {
79
+ super(message, options);
80
+ this.retryAfterSeconds = options?.retryAfterSeconds;
81
+ }
82
+ };
83
+ var NotSupportedError = class extends IssueTrackerError {
84
+ };
85
+
86
+ // ../jira/dist/client.js
87
+ var JiraClient = class {
88
+ baseUrl;
89
+ authHeader;
90
+ constructor(config) {
91
+ const { credentials } = config;
92
+ if (credentials.accountId === void 0 || credentials.accountId === "") {
93
+ throw new AuthError("Jira requires credentials.accountId (the account email) for Basic auth.");
94
+ }
95
+ const token = `${credentials.accountId}:${credentials.apiKey}`;
96
+ this.authHeader = `Basic ${Buffer.from(token).toString("base64")}`;
97
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
98
+ }
99
+ // Perform a request against `/rest/api/3{path}` and return the parsed JSON
100
+ // body, or undefined for empty (204) responses.
101
+ async request(path, options = {}) {
102
+ const url = new URL(`${this.baseUrl}/rest/api/3${path}`);
103
+ if (options.query !== void 0) {
104
+ for (const [key, value] of Object.entries(options.query)) {
105
+ if (value !== void 0) {
106
+ url.searchParams.set(key, String(value));
107
+ }
108
+ }
109
+ }
110
+ const headers = {
111
+ Authorization: this.authHeader,
112
+ Accept: "application/json"
113
+ };
114
+ const init = {
115
+ method: options.method ?? "GET",
116
+ headers,
117
+ signal: options.signal
118
+ };
119
+ if (options.body !== void 0) {
120
+ headers["Content-Type"] = "application/json";
121
+ init.body = JSON.stringify(options.body);
122
+ }
123
+ let response;
124
+ try {
125
+ response = await fetch(url, init);
126
+ } catch (cause) {
127
+ throw new IssueTrackerError(`Network error calling Jira: ${String(cause)}`, { cause });
128
+ }
129
+ if (!response.ok) {
130
+ await this.throwForStatus(response);
131
+ }
132
+ if (response.status === 204) {
133
+ return void 0;
134
+ }
135
+ const text = await response.text();
136
+ if (text === "") {
137
+ return void 0;
138
+ }
139
+ return JSON.parse(text);
140
+ }
141
+ // Map a non-2xx response onto the typed error hierarchy. Always throws.
142
+ async throwForStatus(response) {
143
+ const status = response.status;
144
+ const raw = await response.text().catch(() => "");
145
+ let parsed;
146
+ try {
147
+ parsed = raw === "" ? void 0 : JSON.parse(raw);
148
+ } catch {
149
+ parsed = void 0;
150
+ }
151
+ const messages = parsed?.errorMessages ?? [];
152
+ const fields = parsed?.errors;
153
+ const summary = messages.length > 0 ? messages.join("; ") : fields !== void 0 ? Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join("; ") : raw.slice(0, 500);
154
+ switch (status) {
155
+ case 401:
156
+ throw new AuthError(`Jira authentication failed (401): ${summary}`);
157
+ case 403:
158
+ throw new AuthError(`Jira authorization failed (403): ${summary}`);
159
+ case 404:
160
+ throw new NotFoundError(`Jira resource not found (404): ${summary}`);
161
+ case 400:
162
+ case 422:
163
+ throw new ValidationError(`Jira rejected the request (${status}): ${summary}`, {
164
+ fields
165
+ });
166
+ case 429: {
167
+ const retryHeader = response.headers.get("Retry-After");
168
+ const retryAfterSeconds = retryHeader !== null && retryHeader !== "" ? Number(retryHeader) : void 0;
169
+ throw new RateLimitError(`Jira is throttling requests (429): ${summary}`, {
170
+ retryAfterSeconds: Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : void 0
171
+ });
172
+ }
173
+ default:
174
+ throw new IssueTrackerError(`Jira request failed (${status}): ${summary}`);
175
+ }
176
+ }
177
+ };
178
+
179
+ // ../jira/dist/adf.js
180
+ function adfToText(node) {
181
+ if (node === null || node === void 0) {
182
+ return null;
183
+ }
184
+ return flatten(node).replace(/\n{3,}/g, "\n\n").trim();
185
+ }
186
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
187
+ "paragraph",
188
+ "heading",
189
+ "blockquote",
190
+ "listItem",
191
+ "bulletList",
192
+ "orderedList",
193
+ "codeBlock",
194
+ "rule"
195
+ ]);
196
+ function flatten(node) {
197
+ if (node.type === "text") {
198
+ return node.text ?? "";
199
+ }
200
+ if (node.type === "hardBreak") {
201
+ return "\n";
202
+ }
203
+ const children = node.content ?? [];
204
+ const inner = children.map(flatten).join("");
205
+ if (node.type !== void 0 && BLOCK_TYPES.has(node.type)) {
206
+ return `${inner}
207
+ `;
208
+ }
209
+ return inner;
210
+ }
211
+ function textToAdf(text) {
212
+ return {
213
+ type: "doc",
214
+ version: 1,
215
+ content: [
216
+ {
217
+ type: "paragraph",
218
+ content: [{ type: "text", text }]
219
+ }
220
+ ]
221
+ };
222
+ }
223
+
224
+ // ../jira/dist/mappers.js
225
+ function mapUser(raw) {
226
+ const user2 = {
227
+ id: raw.accountId,
228
+ name: raw.displayName ?? raw.accountId
229
+ };
230
+ if (raw.emailAddress !== void 0) {
231
+ user2.email = raw.emailAddress;
232
+ }
233
+ return user2;
234
+ }
235
+ function mapStatusCategory(key) {
236
+ switch (key) {
237
+ case "new":
238
+ return "open";
239
+ case "indeterminate":
240
+ return "in_progress";
241
+ case "done":
242
+ return "done";
243
+ default:
244
+ return "unknown";
245
+ }
246
+ }
247
+ function mapStatus(raw) {
248
+ return {
249
+ id: raw.id,
250
+ name: raw.name,
251
+ category: mapStatusCategory(raw.statusCategory?.key)
252
+ };
253
+ }
254
+ function mapComment(raw) {
255
+ const item = {
256
+ kind: "comment",
257
+ id: raw.id,
258
+ author: raw.author !== void 0 ? mapUser(raw.author) : { id: "unknown", name: "Unknown" },
259
+ body: adfToText(raw.body) ?? "",
260
+ createdAt: raw.created
261
+ };
262
+ if (raw.updated !== void 0) {
263
+ item.updatedAt = raw.updated;
264
+ }
265
+ return item;
266
+ }
267
+ function mapChangelogHistory(raw) {
268
+ return {
269
+ kind: "change",
270
+ id: raw.id,
271
+ author: raw.author !== void 0 ? mapUser(raw.author) : { id: "unknown", name: "Unknown" },
272
+ createdAt: raw.created,
273
+ changes: (raw.items ?? []).map((item) => ({
274
+ field: item.field ?? "",
275
+ from: item.fromString ?? null,
276
+ to: item.toString ?? null
277
+ }))
278
+ };
279
+ }
280
+ function mapIssue(raw) {
281
+ const fields = raw.fields;
282
+ const items = [];
283
+ for (const history of raw.changelog?.histories ?? []) {
284
+ items.push(mapChangelogHistory(history));
285
+ }
286
+ for (const comment of fields.comment?.comments ?? []) {
287
+ items.push(mapComment(comment));
288
+ }
289
+ const status = fields.status !== void 0 ? mapStatus(fields.status) : { id: "unknown", name: "Unknown", category: "unknown" };
290
+ const issue = {
291
+ id: raw.id,
292
+ key: raw.key,
293
+ title: fields.summary ?? "",
294
+ description: adfToText(fields.description),
295
+ status,
296
+ author: fields.reporter !== void 0 && fields.reporter !== null ? mapUser(fields.reporter) : null,
297
+ assignee: fields.assignee !== void 0 && fields.assignee !== null ? mapUser(fields.assignee) : null,
298
+ attributes: {},
299
+ items,
300
+ createdAt: fields.created,
301
+ updatedAt: fields.updated
302
+ };
303
+ if (fields.project !== void 0) {
304
+ issue.projectId = fields.project.id;
305
+ }
306
+ return issue;
307
+ }
308
+ function mapTransition(raw) {
309
+ return {
310
+ id: raw.id,
311
+ name: raw.name,
312
+ to: raw.to !== void 0 ? mapStatus(raw.to) : { id: "unknown", name: "Unknown", category: "unknown" }
313
+ };
314
+ }
315
+ function mapWorklog(raw, issueId) {
316
+ const entry = {
317
+ id: `${issueId}/${raw.id}`,
318
+ issueId,
319
+ user: raw.author !== void 0 ? mapUser(raw.author) : { id: "unknown", name: "Unknown" },
320
+ date: raw.started,
321
+ durationMinutes: Math.round(raw.timeSpentSeconds / 60),
322
+ createdAt: raw.created,
323
+ updatedAt: raw.updated
324
+ };
325
+ const description = adfToText(raw.comment);
326
+ if (description !== null) {
327
+ entry.description = description;
328
+ }
329
+ return entry;
330
+ }
331
+ function attributeToJiraValue(attr) {
332
+ switch (attr.type) {
333
+ case "string":
334
+ case "text":
335
+ case "number":
336
+ case "date":
337
+ case "boolean":
338
+ return attr.value;
339
+ case "enum":
340
+ return { id: attr.value.id };
341
+ case "multi_enum":
342
+ return attr.value.map((option) => ({ id: option.id }));
343
+ case "user":
344
+ return { accountId: attr.value.id };
345
+ }
346
+ }
347
+
348
+ // ../jira/dist/jql.js
349
+ function jqlString(value) {
350
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
351
+ }
352
+ function statusCategoryToJql(category) {
353
+ switch (category) {
354
+ case "open":
355
+ return "To Do";
356
+ case "in_progress":
357
+ return "In Progress";
358
+ case "done":
359
+ return "Done";
360
+ default:
361
+ return void 0;
362
+ }
363
+ }
364
+ function buildJql(filter) {
365
+ const clauses = [];
366
+ if (filter.text !== void 0 && filter.text !== "") {
367
+ clauses.push(`text ~ ${jqlString(filter.text)}`);
368
+ }
369
+ if (filter.projectId !== void 0) {
370
+ clauses.push(`project = ${jqlString(filter.projectId)}`);
371
+ }
372
+ if (filter.assigneeId !== void 0) {
373
+ clauses.push(`assignee = ${jqlString(filter.assigneeId)}`);
374
+ }
375
+ if (filter.authorId !== void 0) {
376
+ clauses.push(`reporter = ${jqlString(filter.authorId)}`);
377
+ }
378
+ if (filter.statusCategory !== void 0) {
379
+ const mapped = statusCategoryToJql(filter.statusCategory);
380
+ if (mapped !== void 0) {
381
+ clauses.push(`statusCategory = ${jqlString(mapped)}`);
382
+ }
383
+ }
384
+ if (filter.createdAfter !== void 0) {
385
+ clauses.push(`created >= ${jqlString(filter.createdAfter)}`);
386
+ }
387
+ if (filter.createdBefore !== void 0) {
388
+ clauses.push(`created <= ${jqlString(filter.createdBefore)}`);
389
+ }
390
+ if (filter.updatedAfter !== void 0) {
391
+ clauses.push(`updated >= ${jqlString(filter.updatedAfter)}`);
392
+ }
393
+ if (filter.updatedBefore !== void 0) {
394
+ clauses.push(`updated <= ${jqlString(filter.updatedBefore)}`);
395
+ }
396
+ let jql = clauses.join(" AND ");
397
+ if (filter.sort !== void 0) {
398
+ const dir = filter.sort.dir === "desc" ? "DESC" : "ASC";
399
+ jql = `${jql}${jql === "" ? "" : " "}ORDER BY ${filter.sort.field} ${dir}`;
400
+ }
401
+ return jql;
402
+ }
403
+
404
+ // ../jira/dist/issuesTracker.js
405
+ var ISSUE_FIELDS = [
406
+ "summary",
407
+ "description",
408
+ "status",
409
+ "reporter",
410
+ "assignee",
411
+ "project",
412
+ "created",
413
+ "updated"
414
+ ];
415
+ function buildFieldsPayload(input) {
416
+ const fields = {};
417
+ if (input.title !== void 0) {
418
+ fields.summary = input.title;
419
+ }
420
+ if (input.description !== void 0) {
421
+ fields.description = input.description === null ? null : textToAdf(input.description);
422
+ }
423
+ if (input.projectId !== void 0) {
424
+ fields.project = { key: input.projectId };
425
+ }
426
+ if (input.assigneeId !== void 0) {
427
+ fields.assignee = input.assigneeId === null ? null : { accountId: input.assigneeId };
428
+ }
429
+ if (input.attributes !== void 0) {
430
+ for (const [key, attr] of Object.entries(input.attributes)) {
431
+ const value = attr;
432
+ if (key === "issuetype") {
433
+ fields.issuetype = value.type === "enum" ? { id: value.value.id } : attributeToJiraValue(value);
434
+ } else {
435
+ fields[key] = attributeToJiraValue(value);
436
+ }
437
+ }
438
+ }
439
+ return fields;
440
+ }
441
+ var JiraIssuesTracker = class {
442
+ client;
443
+ constructor(client) {
444
+ this.client = client;
445
+ }
446
+ async capabilities() {
447
+ const fields = [
448
+ {
449
+ id: "project",
450
+ name: "Project",
451
+ type: "enum",
452
+ required: true,
453
+ readOnly: false,
454
+ allowedValues: await this.fetchProjects()
455
+ },
456
+ {
457
+ id: "issuetype",
458
+ name: "Issue Type",
459
+ type: "enum",
460
+ required: true,
461
+ readOnly: false,
462
+ allowedValues: await this.fetchIssueTypes()
463
+ },
464
+ { id: "summary", name: "Summary", type: "string", required: true, readOnly: false },
465
+ { id: "description", name: "Description", type: "text", required: false, readOnly: false }
466
+ ];
467
+ return {
468
+ canCreate: true,
469
+ canUpdate: true,
470
+ canDelete: true,
471
+ canComment: true,
472
+ canTransition: true,
473
+ fields
474
+ };
475
+ }
476
+ async fetchProjects() {
477
+ const res = await this.client.request("/project/search", {
478
+ query: { maxResults: 100 }
479
+ });
480
+ return (res.values ?? []).map((project) => ({
481
+ id: project.id,
482
+ name: project.name ?? project.key ?? project.id
483
+ }));
484
+ }
485
+ async fetchIssueTypes() {
486
+ const res = await this.client.request("/issuetype");
487
+ return res.map((type) => ({ id: type.id, name: type.name }));
488
+ }
489
+ async search(filter) {
490
+ const body = {
491
+ jql: buildJql(filter),
492
+ fields: ISSUE_FIELDS
493
+ };
494
+ if (filter.limit !== void 0) {
495
+ body.maxResults = filter.limit;
496
+ }
497
+ if (filter.cursor !== void 0) {
498
+ body.nextPageToken = filter.cursor;
499
+ }
500
+ const res = await this.client.request("/search/jql", {
501
+ method: "POST",
502
+ body
503
+ });
504
+ const page = {
505
+ items: (res.issues ?? []).map(mapIssue)
506
+ };
507
+ if (res.nextPageToken !== void 0 && res.isLast !== true) {
508
+ page.nextCursor = res.nextPageToken;
509
+ }
510
+ return page;
511
+ }
512
+ async get(id) {
513
+ const raw = await this.client.request(`/issue/${encodeURIComponent(id)}`, {
514
+ query: { expand: "changelog" }
515
+ });
516
+ const issue = mapIssue(raw);
517
+ const comments = await this.client.request(`/issue/${encodeURIComponent(id)}/comment`);
518
+ const commentItems = (comments.comments ?? []).map(mapComment);
519
+ issue.items = [...issue.items, ...commentItems];
520
+ return issue;
521
+ }
522
+ async create(input) {
523
+ const fields = buildFieldsPayload(input);
524
+ const created = await this.client.request("/issue", {
525
+ method: "POST",
526
+ body: { fields }
527
+ });
528
+ const idOrKey = created.key ?? created.id;
529
+ if (idOrKey === void 0) {
530
+ throw new ValidationError("Jira did not return an id or key for the created issue.");
531
+ }
532
+ return this.get(idOrKey);
533
+ }
534
+ async update(id, input) {
535
+ const fields = buildFieldsPayload(input);
536
+ await this.client.request(`/issue/${encodeURIComponent(id)}`, {
537
+ method: "PUT",
538
+ body: { fields }
539
+ });
540
+ return this.get(id);
541
+ }
542
+ async delete(id) {
543
+ await this.client.request(`/issue/${encodeURIComponent(id)}`, {
544
+ method: "DELETE"
545
+ });
546
+ }
547
+ async addComment(issueId, body) {
548
+ const created = await this.client.request(`/issue/${encodeURIComponent(issueId)}/comment`, {
549
+ method: "POST",
550
+ body: { body: textToAdf(body) }
551
+ });
552
+ return mapComment(created);
553
+ }
554
+ async getTransitions(issueId) {
555
+ const res = await this.client.request(`/issue/${encodeURIComponent(issueId)}/transitions`);
556
+ return (res.transitions ?? []).map(mapTransition);
557
+ }
558
+ async transition(issueId, transitionId) {
559
+ await this.client.request(`/issue/${encodeURIComponent(issueId)}/transitions`, {
560
+ method: "POST",
561
+ body: { transition: { id: transitionId } }
562
+ });
563
+ return this.get(issueId);
564
+ }
565
+ };
566
+
567
+ // ../jira/dist/timeTracker.js
568
+ function parseWorklogId(id) {
569
+ const slash = id.indexOf("/");
570
+ if (slash === -1) {
571
+ throw new ValidationError(`Invalid worklog id "${id}". Expected the format "<issueId>/<worklogId>".`);
572
+ }
573
+ return { issueId: id.slice(0, slash), worklogId: id.slice(slash + 1) };
574
+ }
575
+ function toJiraStarted(isoDate) {
576
+ const date = new Date(isoDate);
577
+ if (Number.isNaN(date.getTime())) {
578
+ throw new ValidationError(`Invalid date "${isoDate}" for worklog.`);
579
+ }
580
+ const iso = date.toISOString();
581
+ return iso.replace("Z", "+0000");
582
+ }
583
+ var JiraTimeTracker = class {
584
+ client;
585
+ constructor(client) {
586
+ this.client = client;
587
+ }
588
+ capabilities() {
589
+ return Promise.resolve({
590
+ canCreate: true,
591
+ canUpdate: true,
592
+ canDelete: true,
593
+ requiresIssue: true,
594
+ activities: []
595
+ });
596
+ }
597
+ async search(filter) {
598
+ if (filter.issueId === void 0) {
599
+ throw new NotSupportedError("Jira core worklogs are scoped per issue; set filter.issueId to search worklogs.");
600
+ }
601
+ const res = await this.client.request(`/issue/${encodeURIComponent(filter.issueId)}/worklog`);
602
+ let entries = (res.worklogs ?? []).map((worklog) => mapWorklog(worklog, filter.issueId));
603
+ if (filter.userId !== void 0) {
604
+ entries = entries.filter((entry) => entry.user.id === filter.userId);
605
+ }
606
+ if (filter.from !== void 0) {
607
+ const from = new Date(filter.from).getTime();
608
+ entries = entries.filter((entry) => new Date(entry.date).getTime() >= from);
609
+ }
610
+ if (filter.to !== void 0) {
611
+ const to = new Date(filter.to).getTime();
612
+ entries = entries.filter((entry) => new Date(entry.date).getTime() <= to);
613
+ }
614
+ return { items: entries };
615
+ }
616
+ async get(id) {
617
+ const { issueId, worklogId } = parseWorklogId(id);
618
+ const raw = await this.client.request(`/issue/${encodeURIComponent(issueId)}/worklog/${encodeURIComponent(worklogId)}`);
619
+ return mapWorklog(raw, issueId);
620
+ }
621
+ async create(input) {
622
+ const body = {
623
+ started: toJiraStarted(input.date),
624
+ timeSpentSeconds: input.durationMinutes * 60
625
+ };
626
+ if (input.description !== void 0) {
627
+ body.comment = textToAdf(input.description);
628
+ }
629
+ const raw = await this.client.request(`/issue/${encodeURIComponent(input.issueId)}/worklog`, { method: "POST", body });
630
+ return mapWorklog(raw, input.issueId);
631
+ }
632
+ async update(id, input) {
633
+ const { issueId, worklogId } = parseWorklogId(id);
634
+ const body = {};
635
+ if (input.date !== void 0) {
636
+ body.started = toJiraStarted(input.date);
637
+ }
638
+ if (input.durationMinutes !== void 0) {
639
+ body.timeSpentSeconds = input.durationMinutes * 60;
640
+ }
641
+ if (input.description !== void 0) {
642
+ body.comment = textToAdf(input.description);
643
+ }
644
+ const raw = await this.client.request(`/issue/${encodeURIComponent(issueId)}/worklog/${encodeURIComponent(worklogId)}`, { method: "PUT", body });
645
+ return mapWorklog(raw, issueId);
646
+ }
647
+ async delete(id) {
648
+ const { issueId, worklogId } = parseWorklogId(id);
649
+ await this.client.request(`/issue/${encodeURIComponent(issueId)}/worklog/${encodeURIComponent(worklogId)}`, { method: "DELETE" });
650
+ }
651
+ };
652
+
653
+ // ../jira/dist/session.js
654
+ var JiraSession = class {
655
+ tracker;
656
+ user;
657
+ constructor(tracker, user2) {
658
+ this.tracker = tracker;
659
+ this.user = user2;
660
+ }
661
+ currentUser() {
662
+ return Promise.resolve(this.user);
663
+ }
664
+ close() {
665
+ return Promise.resolve();
666
+ }
667
+ };
668
+ var JiraAuthenticator = class {
669
+ async authenticate(config) {
670
+ const client = new JiraClient(config);
671
+ let me;
672
+ try {
673
+ me = await client.request("/myself");
674
+ } catch (cause) {
675
+ if (cause instanceof AuthError) {
676
+ throw cause;
677
+ }
678
+ throw new AuthError(`Failed to validate Jira credentials: ${String(cause)}`, { cause });
679
+ }
680
+ const tracker = {
681
+ issues: new JiraIssuesTracker(client),
682
+ time: new JiraTimeTracker(client)
683
+ };
684
+ return new JiraSession(tracker, mapUser(me));
685
+ }
686
+ };
687
+
688
+ // ../redmine/dist/client.js
689
+ var RedmineClient = class {
690
+ baseUrl;
691
+ apiKey;
692
+ constructor(config) {
693
+ this.apiKey = config.credentials.apiKey;
694
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
695
+ }
696
+ // Perform a request against `{baseUrl}{path}` and return the parsed JSON body,
697
+ // or undefined for empty (204) responses.
698
+ async request(path, options = {}) {
699
+ const url = new URL(`${this.baseUrl}${path}`);
700
+ if (options.query !== void 0) {
701
+ for (const [key, value] of Object.entries(options.query)) {
702
+ if (value === void 0) {
703
+ continue;
704
+ }
705
+ if (Array.isArray(value)) {
706
+ for (const entry of value) {
707
+ url.searchParams.append(key, entry);
708
+ }
709
+ } else {
710
+ url.searchParams.set(key, String(value));
711
+ }
712
+ }
713
+ }
714
+ const headers = {
715
+ "X-Redmine-API-Key": this.apiKey,
716
+ Accept: "application/json"
717
+ };
718
+ const init = {
719
+ method: options.method ?? "GET",
720
+ headers,
721
+ signal: options.signal
722
+ };
723
+ if (options.body !== void 0) {
724
+ headers["Content-Type"] = "application/json";
725
+ init.body = JSON.stringify(options.body);
726
+ }
727
+ let response;
728
+ try {
729
+ response = await fetch(url, init);
730
+ } catch (cause) {
731
+ throw new IssueTrackerError(`Network error calling Redmine: ${String(cause)}`, { cause });
732
+ }
733
+ if (!response.ok) {
734
+ await this.throwForStatus(response);
735
+ }
736
+ if (response.status === 204) {
737
+ return void 0;
738
+ }
739
+ const text = await response.text();
740
+ if (text === "") {
741
+ return void 0;
742
+ }
743
+ return JSON.parse(text);
744
+ }
745
+ // Map a non-2xx response onto the typed error hierarchy. Always throws.
746
+ async throwForStatus(response) {
747
+ const status = response.status;
748
+ const raw = await response.text().catch(() => "");
749
+ let parsed;
750
+ try {
751
+ parsed = raw === "" ? void 0 : JSON.parse(raw);
752
+ } catch {
753
+ parsed = void 0;
754
+ }
755
+ const messages = parsed?.errors ?? [];
756
+ const summary = messages.length > 0 ? messages.join("; ") : raw.slice(0, 500);
757
+ switch (status) {
758
+ case 401:
759
+ throw new AuthError(`Redmine authentication failed (401): ${summary}`);
760
+ case 403:
761
+ throw new AuthError(`Redmine authorization failed (403): ${summary}`);
762
+ case 404:
763
+ throw new NotFoundError(`Redmine resource not found (404): ${summary}`);
764
+ case 422:
765
+ throw new ValidationError(`Redmine rejected the request (422): ${summary}`);
766
+ case 429: {
767
+ const retryHeader = response.headers.get("Retry-After");
768
+ const retryAfterSeconds = retryHeader !== null && retryHeader !== "" ? Number(retryHeader) : void 0;
769
+ throw new RateLimitError(`Redmine is throttling requests (429): ${summary}`, {
770
+ retryAfterSeconds: retryAfterSeconds !== void 0 && Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : void 0
771
+ });
772
+ }
773
+ default:
774
+ throw new IssueTrackerError(`Redmine request failed (${status}): ${summary}`);
775
+ }
776
+ }
777
+ };
778
+
779
+ // ../redmine/dist/mappers.js
780
+ function mapCurrentUser(user2) {
781
+ const name = `${user2.firstname ?? ""} ${user2.lastname ?? ""}`.trim();
782
+ const mapped = {
783
+ id: String(user2.id),
784
+ name: name !== "" ? name : user2.name ?? String(user2.id)
785
+ };
786
+ if (user2.mail !== void 0 && user2.mail !== "") {
787
+ mapped.email = user2.mail;
788
+ }
789
+ return mapped;
790
+ }
791
+ function mapNamedUser(ref) {
792
+ return { id: String(ref.id), name: ref.name };
793
+ }
794
+ function categorizeStatus(status) {
795
+ if (status.is_closed === true) {
796
+ return "done";
797
+ }
798
+ if (/progress|doing|in work/i.test(status.name)) {
799
+ return "in_progress";
800
+ }
801
+ return "open";
802
+ }
803
+ function mapStatus2(ref, categories) {
804
+ if (ref === void 0) {
805
+ return { id: "", name: "", category: "unknown" };
806
+ }
807
+ return {
808
+ id: String(ref.id),
809
+ name: ref.name,
810
+ category: categories.get(ref.id) ?? "open"
811
+ };
812
+ }
813
+ function mapCustomField(field) {
814
+ if (field.multiple === true || Array.isArray(field.value)) {
815
+ const values = Array.isArray(field.value) ? field.value : field.value === null ? [] : [field.value];
816
+ return { type: "multi_enum", value: values.map((v) => ({ id: v, name: v })) };
817
+ }
818
+ if (field.value === null) {
819
+ return void 0;
820
+ }
821
+ return { type: "string", value: field.value };
822
+ }
823
+ function mapJournal(journal) {
824
+ const items = [];
825
+ const author = journal.user !== void 0 ? mapNamedUser(journal.user) : { id: "", name: "" };
826
+ if (journal.notes !== void 0 && journal.notes !== "") {
827
+ items.push({
828
+ kind: "comment",
829
+ id: `${journal.id}-note`,
830
+ author,
831
+ body: journal.notes,
832
+ createdAt: journal.created_on
833
+ });
834
+ }
835
+ const details = journal.details ?? [];
836
+ if (details.length > 0) {
837
+ items.push({
838
+ kind: "change",
839
+ id: `${journal.id}-change`,
840
+ author,
841
+ createdAt: journal.created_on,
842
+ changes: details.map((d) => ({
843
+ field: d.name,
844
+ from: d.old_value,
845
+ to: d.new_value
846
+ }))
847
+ });
848
+ }
849
+ return items;
850
+ }
851
+ function mapIssue2(issue, categories) {
852
+ const attributes2 = {};
853
+ for (const field of issue.custom_fields ?? []) {
854
+ const value = mapCustomField(field);
855
+ if (value !== void 0) {
856
+ attributes2[field.name] = value;
857
+ }
858
+ }
859
+ const items = [];
860
+ for (const journal of issue.journals ?? []) {
861
+ items.push(...mapJournal(journal));
862
+ }
863
+ const mapped = {
864
+ id: String(issue.id),
865
+ title: issue.subject,
866
+ description: issue.description ?? null,
867
+ status: mapStatus2(issue.status, categories),
868
+ author: issue.author !== void 0 ? mapNamedUser(issue.author) : null,
869
+ assignee: issue.assigned_to !== void 0 ? mapNamedUser(issue.assigned_to) : null,
870
+ attributes: attributes2,
871
+ items,
872
+ createdAt: issue.created_on,
873
+ updatedAt: issue.updated_on
874
+ };
875
+ if (issue.project !== void 0) {
876
+ mapped.projectId = String(issue.project.id);
877
+ }
878
+ return mapped;
879
+ }
880
+ function mapTimeEntry(entry) {
881
+ const mapped = {
882
+ id: String(entry.id),
883
+ issueId: entry.issue !== void 0 ? String(entry.issue.id) : "",
884
+ user: mapNamedUser(entry.user),
885
+ date: entry.spent_on,
886
+ durationMinutes: Math.round(entry.hours * 60),
887
+ createdAt: entry.created_on,
888
+ updatedAt: entry.updated_on
889
+ };
890
+ if (entry.comments !== void 0 && entry.comments !== "") {
891
+ mapped.description = entry.comments;
892
+ }
893
+ if (entry.activity !== void 0) {
894
+ mapped.activity = { id: String(entry.activity.id), name: entry.activity.name };
895
+ }
896
+ return mapped;
897
+ }
898
+ function attributeToRedmineValue(attr) {
899
+ switch (attr.type) {
900
+ case "string":
901
+ case "text":
902
+ return attr.value;
903
+ case "number":
904
+ return String(attr.value);
905
+ case "date":
906
+ return attr.value;
907
+ case "boolean":
908
+ return attr.value ? "1" : "0";
909
+ case "enum":
910
+ return attr.value.id;
911
+ case "multi_enum":
912
+ return attr.value.map((o) => o.id);
913
+ case "user":
914
+ return attr.value.id;
915
+ }
916
+ }
917
+
918
+ // ../redmine/dist/issuesTracker.js
919
+ var MAX_LIMIT = 100;
920
+ var DEFAULT_LIMIT = 25;
921
+ var RedmineIssuesTracker = class {
922
+ client;
923
+ statusCache;
924
+ categoryCache;
925
+ constructor(client) {
926
+ this.client = client;
927
+ }
928
+ // Fetch and cache the issue-status definitions. Used both to derive status
929
+ // categories and to expose transitions.
930
+ async loadStatuses() {
931
+ if (this.statusCache === void 0) {
932
+ const res = await this.client.request("/issue_statuses.json");
933
+ this.statusCache = res.issue_statuses;
934
+ }
935
+ return this.statusCache;
936
+ }
937
+ // Build (and cache) the status-id to category lookup.
938
+ async categories() {
939
+ if (this.categoryCache === void 0) {
940
+ const statuses = await this.loadStatuses();
941
+ const map = /* @__PURE__ */ new Map();
942
+ for (const status of statuses) {
943
+ map.set(status.id, categorizeStatus(status));
944
+ }
945
+ this.categoryCache = map;
946
+ }
947
+ return this.categoryCache;
948
+ }
949
+ async capabilities() {
950
+ const fields = [];
951
+ const projectOptions = await this.fetchOptions(() => this.client.request("/projects.json", {
952
+ query: { limit: MAX_LIMIT }
953
+ }), (res) => res.projects);
954
+ fields.push({
955
+ id: "project",
956
+ name: "Project",
957
+ type: "enum",
958
+ required: true,
959
+ readOnly: false,
960
+ allowedValues: projectOptions
961
+ });
962
+ const trackerOptions = await this.fetchOptions(() => this.client.request("/trackers.json"), (res) => res.trackers);
963
+ fields.push({
964
+ id: "tracker",
965
+ name: "Tracker",
966
+ type: "enum",
967
+ required: true,
968
+ readOnly: false,
969
+ allowedValues: trackerOptions
970
+ });
971
+ fields.push({
972
+ id: "subject",
973
+ name: "Subject",
974
+ type: "string",
975
+ required: true,
976
+ readOnly: false
977
+ });
978
+ fields.push({
979
+ id: "description",
980
+ name: "Description",
981
+ type: "text",
982
+ required: false,
983
+ readOnly: false
984
+ });
985
+ const statuses = await this.loadStatuses();
986
+ fields.push({
987
+ id: "status",
988
+ name: "Status",
989
+ type: "enum",
990
+ required: false,
991
+ readOnly: false,
992
+ allowedValues: statuses.map((s) => ({ id: String(s.id), name: s.name }))
993
+ });
994
+ try {
995
+ const res = await this.client.request("/custom_fields.json");
996
+ for (const cf of res.custom_fields) {
997
+ fields.push({
998
+ id: `cf_${cf.id}`,
999
+ name: cf.name,
1000
+ type: "string",
1001
+ required: cf.is_required ?? false,
1002
+ readOnly: false
1003
+ });
1004
+ }
1005
+ } catch {
1006
+ }
1007
+ return {
1008
+ canCreate: true,
1009
+ canUpdate: true,
1010
+ canDelete: true,
1011
+ canComment: true,
1012
+ canTransition: true,
1013
+ fields
1014
+ };
1015
+ }
1016
+ // Helper to fetch a reference list and map it to FieldOption[], swallowing
1017
+ // failures (returns []) so capabilities() degrades gracefully.
1018
+ async fetchOptions(fetcher, extract) {
1019
+ try {
1020
+ const res = await fetcher();
1021
+ return extract(res).map((o) => ({ id: String(o.id), name: o.name }));
1022
+ } catch {
1023
+ return [];
1024
+ }
1025
+ }
1026
+ async search(filter) {
1027
+ const categories = await this.categories();
1028
+ const limit = Math.min(filter.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
1029
+ const offset = filter.cursor !== void 0 ? Number(filter.cursor) : 0;
1030
+ const startOffset = Number.isFinite(offset) && offset > 0 ? offset : 0;
1031
+ const query = {
1032
+ offset: startOffset,
1033
+ limit
1034
+ };
1035
+ if (filter.projectId !== void 0) {
1036
+ query["project_id"] = filter.projectId;
1037
+ }
1038
+ if (filter.assigneeId !== void 0) {
1039
+ query["assigned_to_id"] = filter.assigneeId;
1040
+ }
1041
+ if (filter.authorId !== void 0) {
1042
+ query["author_id"] = filter.authorId;
1043
+ }
1044
+ let clientSideInProgress = false;
1045
+ if (filter.statusCategory === "open") {
1046
+ query["status_id"] = "open";
1047
+ } else if (filter.statusCategory === "done") {
1048
+ query["status_id"] = "closed";
1049
+ } else if (filter.statusCategory === "in_progress") {
1050
+ query["status_id"] = "open";
1051
+ clientSideInProgress = true;
1052
+ }
1053
+ const created = buildDateRange(filter.createdAfter, filter.createdBefore);
1054
+ if (created !== void 0) {
1055
+ query["created_on"] = created;
1056
+ }
1057
+ const updated = buildDateRange(filter.updatedAfter, filter.updatedBefore);
1058
+ if (updated !== void 0) {
1059
+ query["updated_on"] = updated;
1060
+ }
1061
+ if (filter.sort !== void 0) {
1062
+ query["sort"] = `${filter.sort.field}:${filter.sort.dir}`;
1063
+ }
1064
+ if (filter.text !== void 0 && filter.text !== "") {
1065
+ query["f[]"] = ["subject"];
1066
+ query["op[subject]"] = "~";
1067
+ query["v[subject][]"] = filter.text;
1068
+ }
1069
+ const res = await this.client.request("/issues.json", { query });
1070
+ let items = res.issues.map((i) => mapIssue2(i, categories));
1071
+ if (clientSideInProgress) {
1072
+ items = items.filter((i) => i.status.category === "in_progress");
1073
+ }
1074
+ const page = { items, total: res.total_count };
1075
+ const nextOffset = res.offset + res.limit;
1076
+ if (nextOffset < res.total_count) {
1077
+ page.nextCursor = String(nextOffset);
1078
+ }
1079
+ return page;
1080
+ }
1081
+ async get(id) {
1082
+ const categories = await this.categories();
1083
+ const res = await this.client.request(`/issues/${id}.json`, {
1084
+ query: { include: "journals" }
1085
+ });
1086
+ return mapIssue2(res.issue, categories);
1087
+ }
1088
+ async create(input) {
1089
+ const categories = await this.categories();
1090
+ const attributes2 = input.attributes ?? {};
1091
+ const trackerId = readEnumAttribute(attributes2, "tracker");
1092
+ if (trackerId === void 0) {
1093
+ throw new ValidationError("Redmine requires a `tracker` attribute (enum) to create an issue.");
1094
+ }
1095
+ const body = {
1096
+ subject: input.title,
1097
+ tracker_id: Number(trackerId)
1098
+ };
1099
+ if (input.projectId !== void 0) {
1100
+ body.project_id = Number(input.projectId);
1101
+ }
1102
+ if (input.description !== void 0) {
1103
+ body.description = input.description;
1104
+ }
1105
+ if (input.assigneeId !== void 0 && input.assigneeId !== null) {
1106
+ body.assigned_to_id = Number(input.assigneeId);
1107
+ }
1108
+ const statusId = readEnumAttribute(attributes2, "status");
1109
+ if (statusId !== void 0) {
1110
+ body.status_id = Number(statusId);
1111
+ }
1112
+ const customFields = buildCustomFields(attributes2);
1113
+ if (customFields.length > 0) {
1114
+ body.custom_fields = customFields;
1115
+ }
1116
+ const res = await this.client.request("/issues.json", {
1117
+ method: "POST",
1118
+ body: { issue: body }
1119
+ });
1120
+ return mapIssue2(res.issue, categories);
1121
+ }
1122
+ async update(id, input) {
1123
+ const body = {};
1124
+ if (input.title !== void 0) {
1125
+ body.subject = input.title;
1126
+ }
1127
+ if (input.description !== void 0) {
1128
+ body.description = input.description;
1129
+ }
1130
+ if (input.projectId !== void 0) {
1131
+ body.project_id = Number(input.projectId);
1132
+ }
1133
+ if (input.assigneeId !== void 0) {
1134
+ body.assigned_to_id = input.assigneeId === null ? null : Number(input.assigneeId);
1135
+ }
1136
+ if (input.attributes !== void 0) {
1137
+ const statusId = readEnumAttribute(input.attributes, "status");
1138
+ if (statusId !== void 0) {
1139
+ body.status_id = Number(statusId);
1140
+ }
1141
+ const customFields = buildCustomFields(input.attributes);
1142
+ if (customFields.length > 0) {
1143
+ body.custom_fields = customFields;
1144
+ }
1145
+ }
1146
+ await this.client.request(`/issues/${id}.json`, {
1147
+ method: "PUT",
1148
+ body: { issue: body }
1149
+ });
1150
+ return this.get(id);
1151
+ }
1152
+ async delete(id) {
1153
+ await this.client.request(`/issues/${id}.json`, { method: "DELETE" });
1154
+ }
1155
+ async addComment(issueId, body) {
1156
+ await this.client.request(`/issues/${issueId}.json`, {
1157
+ method: "PUT",
1158
+ body: { issue: { notes: body } }
1159
+ });
1160
+ const res = await this.client.request(`/issues/${issueId}.json`, {
1161
+ query: { include: "journals" }
1162
+ });
1163
+ const journals = res.issue.journals ?? [];
1164
+ for (let i = journals.length - 1; i >= 0; i--) {
1165
+ const journal = journals[i];
1166
+ if (journal !== void 0 && journal.notes !== void 0 && journal.notes !== "") {
1167
+ const comment = mapJournal(journal).find((item) => item.kind === "comment");
1168
+ if (comment !== void 0) {
1169
+ return comment;
1170
+ }
1171
+ }
1172
+ }
1173
+ throw new NotFoundError(`Comment was added to issue ${issueId} but could not be retrieved.`);
1174
+ }
1175
+ // Redmine does not enforce workflow transitions through the REST API: status
1176
+ // is set directly. We therefore offer every issue status as a transition.
1177
+ async getTransitions() {
1178
+ const categories = await this.categories();
1179
+ const statuses = await this.loadStatuses();
1180
+ return statuses.map((s) => {
1181
+ const to = mapStatus2({ id: s.id, name: s.name }, categories);
1182
+ return { id: String(s.id), name: s.name, to };
1183
+ });
1184
+ }
1185
+ async transition(issueId, transitionId) {
1186
+ await this.client.request(`/issues/${issueId}.json`, {
1187
+ method: "PUT",
1188
+ body: { issue: { status_id: Number(transitionId) } }
1189
+ });
1190
+ return this.get(issueId);
1191
+ }
1192
+ };
1193
+ function buildDateRange(after, before) {
1194
+ if (after !== void 0 && before !== void 0) {
1195
+ return `><${after}|${before}`;
1196
+ }
1197
+ if (after !== void 0) {
1198
+ return `>=${after}`;
1199
+ }
1200
+ if (before !== void 0) {
1201
+ return `<=${before}`;
1202
+ }
1203
+ return void 0;
1204
+ }
1205
+ function readEnumAttribute(attributes2, key) {
1206
+ const attr = attributes2[key];
1207
+ if (attr === void 0) {
1208
+ return void 0;
1209
+ }
1210
+ if (attr.type === "enum") {
1211
+ return attr.value.id;
1212
+ }
1213
+ if (attr.type === "string" || attr.type === "number") {
1214
+ return String(attr.value);
1215
+ }
1216
+ return void 0;
1217
+ }
1218
+ function buildCustomFields(attributes2) {
1219
+ const result = [];
1220
+ for (const [key, attr] of Object.entries(attributes2)) {
1221
+ const match = /^cf_(\d+)$/.exec(key);
1222
+ if (match === null || match[1] === void 0) {
1223
+ continue;
1224
+ }
1225
+ result.push({ id: Number(match[1]), value: attributeToRedmineValue(attr) });
1226
+ }
1227
+ return result;
1228
+ }
1229
+
1230
+ // ../redmine/dist/timeTracker.js
1231
+ var MAX_LIMIT2 = 100;
1232
+ var DEFAULT_LIMIT2 = 25;
1233
+ var RedmineTimeTracker = class {
1234
+ client;
1235
+ constructor(client) {
1236
+ this.client = client;
1237
+ }
1238
+ async capabilities() {
1239
+ let activities = [];
1240
+ try {
1241
+ const res = await this.client.request("/enumerations/time_entry_activities.json");
1242
+ activities = res.time_entry_activities.map((a) => ({ id: String(a.id), name: a.name }));
1243
+ } catch {
1244
+ }
1245
+ return {
1246
+ canCreate: true,
1247
+ canUpdate: true,
1248
+ canDelete: true,
1249
+ requiresIssue: false,
1250
+ activities
1251
+ };
1252
+ }
1253
+ async search(filter) {
1254
+ const limit = Math.min(filter.limit ?? DEFAULT_LIMIT2, MAX_LIMIT2);
1255
+ const offset = filter.cursor !== void 0 ? Number(filter.cursor) : 0;
1256
+ const startOffset = Number.isFinite(offset) && offset > 0 ? offset : 0;
1257
+ const query = {
1258
+ offset: startOffset,
1259
+ limit
1260
+ };
1261
+ if (filter.issueId !== void 0) {
1262
+ query["issue_id"] = filter.issueId;
1263
+ }
1264
+ if (filter.userId !== void 0) {
1265
+ query["user_id"] = filter.userId;
1266
+ }
1267
+ if (filter.projectId !== void 0) {
1268
+ query["project_id"] = filter.projectId;
1269
+ }
1270
+ if (filter.from !== void 0) {
1271
+ query["from"] = filter.from;
1272
+ }
1273
+ if (filter.to !== void 0) {
1274
+ query["to"] = filter.to;
1275
+ }
1276
+ if (filter.sort !== void 0) {
1277
+ query["sort"] = `${filter.sort.field}:${filter.sort.dir}`;
1278
+ }
1279
+ const res = await this.client.request("/time_entries.json", {
1280
+ query
1281
+ });
1282
+ const page = {
1283
+ items: res.time_entries.map(mapTimeEntry),
1284
+ total: res.total_count
1285
+ };
1286
+ const nextOffset = res.offset + res.limit;
1287
+ if (nextOffset < res.total_count) {
1288
+ page.nextCursor = String(nextOffset);
1289
+ }
1290
+ return page;
1291
+ }
1292
+ async get(id) {
1293
+ const res = await this.client.request(`/time_entries/${id}.json`);
1294
+ return mapTimeEntry(res.time_entry);
1295
+ }
1296
+ async create(input) {
1297
+ const body = {
1298
+ issue_id: Number(input.issueId),
1299
+ hours: input.durationMinutes / 60,
1300
+ spent_on: input.date
1301
+ };
1302
+ if (input.activityId !== void 0) {
1303
+ body.activity_id = Number(input.activityId);
1304
+ }
1305
+ if (input.description !== void 0) {
1306
+ body.comments = input.description;
1307
+ }
1308
+ const res = await this.client.request("/time_entries.json", {
1309
+ method: "POST",
1310
+ body: { time_entry: body }
1311
+ });
1312
+ return mapTimeEntry(res.time_entry);
1313
+ }
1314
+ async update(id, input) {
1315
+ const body = {};
1316
+ if (input.issueId !== void 0) {
1317
+ body.issue_id = Number(input.issueId);
1318
+ }
1319
+ if (input.durationMinutes !== void 0) {
1320
+ body.hours = input.durationMinutes / 60;
1321
+ }
1322
+ if (input.date !== void 0) {
1323
+ body.spent_on = input.date;
1324
+ }
1325
+ if (input.activityId !== void 0) {
1326
+ body.activity_id = Number(input.activityId);
1327
+ }
1328
+ if (input.description !== void 0) {
1329
+ body.comments = input.description;
1330
+ }
1331
+ await this.client.request(`/time_entries/${id}.json`, {
1332
+ method: "PUT",
1333
+ body: { time_entry: body }
1334
+ });
1335
+ return this.get(id);
1336
+ }
1337
+ async delete(id) {
1338
+ await this.client.request(`/time_entries/${id}.json`, { method: "DELETE" });
1339
+ }
1340
+ };
1341
+
1342
+ // ../redmine/dist/session.js
1343
+ var RedmineSession = class {
1344
+ tracker;
1345
+ user;
1346
+ constructor(tracker, user2) {
1347
+ this.tracker = tracker;
1348
+ this.user = user2;
1349
+ }
1350
+ async currentUser() {
1351
+ return this.user;
1352
+ }
1353
+ async close() {
1354
+ }
1355
+ };
1356
+ var RedmineAuthenticator = class {
1357
+ async authenticate(config) {
1358
+ const client = new RedmineClient(config);
1359
+ let user2;
1360
+ try {
1361
+ const res = await client.request("/users/current.json");
1362
+ user2 = mapCurrentUser(res.user);
1363
+ } catch (cause) {
1364
+ if (cause instanceof AuthError) {
1365
+ throw cause;
1366
+ }
1367
+ throw new AuthError(`Redmine credential validation failed: ${String(cause)}`, { cause });
1368
+ }
1369
+ const tracker = {
1370
+ issues: new RedmineIssuesTracker(client),
1371
+ time: new RedmineTimeTracker(client)
1372
+ };
1373
+ return new RedmineSession(tracker, user2);
1374
+ }
1375
+ };
1376
+
1377
+ // src/result.ts
1378
+ function ok(data) {
1379
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1380
+ }
1381
+ function fail(text) {
1382
+ return { content: [{ type: "text", text }], isError: true };
1383
+ }
1384
+ async function runTool(redact, fn) {
1385
+ try {
1386
+ return ok(await fn());
1387
+ } catch (err) {
1388
+ let message;
1389
+ if (err instanceof IssueTrackerError) {
1390
+ message = `${err.name}: ${err.message}`;
1391
+ } else if (err instanceof Error) {
1392
+ message = err.message;
1393
+ } else {
1394
+ message = String(err);
1395
+ }
1396
+ return fail(redact(message));
1397
+ }
1398
+ }
1399
+
1400
+ // src/schemas.ts
1401
+ import { z } from "zod";
1402
+ var fieldOption = z.object({ id: z.string(), name: z.string() });
1403
+ var user = z.object({
1404
+ id: z.string(),
1405
+ name: z.string(),
1406
+ email: z.string().optional()
1407
+ });
1408
+ var attributeValue = z.discriminatedUnion("type", [
1409
+ z.object({ type: z.literal("string"), value: z.string() }),
1410
+ z.object({ type: z.literal("text"), value: z.string() }),
1411
+ z.object({ type: z.literal("number"), value: z.number() }),
1412
+ z.object({ type: z.literal("date"), value: z.string() }),
1413
+ z.object({ type: z.literal("boolean"), value: z.boolean() }),
1414
+ z.object({ type: z.literal("enum"), value: fieldOption }),
1415
+ z.object({ type: z.literal("multi_enum"), value: z.array(fieldOption) }),
1416
+ z.object({ type: z.literal("user"), value: user })
1417
+ ]);
1418
+ var attributes = z.record(z.string(), attributeValue);
1419
+ var statusCategory = z.enum(["open", "in_progress", "done", "unknown"]);
1420
+ var sort = z.object({ field: z.string(), dir: z.enum(["asc", "desc"]) });
1421
+ var issueFilterShape = {
1422
+ text: z.string().optional(),
1423
+ projectId: z.string().optional(),
1424
+ statusCategory: statusCategory.optional(),
1425
+ assigneeId: z.string().optional(),
1426
+ authorId: z.string().optional(),
1427
+ updatedAfter: z.string().optional(),
1428
+ updatedBefore: z.string().optional(),
1429
+ createdAfter: z.string().optional(),
1430
+ createdBefore: z.string().optional(),
1431
+ sort: sort.optional(),
1432
+ limit: z.number().int().positive().optional(),
1433
+ cursor: z.string().optional()
1434
+ };
1435
+ var getIssueShape = { id: z.string() };
1436
+ var createIssueShape = {
1437
+ title: z.string(),
1438
+ description: z.string().nullable().optional(),
1439
+ projectId: z.string().optional(),
1440
+ assigneeId: z.string().nullable().optional(),
1441
+ attributes: attributes.optional()
1442
+ };
1443
+ var updateIssueShape = {
1444
+ id: z.string(),
1445
+ title: z.string().optional(),
1446
+ description: z.string().nullable().optional(),
1447
+ projectId: z.string().optional(),
1448
+ assigneeId: z.string().nullable().optional(),
1449
+ attributes: attributes.optional()
1450
+ };
1451
+ var deleteIssueShape = { id: z.string() };
1452
+ var addCommentShape = { issueId: z.string(), body: z.string() };
1453
+ var getTransitionsShape = { issueId: z.string() };
1454
+ var transitionShape = { issueId: z.string(), transitionId: z.string() };
1455
+ var timeEntryFilterShape = {
1456
+ issueId: z.string().optional(),
1457
+ userId: z.string().optional(),
1458
+ projectId: z.string().optional(),
1459
+ from: z.string().optional(),
1460
+ to: z.string().optional(),
1461
+ sort: sort.optional(),
1462
+ limit: z.number().int().positive().optional(),
1463
+ cursor: z.string().optional()
1464
+ };
1465
+ var getTimeEntryShape = { id: z.string() };
1466
+ var createTimeEntryShape = {
1467
+ issueId: z.string(),
1468
+ date: z.string(),
1469
+ durationMinutes: z.number().positive(),
1470
+ description: z.string().optional(),
1471
+ activityId: z.string().optional(),
1472
+ attributes: attributes.optional()
1473
+ };
1474
+ var updateTimeEntryShape = {
1475
+ id: z.string(),
1476
+ issueId: z.string().optional(),
1477
+ date: z.string().optional(),
1478
+ durationMinutes: z.number().positive().optional(),
1479
+ description: z.string().optional(),
1480
+ activityId: z.string().optional(),
1481
+ attributes: attributes.optional()
1482
+ };
1483
+ var deleteTimeEntryShape = { id: z.string() };
1484
+
1485
+ // src/tools.ts
1486
+ var READ_ONLY = { readOnlyHint: true };
1487
+ var DESTRUCTIVE = { destructiveHint: true };
1488
+ function registerTrackerTools(server, kind, session, redact) {
1489
+ const { issues, time } = session.tracker;
1490
+ const name = (op) => `${kind}_${op}`;
1491
+ const run = (fn) => runTool(redact, fn);
1492
+ server.registerTool(
1493
+ name("whoami"),
1494
+ {
1495
+ title: `${kind}: current user`,
1496
+ description: `Return the authenticated ${kind} account.`,
1497
+ annotations: READ_ONLY
1498
+ },
1499
+ () => run(() => session.currentUser())
1500
+ );
1501
+ server.registerTool(
1502
+ name("capabilities"),
1503
+ {
1504
+ title: `${kind}: capabilities`,
1505
+ description: `Describe what the ${kind} tracker supports, including the fields accepted on create/update. Consult this before constructing a create_issue payload.`,
1506
+ annotations: READ_ONLY
1507
+ },
1508
+ () => run(async () => ({
1509
+ issues: await issues.capabilities(),
1510
+ time: time ? await time.capabilities() : null
1511
+ }))
1512
+ );
1513
+ server.registerTool(
1514
+ name("search_issues"),
1515
+ {
1516
+ title: `${kind}: search issues`,
1517
+ description: `Search ${kind} issues with a normalized filter. Returns a page of issues with an optional nextCursor.`,
1518
+ inputSchema: issueFilterShape,
1519
+ annotations: READ_ONLY
1520
+ },
1521
+ (args) => run(() => issues.search(args))
1522
+ );
1523
+ server.registerTool(
1524
+ name("get_issue"),
1525
+ {
1526
+ title: `${kind}: get issue`,
1527
+ description: `Fetch a single ${kind} issue by id (or key), including its timeline items.`,
1528
+ inputSchema: getIssueShape,
1529
+ annotations: READ_ONLY
1530
+ },
1531
+ ({ id }) => run(() => issues.get(id))
1532
+ );
1533
+ server.registerTool(
1534
+ name("create_issue"),
1535
+ {
1536
+ title: `${kind}: create issue`,
1537
+ description: `Create a ${kind} issue. Tracker-specific required fields go in \`attributes\`; check \`${kind}_capabilities\` first.`,
1538
+ inputSchema: createIssueShape
1539
+ },
1540
+ (args) => run(() => issues.create(args))
1541
+ );
1542
+ server.registerTool(
1543
+ name("update_issue"),
1544
+ {
1545
+ title: `${kind}: update issue`,
1546
+ description: `Update fields on a ${kind} issue. Status is changed via transitions, not here.`,
1547
+ inputSchema: updateIssueShape,
1548
+ annotations: { idempotentHint: true }
1549
+ },
1550
+ ({ id, ...input }) => run(() => issues.update(id, input))
1551
+ );
1552
+ server.registerTool(
1553
+ name("delete_issue"),
1554
+ {
1555
+ title: `${kind}: delete issue`,
1556
+ description: `Permanently delete a ${kind} issue.`,
1557
+ inputSchema: deleteIssueShape,
1558
+ annotations: DESTRUCTIVE
1559
+ },
1560
+ ({ id }) => run(() => issues.delete(id).then(() => ({ deleted: id })))
1561
+ );
1562
+ server.registerTool(
1563
+ name("add_comment"),
1564
+ {
1565
+ title: `${kind}: add comment`,
1566
+ description: `Add a comment to a ${kind} issue.`,
1567
+ inputSchema: addCommentShape
1568
+ },
1569
+ ({ issueId, body }) => run(() => issues.addComment(issueId, body))
1570
+ );
1571
+ server.registerTool(
1572
+ name("get_transitions"),
1573
+ {
1574
+ title: `${kind}: get transitions`,
1575
+ description: `List the status transitions currently available for a ${kind} issue.`,
1576
+ inputSchema: getTransitionsShape,
1577
+ annotations: READ_ONLY
1578
+ },
1579
+ ({ issueId }) => run(() => issues.getTransitions(issueId))
1580
+ );
1581
+ server.registerTool(
1582
+ name("transition_issue"),
1583
+ {
1584
+ title: `${kind}: transition issue`,
1585
+ description: `Move a ${kind} issue through a transition (use get_transitions for valid ids).`,
1586
+ inputSchema: transitionShape
1587
+ },
1588
+ ({ issueId, transitionId }) => run(() => issues.transition(issueId, transitionId))
1589
+ );
1590
+ if (!time) return;
1591
+ server.registerTool(
1592
+ name("search_time_entries"),
1593
+ {
1594
+ title: `${kind}: search time entries`,
1595
+ description: `Search ${kind} time entries with a normalized filter.`,
1596
+ inputSchema: timeEntryFilterShape,
1597
+ annotations: READ_ONLY
1598
+ },
1599
+ (args) => run(() => time.search(args))
1600
+ );
1601
+ server.registerTool(
1602
+ name("get_time_entry"),
1603
+ {
1604
+ title: `${kind}: get time entry`,
1605
+ description: `Fetch a single ${kind} time entry by id.`,
1606
+ inputSchema: getTimeEntryShape,
1607
+ annotations: READ_ONLY
1608
+ },
1609
+ ({ id }) => run(() => time.get(id))
1610
+ );
1611
+ server.registerTool(
1612
+ name("create_time_entry"),
1613
+ {
1614
+ title: `${kind}: log time`,
1615
+ description: `Log a ${kind} time entry against an issue. durationMinutes is in minutes.`,
1616
+ inputSchema: createTimeEntryShape
1617
+ },
1618
+ (args) => run(() => time.create(args))
1619
+ );
1620
+ server.registerTool(
1621
+ name("update_time_entry"),
1622
+ {
1623
+ title: `${kind}: update time entry`,
1624
+ description: `Update a ${kind} time entry.`,
1625
+ inputSchema: updateTimeEntryShape,
1626
+ annotations: { idempotentHint: true }
1627
+ },
1628
+ ({ id, ...input }) => run(() => time.update(id, input))
1629
+ );
1630
+ server.registerTool(
1631
+ name("delete_time_entry"),
1632
+ {
1633
+ title: `${kind}: delete time entry`,
1634
+ description: `Permanently delete a ${kind} time entry.`,
1635
+ inputSchema: deleteTimeEntryShape,
1636
+ annotations: DESTRUCTIVE
1637
+ },
1638
+ ({ id }) => run(() => time.delete(id).then(() => ({ deleted: id })))
1639
+ );
1640
+ }
1641
+
1642
+ // src/server.ts
1643
+ var AUTHENTICATORS = {
1644
+ jira: () => new JiraAuthenticator(),
1645
+ redmine: () => new RedmineAuthenticator()
1646
+ };
1647
+ async function createServer(env = process.env) {
1648
+ const { trackers, secrets, warnings: configWarnings } = loadConfig(env);
1649
+ const redact = makeRedactor(secrets);
1650
+ const server = new McpServer({ name: "issues-mcp", version: "1.0.0" });
1651
+ const sessions = [];
1652
+ const warnings = [...configWarnings];
1653
+ for (const { kind, connection } of trackers) {
1654
+ try {
1655
+ const session = await AUTHENTICATORS[kind]().authenticate(connection);
1656
+ sessions.push(session);
1657
+ registerTrackerTools(server, kind, session, redact);
1658
+ } catch (err) {
1659
+ const message = err instanceof Error ? err.message : String(err);
1660
+ warnings.push(`${kind}: authentication failed \u2014 ${redact(message)}`);
1661
+ }
1662
+ }
1663
+ return { server, sessions, warnings };
1664
+ }
1665
+
1666
+ export {
1667
+ loadConfig,
1668
+ makeRedactor,
1669
+ createServer
1670
+ };
1671
+ //# sourceMappingURL=chunk-Y53ZARWV.js.map