@thammarongg/jira-mcp 0.3.0 → 0.4.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/README.md +25 -8
- package/dist/adf.js +71 -0
- package/dist/client.js +6 -1
- package/dist/config.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/boards.js +3 -3
- package/dist/tools/epics.js +30 -12
- package/dist/tools/issues.js +130 -32
- package/dist/tools/projects.js +4 -2
- package/dist/tools/sprints.js +41 -23
- package/dist/tools/users.js +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +30 -5
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ npm run build
|
|
|
101
101
|
|
|
102
102
|
| Variable | Required | Description |
|
|
103
103
|
| --- | --- | --- |
|
|
104
|
-
| `JIRA_BASE_URL` | yes | `https://your-org.atlassian.net` (Cloud) or `https://jira.yourcompany.com` (DC) |
|
|
104
|
+
| `JIRA_BASE_URL` | yes | `https://your-org.atlassian.net` (Cloud) or `https://jira.yourcompany.com` (DC); a DC context path such as `/jira` is preserved |
|
|
105
105
|
| `JIRA_EMAIL` | Cloud | Your Atlassian account email |
|
|
106
106
|
| `JIRA_API_TOKEN` | yes | API token (Cloud: id.atlassian.com → Security → API tokens; DC: personal access token) |
|
|
107
107
|
| `JIRA_USERNAME` | DC | Username (alternative to `JIRA_EMAIL`) |
|
|
@@ -160,20 +160,20 @@ mkdir -p ~/.agents/skills/jira && cp skill/SKILL.md ~/.agents/skills/jira/
|
|
|
160
160
|
|
|
161
161
|
## Tools
|
|
162
162
|
|
|
163
|
-
### Boards & sprints (Agile
|
|
163
|
+
### Boards & sprints (Agile + Software APIs)
|
|
164
164
|
|
|
165
165
|
| Tool | Description |
|
|
166
166
|
| --- | --- |
|
|
167
|
-
| `list_boards` | List boards (filter by type/name/project) |
|
|
167
|
+
| `list_boards` | List boards (filter by type/name/project key or ID) |
|
|
168
168
|
| `get_board` | Board details incl. projects |
|
|
169
169
|
| `list_sprints` | Sprints on a board (state: active/closed/future) |
|
|
170
170
|
| `get_sprint` | Sprint details |
|
|
171
171
|
| `create_sprint` | New sprint on a board |
|
|
172
172
|
| `update_sprint` | Rename, reschedule, change goal/state |
|
|
173
173
|
| `close_sprint` | Close a sprint |
|
|
174
|
-
| `get_sprint_issues` | Issues in a sprint |
|
|
174
|
+
| `get_sprint_issues` | Issues in a sprint (Cloud cursor or DC offset paging) |
|
|
175
175
|
| `get_sprint_view` | Full UI-like sprint view (board + sprint + issues in one call) |
|
|
176
|
-
| `get_backlog` | Board backlog (Agile
|
|
176
|
+
| `get_backlog` | Board backlog (Cloud Software enhanced or DC Agile, rank-ordered) |
|
|
177
177
|
|
|
178
178
|
### Epics
|
|
179
179
|
|
|
@@ -194,10 +194,10 @@ mkdir -p ~/.agents/skills/jira && cp skill/SKILL.md ~/.agents/skills/jira/
|
|
|
194
194
|
| `update_issue` | Set fields and/or relative `update` ops |
|
|
195
195
|
| `delete_issue` | Delete |
|
|
196
196
|
| `search_issues` | **JQL search** — enhanced search (`/search/jql`) on Cloud, legacy `/search` on Data Center |
|
|
197
|
-
| `get_issue_create_meta` | Discover projects/types/required fields |
|
|
197
|
+
| `get_issue_create_meta` | Discover projects/types/required fields (current per-project metadata endpoints) |
|
|
198
198
|
| `get_issue_transitions` / `transition_issue` | Workflow transitions |
|
|
199
199
|
| `assign_issue` | Assign/unassign |
|
|
200
|
-
| `add_comment` / `list_comments` / `delete_comment` | Comments |
|
|
200
|
+
| `add_comment` / `list_comments` / `delete_comment` | Comments (plain text in, auto-converted to ADF on Cloud/v3) |
|
|
201
201
|
| `get_issue_worklogs` / `add_worklog` | Time tracking |
|
|
202
202
|
|
|
203
203
|
### Projects, users, meta
|
|
@@ -219,6 +219,17 @@ mkdir -p ~/.agents/skills/jira && cp skill/SKILL.md ~/.agents/skills/jira/
|
|
|
219
219
|
5. `create_issue` / `transition_issue` / `add_comment` to act
|
|
220
220
|
6. Anything else → `jira_api`
|
|
221
221
|
|
|
222
|
+
## Rich text and ADF
|
|
223
|
+
|
|
224
|
+
Jira Cloud REST v3 stores descriptions, comment bodies and worklog comments as
|
|
225
|
+
[Atlassian Document Format](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/),
|
|
226
|
+
not plain strings. Pass plain text to `create_issue`, `create_epic`,
|
|
227
|
+
`update_issue`, `transition_issue`, `add_comment` and `add_worklog` — the server
|
|
228
|
+
wraps it in ADF when `JIRA_API_VERSION` is `3`, and leaves it alone on v2 (Data
|
|
229
|
+
Center), where wiki markup applies. Blank lines start a new paragraph and single
|
|
230
|
+
newlines become line breaks; **Markdown is not rendered** (`**bold**` shows
|
|
231
|
+
literally). Already-built ADF objects are passed through untouched.
|
|
232
|
+
|
|
222
233
|
## Development
|
|
223
234
|
|
|
224
235
|
```bash
|
|
@@ -231,11 +242,17 @@ node scripts/smoke.mjs # stdio handshake + tools/list smoke test
|
|
|
231
242
|
## Notes & limitations
|
|
232
243
|
|
|
233
244
|
- Auth is HTTP Basic (email+token for Cloud, username+token/password for DC) — the standard for Jira REST.
|
|
245
|
+
- `JIRA_BASE_URL` may include a Data Center context path (for example `https://jira.example.com/jira`); it is retained when resolving `/rest/...` endpoints.
|
|
234
246
|
- Pagination: most list tools return Jira's native `startAt`/`maxResults`/`total`; pass `startAt` to page.
|
|
247
|
+
- Platform routing: Cloud-only enhanced Software reads use `/rest/software/1.0` for backlog, sprint issues, and epic issues (cursor `nextPageToken`); Data Center uses the corresponding Agile 1.0 endpoints with `startAt`/`total`. `list_boards` passes its project filter as the Agile API's singular `projectKeyOrId` parameter.
|
|
248
|
+
- `create_sprint` uses the documented top-level Agile `POST /rest/agile/1.0/sprint` route and sends the selected board as `originBoardId`.
|
|
235
249
|
- JQL search on **Jira Cloud** uses `/rest/api/3/search/jql`, since Atlassian removed `GET /rest/api/{2,3}/search` on 2025-05-01 ([CHANGE-2046](https://developer.atlassian.com/changelog/#CHANGE-2046) — the old endpoint now returns HTTP 410). Consequences for `search_issues` on Cloud: the JQL must be **bounded** (include a restriction such as `project`, `assignee`, or `key`), the response carries **no `total`**, and paging is by cursor — pass the returned `nextPageToken` back and stop when `isLast` is true. `startAt` is rejected there rather than silently ignored, and `includeApproximateTotal: true` adds an approximate match count via `/search/approximate-count`.
|
|
236
250
|
- Jira **Data Center** keeps the legacy `/search` endpoint with `startAt`/`total`; if a Cloud site on a custom domain is misdetected as DC, a 410 from `/search` transparently retries against `/search/jql`.
|
|
237
|
-
- `
|
|
251
|
+
- `get_issue_create_meta` composes the current per-project and per-issue-type metadata endpoints (`/issue/createmeta/{project}/issuetypes` and `/issue/createmeta/{project}/issuetypes/{issueType}`), because the old `/issue/createmeta` query form is deprecated on Cloud and removed from Jira Data Center 9.0+. Its output keeps the familiar `projects[].issuetypes[].fields` shape. Pass `projectKeys` when fields are expanded (the default); omitting it is allowed only with `expand: "projects.issuetypes"` for bounded issue-type summaries. Project metadata is fetched with a fixed concurrency of four, and issue-type field metadata is likewise limited to four concurrent requests.
|
|
252
|
+
- `get_backlog`, `get_sprint_issues`, and `get_epic_issues` use Cloud's current Software enhanced endpoints with cursor paging; pass `nextPageToken` for the next page. Data Center keeps Agile 1.0 and `startAt`/`total`. The older Cloud Agile issue-list endpoints remain available but are deprecated.
|
|
238
253
|
- Rapid view IDs are computed as `boardId * 10^13 + sprintId` (Jira's documented convention).
|
|
239
254
|
- Comment bodies use the `body` field on both Cloud (v3) and Data Center (v2).
|
|
255
|
+
- User references use `accountId` on API v3/Cloud and `name` on API v2/Data Center. `get_user` calls `/user?accountId=...` on v3; the v3 path form `/user/{accountId}` is not a current endpoint. `create_issue`'s `dueDate` input is sent as Jira's `duedate` field; `add_worklog` accepts an optional `started` timestamp and Jira defaults it to now when omitted.
|
|
240
256
|
- Epics: the Agile epic API (`/rest/agile/1.0/epic/...`) only understands company-managed epics and returns HTTP 400 on team-managed ("next-gen") projects. `get_epic`, `get_epic_issues`, and `move_issue_to_epic` detect that and fall back to the issue/search APIs, where an epic is an ordinary issue linked to its children by `parent`.
|
|
257
|
+
- Jira Data Center exposes platform REST API v2/latest, not `/rest/api/3`. Leave `JIRA_API_VERSION` at its default (`2`) for DC; forcing `3` against a DC host is unsupported and causes the server to request unavailable `/rest/api/3/...` routes. Cloud-vs-DC endpoint routing is based on the detected deployment (`*.atlassian.net` is Cloud), while field representations are based on the selected API version.
|
|
241
258
|
- `jira_api` paths must resolve under `/rest/` — paths that would escape it (e.g. via `..` segments) are rejected, and `?`/`#` must be passed via `query`.
|
package/dist/adf.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira Cloud REST v3 takes rich-text fields (descriptions, comment bodies,
|
|
3
|
+
* worklog comments) as Atlassian Document Format, not plain strings. v2 (Data
|
|
4
|
+
* Center) still takes wiki-markup strings, so every conversion is gated on the
|
|
5
|
+
* API version the client is talking to.
|
|
6
|
+
*/
|
|
7
|
+
/** Wrap plain text in a minimal ADF document. Markdown is not interpreted. */
|
|
8
|
+
export function toAdfDoc(text) {
|
|
9
|
+
const content = text
|
|
10
|
+
.replace(/\r\n/g, "\n")
|
|
11
|
+
.split(/\n{2,}/)
|
|
12
|
+
.map((block) => block.split("\n").filter((line) => line !== ""))
|
|
13
|
+
.filter((lines) => lines.length > 0)
|
|
14
|
+
.map((lines) => ({
|
|
15
|
+
type: "paragraph",
|
|
16
|
+
content: lines.flatMap((line, i) => i === 0 ? [{ type: "text", text: line }] : [{ type: "hardBreak" }, { type: "text", text: line }]),
|
|
17
|
+
}));
|
|
18
|
+
return { type: "doc", version: 1, content };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert a rich-text value for the given API version. Strings become ADF on
|
|
22
|
+
* v3; anything else (a caller-built ADF doc, undefined) passes through.
|
|
23
|
+
*/
|
|
24
|
+
export function richText(value, apiVersion) {
|
|
25
|
+
if (apiVersion !== "3" || typeof value !== "string")
|
|
26
|
+
return value;
|
|
27
|
+
return toAdfDoc(value);
|
|
28
|
+
}
|
|
29
|
+
const RICH_TEXT_FIELDS = ["description", "environment"];
|
|
30
|
+
/** Convert the known rich-text keys of an issue `fields` payload in place-safe fashion. */
|
|
31
|
+
export function richTextFields(fields, apiVersion) {
|
|
32
|
+
if (!fields || apiVersion !== "3")
|
|
33
|
+
return fields;
|
|
34
|
+
let out;
|
|
35
|
+
for (const key of RICH_TEXT_FIELDS) {
|
|
36
|
+
if (typeof fields[key] === "string") {
|
|
37
|
+
out ??= { ...fields };
|
|
38
|
+
out[key] = toAdfDoc(fields[key]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out ?? fields;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert comment bodies inside an issue `update` payload, e.g.
|
|
45
|
+
* { comment: [{ add: { body: "text" } }] }.
|
|
46
|
+
*/
|
|
47
|
+
export function richTextUpdate(update, apiVersion) {
|
|
48
|
+
if (!update || apiVersion !== "3")
|
|
49
|
+
return update;
|
|
50
|
+
const ops = update.comment;
|
|
51
|
+
if (!Array.isArray(ops))
|
|
52
|
+
return update;
|
|
53
|
+
let changed = false;
|
|
54
|
+
const comment = ops.map((op) => {
|
|
55
|
+
if (!op || typeof op !== "object")
|
|
56
|
+
return op;
|
|
57
|
+
const entry = op;
|
|
58
|
+
let next;
|
|
59
|
+
for (const verb of ["add", "edit"]) {
|
|
60
|
+
const payload = entry[verb];
|
|
61
|
+
if (payload && typeof payload === "object" && typeof payload.body === "string") {
|
|
62
|
+
next ??= { ...entry };
|
|
63
|
+
const inner = payload;
|
|
64
|
+
next[verb] = { ...inner, body: toAdfDoc(inner.body) };
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return next ?? entry;
|
|
69
|
+
});
|
|
70
|
+
return changed ? { ...update, comment } : update;
|
|
71
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -21,7 +21,9 @@ export class JiraClient {
|
|
|
21
21
|
}
|
|
22
22
|
async request(method, path, opts = {}) {
|
|
23
23
|
const url = new URL(`${this.cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
24
|
-
|
|
24
|
+
const basePath = new URL(this.cfg.baseUrl).pathname.replace(/\/+$/, "");
|
|
25
|
+
const restPrefix = `${basePath}/rest/`;
|
|
26
|
+
if (!url.pathname.startsWith(restPrefix)) {
|
|
25
27
|
throw new Error(`Jira API path must start with /rest/ (resolved to ${url.pathname})`);
|
|
26
28
|
}
|
|
27
29
|
if (opts.query) {
|
|
@@ -81,6 +83,9 @@ export class JiraClient {
|
|
|
81
83
|
agilePut(path, body) {
|
|
82
84
|
return this.request("PUT", `/rest/agile/1.0${path}`, { body });
|
|
83
85
|
}
|
|
86
|
+
softwareGet(path, query) {
|
|
87
|
+
return this.request("GET", `/rest/software/1.0${path}`, { query });
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
function extractErrorMessage(body) {
|
|
86
91
|
if (typeof body === "string" && body.trim()) {
|
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@ export function loadConfig(env = process.env) {
|
|
|
8
8
|
const url = new URL(rawBase);
|
|
9
9
|
if (!/^https?:$/.test(url.protocol))
|
|
10
10
|
throw new Error("bad protocol");
|
|
11
|
-
baseUrl = `${url.protocol}//${url.host}`;
|
|
11
|
+
baseUrl = `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}`;
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
throw new Error(`JIRA_BASE_URL is not a valid URL: ${rawBase}`);
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ async function main() {
|
|
|
19
19
|
}
|
|
20
20
|
const config = loadConfig();
|
|
21
21
|
const client = new JiraClient(config);
|
|
22
|
-
const server = new McpServer({ name: "jira", version: "0.
|
|
22
|
+
const server = new McpServer({ name: "jira", version: "0.4.0" });
|
|
23
23
|
registerBoardTools(server, client);
|
|
24
24
|
registerSprintTools(server, client);
|
|
25
25
|
registerEpicTools(server, client);
|
package/dist/tools/boards.js
CHANGED
|
@@ -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
|
|
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("
|
|
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.",
|
package/dist/tools/epics.js
CHANGED
|
@@ -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".
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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(() =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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)
|
|
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
|
},
|
package/dist/tools/issues.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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)}`, {
|
|
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
|
|
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.
|
|
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 (
|
|
143
|
-
body.accountId = accountId;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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`, {
|
|
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
|
}
|
package/dist/tools/projects.js
CHANGED
|
@@ -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.
|
|
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.",
|
package/dist/tools/sprints.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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:
|
|
130
|
+
fields: issueFields,
|
|
113
131
|
maxResults,
|
|
114
132
|
startAt,
|
|
115
|
-
})
|
|
133
|
+
});
|
|
116
134
|
}
|
package/dist/tools/users.js
CHANGED
|
@@ -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(
|
|
32
|
+
return client.apiGet("/user", { accountId });
|
|
33
33
|
}
|
|
34
34
|
return client.apiGet("/user", { username: username ?? accountId });
|
|
35
35
|
}));
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -27,7 +27,8 @@ Or non-interactive: `npx -y @thammarongg/jira-mcp install --agents all
|
|
|
27
27
|
--yes`. Required env vars:
|
|
28
28
|
|
|
29
29
|
- `JIRA_BASE_URL` — `https://your-org.atlassian.net` (Cloud) or
|
|
30
|
-
`https://jira.yourcompany.com` (DC)
|
|
30
|
+
`https://jira.yourcompany.com` (DC); a DC context path such as `/jira` is
|
|
31
|
+
supported and preserved
|
|
31
32
|
- Cloud: `JIRA_EMAIL` + `JIRA_API_TOKEN` (token from id.atlassian.com →
|
|
32
33
|
Security → API tokens)
|
|
33
34
|
- DC: `JIRA_USERNAME` + `JIRA_API_TOKEN` (PAT) or `JIRA_PASSWORD` (app password)
|
|
@@ -64,12 +65,17 @@ After setup, verify with the `get_current_user` tool.
|
|
|
64
65
|
|
|
65
66
|
## Tool map
|
|
66
67
|
|
|
67
|
-
- **Boards/sprints**: `list_boards
|
|
68
|
+
- **Boards/sprints**: `list_boards` (the project filter is one Agile
|
|
69
|
+
`projectKeyOrId` value), `get_board`, `list_sprints`, `get_sprint`,
|
|
68
70
|
`create_sprint`, `update_sprint`, `close_sprint`, `get_sprint_issues`,
|
|
69
|
-
`get_sprint_view` (UI-like full view), `get_backlog
|
|
71
|
+
`get_sprint_view` (UI-like full view), `get_backlog`. Cloud backlog and
|
|
72
|
+
issue-list reads use Software 1.0 enhanced endpoints with cursor paging;
|
|
73
|
+
Data Center uses Agile 1.0 with `startAt`/`total`. `create_sprint` sends
|
|
74
|
+
`originBoardId` to the top-level Agile sprint-create route.
|
|
70
75
|
- **Epics**: `list_epics` (by board), `get_epic`, `get_epic_issues`,
|
|
71
76
|
`create_epic`, `move_issue_to_epic`, `get_epic_meta` — epics are addressed by
|
|
72
|
-
key or numeric ID
|
|
77
|
+
key or numeric ID. Cloud `get_epic_issues` uses Software 1.0 enhanced
|
|
78
|
+
cursor paging; team-managed projects fall back to JQL/search as needed.
|
|
73
79
|
- **Issues**: `get_issue`, `create_issue`, `update_issue`, `delete_issue`,
|
|
74
80
|
`search_issues` (JQL), `get_issue_create_meta`, `get_issue_transitions`,
|
|
75
81
|
`transition_issue`, `assign_issue`, `add_comment`, `list_comments`,
|
|
@@ -86,7 +92,8 @@ After setup, verify with the `get_current_user` tool.
|
|
|
86
92
|
`active`) → `get_sprint_issues` or `get_sprint_view` for the full picture.
|
|
87
93
|
|
|
88
94
|
**Backlog review**: `get_backlog(boardId)` — issues on the board that are not
|
|
89
|
-
in a sprint, ordered by rank; pass `jql` to narrow it.
|
|
95
|
+
in a sprint, ordered by rank; pass `jql` to narrow it. On Cloud, pass the
|
|
96
|
+
returned `nextPageToken` for the next page; on Data Center, pass `startAt`.
|
|
90
97
|
|
|
91
98
|
**Epic breakdown**: `list_epics(boardId)` → `get_epic_issues(epicIdOrKey)` for
|
|
92
99
|
the children. To create one, `get_epic_meta(projectKey)` gives the epic issue
|
|
@@ -96,6 +103,16 @@ custom field via `customFields`.
|
|
|
96
103
|
**Create an issue**: call `get_issue_create_meta(projectKeys=...)` first to
|
|
97
104
|
discover valid issue types and required fields, then `create_issue`
|
|
98
105
|
(custom fields go in `customFields`, e.g. `{ "customfield_10010": "..." }`).
|
|
106
|
+
The metadata tool composes the current per-project and per-issue-type
|
|
107
|
+
endpoints; the deprecated `/issue/createmeta` query form is not used. Pass
|
|
108
|
+
`projectKeys` when fields are expanded (the default); omit it only with
|
|
109
|
+
`expand: "projects.issuetypes"` for issue-type summaries. Metadata requests
|
|
110
|
+
use a fixed concurrency limit of four.
|
|
111
|
+
|
|
112
|
+
**Comments and descriptions**: pass plain text — `add_comment`, `create_issue`,
|
|
113
|
+
`create_epic`, `update_issue` and `add_worklog` convert it to ADF automatically
|
|
114
|
+
on Jira Cloud (v3). Blank lines separate paragraphs; Markdown is not rendered.
|
|
115
|
+
No need to hand-build an ADF document or fall back to `jira_api`.
|
|
99
116
|
|
|
100
117
|
**Move work through the workflow**: `get_issue_transitions(issueKey)` to see
|
|
101
118
|
available transitions and required fields, then `transition_issue`.
|
|
@@ -120,6 +137,14 @@ available transitions and required fields, then `transition_issue`.
|
|
|
120
137
|
On Data Center it still uses `startAt`/`total`.
|
|
121
138
|
- Assignees: pass an account ID on Cloud, a username on DC — the server maps
|
|
122
139
|
it to the right field shape automatically.
|
|
140
|
+
- `get_user` uses `/user?accountId=...` on Cloud/v3 and `/user?username=...` on
|
|
141
|
+
Data Center/v2. `create_issue` maps its `dueDate` input to Jira's `duedate`
|
|
142
|
+
field, and `add_worklog` accepts an optional `started` timestamp (Jira
|
|
143
|
+
defaults it to now when omitted).
|
|
144
|
+
- Data Center exposes REST API v2/latest, not `/rest/api/3`; do not force
|
|
145
|
+
`JIRA_API_VERSION=3` for a DC host. The default deployment detection routes
|
|
146
|
+
Cloud (`*.atlassian.net`) and DC endpoints separately, while ADF and user
|
|
147
|
+
field shapes follow the selected API version.
|
|
123
148
|
- To remove an issue from an epic: `update_issue` with
|
|
124
149
|
`fields: { "epic": null }`.
|
|
125
150
|
- Anything not covered by a dedicated tool → `jira_api` with method, path
|