@geeveeh/atlassian-tools 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -3
- package/dist/cli/confluence.d.ts.map +1 -1
- package/dist/cli/confluence.js +242 -10
- package/dist/cli/confluence.js.map +1 -1
- package/dist/cli/jira.d.ts.map +1 -1
- package/dist/cli/jira.js +310 -2
- package/dist/cli/jira.js.map +1 -1
- package/dist/confluence/client.d.ts +7 -1
- package/dist/confluence/client.d.ts.map +1 -1
- package/dist/confluence/client.js +47 -20
- package/dist/confluence/client.js.map +1 -1
- package/dist/confluence/index.d.ts +1 -0
- package/dist/confluence/index.d.ts.map +1 -1
- package/dist/confluence/index.js +1 -0
- package/dist/confluence/index.js.map +1 -1
- package/dist/confluence/markdown.d.ts +15 -0
- package/dist/confluence/markdown.d.ts.map +1 -0
- package/dist/confluence/markdown.js +83 -0
- package/dist/confluence/markdown.js.map +1 -0
- package/dist/confluence/types.d.ts +55 -0
- package/dist/confluence/types.d.ts.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +22 -5
- package/dist/core/auth.js.map +1 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +56 -18
- package/dist/core/client.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/mime.d.ts +2 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/mime.js +21 -0
- package/dist/core/mime.js.map +1 -0
- package/dist/jira/client.d.ts +10 -1
- package/dist/jira/client.d.ts.map +1 -1
- package/dist/jira/client.js +59 -20
- package/dist/jira/client.js.map +1 -1
- package/dist/jira/types.d.ts +38 -0
- package/dist/jira/types.d.ts.map +1 -1
- package/dist/mcp/confluence.d.ts +3 -0
- package/dist/mcp/confluence.d.ts.map +1 -0
- package/dist/mcp/confluence.js +405 -0
- package/dist/mcp/confluence.js.map +1 -0
- package/dist/mcp/helpers.d.ts +3 -0
- package/dist/mcp/helpers.d.ts.map +1 -0
- package/dist/mcp/helpers.js +15 -0
- package/dist/mcp/helpers.js.map +1 -0
- package/dist/mcp/index.js +8 -646
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/jira.d.ts +3 -0
- package/dist/mcp/jira.d.ts.map +1 -0
- package/dist/mcp/jira.js +479 -0
- package/dist/mcp/jira.js.map +1 -0
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -1,652 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function getConfluenceClient() {
|
|
13
|
-
if (!_confluenceClient) {
|
|
14
|
-
_confluenceClient = new ConfluenceClient(loadConfigFromEnv());
|
|
15
|
-
}
|
|
16
|
-
return _confluenceClient;
|
|
17
|
-
}
|
|
18
|
-
function getJiraClient() {
|
|
19
|
-
if (!_jiraClient) {
|
|
20
|
-
_jiraClient = new JiraClient(loadConfigFromEnv());
|
|
21
|
-
}
|
|
22
|
-
return _jiraClient;
|
|
23
|
-
}
|
|
24
|
-
function formatError(err) {
|
|
25
|
-
if (err instanceof AtlassianApiError) {
|
|
26
|
-
return `Atlassian API error ${err.statusCode}: ${err.message}${err.data ? "\n" + JSON.stringify(err.data, null, 2) : ""}`;
|
|
27
|
-
}
|
|
28
|
-
if (err instanceof Error)
|
|
29
|
-
return err.message;
|
|
30
|
-
return String(err);
|
|
31
|
-
}
|
|
32
|
-
// ── Server setup ───────────────────────────────────────────────────
|
|
33
|
-
const server = new McpServer({
|
|
34
|
-
name: "atlassian",
|
|
35
|
-
version: "0.2.0",
|
|
36
|
-
});
|
|
37
|
-
// ── Confluence tools ───────────────────────────────────────────────
|
|
38
|
-
server.tool("confluence_auth", "Verify the Confluence connection and list accessible spaces", {}, async () => {
|
|
39
|
-
try {
|
|
40
|
-
const spaces = await getConfluenceClient().verifyConnection();
|
|
41
|
-
const text = [
|
|
42
|
-
"Connected successfully.",
|
|
43
|
-
`Found ${spaces.length} space(s):`,
|
|
44
|
-
...spaces.map((s) => `• ${s.name} [${s.key}] (id: ${s.id})`),
|
|
45
|
-
].join("\n");
|
|
46
|
-
return { content: [{ type: "text", text }] };
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
server.tool("confluence_list_spaces", "List available Confluence spaces", { limit: z.number().optional().describe("Max number of spaces to return (default 25)") }, async ({ limit }) => {
|
|
53
|
-
try {
|
|
54
|
-
const spaces = await getConfluenceClient().listSpaces(limit ?? 25);
|
|
55
|
-
const text = spaces
|
|
56
|
-
.map((s) => `${s.name} [${s.key}] (id: ${s.id}, ${s.status})`)
|
|
57
|
-
.join("\n");
|
|
58
|
-
return { content: [{ type: "text", text: text || "No spaces found." }] };
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
server.tool("confluence_read_page", "Read a Confluence page by its ID, returning title, metadata, and body content", { pageId: z.string().describe("The Confluence page ID") }, async ({ pageId }) => {
|
|
65
|
-
try {
|
|
66
|
-
const page = await getConfluenceClient().getPage(pageId);
|
|
67
|
-
const text = [
|
|
68
|
-
`Title: ${page.title}`,
|
|
69
|
-
`ID: ${page.id}`,
|
|
70
|
-
`Status: ${page.status}`,
|
|
71
|
-
`Version: ${page.version.number}`,
|
|
72
|
-
`Space ID: ${page.spaceId}`,
|
|
73
|
-
"",
|
|
74
|
-
"--- Body (storage format) ---",
|
|
75
|
-
page.body?.storage?.value ?? "(empty)",
|
|
76
|
-
].join("\n");
|
|
77
|
-
return { content: [{ type: "text", text }] };
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
server.tool("confluence_search_pages", "Search for pages in a Confluence space by title", {
|
|
84
|
-
spaceKey: z.string().describe("The space key (e.g. 'DEV', 'HR')"),
|
|
85
|
-
title: z.string().optional().describe("Filter by page title (partial match)"),
|
|
86
|
-
limit: z.number().optional().describe("Max results (default 25)"),
|
|
87
|
-
}, async ({ spaceKey, title, limit }) => {
|
|
88
|
-
try {
|
|
89
|
-
const pages = await getConfluenceClient().searchPages({ spaceKey, title, limit });
|
|
90
|
-
if (pages.length === 0) {
|
|
91
|
-
return { content: [{ type: "text", text: "No pages found." }] };
|
|
92
|
-
}
|
|
93
|
-
const text = pages
|
|
94
|
-
.map((p) => `${p.title} (id: ${p.id}, v${p.version?.number ?? "?"})`)
|
|
95
|
-
.join("\n");
|
|
96
|
-
return { content: [{ type: "text", text }] };
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
server.tool("confluence_create_page", "Create a new Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
103
|
-
spaceKey: z.string().describe("The space key to create the page in"),
|
|
104
|
-
title: z.string().describe("Page title"),
|
|
105
|
-
body: z.string().describe("Page content in Markdown or Confluence storage format (XHTML)"),
|
|
106
|
-
parentId: z.string().optional().describe("Parent page ID (omit for top-level)"),
|
|
107
|
-
draft: z.boolean().optional().describe("Create as draft instead of published"),
|
|
108
|
-
}, async ({ spaceKey, title, body, parentId, draft }) => {
|
|
109
|
-
try {
|
|
110
|
-
const client = getConfluenceClient();
|
|
111
|
-
const space = await client.getSpaceByKey(spaceKey);
|
|
112
|
-
const resolvedBody = await resolveBody(body);
|
|
113
|
-
const page = await client.createPage({
|
|
114
|
-
spaceId: space.id,
|
|
115
|
-
title,
|
|
116
|
-
body: resolvedBody,
|
|
117
|
-
parentId,
|
|
118
|
-
status: draft ? "draft" : "current",
|
|
119
|
-
});
|
|
120
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
121
|
-
const text = [
|
|
122
|
-
`✓ Page created successfully.`,
|
|
123
|
-
` Title: ${page.title}`,
|
|
124
|
-
` ID: ${page.id}`,
|
|
125
|
-
` Space: ${space.name} [${space.key}]`,
|
|
126
|
-
` Status: ${page.status}`,
|
|
127
|
-
` URL: ${baseUrl}/wiki/pages/${page.id}`,
|
|
128
|
-
].join("\n");
|
|
129
|
-
return { content: [{ type: "text", text }] };
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
server.tool("confluence_update_page", "Update an existing Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
136
|
-
pageId: z.string().describe("The page ID to update"),
|
|
137
|
-
title: z.string().optional().describe("New title (omit to keep current)"),
|
|
138
|
-
body: z.string().optional().describe("New content in Markdown or Confluence storage format"),
|
|
139
|
-
versionMessage: z.string().optional().describe("Version change message"),
|
|
140
|
-
}, async ({ pageId, title, body, versionMessage }) => {
|
|
141
|
-
try {
|
|
142
|
-
const resolvedBody = body ? await resolveBody(body) : undefined;
|
|
143
|
-
const page = await getConfluenceClient().updatePage({
|
|
144
|
-
pageId,
|
|
145
|
-
title,
|
|
146
|
-
body: resolvedBody,
|
|
147
|
-
versionMessage,
|
|
148
|
-
});
|
|
149
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
150
|
-
const text = [
|
|
151
|
-
`✓ Page updated successfully.`,
|
|
152
|
-
` Title: ${page.title}`,
|
|
153
|
-
` ID: ${page.id}`,
|
|
154
|
-
` Version: ${page.version.number}`,
|
|
155
|
-
` URL: ${baseUrl}/wiki/pages/${page.id}`,
|
|
156
|
-
].join("\n");
|
|
157
|
-
return { content: [{ type: "text", text }] };
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
server.tool("confluence_delete_page", "Delete a Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", { pageId: z.string().describe("The page ID to delete") }, async ({ pageId }) => {
|
|
164
|
-
try {
|
|
165
|
-
const client = getConfluenceClient();
|
|
166
|
-
const page = await client.getPage(pageId);
|
|
167
|
-
await client.deletePage(pageId);
|
|
168
|
-
return {
|
|
169
|
-
content: [
|
|
170
|
-
{ type: "text", text: `✓ Deleted page "${page.title}" (id: ${pageId})` },
|
|
171
|
-
],
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
server.tool("confluence_list_comments", "List comments on a Confluence page", {
|
|
179
|
-
pageId: z.string().describe("The Confluence page ID"),
|
|
180
|
-
limit: z.number().optional().describe("Max results (default 25)"),
|
|
181
|
-
}, async ({ pageId, limit }) => {
|
|
182
|
-
try {
|
|
183
|
-
const comments = await getConfluenceClient().listComments(pageId, limit ?? 25);
|
|
184
|
-
if (comments.length === 0) {
|
|
185
|
-
return { content: [{ type: "text", text: "No comments." }] };
|
|
186
|
-
}
|
|
187
|
-
const text = comments.map((c) => {
|
|
188
|
-
const author = c.history?.createdBy?.displayName ?? "Unknown";
|
|
189
|
-
const date = c.history?.createdDate ? new Date(c.history.createdDate).toLocaleDateString() : "";
|
|
190
|
-
const body = c.body?.storage?.value ?? "";
|
|
191
|
-
return `[${author}${date ? ` · ${date}` : ""}]\n${body}`;
|
|
192
|
-
}).join("\n\n");
|
|
193
|
-
return { content: [{ type: "text", text }] };
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
server.tool("confluence_add_comment", "Add a comment to a Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
200
|
-
pageId: z.string().describe("The Confluence page ID"),
|
|
201
|
-
text: z.string().describe("Comment text (plain text or Confluence storage format XHTML)"),
|
|
202
|
-
}, async ({ pageId, text }) => {
|
|
203
|
-
try {
|
|
204
|
-
const body = text.trimStart().startsWith("<") ? text : `<p>${text}</p>`;
|
|
205
|
-
await getConfluenceClient().addComment(pageId, body);
|
|
206
|
-
return { content: [{ type: "text", text: "✓ Comment added." }] };
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
server.tool("confluence_list_labels", "List labels on a Confluence page", { pageId: z.string().describe("The Confluence page ID") }, async ({ pageId }) => {
|
|
213
|
-
try {
|
|
214
|
-
const labels = await getConfluenceClient().listLabels(pageId);
|
|
215
|
-
if (labels.length === 0) {
|
|
216
|
-
return { content: [{ type: "text", text: "No labels." }] };
|
|
217
|
-
}
|
|
218
|
-
return { content: [{ type: "text", text: labels.map((l) => l.name).join(", ") }] };
|
|
219
|
-
}
|
|
220
|
-
catch (err) {
|
|
221
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
server.tool("confluence_add_labels", "Add labels to a Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
225
|
-
pageId: z.string().describe("The Confluence page ID"),
|
|
226
|
-
labels: z.array(z.string()).describe("Label names to add"),
|
|
227
|
-
}, async ({ pageId, labels }) => {
|
|
228
|
-
try {
|
|
229
|
-
const result = await getConfluenceClient().addLabels(pageId, labels);
|
|
230
|
-
const text = `✓ Added labels: ${result.map((l) => l.name).join(", ")}`;
|
|
231
|
-
return { content: [{ type: "text", text }] };
|
|
232
|
-
}
|
|
233
|
-
catch (err) {
|
|
234
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
server.tool("confluence_remove_label", "Remove a label from a Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
238
|
-
pageId: z.string().describe("The Confluence page ID"),
|
|
239
|
-
label: z.string().describe("Label name to remove"),
|
|
240
|
-
}, async ({ pageId, label }) => {
|
|
241
|
-
try {
|
|
242
|
-
await getConfluenceClient().removeLabel(pageId, label);
|
|
243
|
-
return { content: [{ type: "text", text: `✓ Removed label "${label}"` }] };
|
|
244
|
-
}
|
|
245
|
-
catch (err) {
|
|
246
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
server.tool("confluence_list_child_pages", "List child pages of a Confluence page", {
|
|
250
|
-
pageId: z.string().describe("The parent page ID"),
|
|
251
|
-
limit: z.number().optional().describe("Max results (default 25)"),
|
|
252
|
-
}, async ({ pageId, limit }) => {
|
|
253
|
-
try {
|
|
254
|
-
const pages = await getConfluenceClient().listChildPages(pageId, limit ?? 25);
|
|
255
|
-
if (pages.length === 0) {
|
|
256
|
-
return { content: [{ type: "text", text: "No child pages found." }] };
|
|
257
|
-
}
|
|
258
|
-
const text = pages
|
|
259
|
-
.map((p) => `${p.title} (id: ${p.id}, v${p.version?.number ?? "?"})`)
|
|
260
|
-
.join("\n");
|
|
261
|
-
return { content: [{ type: "text", text }] };
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
server.tool("confluence_upload_attachment", "Upload a file as an attachment to a Confluence page. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
268
|
-
pageId: z.string().describe("The page ID to attach the file to"),
|
|
269
|
-
filePath: z.string().describe("Absolute path to the file to upload"),
|
|
270
|
-
comment: z.string().optional().describe("Optional comment for the attachment"),
|
|
271
|
-
}, async ({ pageId, filePath, comment }) => {
|
|
272
|
-
try {
|
|
273
|
-
const att = await getConfluenceClient().uploadAttachment({ pageId, filePath, comment });
|
|
274
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
275
|
-
const downloadUrl = att._links?.download ? `${baseUrl}/wiki${att._links.download}` : "";
|
|
276
|
-
const text = [
|
|
277
|
-
`✓ Attachment uploaded successfully.`,
|
|
278
|
-
` File: ${att.title}`,
|
|
279
|
-
` ID: ${att.id}`,
|
|
280
|
-
` Type: ${att.mediaType}`,
|
|
281
|
-
...(downloadUrl ? [` Download: ${downloadUrl}`] : []),
|
|
282
|
-
].join("\n");
|
|
283
|
-
return { content: [{ type: "text", text }] };
|
|
284
|
-
}
|
|
285
|
-
catch (err) {
|
|
286
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
server.tool("confluence_list_attachments", "List all attachments on a Confluence page", { pageId: z.string().describe("The Confluence page ID") }, async ({ pageId }) => {
|
|
290
|
-
try {
|
|
291
|
-
const attachments = await getConfluenceClient().listAttachments(pageId);
|
|
292
|
-
if (attachments.length === 0) {
|
|
293
|
-
return { content: [{ type: "text", text: "No attachments found." }] };
|
|
294
|
-
}
|
|
295
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
296
|
-
const text = attachments
|
|
297
|
-
.map((a) => {
|
|
298
|
-
const downloadUrl = a._links?.download ? `${baseUrl}/wiki${a._links.download}` : "";
|
|
299
|
-
return [
|
|
300
|
-
`${a.title} (id: ${a.id}, ${a.mediaType}${a.fileSize ? `, ${a.fileSize} bytes` : ""})`,
|
|
301
|
-
...(downloadUrl ? [` ${downloadUrl}`] : []),
|
|
302
|
-
].join("\n");
|
|
303
|
-
})
|
|
304
|
-
.join("\n");
|
|
305
|
-
return { content: [{ type: "text", text }] };
|
|
306
|
-
}
|
|
307
|
-
catch (err) {
|
|
308
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
// ── Jira tools ─────────────────────────────────────────────────────
|
|
312
|
-
server.tool("jira_auth", "Verify the Jira connection and list accessible projects", {}, async () => {
|
|
313
|
-
try {
|
|
314
|
-
const projects = await getJiraClient().verifyConnection();
|
|
315
|
-
const text = [
|
|
316
|
-
"Connected successfully.",
|
|
317
|
-
`Found ${projects.length} project(s):`,
|
|
318
|
-
...projects.map((p) => `• ${p.name} [${p.key}] (id: ${p.id})`),
|
|
319
|
-
].join("\n");
|
|
320
|
-
return { content: [{ type: "text", text }] };
|
|
321
|
-
}
|
|
322
|
-
catch (err) {
|
|
323
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
server.tool("jira_list_projects", "List available Jira projects", { limit: z.number().optional().describe("Max number of projects to return (default 25)") }, async ({ limit }) => {
|
|
327
|
-
try {
|
|
328
|
-
const projects = await getJiraClient().listProjects(limit ?? 25);
|
|
329
|
-
const text = projects
|
|
330
|
-
.map((p) => `${p.name} [${p.key}] (id: ${p.id})`)
|
|
331
|
-
.join("\n");
|
|
332
|
-
return { content: [{ type: "text", text: text || "No projects found." }] };
|
|
333
|
-
}
|
|
334
|
-
catch (err) {
|
|
335
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
server.tool("jira_get_issue", "Get a Jira issue by key, returning summary, status, assignee, and description", { issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')") }, async ({ issueKey }) => {
|
|
339
|
-
try {
|
|
340
|
-
const client = getJiraClient();
|
|
341
|
-
const issue = await client.getIssue(issueKey);
|
|
342
|
-
const desc = client.descriptionToText(issue);
|
|
343
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
344
|
-
const text = [
|
|
345
|
-
`Key: ${issue.key}`,
|
|
346
|
-
`Summary: ${issue.fields.summary}`,
|
|
347
|
-
`Type: ${issue.fields.issuetype.name}`,
|
|
348
|
-
`Status: ${issue.fields.status.name}`,
|
|
349
|
-
`Priority: ${issue.fields.priority?.name ?? "—"}`,
|
|
350
|
-
`Assignee: ${issue.fields.assignee?.displayName ?? "Unassigned"}`,
|
|
351
|
-
`Reporter: ${issue.fields.reporter?.displayName ?? "—"}`,
|
|
352
|
-
`Labels: ${issue.fields.labels?.join(", ") || "—"}`,
|
|
353
|
-
`Created: ${issue.fields.created}`,
|
|
354
|
-
`Updated: ${issue.fields.updated}`,
|
|
355
|
-
`URL: ${baseUrl}/browse/${issue.key}`,
|
|
356
|
-
"",
|
|
357
|
-
"--- Description ---",
|
|
358
|
-
desc || "(empty)",
|
|
359
|
-
].join("\n");
|
|
360
|
-
return { content: [{ type: "text", text }] };
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
server.tool("jira_search_issues", "Search for Jira issues using JQL or filters", {
|
|
367
|
-
jql: z.string().optional().describe("Raw JQL query (overrides other filters)"),
|
|
368
|
-
project: z.string().optional().describe("Filter by project key"),
|
|
369
|
-
status: z.string().optional().describe("Filter by status name"),
|
|
370
|
-
assignee: z.string().optional().describe("Filter by assignee name"),
|
|
371
|
-
type: z.string().optional().describe("Filter by issue type (Bug, Task, Story, etc.)"),
|
|
372
|
-
limit: z.number().optional().describe("Max results (default 25)"),
|
|
373
|
-
}, async ({ jql, project, status, assignee, type, limit }) => {
|
|
374
|
-
try {
|
|
375
|
-
const issues = await getJiraClient().searchIssues({
|
|
376
|
-
jql, project, status, assignee, type, limit,
|
|
377
|
-
});
|
|
378
|
-
if (issues.length === 0) {
|
|
379
|
-
return { content: [{ type: "text", text: "No issues found." }] };
|
|
380
|
-
}
|
|
381
|
-
const text = issues
|
|
382
|
-
.map((i) => `${i.key} — ${i.fields.summary} [${i.fields.status.name}] (${i.fields.issuetype.name}, ${i.fields.assignee?.displayName ?? "Unassigned"})`)
|
|
383
|
-
.join("\n");
|
|
384
|
-
return { content: [{ type: "text", text }] };
|
|
385
|
-
}
|
|
386
|
-
catch (err) {
|
|
387
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
server.tool("jira_create_issue", "Create a new Jira issue. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
391
|
-
projectKey: z.string().describe("The project key (e.g. 'PROJ')"),
|
|
392
|
-
issueType: z.string().describe("Issue type (Bug, Task, Story, Epic, etc.)"),
|
|
393
|
-
summary: z.string().describe("Issue summary/title"),
|
|
394
|
-
description: z.string().optional().describe("Issue description (plain text)"),
|
|
395
|
-
priority: z.string().optional().describe("Priority (Highest, High, Medium, Low, Lowest)"),
|
|
396
|
-
labels: z.array(z.string()).optional().describe("Labels to apply"),
|
|
397
|
-
parentKey: z.string().optional().describe("Parent issue key for creating a subtask (e.g. 'PROJ-10')"),
|
|
398
|
-
}, async ({ projectKey, issueType, summary, description, priority, labels, parentKey }) => {
|
|
399
|
-
try {
|
|
400
|
-
const client = getJiraClient();
|
|
401
|
-
const issue = await client.createIssue({
|
|
402
|
-
projectKey, issueType, summary, description, priority, labels, parentKey,
|
|
403
|
-
});
|
|
404
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
405
|
-
const text = [
|
|
406
|
-
`✓ Issue created successfully.`,
|
|
407
|
-
` Key: ${issue.key}`,
|
|
408
|
-
` Summary: ${issue.fields.summary}`,
|
|
409
|
-
` Type: ${issue.fields.issuetype.name}`,
|
|
410
|
-
` Status: ${issue.fields.status.name}`,
|
|
411
|
-
` URL: ${baseUrl}/browse/${issue.key}`,
|
|
412
|
-
].join("\n");
|
|
413
|
-
return { content: [{ type: "text", text }] };
|
|
414
|
-
}
|
|
415
|
-
catch (err) {
|
|
416
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
server.tool("jira_update_issue", "Update an existing Jira issue. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
420
|
-
issueKey: z.string().describe("The issue key to update (e.g. 'PROJ-123')"),
|
|
421
|
-
summary: z.string().optional().describe("New summary/title"),
|
|
422
|
-
description: z.string().optional().describe("New description (plain text)"),
|
|
423
|
-
priority: z.string().optional().describe("New priority"),
|
|
424
|
-
labels: z.array(z.string()).optional().describe("New labels (replaces existing)"),
|
|
425
|
-
}, async ({ issueKey, summary, description, priority, labels }) => {
|
|
426
|
-
try {
|
|
427
|
-
const client = getJiraClient();
|
|
428
|
-
const issue = await client.updateIssue({
|
|
429
|
-
issueKey, summary, description, priority, labels,
|
|
430
|
-
});
|
|
431
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
432
|
-
const text = [
|
|
433
|
-
`✓ Issue updated successfully.`,
|
|
434
|
-
` Key: ${issue.key}`,
|
|
435
|
-
` Summary: ${issue.fields.summary}`,
|
|
436
|
-
` Status: ${issue.fields.status.name}`,
|
|
437
|
-
` URL: ${baseUrl}/browse/${issue.key}`,
|
|
438
|
-
].join("\n");
|
|
439
|
-
return { content: [{ type: "text", text }] };
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
server.tool("jira_transition_issue", "Transition a Jira issue to a new status. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
446
|
-
issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')"),
|
|
447
|
-
transitionName: z.string().optional().describe("Target transition name (omit to list available transitions)"),
|
|
448
|
-
}, async ({ issueKey, transitionName }) => {
|
|
449
|
-
try {
|
|
450
|
-
const client = getJiraClient();
|
|
451
|
-
const transitions = await client.getTransitions(issueKey);
|
|
452
|
-
if (!transitionName) {
|
|
453
|
-
const text = [
|
|
454
|
-
`Available transitions for ${issueKey}:`,
|
|
455
|
-
...transitions.map((t) => `• ${t.name} → ${t.to.name} (id: ${t.id})`),
|
|
456
|
-
].join("\n");
|
|
457
|
-
return { content: [{ type: "text", text }] };
|
|
458
|
-
}
|
|
459
|
-
const match = transitions.find((t) => t.name.toLowerCase() === transitionName.toLowerCase());
|
|
460
|
-
if (!match) {
|
|
461
|
-
const available = transitions.map((t) => `${t.name} → ${t.to.name}`).join(", ");
|
|
462
|
-
return {
|
|
463
|
-
content: [{ type: "text", text: `No transition named "${transitionName}". Available: ${available}` }],
|
|
464
|
-
isError: true,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
await client.transitionIssue(issueKey, match.id);
|
|
468
|
-
return {
|
|
469
|
-
content: [{ type: "text", text: `✓ Transitioned ${issueKey} → ${match.to.name}` }],
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
catch (err) {
|
|
473
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
server.tool("jira_delete_issue", "Delete a Jira issue. IMPORTANT: Ask the user for confirmation before calling this tool.", { issueKey: z.string().describe("The issue key to delete (e.g. 'PROJ-123')") }, async ({ issueKey }) => {
|
|
477
|
-
try {
|
|
478
|
-
const client = getJiraClient();
|
|
479
|
-
const issue = await client.getIssue(issueKey);
|
|
480
|
-
await client.deleteIssue(issueKey);
|
|
481
|
-
return {
|
|
482
|
-
content: [
|
|
483
|
-
{ type: "text", text: `✓ Deleted issue ${issueKey} "${issue.fields.summary}"` },
|
|
484
|
-
],
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
catch (err) {
|
|
488
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
server.tool("jira_list_worklogs", "List work log entries on a Jira issue", { issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')") }, async ({ issueKey }) => {
|
|
492
|
-
try {
|
|
493
|
-
const worklogs = await getJiraClient().listWorklogs(issueKey);
|
|
494
|
-
if (worklogs.length === 0) {
|
|
495
|
-
return { content: [{ type: "text", text: "No work logged." }] };
|
|
496
|
-
}
|
|
497
|
-
const text = worklogs.map((w) => {
|
|
498
|
-
const author = w.author?.displayName ?? "Unknown";
|
|
499
|
-
const date = new Date(w.started).toLocaleDateString();
|
|
500
|
-
return `${author} · ${date} · ${w.timeSpent}`;
|
|
501
|
-
}).join("\n");
|
|
502
|
-
return { content: [{ type: "text", text }] };
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
server.tool("jira_log_work", "Log time worked on a Jira issue. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
509
|
-
issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')"),
|
|
510
|
-
timeSpent: z.string().describe("Time spent, e.g. '2h', '30m', '1d 2h'"),
|
|
511
|
-
started: z.string().optional().describe("When work started (ISO datetime, defaults to now)"),
|
|
512
|
-
comment: z.string().optional().describe("Work description"),
|
|
513
|
-
}, async ({ issueKey, timeSpent, started, comment }) => {
|
|
514
|
-
try {
|
|
515
|
-
const log = await getJiraClient().addWorklog({ issueKey, timeSpent, started, comment });
|
|
516
|
-
const text = [
|
|
517
|
-
`✓ Logged ${log.timeSpent} on ${issueKey}.`,
|
|
518
|
-
` Started: ${new Date(log.started).toLocaleString()}`,
|
|
519
|
-
].join("\n");
|
|
520
|
-
return { content: [{ type: "text", text }] };
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
server.tool("jira_list_link_types", "List available Jira issue link types (e.g. Blocks, Clones, Relates)", {}, async () => {
|
|
527
|
-
try {
|
|
528
|
-
const types = await getJiraClient().listIssueLinkTypes();
|
|
529
|
-
const text = types
|
|
530
|
-
.map((t) => `${t.name} — outward: "${t.outward}", inward: "${t.inward}"`)
|
|
531
|
-
.join("\n");
|
|
532
|
-
return { content: [{ type: "text", text: text || "No link types found." }] };
|
|
533
|
-
}
|
|
534
|
-
catch (err) {
|
|
535
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
server.tool("jira_list_issue_links", "List links on a Jira issue", { issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')") }, async ({ issueKey }) => {
|
|
539
|
-
try {
|
|
540
|
-
const links = await getJiraClient().listIssueLinks(issueKey);
|
|
541
|
-
if (links.length === 0) {
|
|
542
|
-
return { content: [{ type: "text", text: "No issue links." }] };
|
|
543
|
-
}
|
|
544
|
-
const text = links.map((l) => {
|
|
545
|
-
if (l.outwardIssue) {
|
|
546
|
-
return `${l.type.outward}: ${l.outwardIssue.key} — ${l.outwardIssue.fields.summary} [${l.outwardIssue.fields.status.name}] (link id: ${l.id})`;
|
|
547
|
-
}
|
|
548
|
-
if (l.inwardIssue) {
|
|
549
|
-
return `${l.type.inward}: ${l.inwardIssue.key} — ${l.inwardIssue.fields.summary} [${l.inwardIssue.fields.status.name}] (link id: ${l.id})`;
|
|
550
|
-
}
|
|
551
|
-
return `${l.type.name} (link id: ${l.id})`;
|
|
552
|
-
}).join("\n");
|
|
553
|
-
return { content: [{ type: "text", text }] };
|
|
554
|
-
}
|
|
555
|
-
catch (err) {
|
|
556
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
server.tool("jira_link_issues", "Link two Jira issues together. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
560
|
-
issueKey: z.string().describe("The source issue key (outward side, e.g. 'PROJ-1')"),
|
|
561
|
-
linkType: z.string().describe("Link type name (e.g. 'Blocks', 'Clones', 'Relates to') — use jira_list_link_types to see options"),
|
|
562
|
-
targetIssueKey: z.string().describe("The target issue key (inward side, e.g. 'PROJ-2')"),
|
|
563
|
-
}, async ({ issueKey, linkType, targetIssueKey }) => {
|
|
564
|
-
try {
|
|
565
|
-
await getJiraClient().linkIssues(issueKey, linkType, targetIssueKey);
|
|
566
|
-
return { content: [{ type: "text", text: `✓ Linked: ${issueKey} "${linkType}" ${targetIssueKey}` }] };
|
|
567
|
-
}
|
|
568
|
-
catch (err) {
|
|
569
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
server.tool("jira_remove_issue_link", "Remove a link between two Jira issues. IMPORTANT: Ask the user for confirmation before calling this tool.", { linkId: z.string().describe("The link ID to remove (from jira_list_issue_links)") }, async ({ linkId }) => {
|
|
573
|
-
try {
|
|
574
|
-
await getJiraClient().removeIssueLink(linkId);
|
|
575
|
-
return { content: [{ type: "text", text: `✓ Link ${linkId} removed.` }] };
|
|
576
|
-
}
|
|
577
|
-
catch (err) {
|
|
578
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
server.tool("jira_list_comments", "List comments on a Jira issue", { issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')") }, async ({ issueKey }) => {
|
|
582
|
-
try {
|
|
583
|
-
const comments = await getJiraClient().listComments(issueKey);
|
|
584
|
-
if (comments.length === 0) {
|
|
585
|
-
return { content: [{ type: "text", text: "No comments." }] };
|
|
586
|
-
}
|
|
587
|
-
const client = getJiraClient();
|
|
588
|
-
const text = comments.map((c) => {
|
|
589
|
-
const author = c.author?.displayName ?? "Unknown";
|
|
590
|
-
const date = new Date(c.created).toLocaleDateString();
|
|
591
|
-
const body = client.descriptionToText({ fields: { description: c.body } });
|
|
592
|
-
return `[${author} · ${date}]\n${body}`;
|
|
593
|
-
}).join("\n\n");
|
|
594
|
-
return { content: [{ type: "text", text }] };
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
server.tool("jira_add_comment", "Add a comment to a Jira issue. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
601
|
-
issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')"),
|
|
602
|
-
text: z.string().describe("Comment text (plain text)"),
|
|
603
|
-
}, async ({ issueKey, text }) => {
|
|
604
|
-
try {
|
|
605
|
-
await getJiraClient().addComment(issueKey, text);
|
|
606
|
-
return { content: [{ type: "text", text: `✓ Comment added to ${issueKey}.` }] };
|
|
607
|
-
}
|
|
608
|
-
catch (err) {
|
|
609
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
server.tool("jira_upload_attachment", "Upload a file as an attachment to a Jira issue. IMPORTANT: Ask the user for confirmation before calling this tool.", {
|
|
613
|
-
issueKey: z.string().describe("The issue key to attach the file to (e.g. 'PROJ-123')"),
|
|
614
|
-
filePath: z.string().describe("Absolute path to the file to upload"),
|
|
615
|
-
}, async ({ issueKey, filePath }) => {
|
|
616
|
-
try {
|
|
617
|
-
const att = await getJiraClient().uploadAttachment({ issueKey, filePath });
|
|
618
|
-
const baseUrl = process.env.ATLASSIAN_URL ?? process.env.CONFLUENCE_URL?.replace(/\/wiki\/?$/, "") ?? "";
|
|
619
|
-
const text = [
|
|
620
|
-
`✓ Attachment uploaded successfully.`,
|
|
621
|
-
` File: ${att.filename}`,
|
|
622
|
-
` ID: ${att.id}`,
|
|
623
|
-
` Type: ${att.mimeType}`,
|
|
624
|
-
` Size: ${att.size} bytes`,
|
|
625
|
-
...(att.content ? [` Download: ${att.content}`] : []),
|
|
626
|
-
` Issue: ${baseUrl}/browse/${issueKey}`,
|
|
627
|
-
].join("\n");
|
|
628
|
-
return { content: [{ type: "text", text }] };
|
|
629
|
-
}
|
|
630
|
-
catch (err) {
|
|
631
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
server.tool("jira_list_attachments", "List all attachments on a Jira issue", { issueKey: z.string().describe("The issue key (e.g. 'PROJ-123')") }, async ({ issueKey }) => {
|
|
635
|
-
try {
|
|
636
|
-
const attachments = await getJiraClient().listAttachments(issueKey);
|
|
637
|
-
if (attachments.length === 0) {
|
|
638
|
-
return { content: [{ type: "text", text: "No attachments found." }] };
|
|
639
|
-
}
|
|
640
|
-
const text = attachments
|
|
641
|
-
.map((a) => `${a.filename} (id: ${a.id}, ${a.mimeType}, ${a.size} bytes)\n ${a.content}`)
|
|
642
|
-
.join("\n");
|
|
643
|
-
return { content: [{ type: "text", text }] };
|
|
644
|
-
}
|
|
645
|
-
catch (err) {
|
|
646
|
-
return { content: [{ type: "text", text: formatError(err) }], isError: true };
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
// ── Start ──────────────────────────────────────────────────────────
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { registerConfluenceTools } from "./confluence.js";
|
|
6
|
+
import { registerJiraTools } from "./jira.js";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = require("../../package.json");
|
|
9
|
+
const server = new McpServer({ name: "atlassian", version });
|
|
10
|
+
registerConfluenceTools(server);
|
|
11
|
+
registerJiraTools(server);
|
|
650
12
|
async function main() {
|
|
651
13
|
const transport = new StdioServerTransport();
|
|
652
14
|
await server.connect(transport);
|