@ecubelabs/atlassian-mcp 1.10.0 → 1.11.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,9 @@ 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."'),
|
|
991
999
|
visibility: z
|
|
992
1000
|
.object({
|
|
993
1001
|
type: z.enum(['group', 'role']).describe('Visibility type'),
|
|
@@ -1027,7 +1035,9 @@ export const registerJiraTools = (server) => {
|
|
|
1027
1035
|
server.tool('update-comment', 'Update an existing comment', {
|
|
1028
1036
|
issueKey: z.string().describe('Issue key (e.g. PROJ-123)'),
|
|
1029
1037
|
commentId: z.string().describe('Comment ID to update'),
|
|
1030
|
-
body: z
|
|
1038
|
+
body: z
|
|
1039
|
+
.string()
|
|
1040
|
+
.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
1041
|
visibility: z
|
|
1032
1042
|
.object({
|
|
1033
1043
|
type: z.enum(['group', 'role']).describe('Visibility type'),
|
|
@@ -1175,7 +1185,9 @@ export const registerJiraTools = (server) => {
|
|
|
1175
1185
|
.describe('Outward issue key (either this or outwardIssueId must be provided)'),
|
|
1176
1186
|
comment: z
|
|
1177
1187
|
.object({
|
|
1178
|
-
body: z
|
|
1188
|
+
body: z
|
|
1189
|
+
.string()
|
|
1190
|
+
.describe('Comment text in markdown. Supports mention syntax <@accountId:ACCOUNT_ID>.'),
|
|
1179
1191
|
visibility: z
|
|
1180
1192
|
.object({
|
|
1181
1193
|
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,7 +544,7 @@ 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
549
|
if (options.visibility) {
|
|
550
550
|
body.visibility = options.visibility;
|
|
@@ -560,7 +560,7 @@ export class JiraService extends BaseApiService {
|
|
|
560
560
|
*/
|
|
561
561
|
async updateComment(issueKey, commentId, options) {
|
|
562
562
|
const body = {
|
|
563
|
-
body:
|
|
563
|
+
body: markdownToAdfWithMentions(options.body),
|
|
564
564
|
};
|
|
565
565
|
if (options.visibility) {
|
|
566
566
|
body.visibility = options.visibility;
|
|
@@ -614,7 +614,7 @@ export class JiraService extends BaseApiService {
|
|
|
614
614
|
};
|
|
615
615
|
if (options.comment) {
|
|
616
616
|
body.comment = {
|
|
617
|
-
body:
|
|
617
|
+
body: markdownToAdfWithMentions(options.comment.body),
|
|
618
618
|
...(options.comment.visibility && {
|
|
619
619
|
visibility: options.comment.visibility,
|
|
620
620
|
}),
|