@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/README.md +56 -11
- package/dist/adf.js +71 -0
- package/dist/agents.js +412 -0
- package/dist/client.js +6 -1
- package/dist/config.js +1 -1
- package/dist/index.js +54 -3
- package/dist/install.js +47 -381
- package/dist/prompt.js +139 -0
- package/dist/status.js +81 -0
- 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/dist/uninstall.js +140 -0
- package/dist/update.js +71 -0
- package/package.json +1 -1
- package/skill/SKILL.md +53 -5
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
|
+
}
|
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
|
}));
|