@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.string().optional().describe('Issue description (plain text)'),
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.string().optional().describe('New description (plain text)'),
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.string().describe('Comment text'),
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.string().describe('New comment text'),
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.string().describe('Comment text'),
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
+ }
@@ -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 = markdownToAdf(options.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 = markdownToAdf(options.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: markdownToAdf(options.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: markdownToAdf(options.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: markdownToAdf(options.comment.body),
617
+ body: markdownToAdfWithMentions(options.comment.body),
618
618
  ...(options.comment.visibility && {
619
619
  visibility: options.comment.visibility,
620
620
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecubelabs/atlassian-mcp",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "bin": "./dist/index.js",
5
5
  "repository": {
6
6
  "url": "https://github.com/Ecube-Labs/skynet.git"