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