@stubbedev/atlassian-mcp 0.3.5 → 0.3.6

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 CHANGED
@@ -67,6 +67,8 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
67
67
  - "list releases for PAY" → `jira_search` with `resource=versions`, `project=PAY`
68
68
  - "release version 12345" → `jira_version` with `action=release`, `id=12345`
69
69
  - "set fix version 9.1.0 on FOO-123" → `jira_mutate` with `update.fixVersion=9.1.0`
70
+ - "create a task under epic FOO-100" → `jira_mutate` with `create.issueType=Task`, `create.parent=FOO-100` (auto-detects Epic and sets Epic Link)
71
+ - "move FOO-123 under epic FOO-100" → `jira_mutate` with `update.epicLink=FOO-100`
70
72
 
71
73
  ---
72
74
 
package/dist/index.js CHANGED
@@ -261,7 +261,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
261
261
  priority: { type: 'string', description: 'Priority name (optional)' },
262
262
  labels: { type: 'array', items: { type: 'string' }, description: 'Labels to apply (optional)' },
263
263
  fixVersion: { type: 'string', description: 'Fix version name (optional)' },
264
- parent: { type: 'string', description: 'Parent issue key for subtasks (optional)' },
264
+ parent: { type: 'string', description: 'Parent issue key. For Sub-task issue types this sets the Jira parent. If the key points to an Epic, the Epic Link custom field is set automatically instead (Jira Server epic membership).' },
265
+ epicLink: { type: 'string', description: 'Epic issue key to attach the new issue to as an epic child (Jira Server Epic Link). Overrides parent if both are passed.' },
265
266
  },
266
267
  required: ['issueType', 'summary'],
267
268
  },
@@ -274,6 +275,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
274
275
  priority: { type: 'string', description: 'New priority name (optional)' },
275
276
  labels: { type: 'array', items: { type: 'string' }, description: 'Replace label set (pass [] to clear)' },
276
277
  fixVersion: { type: 'string', description: 'Fix version name, or empty string to clear (optional)' },
278
+ epicLink: { type: 'string', description: 'Epic issue key to set as parent epic (Jira Server Epic Link), or empty string to clear.' },
277
279
  },
278
280
  },
279
281
  sprintId: { type: 'number', description: 'Sprint ID to add the issue into (optional)' },
package/dist/jira.js CHANGED
@@ -97,6 +97,8 @@ export class JiraClient {
97
97
  currentUserCache;
98
98
  projectsCache;
99
99
  issueLinkingEnabled;
100
+ epicLinkFieldIdCache;
101
+ issueTypeCache = new Map();
100
102
  constructor(baseUrl, token) {
101
103
  this.baseUrl = baseUrl.replace(/\/$/, '');
102
104
  this.headers = {
@@ -128,7 +130,17 @@ export class JiraClient {
128
130
  const details = parseJiraErrorDetails(errText);
129
131
  throw new Error(formatJiraError(res.status, method, path, details));
130
132
  }
131
- return res.status === 204 ? null : res.json();
133
+ if (res.status === 204)
134
+ return null;
135
+ const raw = await res.text();
136
+ if (!raw)
137
+ return null;
138
+ try {
139
+ return JSON.parse(raw);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
132
144
  }
133
145
  async request(method, path, body) {
134
146
  return this.requestWithBase('/rest/api/2', method, path, body);
@@ -159,6 +171,24 @@ export class JiraClient {
159
171
  this.issueLinkingEnabled = config?.issueLinkingEnabled ?? false;
160
172
  return this.issueLinkingEnabled;
161
173
  }
174
+ async getEpicLinkFieldId() {
175
+ if (this.epicLinkFieldIdCache !== undefined)
176
+ return this.epicLinkFieldIdCache;
177
+ const fields = (await this.request('GET', '/field')) ?? [];
178
+ const match = fields.find((f) => f.schema?.custom === 'com.pyxis.greenhopper.jira:gh-epic-link');
179
+ this.epicLinkFieldIdCache = match?.id ?? null;
180
+ return this.epicLinkFieldIdCache;
181
+ }
182
+ async getIssueType(issueKey) {
183
+ const cached = this.issueTypeCache.get(issueKey);
184
+ if (cached)
185
+ return cached;
186
+ const data = await this.request('GET', `/issue/${encodeURIComponent(issueKey)}?fields=issuetype`);
187
+ const name = data?.fields?.issuetype?.name ?? null;
188
+ if (name)
189
+ this.issueTypeCache.set(issueKey, name);
190
+ return name;
191
+ }
162
192
  async assertOwnComment(comment) {
163
193
  const me = await this.getCurrentUser();
164
194
  const commentAuthorName = this.normalizeIdentity(comment.author.name);
@@ -200,8 +230,23 @@ export class JiraClient {
200
230
  fields.labels = args.labels;
201
231
  if (args.fixVersion)
202
232
  fields.fixVersions = [{ name: args.fixVersion }];
203
- if (args.parent)
204
- fields.parent = { key: args.parent };
233
+ let epicLinkTarget = args.epicLink?.trim() || undefined;
234
+ if (args.parent) {
235
+ const parentType = await this.getIssueType(args.parent);
236
+ if (parentType === 'Epic') {
237
+ epicLinkTarget = epicLinkTarget ?? args.parent;
238
+ }
239
+ else {
240
+ fields.parent = { key: args.parent };
241
+ }
242
+ }
243
+ if (epicLinkTarget) {
244
+ const epicFieldId = await this.getEpicLinkFieldId();
245
+ if (!epicFieldId) {
246
+ throw new Error('Epic Link custom field not found on this Jira instance. Set it manually in the Jira UI.');
247
+ }
248
+ fields[epicFieldId] = epicLinkTarget;
249
+ }
205
250
  return this.request('POST', '/issue', { fields });
206
251
  }
207
252
  async updateIssueFieldsInternal(args) {
@@ -218,6 +263,13 @@ export class JiraClient {
218
263
  fields.labels = args.labels;
219
264
  if (args.fixVersion !== undefined)
220
265
  fields.fixVersions = args.fixVersion ? [{ name: args.fixVersion }] : [];
266
+ if (args.epicLink !== undefined) {
267
+ const epicFieldId = await this.getEpicLinkFieldId();
268
+ if (!epicFieldId) {
269
+ throw new Error('Epic Link custom field not found on this Jira instance. Set it manually in the Jira UI.');
270
+ }
271
+ fields[epicFieldId] = args.epicLink ? args.epicLink : null;
272
+ }
221
273
  if (Object.keys(fields).length === 0)
222
274
  return false;
223
275
  await this.request('PUT', `/issue/${encodeURIComponent(args.issueKey)}`, { fields });
@@ -401,11 +453,16 @@ export class JiraClient {
401
453
  const includeSprint = args.includeSprint ?? true;
402
454
  const commentsMaxResults = args.commentsMaxResults ?? 10;
403
455
  const commentsStartAt = args.commentsStartAt ?? 0;
404
- const fields = 'summary,description,status,assignee,priority,issuetype,labels,components,parent,fixVersions,issuelinks,subtasks,attachment';
456
+ const baseFields = 'summary,description,status,assignee,priority,issuetype,labels,components,parent,fixVersions,issuelinks,subtasks,attachment';
457
+ const epicFieldId = await this.getEpicLinkFieldId();
458
+ const fields = epicFieldId ? `${baseFields},${epicFieldId}` : baseFields;
405
459
  const issue = await this.request('GET', `/issue/${encodeURIComponent(args.issueKey)}?fields=${fields}`);
406
460
  if (!issue)
407
461
  return text('Issue not found.');
408
462
  const f = issue.fields;
463
+ const epicLinkKey = epicFieldId && typeof f[epicFieldId] === 'string'
464
+ ? f[epicFieldId]
465
+ : undefined;
409
466
  const lines = [
410
467
  `Issue: ${issue.key} — ${f.summary}`,
411
468
  `URL: ${this.issueUrl(issue.key)}`,
@@ -416,6 +473,7 @@ export class JiraClient {
416
473
  `Labels: ${f.labels?.join(', ') || 'None'}`,
417
474
  `Components: ${f.components?.map((c) => c.name).join(', ') || 'None'}`,
418
475
  ...(f.parent ? [`Parent: [${f.parent.key}] ${f.parent.fields.summary} (${f.parent.fields.issuetype.name})`] : []),
476
+ ...(epicLinkKey ? [`Epic Link: ${epicLinkKey}`] : []),
419
477
  ...(f.fixVersions?.length ? [`Fix Vers: ${f.fixVersions.map((v) => v.name).join(', ')}`] : []),
420
478
  ...(f.subtasks?.length ? [`Subtasks: ${f.subtasks.map((s) => `[${s.key}] ${s.fields.summary} (${s.fields.status.name})`).join(', ')}`] : []),
421
479
  ...(f.issuelinks?.length ? [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",