@ecubelabs/atlassian-mcp 1.10.0 → 1.12.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.
|
@@ -33,6 +33,7 @@ export const registerConfluenceStorageFormatHelp = (server) => {
|
|
|
33
33
|
1. **Jira**: <ac:structured-macro ac:name="jira"><ac:parameter ac:name="key">HA-2584</ac:parameter></ac:structured-macro>
|
|
34
34
|
2. **Date**: <time datetime="2025-12-03" />
|
|
35
35
|
3. **User**: <ac:link><ri:user ri:account-id="ACCOUNT_ID" /></ac:link>
|
|
36
|
+
To find ACCOUNT_ID: Use Jira "search-users" or "get-user-picker" tool. Atlassian Cloud accountIds work across both Jira and Confluence.
|
|
36
37
|
|
|
37
38
|
## Key Rules
|
|
38
39
|
- Use h2/h3 for headings (not h1)
|
|
@@ -129,6 +130,8 @@ Languages: javascript, typescript, python, bash, json, yaml, etc.`,
|
|
|
129
130
|
<ri:user ri:account-id="ACCOUNT_ID" />
|
|
130
131
|
</ac:link>
|
|
131
132
|
|
|
133
|
+
To find ACCOUNT_ID: Use Jira "search-users" or "get-user-picker" tool to look up the user's accountId. Atlassian Cloud accountIds work across both Jira and Confluence.
|
|
134
|
+
|
|
132
135
|
## External Link
|
|
133
136
|
<a href="https://example.com">Link text</a>
|
|
134
137
|
|
|
@@ -162,6 +165,15 @@ Attach images to page first, then reference by filename.`,
|
|
|
162
165
|
✅ ALWAYS:
|
|
163
166
|
<time datetime="2025-12-03" />
|
|
164
167
|
|
|
168
|
+
## User Mention (MANDATORY for people)
|
|
169
|
+
❌ NEVER: "@username" or "John Doe"
|
|
170
|
+
✅ ALWAYS:
|
|
171
|
+
<ac:link>
|
|
172
|
+
<ri:user ri:account-id="ACCOUNT_ID" />
|
|
173
|
+
</ac:link>
|
|
174
|
+
|
|
175
|
+
To find ACCOUNT_ID: Use Jira "search-users" or "get-user-picker" tool.
|
|
176
|
+
|
|
165
177
|
## Status Chip
|
|
166
178
|
<ac:structured-macro ac:name="status" ac:schema-version="1">
|
|
167
179
|
<ac:parameter ac:name="colour">Green</ac:parameter>
|
|
@@ -187,7 +199,7 @@ Types: two_equal, three_equal. Use sparingly.`,
|
|
|
187
199
|
## MANDATORY Rules
|
|
188
200
|
1. Jira keys → MUST use Jira macro
|
|
189
201
|
2. Dates → MUST use <time> element
|
|
190
|
-
3. People → MUST use user mention link
|
|
202
|
+
3. People → MUST use user mention link (use Jira "search-users" or "get-user-picker" to find accountId)
|
|
191
203
|
4. Lists → NEVER use <p> inside <li>
|
|
192
204
|
|
|
193
205
|
## Document Structure
|
|
@@ -353,7 +353,10 @@ export const registerJiraTools = (server) => {
|
|
|
353
353
|
summary: z.string().describe('Issue summary'),
|
|
354
354
|
issueTypeId: z.string().optional().describe('Issue type ID'),
|
|
355
355
|
issueTypeName: z.string().optional().describe('Issue type name'),
|
|
356
|
-
description: z
|
|
356
|
+
description: z
|
|
357
|
+
.string()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe('Issue description in markdown. To mention a user, first use search-users or get-user-picker to get their accountId, then use syntax <@accountId:ACCOUNT_ID>. Example: "Assigned to <@accountId:5b10a284> for review."'),
|
|
357
360
|
assigneeAccountId: z.string().optional().describe('Assignee accountId (Jira Cloud)'),
|
|
358
361
|
reporterAccountId: z
|
|
359
362
|
.string()
|
|
@@ -429,7 +432,10 @@ export const registerJiraTools = (server) => {
|
|
|
429
432
|
server.tool('edit-issue', 'Edit fields on a Jira issue', {
|
|
430
433
|
issueKey: z.string().describe('Jira issue key (e.g. PROJ-123)'),
|
|
431
434
|
summary: z.string().optional().describe('New summary'),
|
|
432
|
-
description: z
|
|
435
|
+
description: z
|
|
436
|
+
.string()
|
|
437
|
+
.optional()
|
|
438
|
+
.describe('New description in markdown. To mention a user, first use search-users or get-user-picker to get their accountId, then use syntax <@accountId:ACCOUNT_ID>.'),
|
|
433
439
|
assigneeAccountId: z
|
|
434
440
|
.string()
|
|
435
441
|
.optional()
|
|
@@ -469,7 +475,7 @@ export const registerJiraTools = (server) => {
|
|
|
469
475
|
};
|
|
470
476
|
}
|
|
471
477
|
});
|
|
472
|
-
server.tool('search-users', 'Search for users', {
|
|
478
|
+
server.tool('search-users', 'Search for users to get their accountId (useful for mentioning users in Jira comments/descriptions with <@accountId:ID> syntax, or in Confluence pages with <ac:link><ri:user ri:account-id="ID" /></ac:link>). Atlassian Cloud accountIds work across both Jira and Confluence.', {
|
|
473
479
|
query: z.string().optional().describe('Query string to search users'),
|
|
474
480
|
accountId: z.string().optional().describe('Account ID to search for'),
|
|
475
481
|
startAt: z.number().min(0).optional().describe('The index of the first item to return'),
|
|
@@ -499,7 +505,7 @@ export const registerJiraTools = (server) => {
|
|
|
499
505
|
};
|
|
500
506
|
}
|
|
501
507
|
});
|
|
502
|
-
server.tool('get-user-picker', 'Get user picker suggestions for autocomplete', {
|
|
508
|
+
server.tool('get-user-picker', 'Get user picker suggestions for autocomplete. Use this to find accountIds for mentioning users in Jira (with <@accountId:ID> syntax) or Confluence (with <ac:link><ri:user ri:account-id="ID" /></ac:link>). Atlassian Cloud accountIds work across both Jira and Confluence.', {
|
|
503
509
|
query: z.string().describe('Query string for user search'),
|
|
504
510
|
maxResults: z.number().min(1).max(1000).optional().describe('Maximum number of users to return'),
|
|
505
511
|
showAvatar: z.boolean().optional().describe('Whether to show user avatars'),
|
|
@@ -987,7 +993,13 @@ export const registerJiraTools = (server) => {
|
|
|
987
993
|
});
|
|
988
994
|
server.tool('create-comment', 'Add a comment to an issue', {
|
|
989
995
|
issueKey: z.string().describe('Issue key (e.g. PROJ-123)'),
|
|
990
|
-
body: z
|
|
996
|
+
body: z
|
|
997
|
+
.string()
|
|
998
|
+
.describe('Comment text in markdown. To mention a user, use search-users or get-user-picker to get their accountId, then use syntax <@accountId:ACCOUNT_ID>. Example: "Hey <@accountId:5b10a284>, please review this."'),
|
|
999
|
+
parentId: z
|
|
1000
|
+
.string()
|
|
1001
|
+
.optional()
|
|
1002
|
+
.describe('Parent comment ID to reply to. Use get-comments to find the comment ID to reply to.'),
|
|
991
1003
|
visibility: z
|
|
992
1004
|
.object({
|
|
993
1005
|
type: z.enum(['group', 'role']).describe('Visibility type'),
|
|
@@ -1002,10 +1014,11 @@ export const registerJiraTools = (server) => {
|
|
|
1002
1014
|
}))
|
|
1003
1015
|
.optional()
|
|
1004
1016
|
.describe('Comment properties'),
|
|
1005
|
-
}, async ({ issueKey, body, visibility, properties }) => {
|
|
1017
|
+
}, async ({ issueKey, body, parentId, visibility, properties }) => {
|
|
1006
1018
|
try {
|
|
1007
1019
|
const comment = await jiraService.createComment(issueKey, {
|
|
1008
1020
|
body,
|
|
1021
|
+
parentId,
|
|
1009
1022
|
visibility,
|
|
1010
1023
|
properties: properties,
|
|
1011
1024
|
});
|
|
@@ -1027,7 +1040,9 @@ export const registerJiraTools = (server) => {
|
|
|
1027
1040
|
server.tool('update-comment', 'Update an existing comment', {
|
|
1028
1041
|
issueKey: z.string().describe('Issue key (e.g. PROJ-123)'),
|
|
1029
1042
|
commentId: z.string().describe('Comment ID to update'),
|
|
1030
|
-
body: z
|
|
1043
|
+
body: z
|
|
1044
|
+
.string()
|
|
1045
|
+
.describe('New comment text in markdown. To mention a user, use search-users or get-user-picker to get their accountId, then use syntax <@accountId:ACCOUNT_ID>.'),
|
|
1031
1046
|
visibility: z
|
|
1032
1047
|
.object({
|
|
1033
1048
|
type: z.enum(['group', 'role']).describe('Visibility type'),
|
|
@@ -1175,7 +1190,9 @@ export const registerJiraTools = (server) => {
|
|
|
1175
1190
|
.describe('Outward issue key (either this or outwardIssueId must be provided)'),
|
|
1176
1191
|
comment: z
|
|
1177
1192
|
.object({
|
|
1178
|
-
body: z
|
|
1193
|
+
body: z
|
|
1194
|
+
.string()
|
|
1195
|
+
.describe('Comment text in markdown. Supports mention syntax <@accountId:ACCOUNT_ID>.'),
|
|
1179
1196
|
visibility: z
|
|
1180
1197
|
.object({
|
|
1181
1198
|
type: z.enum(['group', 'role']).describe('Visibility type'),
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { markdownToAdf } from 'marklassian';
|
|
2
|
+
const MENTION_PATTERN = /<@accountId:([^>]+)>/g;
|
|
3
|
+
function hasCodeMark(marks) {
|
|
4
|
+
return marks?.some((m) => m.type === 'code') ?? false;
|
|
5
|
+
}
|
|
6
|
+
function splitTextWithMentions(node) {
|
|
7
|
+
if (!node.text || hasCodeMark(node.marks)) {
|
|
8
|
+
return [node];
|
|
9
|
+
}
|
|
10
|
+
const text = node.text;
|
|
11
|
+
const marks = node.marks;
|
|
12
|
+
const result = [];
|
|
13
|
+
let lastIndex = 0;
|
|
14
|
+
for (const match of text.matchAll(MENTION_PATTERN)) {
|
|
15
|
+
const matchStart = match.index;
|
|
16
|
+
const accountId = match[1];
|
|
17
|
+
// Text before the mention
|
|
18
|
+
if (matchStart > lastIndex) {
|
|
19
|
+
const before = text.slice(lastIndex, matchStart);
|
|
20
|
+
const textNode = { type: 'text', text: before };
|
|
21
|
+
if (marks?.length)
|
|
22
|
+
textNode.marks = marks;
|
|
23
|
+
result.push(textNode);
|
|
24
|
+
}
|
|
25
|
+
// Mention node (marks are NOT applied to mentions)
|
|
26
|
+
result.push({
|
|
27
|
+
type: 'mention',
|
|
28
|
+
attrs: {
|
|
29
|
+
id: accountId,
|
|
30
|
+
text: `@${accountId}`,
|
|
31
|
+
accessLevel: '',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
lastIndex = matchStart + match[0].length;
|
|
35
|
+
}
|
|
36
|
+
// Remaining text after last mention
|
|
37
|
+
if (lastIndex < text.length) {
|
|
38
|
+
const after = text.slice(lastIndex);
|
|
39
|
+
const textNode = { type: 'text', text: after };
|
|
40
|
+
if (marks?.length)
|
|
41
|
+
textNode.marks = marks;
|
|
42
|
+
result.push(textNode);
|
|
43
|
+
}
|
|
44
|
+
return result.length > 0 ? result : [node];
|
|
45
|
+
}
|
|
46
|
+
function processNode(node, parentType) {
|
|
47
|
+
// Skip codeBlock contents entirely
|
|
48
|
+
if (node.type === 'codeBlock') {
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
// Process text nodes that may contain mention patterns
|
|
52
|
+
if (node.type === 'text' && node.text && parentType !== 'codeBlock') {
|
|
53
|
+
// splitTextWithMentions handles code mark check internally
|
|
54
|
+
const parts = splitTextWithMentions(node);
|
|
55
|
+
// If no mention was found, return original node
|
|
56
|
+
if (parts.length === 1 && parts[0] === node) {
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
// Return a marker that the parent should expand
|
|
60
|
+
return { type: '__mention_expanded__', content: parts };
|
|
61
|
+
}
|
|
62
|
+
// Recurse into children
|
|
63
|
+
if (node.content) {
|
|
64
|
+
const newContent = [];
|
|
65
|
+
for (const child of node.content) {
|
|
66
|
+
const processed = processNode(child, node.type);
|
|
67
|
+
if (processed.type === '__mention_expanded__' && processed.content) {
|
|
68
|
+
newContent.push(...processed.content);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
newContent.push(processed);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ...node, content: newContent };
|
|
75
|
+
}
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Converts markdown to ADF with support for user mentions.
|
|
80
|
+
*
|
|
81
|
+
* Use the syntax `<@accountId:ACCOUNT_ID>` in markdown text to create
|
|
82
|
+
* ADF mention nodes. The ACCOUNT_ID can be obtained via the Jira
|
|
83
|
+
* `search-users` or `get-user-picker` tools.
|
|
84
|
+
*
|
|
85
|
+
* Example: "Hey <@accountId:5b10a284>, please review this."
|
|
86
|
+
*/
|
|
87
|
+
export function markdownToAdfWithMentions(markdown) {
|
|
88
|
+
const doc = markdownToAdf(markdown);
|
|
89
|
+
// Fast path: no mention patterns in the source text
|
|
90
|
+
if (!MENTION_PATTERN.test(markdown)) {
|
|
91
|
+
return doc;
|
|
92
|
+
}
|
|
93
|
+
// Reset regex lastIndex after test()
|
|
94
|
+
MENTION_PATTERN.lastIndex = 0;
|
|
95
|
+
return {
|
|
96
|
+
...doc,
|
|
97
|
+
content: doc.content.map((node) => {
|
|
98
|
+
const processed = processNode(node);
|
|
99
|
+
if (processed.type === '__mention_expanded__' && processed.content) {
|
|
100
|
+
return { type: 'paragraph', content: processed.content };
|
|
101
|
+
}
|
|
102
|
+
return processed;
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/libs/jira-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { markdownToAdf } from 'marklassian';
|
|
2
1
|
import { atlassianConfig } from '../config.js';
|
|
2
|
+
import { markdownToAdfWithMentions } from './adf-mention-utils.js';
|
|
3
3
|
import { BaseApiService } from './base-client.js';
|
|
4
4
|
export class JiraService extends BaseApiService {
|
|
5
5
|
constructor() {
|
|
@@ -174,7 +174,7 @@ export class JiraService extends BaseApiService {
|
|
|
174
174
|
}
|
|
175
175
|
// Optional fields
|
|
176
176
|
if (options.description) {
|
|
177
|
-
fields.description =
|
|
177
|
+
fields.description = markdownToAdfWithMentions(options.description);
|
|
178
178
|
}
|
|
179
179
|
if (options.assigneeAccountId) {
|
|
180
180
|
fields.assignee = { accountId: options.assigneeAccountId };
|
|
@@ -212,7 +212,7 @@ export class JiraService extends BaseApiService {
|
|
|
212
212
|
fields.summary = options.summary;
|
|
213
213
|
}
|
|
214
214
|
if (options.description) {
|
|
215
|
-
fields.description =
|
|
215
|
+
fields.description = markdownToAdfWithMentions(options.description);
|
|
216
216
|
}
|
|
217
217
|
if (options.unassignAssignee) {
|
|
218
218
|
fields.assignee = null;
|
|
@@ -544,8 +544,11 @@ export class JiraService extends BaseApiService {
|
|
|
544
544
|
*/
|
|
545
545
|
async createComment(issueKey, options) {
|
|
546
546
|
const body = {
|
|
547
|
-
body:
|
|
547
|
+
body: markdownToAdfWithMentions(options.body),
|
|
548
548
|
};
|
|
549
|
+
if (options.parentId) {
|
|
550
|
+
body.parentId = options.parentId;
|
|
551
|
+
}
|
|
549
552
|
if (options.visibility) {
|
|
550
553
|
body.visibility = options.visibility;
|
|
551
554
|
}
|
|
@@ -560,7 +563,7 @@ export class JiraService extends BaseApiService {
|
|
|
560
563
|
*/
|
|
561
564
|
async updateComment(issueKey, commentId, options) {
|
|
562
565
|
const body = {
|
|
563
|
-
body:
|
|
566
|
+
body: markdownToAdfWithMentions(options.body),
|
|
564
567
|
};
|
|
565
568
|
if (options.visibility) {
|
|
566
569
|
body.visibility = options.visibility;
|
|
@@ -614,7 +617,7 @@ export class JiraService extends BaseApiService {
|
|
|
614
617
|
};
|
|
615
618
|
if (options.comment) {
|
|
616
619
|
body.comment = {
|
|
617
|
-
body:
|
|
620
|
+
body: markdownToAdfWithMentions(options.comment.body),
|
|
618
621
|
...(options.comment.visibility && {
|
|
619
622
|
visibility: options.comment.visibility,
|
|
620
623
|
}),
|