@thammarongg/jira-mcp 0.3.0 → 0.5.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.
package/dist/status.js ADDED
@@ -0,0 +1,81 @@
1
+ import { AGENTS, hasAgentEntry, PACKAGE_VERSION, readAgentEnv } from "./agents.js";
2
+ const REGISTRY_URL = "https://registry.npmjs.org/@thammarongg/jira-mcp/latest";
3
+ function accountOf(env) {
4
+ return env?.JIRA_EMAIL ?? env?.JIRA_USERNAME ?? "unknown account";
5
+ }
6
+ function baseUrlOf(env) {
7
+ return env?.JIRA_BASE_URL ?? "unknown URL";
8
+ }
9
+ function versionParts(version) {
10
+ const match = version.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
11
+ if (!match)
12
+ return undefined;
13
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
14
+ }
15
+ function isNewer(candidate, installed) {
16
+ const next = versionParts(candidate);
17
+ const current = versionParts(installed);
18
+ if (!next || !current)
19
+ return false;
20
+ for (let i = 0; i < next.length; i++) {
21
+ if (next[i] !== current[i])
22
+ return next[i] > current[i];
23
+ }
24
+ return false;
25
+ }
26
+ async function latestVersion() {
27
+ const controller = new AbortController();
28
+ let deadlineTimer;
29
+ try {
30
+ const request = fetch(REGISTRY_URL, { signal: controller.signal }).then(async (response) => {
31
+ if (!response.ok)
32
+ return undefined;
33
+ const data = await response.json();
34
+ if (!data || typeof data !== "object" || Array.isArray(data))
35
+ return undefined;
36
+ const version = data.version;
37
+ return typeof version === "string" && isNewer(version, PACKAGE_VERSION) ? version : undefined;
38
+ });
39
+ return await Promise.race([
40
+ request,
41
+ new Promise((_, reject) => {
42
+ deadlineTimer = setTimeout(() => {
43
+ controller.abort();
44
+ reject(new Error("registry request timed out"));
45
+ }, 2_000);
46
+ }),
47
+ ]);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ finally {
53
+ if (deadlineTimer !== undefined)
54
+ clearTimeout(deadlineTimer);
55
+ }
56
+ }
57
+ function configuredAgents() {
58
+ return AGENTS.filter((agent) => hasAgentEntry(agent));
59
+ }
60
+ export async function renderStatus() {
61
+ const update = await latestVersion();
62
+ const configured = configuredAgents();
63
+ const configuredIds = new Set(configured.map((agent) => agent.id));
64
+ const notConfigured = AGENTS.filter((agent) => !configuredIds.has(agent.id));
65
+ const lines = [`Installed version: ${PACKAGE_VERSION}`];
66
+ if (update)
67
+ lines.push(`update available: ${PACKAGE_VERSION} -> ${update}`);
68
+ lines.push("", "Configured agents:");
69
+ if (configured.length === 0) {
70
+ lines.push(" none");
71
+ }
72
+ else {
73
+ for (const agent of configured) {
74
+ const env = readAgentEnv(agent);
75
+ lines.push(` - ${agent.label}: ${agent.path} — ${baseUrlOf(env)} (${accountOf(env)})`);
76
+ }
77
+ }
78
+ lines.push(`Not configured: ${notConfigured.length > 0 ? notConfigured.map((agent) => agent.label).join(", ") : "none"}`);
79
+ lines.push("", "Commands:", " install Configure Jira for one or more agents", " update Refresh existing Jira entries from their own credentials", " uninstall Remove Jira entries from selected agents", " --version, -v Print the installed version", " --help, -h Show command help", "", "This command is the MCP server: agents run it over stdio with JIRA_* env vars; running it bare without those variables reports a missing JIRA_BASE_URL.");
80
+ return lines.join("\n");
81
+ }
@@ -3,15 +3,15 @@ import { run } from "../util.js";
3
3
  export function registerBoardTools(server, client) {
4
4
  server.registerTool("list_boards", {
5
5
  title: "List Jira boards",
6
- description: "List Jira agile boards (scrum or kanban), optionally filtered by type, name, or project keys.",
6
+ description: "List Jira agile boards (scrum or kanban), optionally filtered by type, name, or one project key/ID.",
7
7
  inputSchema: {
8
8
  type: z.enum(["scrum", "kanban"]).optional().describe("Board type filter"),
9
9
  name: z.string().optional().describe("Filter by board name (partial match)"),
10
- projectKeys: z.string().optional().describe("Comma-separated project keys, e.g. 'PROJ1,PROJ2'"),
10
+ projectKeys: z.string().optional().describe("Project key or ID (the Agile API accepts one projectKeyOrId value)"),
11
11
  maxResults: z.number().int().min(1).max(1000).default(50),
12
12
  startAt: z.number().int().min(0).default(0),
13
13
  },
14
- }, async ({ type, name, projectKeys, maxResults, startAt }) => run(() => client.agileGet("/board", { type, name, projectKeys, maxResults, startAt })));
14
+ }, async ({ type, name, projectKeys, maxResults, startAt }) => run(() => client.agileGet("/board", { type, name, projectKeyOrId: projectKeys, maxResults, startAt })));
15
15
  server.registerTool("get_board", {
16
16
  title: "Get board",
17
17
  description: "Get a single Jira board by ID, including its projects, location, and settings.",
@@ -2,13 +2,16 @@ import { z } from "zod";
2
2
  import { JiraApiError } from "../client.js";
3
3
  import { run } from "../util.js";
4
4
  import { searchIssues } from "../search.js";
5
+ import { richText } from "../adf.js";
5
6
  const DEFAULT_EPIC_ISSUE_FIELDS = "key,summary,status,assignee";
6
7
  /**
7
8
  * The Agile epic endpoints (/rest/agile/1.0/epic/...) only understand classic
8
9
  * (company-managed) epics: on a team-managed ("next-gen") project they answer
9
- * HTTP 400 "The request contains a next-gen issue". There, an epic is an ordinary
10
- * issue and its children are linked by `parent`, so each tool falls back to the
11
- * plain issue/search APIs when the Agile call rejects the project style.
10
+ * HTTP 400 "The request contains a next-gen issue". Cloud issue listing uses
11
+ * the current Software 1.0 enhanced endpoint; Data Center uses Agile 1.0.
12
+ * There, an epic is an ordinary issue and its children are linked by `parent`,
13
+ * so each tool falls back to the plain issue/search APIs when the endpoint
14
+ * rejects the project style.
12
15
  */
13
16
  function isNextGenRejection(err) {
14
17
  return err instanceof JiraApiError && (err.status === 400 || err.status === 404);
@@ -50,7 +53,7 @@ export function registerEpicTools(server, client) {
50
53
  }));
51
54
  server.registerTool("get_epic_issues", {
52
55
  title: "Get epic issues",
53
- description: "List the issues belonging to an epic. Falls back to a JQL 'parent = <epic>' search on team-managed projects, where the Agile epic API does not apply.",
56
+ description: "List the issues belonging to an epic. Cloud uses the current Software API enhanced endpoint; Data Center uses Agile 1.0. Falls back to a JQL 'parent = <epic>' search on team-managed projects, where the epic API does not apply.",
54
57
  inputSchema: {
55
58
  epicIdOrKey: z.string().describe("Epic key, e.g. PROJ-1, or numeric issue ID"),
56
59
  fields: z
@@ -66,11 +69,23 @@ export function registerEpicTools(server, client) {
66
69
  },
67
70
  }, async ({ epicIdOrKey, fields, maxResults, startAt, nextPageToken }) => run(() => {
68
71
  const issueFields = fields ?? DEFAULT_EPIC_ISSUE_FIELDS;
69
- return withNextGenFallback(() => client.agileGet(`/epic/${encodeURIComponent(epicIdOrKey)}/issue`, {
70
- fields: issueFields,
71
- maxResults,
72
- startAt,
73
- }), () => searchIssues(client, {
72
+ return withNextGenFallback(() => {
73
+ if (client.isCloud) {
74
+ if (startAt > 0) {
75
+ throw new Error("Jira Cloud Software epic issue paging uses nextPageToken, not startAt.");
76
+ }
77
+ return client.softwareGet(`/epic/${encodeURIComponent(epicIdOrKey)}/issue`, {
78
+ fields: issueFields,
79
+ maxResults,
80
+ nextPageToken,
81
+ });
82
+ }
83
+ return client.agileGet(`/epic/${encodeURIComponent(epicIdOrKey)}/issue`, {
84
+ fields: issueFields,
85
+ maxResults,
86
+ startAt,
87
+ });
88
+ }, () => searchIssues(client, {
74
89
  // JQL `parent` accepts a key or a numeric issue ID.
75
90
  jql: `parent = ${epicIdOrKey} ORDER BY rank`,
76
91
  fields: issueFields,
@@ -85,7 +100,10 @@ export function registerEpicTools(server, client) {
85
100
  inputSchema: {
86
101
  projectKey: z.string().describe("Project key, e.g. PROJ"),
87
102
  name: z.string().describe("Epic name (used as the issue summary)"),
88
- description: z.string().optional(),
103
+ description: z
104
+ .string()
105
+ .optional()
106
+ .describe("Plain text; converted to ADF on Cloud/v3. Markdown is not rendered."),
89
107
  issueType: z
90
108
  .string()
91
109
  .optional()
@@ -103,7 +121,7 @@ export function registerEpicTools(server, client) {
103
121
  project: { key: projectKey },
104
122
  summary: name,
105
123
  issuetype: /^\d+$/.test(type) ? { id: type } : { name: type },
106
- description,
124
+ description: richText(description, client.apiVersion),
107
125
  assignee: assignee
108
126
  ? client.apiVersion === "3"
109
127
  ? { accountId: assignee }
@@ -130,7 +148,7 @@ export function registerEpicTools(server, client) {
130
148
  })));
131
149
  server.registerTool("get_epic_meta", {
132
150
  title: "Get epic meta",
133
- description: "List the epic-level issue types available in a project (hierarchy level above Story/Task), with the fields required to create one.",
151
+ description: "List the epic-level issue types available in a project (hierarchy level above Story/Task). Use get_issue_create_meta for the fields required to create one.",
134
152
  inputSchema: {
135
153
  projectKey: z.string().describe("Project key, e.g. PROJ"),
136
154
  },
@@ -1,7 +1,92 @@
1
1
  import { z } from "zod";
2
2
  import { run } from "../util.js";
3
3
  import { searchIssues } from "../search.js";
4
- const DEFAULT_ISSUE_FIELDS = "summary,description,status,assignee,reporter,issuetype,labels,components,priority,created,updated,due,project";
4
+ import { richText, richTextFields, richTextUpdate } from "../adf.js";
5
+ const DEFAULT_ISSUE_FIELDS = "summary,description,status,assignee,reporter,issuetype,labels,components,priority,created,updated,duedate,project";
6
+ async function getIssueCreateMeta(client, projectKeys, issuetypeIds, expand) {
7
+ const projectFilter = new Set(projectKeys
8
+ ?.split(",")
9
+ .map((value) => value.trim())
10
+ .filter(Boolean));
11
+ const typeFilter = new Set(issuetypeIds
12
+ ?.split(",")
13
+ .map((value) => value.trim())
14
+ .filter(Boolean));
15
+ const includeFields = !expand || expand.includes("fields");
16
+ if (includeFields && projectFilter.size === 0) {
17
+ throw new Error("projectKeys is required when expand includes fields (the default); pass one or more project keys to bound metadata requests, or use expand=projects.issuetypes for issue-type summaries.");
18
+ }
19
+ const projects = projectFilter.size > 0
20
+ ? await mapWithConcurrency([...projectFilter], 4, async (key) => (await client.apiGet(`/project/${encodeURIComponent(key)}`)))
21
+ : await listProjectsForCreateMeta(client);
22
+ const result = [];
23
+ for (const project of projects) {
24
+ const projectIdOrKey = project.key ?? project.id;
25
+ if (!projectIdOrKey) {
26
+ result.push({ ...project, issuetypes: [] });
27
+ continue;
28
+ }
29
+ const issueTypePage = await client.apiGet(`/issue/createmeta/${encodeURIComponent(projectIdOrKey)}/issuetypes`, { maxResults: 200, startAt: 0 });
30
+ const issueTypes = issueTypesFromMeta(issueTypePage).filter((issueType) => typeFilter.size === 0 || typeFilter.has(issueType.id));
31
+ const enriched = includeFields
32
+ ? await mapWithConcurrency(issueTypes, 4, async (issueType) => {
33
+ const fieldPage = await client.apiGet(`/issue/createmeta/${encodeURIComponent(projectIdOrKey)}/issuetypes/${encodeURIComponent(issueType.id)}`, { maxResults: 200, startAt: 0 });
34
+ const fields = fieldsFromMeta(fieldPage);
35
+ return { ...issueType, expand: "fields", fields };
36
+ })
37
+ : issueTypes;
38
+ result.push({ ...project, expand: "issuetypes", issuetypes: enriched });
39
+ }
40
+ return { expand: "projects", projects: result };
41
+ }
42
+ async function mapWithConcurrency(values, limit, fn) {
43
+ const results = new Array(values.length);
44
+ let nextIndex = 0;
45
+ async function worker() {
46
+ const index = nextIndex++;
47
+ if (index >= values.length)
48
+ return;
49
+ results[index] = await fn(values[index], index);
50
+ return worker();
51
+ }
52
+ await Promise.all(Array.from({ length: Math.min(limit, values.length) }, () => worker()));
53
+ return results;
54
+ }
55
+ async function listProjectsForCreateMeta(client) {
56
+ if (client.isCloud) {
57
+ const page = (await client.apiGet("/project/search", { maxResults: 100, startAt: 0 }));
58
+ return page.values ?? [];
59
+ }
60
+ return (await client.apiGet("/project", { maxResults: 1000, startAt: 0 }));
61
+ }
62
+ function issueTypesFromMeta(value) {
63
+ if (Array.isArray(value))
64
+ return value;
65
+ if (!value || typeof value !== "object")
66
+ return [];
67
+ const object = value;
68
+ if (Array.isArray(object.issueTypes))
69
+ return object.issueTypes;
70
+ return typeof object.id === "string" && typeof object.name === "string"
71
+ ? [object]
72
+ : [];
73
+ }
74
+ function fieldsFromMeta(value) {
75
+ if (!value || typeof value !== "object")
76
+ return {};
77
+ const object = value;
78
+ if (Array.isArray(object.fields)) {
79
+ return Object.fromEntries(object.fields
80
+ .filter((field) => typeof field.key === "string" || typeof field.fieldId === "string")
81
+ .map((field) => [String(field.key ?? field.fieldId), field]));
82
+ }
83
+ if (object.fields && typeof object.fields === "object" && !Array.isArray(object.fields)) {
84
+ return object.fields;
85
+ }
86
+ if (typeof object.fieldId === "string")
87
+ return { [object.fieldId]: object };
88
+ return {};
89
+ }
5
90
  export function registerIssueTools(server, client) {
6
91
  server.registerTool("get_issue", {
7
92
  title: "Get issue",
@@ -9,7 +94,10 @@ export function registerIssueTools(server, client) {
9
94
  inputSchema: {
10
95
  issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
11
96
  fields: z.string().optional().describe(`Comma-separated fields (default: ${DEFAULT_ISSUE_FIELDS})`),
12
- expand: z.string().optional().describe("Comma-separated expansions, e.g. renderedFields,versions,changelog"),
97
+ expand: z
98
+ .string()
99
+ .optional()
100
+ .describe("Comma-separated expansions, e.g. renderedFields,names,schema,transitions,editmeta,changelog"),
13
101
  },
14
102
  }, async ({ issueKey, fields, expand }) => run(() => client.apiGet(`/issue/${encodeURIComponent(issueKey)}`, { fields: fields ?? DEFAULT_ISSUE_FIELDS, expand })));
15
103
  server.registerTool("create_issue", {
@@ -19,12 +107,15 @@ export function registerIssueTools(server, client) {
19
107
  projectKey: z.string().describe("Project key, e.g. PROJ"),
20
108
  summary: z.string().describe("Issue summary"),
21
109
  issueType: z.string().describe("Issue type name or ID, e.g. Story, Task, Bug, or 10004"),
22
- description: z.string().optional(),
110
+ description: z
111
+ .string()
112
+ .optional()
113
+ .describe("Plain text; converted to ADF on Cloud/v3. Markdown is not rendered."),
23
114
  assignee: z.string().optional().describe("Account ID on Cloud/v3, username on DC/v2"),
24
115
  reporter: z.string().optional().describe("Account ID on Cloud/v3, username on DC/v2"),
25
116
  labels: z.array(z.string()).optional(),
26
117
  components: z.array(z.string()).optional().describe("Component names"),
27
- dueDate: z.string().optional().describe("Due date, e.g. 2026-09-30"),
118
+ dueDate: z.string().optional().describe("Due date, e.g. 2026-09-30 (sent to Jira as duedate)"),
28
119
  priority: z.string().optional().describe("Priority name, e.g. High"),
29
120
  parent: z.string().optional().describe("Parent issue key (for sub-tasks)"),
30
121
  customFields: z.record(z.string(), z.unknown()).optional().describe("Custom field IDs mapped to values"),
@@ -36,12 +127,12 @@ export function registerIssueTools(server, client) {
36
127
  project: { key: projectKey },
37
128
  summary,
38
129
  issuetype: /^\d+$/.test(issueType) ? { id: issueType } : { name: issueType },
39
- description,
130
+ description: richText(description, client.apiVersion),
40
131
  assignee: assignee ? userRef(assignee) : undefined,
41
132
  reporter: reporter ? userRef(reporter) : undefined,
42
133
  labels,
43
134
  components: components?.map((name) => ({ name })),
44
- dueDate,
135
+ duedate: dueDate,
45
136
  priority: priority ? { name: priority } : undefined,
46
137
  parent: parent ? { key: parent } : undefined,
47
138
  ...customFields,
@@ -53,10 +144,16 @@ export function registerIssueTools(server, client) {
53
144
  description: "Update an issue. Pass absolute values in fields (e.g. { summary: 'new' }) and/or relative changes in update (e.g. { labels: [{ add: 'x' }, { remove: 'y' }] }).",
54
145
  inputSchema: {
55
146
  issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
56
- fields: z.record(z.string(), z.unknown()).optional().describe("Field values to set"),
147
+ fields: z
148
+ .record(z.string(), z.unknown())
149
+ .optional()
150
+ .describe("Field values to set. Plain-text description/environment are converted to ADF on Cloud/v3."),
57
151
  update: z.record(z.string(), z.unknown()).optional().describe("Relative updates (add/remove operations)"),
58
152
  },
59
- }, async ({ issueKey, fields, update }) => run(() => client.apiPut(`/issue/${encodeURIComponent(issueKey)}`, { fields, update })));
153
+ }, async ({ issueKey, fields, update }) => run(() => client.apiPut(`/issue/${encodeURIComponent(issueKey)}`, {
154
+ fields: richTextFields(fields, client.apiVersion),
155
+ update: richTextUpdate(update, client.apiVersion),
156
+ })));
60
157
  server.registerTool("delete_issue", {
61
158
  title: "Delete issue",
62
159
  description: "Delete an issue. This is destructive.",
@@ -96,20 +193,19 @@ export function registerIssueTools(server, client) {
96
193
  })));
97
194
  server.registerTool("get_issue_create_meta", {
98
195
  title: "Get issue create metadata",
99
- description: "Discover valid projects, issue types, and their fields (with required flags) before creating an issue.",
196
+ description: "Discover valid projects, issue types, and their fields (with required flags) before creating an issue. Uses the current per-project metadata endpoints instead of the deprecated /issue/createmeta query form. Pass projectKeys when fields are expanded (the default) to bound requests; omit it only with expand=projects.issuetypes for issue-type summaries.",
100
197
  inputSchema: {
101
- projectKeys: z.string().optional().describe("Comma-separated project keys to filter"),
198
+ projectKeys: z
199
+ .string()
200
+ .optional()
201
+ .describe("Comma-separated project keys; required when expand includes fields (the default)"),
102
202
  issuetypeIds: z.string().optional().describe("Comma-separated issue type IDs to filter"),
103
203
  expand: z
104
204
  .string()
105
205
  .optional()
106
206
  .describe("Use 'projects.issuetypes.fields' for full field details (default)"),
107
207
  },
108
- }, async ({ projectKeys, issuetypeIds, expand }) => run(() => client.apiGet("/issue/createmeta", {
109
- projectKeys,
110
- issuetypeIds,
111
- expand: expand ?? "projects.issuetypes.fields",
112
- })));
208
+ }, async ({ projectKeys, issuetypeIds, expand }) => run(() => getIssueCreateMeta(client, projectKeys, issuetypeIds, expand ?? "projects.issuetypes.fields")));
113
209
  server.registerTool("get_issue_transitions", {
114
210
  title: "Get issue transitions",
115
211
  description: "List the workflow transitions available for an issue.",
@@ -127,11 +223,11 @@ export function registerIssueTools(server, client) {
127
223
  },
128
224
  }, async ({ issueKey, transitionId, fields }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/transitions`, {
129
225
  transition: { id: transitionId },
130
- fields,
226
+ fields: richTextFields(fields, client.apiVersion),
131
227
  })));
132
228
  server.registerTool("assign_issue", {
133
229
  title: "Assign issue",
134
- description: "Assign (or unassign, with an empty value) an issue to a user.",
230
+ description: "Assign (or unassign, with an empty value) an issue to a user. Uses PUT /issue/{key}/assignee with accountId on v3 and name on v2.",
135
231
  inputSchema: {
136
232
  issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
137
233
  accountId: z.string().optional().describe("Account ID (Jira Cloud)"),
@@ -139,28 +235,26 @@ export function registerIssueTools(server, client) {
139
235
  },
140
236
  }, async ({ issueKey, accountId, name }) => run(() => {
141
237
  const body = {};
142
- if (accountId)
143
- body.accountId = accountId;
144
- else if (name)
145
- body.name = name;
146
- else if (client.apiVersion === "3")
147
- body.accountId = null;
148
- else
149
- body.name = null;
150
- return client.apiPost(`/issue/${encodeURIComponent(issueKey)}/assignee`, body);
238
+ if (client.apiVersion === "3") {
239
+ body.accountId = accountId ?? name ?? null;
240
+ }
241
+ else {
242
+ body.name = name ?? accountId ?? null;
243
+ }
244
+ return client.apiPut(`/issue/${encodeURIComponent(issueKey)}/assignee`, body);
151
245
  }));
152
246
  server.registerTool("add_comment", {
153
247
  title: "Add comment",
154
248
  description: "Add a comment to an issue.",
155
249
  inputSchema: {
156
250
  issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
157
- body: z.string().describe("Comment text"),
251
+ body: z.string().describe("Comment text (plain text; converted to ADF on Cloud/v3, Markdown is not rendered)"),
158
252
  visibility: z
159
253
  .object({ type: z.string(), value: z.string() })
160
254
  .optional()
161
255
  .describe("Comment visibility restriction"),
162
256
  },
163
- }, async ({ issueKey, body, visibility }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/comment`, { body, visibility })));
257
+ }, async ({ issueKey, body, visibility }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/comment`, { body: richText(body, client.apiVersion), visibility })));
164
258
  server.registerTool("list_comments", {
165
259
  title: "List issue comments",
166
260
  description: "List comments on an issue.",
@@ -191,12 +285,16 @@ export function registerIssueTools(server, client) {
191
285
  }, async ({ issueKey, maxResults, startAt, orderBy }) => run(() => client.apiGet(`/issue/${encodeURIComponent(issueKey)}/worklog`, { maxResults, startAt, orderBy })));
192
286
  server.registerTool("add_worklog", {
193
287
  title: "Add worklog",
194
- description: "Add a worklog (time spent) to an issue. timeSpent uses Jira format, e.g. '2h 30m' or '1d'.",
288
+ description: "Add a worklog (time spent) to an issue. started is optional and defaults to now when omitted; timeSpent uses Jira format, e.g. '2h 30m' or '1d'.",
195
289
  inputSchema: {
196
290
  issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
197
291
  timeSpent: z.string().describe("Time spent, e.g. '1h 30m'"),
198
- comment: z.string().optional(),
199
- started: z.string().optional().describe("Start timestamp (ISO-8601)"),
292
+ comment: z.string().optional().describe("Worklog comment (plain text; converted to ADF on Cloud/v3)"),
293
+ started: z.string().optional().describe("Start timestamp (ISO-8601; Jira defaults it to now when omitted)"),
200
294
  },
201
- }, async ({ issueKey, timeSpent, comment, started }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/worklog`, { timeSpent, comment, started })));
295
+ }, async ({ issueKey, timeSpent, comment, started }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/worklog`, {
296
+ timeSpent,
297
+ comment: richText(comment, client.apiVersion),
298
+ started,
299
+ })));
202
300
  }
@@ -3,12 +3,14 @@ import { run } from "../util.js";
3
3
  export function registerProjectTools(server, client) {
4
4
  server.registerTool("list_projects", {
5
5
  title: "List projects",
6
- description: "List all Jira projects visible to the current user.",
6
+ description: "List all Jira projects visible to the current user. Cloud uses paginated /project/search; Data Center uses /project.",
7
7
  inputSchema: {
8
8
  maxResults: z.number().int().min(1).max(1000).default(100),
9
9
  startAt: z.number().int().min(0).default(0),
10
10
  },
11
- }, async ({ maxResults, startAt }) => run(() => client.apiGet("/project", { maxResults, startAt })));
11
+ }, async ({ maxResults, startAt }) => run(() => client.isCloud
12
+ ? client.apiGet("/project/search", { maxResults, startAt })
13
+ : client.apiGet("/project", { maxResults, startAt })));
12
14
  server.registerTool("get_project", {
13
15
  title: "Get project",
14
16
  description: "Get a single project by key or ID.",
@@ -29,7 +29,7 @@ export function registerSprintTools(server, client) {
29
29
  endDate: z.string().optional().describe("End date (ISO-8601)"),
30
30
  goal: z.string().optional().describe("Sprint goal"),
31
31
  },
32
- }, async ({ boardId, name, startDate, endDate, goal }) => run(() => client.agilePost(`/board/${boardId}/sprint`, { name, startDate, endDate, goal })));
32
+ }, async ({ boardId, name, startDate, endDate, goal }) => run(() => client.agilePost("/sprint", { name, originBoardId: boardId, startDate, endDate, goal })));
33
33
  server.registerTool("update_sprint", {
34
34
  title: "Update sprint",
35
35
  description: "Update a sprint's name, goal, dates, or state (active/closed/future). Only provided fields are changed.",
@@ -51,7 +51,7 @@ export function registerSprintTools(server, client) {
51
51
  }, async ({ sprintId }) => run(() => client.agilePut(`/sprint/${sprintId}`, { state: "closed" })));
52
52
  server.registerTool("get_sprint_issues", {
53
53
  title: "Get sprint issues",
54
- description: "List the issues in a sprint.",
54
+ description: "List the issues in a sprint. Cloud uses the current Software API enhanced endpoint with cursor paging; Data Center uses Agile 1.0 with startAt/total.",
55
55
  inputSchema: {
56
56
  sprintId: z.number().int().describe("Sprint ID"),
57
57
  fields: z
@@ -59,16 +59,13 @@ export function registerSprintTools(server, client) {
59
59
  .optional()
60
60
  .describe("Comma-separated issue fields to return (default: key, summary, status, assignee)"),
61
61
  maxResults: z.number().int().min(1).max(1000).default(100),
62
- startAt: z.number().int().min(0).default(0),
62
+ startAt: z.number().int().min(0).default(0).describe("Offset paging, Data Center only; rejected on Cloud, use nextPageToken"),
63
+ nextPageToken: z.string().optional().describe("Jira Cloud only: cursor from the previous response"),
63
64
  },
64
- }, async ({ sprintId, fields, maxResults, startAt }) => run(() => client.agileGet(`/sprint/${sprintId}/issue`, {
65
- fields: fields ?? "key,summary,status,assignee",
66
- maxResults,
67
- startAt,
68
- })));
65
+ }, async ({ sprintId, fields, maxResults, startAt, nextPageToken }) => run(() => getSprintIssues(client, sprintId, fields, maxResults, startAt, nextPageToken)));
69
66
  server.registerTool("get_sprint_view", {
70
67
  title: "Get sprint view",
71
- description: "Full sprint view like the Jira UI: board details, the sprint, and its issues, in one call.",
68
+ description: "Full sprint view like the Jira UI: board details, the sprint, and its issues, in one call. Cloud issue results use Software API cursor paging; Data Center uses Agile 1.0 offset paging.",
72
69
  inputSchema: {
73
70
  boardId: z.number().int().describe("Board ID"),
74
71
  sprintId: z.number().int().describe("Sprint ID"),
@@ -77,26 +74,21 @@ export function registerSprintTools(server, client) {
77
74
  .optional()
78
75
  .describe("Comma-separated issue fields to return (default: key, summary, status, assignee)"),
79
76
  maxResults: z.number().int().min(1).max(1000).default(100),
80
- startAt: z.number().int().min(0).default(0),
77
+ startAt: z.number().int().min(0).default(0).describe("Offset paging, Data Center only; rejected on Cloud, use nextPageToken"),
78
+ nextPageToken: z.string().optional().describe("Jira Cloud only: cursor from the previous response"),
81
79
  },
82
- }, async ({ boardId, sprintId, fields, maxResults, startAt }) => run(async () => {
83
- // Composed from three documented Agile endpoints; the old /rest/agile/1.0/rapid
84
- // path this used to call is not a real endpoint and answered 404.
80
+ }, async ({ boardId, sprintId, fields, maxResults, startAt, nextPageToken }) => run(async () => {
85
81
  const [board, sprint, issues] = await Promise.all([
86
82
  client.agileGet(`/board/${boardId}`),
87
83
  client.agileGet(`/sprint/${sprintId}`),
88
- client.agileGet(`/sprint/${sprintId}/issue`, {
89
- fields: fields ?? "key,summary,status,assignee",
90
- maxResults,
91
- startAt,
92
- }),
84
+ getSprintIssues(client, sprintId, fields, maxResults, startAt, nextPageToken),
93
85
  ]);
94
86
  return { board, sprint, issues };
95
87
  }));
96
88
  server.registerTool("get_backlog", {
97
89
  title: "Get board backlog",
98
90
  description: "List a board's backlog issues (in the board's filter, not in a sprint), ordered by rank. " +
99
- "Uses the Agile API's own backlog endpoint, so it pages with startAt/total on both Cloud and Data Center.",
91
+ "On Cloud this uses the current Software API enhanced backlog endpoint and cursor paging; on Data Center it uses Agile 1.0 with startAt/total.",
100
92
  inputSchema: {
101
93
  boardId: z.number().int().describe("Board ID"),
102
94
  jql: z.string().optional().describe("Extra JQL to narrow the backlog, e.g. 'assignee IS EMPTY'"),
@@ -105,12 +97,38 @@ export function registerSprintTools(server, client) {
105
97
  .optional()
106
98
  .describe("Comma-separated issue fields to return (default: key, summary, status, assignee)"),
107
99
  maxResults: z.number().int().min(1).max(100).default(50),
108
- startAt: z.number().int().min(0).default(0),
100
+ startAt: z.number().int().min(0).default(0).describe("Offset paging, Data Center only; rejected on Cloud, use nextPageToken"),
101
+ nextPageToken: z.string().optional().describe("Jira Cloud only: cursor from the previous response"),
109
102
  },
110
- }, async ({ boardId, jql, fields, maxResults, startAt }) => run(() => client.agileGet(`/board/${boardId}/backlog`, {
103
+ }, async ({ boardId, jql, fields, maxResults, startAt, nextPageToken }) => run(() => getBacklog(client, boardId, jql, fields, maxResults, startAt, nextPageToken)));
104
+ }
105
+ async function getSprintIssues(client, sprintId, fields, maxResults, startAt, nextPageToken) {
106
+ const issueFields = fields ?? "key,summary,status,assignee";
107
+ if (client.isCloud) {
108
+ if (startAt > 0) {
109
+ throw new Error("Jira Cloud Software sprint issue paging uses nextPageToken, not startAt.");
110
+ }
111
+ return client.softwareGet(`/sprint/${sprintId}/issue`, { fields: issueFields, maxResults, nextPageToken });
112
+ }
113
+ return client.agileGet(`/sprint/${sprintId}/issue`, { fields: issueFields, maxResults, startAt });
114
+ }
115
+ async function getBacklog(client, boardId, jql, fields, maxResults, startAt, nextPageToken) {
116
+ const issueFields = fields ?? "key,summary,status,assignee";
117
+ if (client.isCloud) {
118
+ if (startAt > 0) {
119
+ throw new Error("Jira Cloud Software backlog paging uses nextPageToken, not startAt.");
120
+ }
121
+ return client.softwareGet(`/board/${boardId}/backlog`, {
122
+ jql,
123
+ fields: issueFields,
124
+ maxResults,
125
+ nextPageToken,
126
+ });
127
+ }
128
+ return client.agileGet(`/board/${boardId}/backlog`, {
111
129
  jql,
112
- fields: fields ?? "key,summary,status,assignee",
130
+ fields: issueFields,
113
131
  maxResults,
114
132
  startAt,
115
- })));
133
+ });
116
134
  }
@@ -29,7 +29,7 @@ export function registerUserTools(server, client) {
29
29
  if (!accountId) {
30
30
  throw new Error("API v3 requires accountId for get_user. Use find_users to look up the account ID for a username.");
31
31
  }
32
- return client.apiGet(`/user/${encodeURIComponent(accountId)}`);
32
+ return client.apiGet("/user", { accountId });
33
33
  }
34
34
  return client.apiGet("/user", { username: username ?? accountId });
35
35
  }));