@thammarongg/jira-mcp 0.1.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 +205 -0
- package/dist/client.js +102 -0
- package/dist/config.js +38 -0
- package/dist/index.js +38 -0
- package/dist/install.js +302 -0
- package/dist/tools/boards.js +22 -0
- package/dist/tools/epics.js +61 -0
- package/dist/tools/generic.js +18 -0
- package/dist/tools/issues.js +188 -0
- package/dist/tools/meta.js +13 -0
- package/dist/tools/projects.js +59 -0
- package/dist/tools/sprints.js +106 -0
- package/dist/tools/users.js +36 -0
- package/dist/util.js +24 -0
- package/package.json +51 -0
- package/skill/SKILL.md +102 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { run } from "../util.js";
|
|
3
|
+
export function registerBoardTools(server, client) {
|
|
4
|
+
server.registerTool("list_boards", {
|
|
5
|
+
title: "List Jira boards",
|
|
6
|
+
description: "List Jira agile boards (scrum or kanban), optionally filtered by type, name, or project keys.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: z.enum(["scrum", "kanban"]).optional().describe("Board type filter"),
|
|
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'"),
|
|
11
|
+
maxResults: z.number().int().min(1).max(1000).default(50),
|
|
12
|
+
startAt: z.number().int().min(0).default(0),
|
|
13
|
+
},
|
|
14
|
+
}, async ({ type, name, projectKeys, maxResults, startAt }) => run(() => client.agileGet("/board", { type, name, projectKeys, maxResults, startAt })));
|
|
15
|
+
server.registerTool("get_board", {
|
|
16
|
+
title: "Get board",
|
|
17
|
+
description: "Get a single Jira board by ID, including its projects, location, and settings.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
boardId: z.number().int().describe("Board ID (numeric, from list_boards)"),
|
|
20
|
+
},
|
|
21
|
+
}, async ({ boardId }) => run(() => client.agileGet(`/board/${boardId}`)));
|
|
22
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rapidViewId, run } from "../util.js";
|
|
3
|
+
export function registerEpicTools(server, client) {
|
|
4
|
+
server.registerTool("list_epics", {
|
|
5
|
+
title: "List epics",
|
|
6
|
+
description: "List epics visible in a board's sprint view (agile rapid view).",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
boardId: z.number().int().describe("Board ID"),
|
|
9
|
+
sprintId: z.number().int().describe("Sprint ID (any sprint on the board works)"),
|
|
10
|
+
maxResults: z.number().int().min(1).max(1000).default(100),
|
|
11
|
+
startAt: z.number().int().min(0).default(0),
|
|
12
|
+
},
|
|
13
|
+
}, async ({ boardId, sprintId, maxResults, startAt }) => run(() => client.agileGet(`/rapid/${rapidViewId(boardId, sprintId)}/epic`, { maxResults, startAt })));
|
|
14
|
+
server.registerTool("get_epic", {
|
|
15
|
+
title: "Get epic",
|
|
16
|
+
description: "Get a single epic by its issue ID (numeric).",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
epicId: z.number().int().describe("Epic issue ID (numeric, not the key)"),
|
|
19
|
+
},
|
|
20
|
+
}, async ({ epicId }) => run(() => client.agileGet(`/epic/${epicId}`)));
|
|
21
|
+
server.registerTool("get_epic_issues", {
|
|
22
|
+
title: "Get epic issues",
|
|
23
|
+
description: "List the issues belonging to an epic.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
epicId: z.number().int().describe("Epic issue ID (numeric)"),
|
|
26
|
+
maxResults: z.number().int().min(1).max(1000).default(100),
|
|
27
|
+
startAt: z.number().int().min(0).default(0),
|
|
28
|
+
},
|
|
29
|
+
}, async ({ epicId, maxResults, startAt }) => run(() => client.agileGet(`/epic/${epicId}/issue`, { maxResults, startAt })));
|
|
30
|
+
server.registerTool("create_epic", {
|
|
31
|
+
title: "Create epic",
|
|
32
|
+
description: "Create a new epic on a board (agile rapid view).",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
boardId: z.number().int().describe("Board ID"),
|
|
35
|
+
sprintId: z.number().int().describe("Sprint ID (any sprint on the board works)"),
|
|
36
|
+
name: z.string().describe("Epic name"),
|
|
37
|
+
description: z.string().optional(),
|
|
38
|
+
lead: z.string().optional().describe("Epic lead (user account ID or username)"),
|
|
39
|
+
},
|
|
40
|
+
}, async ({ boardId, sprintId, name, description, lead }) => run(() => client.agilePost(`/rapid/${rapidViewId(boardId, sprintId)}/epic`, {
|
|
41
|
+
name,
|
|
42
|
+
description,
|
|
43
|
+
lead,
|
|
44
|
+
})));
|
|
45
|
+
server.registerTool("move_issue_to_epic", {
|
|
46
|
+
title: "Move issue to epic",
|
|
47
|
+
description: 'Add an issue to an epic. To remove an issue from an epic, use `update_issue` with fields `{ "epic": null }`.',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
epicId: z.number().int().describe("Epic issue ID (numeric)"),
|
|
50
|
+
issueId: z.number().int().describe("Issue ID to move (numeric)"),
|
|
51
|
+
epicKey: z.string().describe("Epic key, e.g. PROJ-1"),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ epicId, issueId, epicKey }) => run(() => client.agilePut(`/epic/${epicId}/issue/${issueId}`, { epic: { key: epicKey } })));
|
|
54
|
+
server.registerTool("get_epic_meta", {
|
|
55
|
+
title: "Get epic meta",
|
|
56
|
+
description: "Get epic metadata (available epic issue types) for a project.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
projectKey: z.string().optional().describe("Project key filter"),
|
|
59
|
+
},
|
|
60
|
+
}, async ({ projectKey }) => run(() => client.agileGet("/epic/meta", { projectKey })));
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { run } from "../util.js";
|
|
3
|
+
export function registerGenericTools(server, client) {
|
|
4
|
+
server.registerTool("jira_api", {
|
|
5
|
+
title: "Raw Jira REST API call",
|
|
6
|
+
description: "Call any Jira REST API endpoint not covered by a dedicated tool. Path must start with /rest/ and stay under it (e.g. '/rest/api/3/issue/PROJ-1/names' or '/rest/agile/1.0/board/1'). Query params go in query, JSON payload in body. Returns the raw JSON response.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE"]).describe("HTTP method"),
|
|
9
|
+
path: z
|
|
10
|
+
.string()
|
|
11
|
+
.startsWith("/rest/")
|
|
12
|
+
.refine((p) => !p.includes("?") && !p.includes("#"), "path must not contain ? or #; put query params in query")
|
|
13
|
+
.describe("API path starting with /rest/, e.g. /rest/api/3/issue/PROJ-1"),
|
|
14
|
+
query: z.record(z.string(), z.unknown()).optional().describe("Query string parameters"),
|
|
15
|
+
body: z.unknown().optional().describe("JSON request body (POST/PUT)"),
|
|
16
|
+
},
|
|
17
|
+
}, async ({ method, path, query, body }) => run(() => client.request(method, path, { query, body })));
|
|
18
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { run } from "../util.js";
|
|
3
|
+
const DEFAULT_ISSUE_FIELDS = "summary,description,status,assignee,reporter,issuetype,labels,components,priority,created,updated,due,project";
|
|
4
|
+
export function registerIssueTools(server, client) {
|
|
5
|
+
server.registerTool("get_issue", {
|
|
6
|
+
title: "Get issue",
|
|
7
|
+
description: "Get a single Jira issue by key (e.g. PROJ-123).",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
10
|
+
fields: z.string().optional().describe(`Comma-separated fields (default: ${DEFAULT_ISSUE_FIELDS})`),
|
|
11
|
+
expand: z.string().optional().describe("Comma-separated expansions, e.g. renderedFields,versions,changelog"),
|
|
12
|
+
},
|
|
13
|
+
}, async ({ issueKey, fields, expand }) => run(() => client.apiGet(`/issue/${encodeURIComponent(issueKey)}`, { fields: fields ?? DEFAULT_ISSUE_FIELDS, expand })));
|
|
14
|
+
server.registerTool("create_issue", {
|
|
15
|
+
title: "Create issue",
|
|
16
|
+
description: "Create a new issue. Use get_issue_create_meta to discover valid projects, issue types, and required fields. Extra custom fields can be passed via customFields (e.g. { 'customfield_10010': 'value' }).",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
projectKey: z.string().describe("Project key, e.g. PROJ"),
|
|
19
|
+
summary: z.string().describe("Issue summary"),
|
|
20
|
+
issueType: z.string().describe("Issue type name or ID, e.g. Story, Task, Bug, or 10004"),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
assignee: z.string().optional().describe("Account ID on Cloud/v3, username on DC/v2"),
|
|
23
|
+
reporter: z.string().optional().describe("Account ID on Cloud/v3, username on DC/v2"),
|
|
24
|
+
labels: z.array(z.string()).optional(),
|
|
25
|
+
components: z.array(z.string()).optional().describe("Component names"),
|
|
26
|
+
dueDate: z.string().optional().describe("Due date, e.g. 2026-09-30"),
|
|
27
|
+
priority: z.string().optional().describe("Priority name, e.g. High"),
|
|
28
|
+
parent: z.string().optional().describe("Parent issue key (for sub-tasks)"),
|
|
29
|
+
customFields: z.record(z.string(), z.unknown()).optional().describe("Custom field IDs mapped to values"),
|
|
30
|
+
},
|
|
31
|
+
}, async ({ projectKey, summary, issueType, description, assignee, reporter, labels, components, dueDate, priority, parent, customFields, }) => run(() => {
|
|
32
|
+
const userRef = (value) => client.apiVersion === "3" ? { accountId: value } : { name: value };
|
|
33
|
+
return client.apiPost("/issue", {
|
|
34
|
+
fields: {
|
|
35
|
+
project: { key: projectKey },
|
|
36
|
+
summary,
|
|
37
|
+
issuetype: /^\d+$/.test(issueType) ? { id: issueType } : { name: issueType },
|
|
38
|
+
description,
|
|
39
|
+
assignee: assignee ? userRef(assignee) : undefined,
|
|
40
|
+
reporter: reporter ? userRef(reporter) : undefined,
|
|
41
|
+
labels,
|
|
42
|
+
components: components?.map((name) => ({ name })),
|
|
43
|
+
dueDate,
|
|
44
|
+
priority: priority ? { name: priority } : undefined,
|
|
45
|
+
parent: parent ? { key: parent } : undefined,
|
|
46
|
+
...customFields,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}));
|
|
50
|
+
server.registerTool("update_issue", {
|
|
51
|
+
title: "Update issue",
|
|
52
|
+
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' }] }).",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
55
|
+
fields: z.record(z.string(), z.unknown()).optional().describe("Field values to set"),
|
|
56
|
+
update: z.record(z.string(), z.unknown()).optional().describe("Relative updates (add/remove operations)"),
|
|
57
|
+
},
|
|
58
|
+
}, async ({ issueKey, fields, update }) => run(() => client.apiPut(`/issue/${encodeURIComponent(issueKey)}`, { fields, update })));
|
|
59
|
+
server.registerTool("delete_issue", {
|
|
60
|
+
title: "Delete issue",
|
|
61
|
+
description: "Delete an issue. This is destructive.",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
64
|
+
},
|
|
65
|
+
}, async ({ issueKey }) => run(() => client.apiDelete(`/issue/${encodeURIComponent(issueKey)}`)));
|
|
66
|
+
server.registerTool("search_issues", {
|
|
67
|
+
title: "Search issues (JQL)",
|
|
68
|
+
description: "Search issues with JQL, e.g. 'project = PROJ AND sprint = 42 ORDER BY rank' or 'project = PROJ AND sprint IS NONE'. Returns issues plus total count and pagination info.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
jql: z.string().describe("JQL query string"),
|
|
71
|
+
fields: z.string().optional().describe(`Comma-separated fields (default: ${DEFAULT_ISSUE_FIELDS})`),
|
|
72
|
+
maxResults: z.number().int().min(1).max(100).default(25),
|
|
73
|
+
startAt: z.number().int().min(0).default(0),
|
|
74
|
+
expand: z.string().optional().describe("e.g. names, uris"),
|
|
75
|
+
},
|
|
76
|
+
}, async ({ jql, fields, maxResults, startAt, expand }) => run(() => client.apiGet("/search", {
|
|
77
|
+
jql,
|
|
78
|
+
fields: fields ?? DEFAULT_ISSUE_FIELDS,
|
|
79
|
+
maxResults,
|
|
80
|
+
startAt,
|
|
81
|
+
expand,
|
|
82
|
+
})));
|
|
83
|
+
server.registerTool("get_issue_create_meta", {
|
|
84
|
+
title: "Get issue create metadata",
|
|
85
|
+
description: "Discover valid projects, issue types, and their fields (with required flags) before creating an issue.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
projectKeys: z.string().optional().describe("Comma-separated project keys to filter"),
|
|
88
|
+
issuetypeIds: z.string().optional().describe("Comma-separated issue type IDs to filter"),
|
|
89
|
+
expand: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Use 'projects.issuetypes.fields' for full field details (default)"),
|
|
93
|
+
},
|
|
94
|
+
}, async ({ projectKeys, issuetypeIds, expand }) => run(() => client.apiGet("/issue/createmeta", {
|
|
95
|
+
projectKeys,
|
|
96
|
+
issuetypeIds,
|
|
97
|
+
expand: expand ?? "projects.issuetypes.fields",
|
|
98
|
+
})));
|
|
99
|
+
server.registerTool("get_issue_transitions", {
|
|
100
|
+
title: "Get issue transitions",
|
|
101
|
+
description: "List the workflow transitions available for an issue.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
104
|
+
},
|
|
105
|
+
}, async ({ issueKey }) => run(() => client.apiGet(`/issue/${encodeURIComponent(issueKey)}/transitions`)));
|
|
106
|
+
server.registerTool("transition_issue", {
|
|
107
|
+
title: "Transition issue",
|
|
108
|
+
description: "Move an issue through its workflow. Use get_issue_transitions first to find the transition ID and required fields.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
111
|
+
transitionId: z.string().describe("Transition ID (string) from get_issue_transitions"),
|
|
112
|
+
fields: z.record(z.string(), z.unknown()).optional().describe("Fields required by the transition"),
|
|
113
|
+
},
|
|
114
|
+
}, async ({ issueKey, transitionId, fields }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/transitions`, {
|
|
115
|
+
transition: { id: transitionId },
|
|
116
|
+
fields,
|
|
117
|
+
})));
|
|
118
|
+
server.registerTool("assign_issue", {
|
|
119
|
+
title: "Assign issue",
|
|
120
|
+
description: "Assign (or unassign, with an empty value) an issue to a user.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
123
|
+
accountId: z.string().optional().describe("Account ID (Jira Cloud)"),
|
|
124
|
+
name: z.string().optional().describe("Username (Data Center) or name"),
|
|
125
|
+
},
|
|
126
|
+
}, async ({ issueKey, accountId, name }) => run(() => {
|
|
127
|
+
const body = {};
|
|
128
|
+
if (accountId)
|
|
129
|
+
body.accountId = accountId;
|
|
130
|
+
else if (name)
|
|
131
|
+
body.name = name;
|
|
132
|
+
else if (client.apiVersion === "3")
|
|
133
|
+
body.accountId = null;
|
|
134
|
+
else
|
|
135
|
+
body.name = null;
|
|
136
|
+
return client.apiPost(`/issue/${encodeURIComponent(issueKey)}/assignee`, body);
|
|
137
|
+
}));
|
|
138
|
+
server.registerTool("add_comment", {
|
|
139
|
+
title: "Add comment",
|
|
140
|
+
description: "Add a comment to an issue.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
143
|
+
body: z.string().describe("Comment text"),
|
|
144
|
+
visibility: z
|
|
145
|
+
.object({ type: z.string(), value: z.string() })
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("Comment visibility restriction"),
|
|
148
|
+
},
|
|
149
|
+
}, async ({ issueKey, body, visibility }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/comment`, { body, visibility })));
|
|
150
|
+
server.registerTool("list_comments", {
|
|
151
|
+
title: "List issue comments",
|
|
152
|
+
description: "List comments on an issue.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
155
|
+
maxResults: z.number().int().min(1).max(1000).default(50),
|
|
156
|
+
startAt: z.number().int().min(0).default(0),
|
|
157
|
+
orderBy: z.string().optional().describe("'created' or '-created'"),
|
|
158
|
+
},
|
|
159
|
+
}, async ({ issueKey, maxResults, startAt, orderBy }) => run(() => client.apiGet(`/issue/${encodeURIComponent(issueKey)}/comment`, { maxResults, startAt, orderBy })));
|
|
160
|
+
server.registerTool("delete_comment", {
|
|
161
|
+
title: "Delete comment",
|
|
162
|
+
description: "Delete a comment by its ID (from list_comments).",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
165
|
+
commentId: z.string().describe("Comment ID"),
|
|
166
|
+
},
|
|
167
|
+
}, async ({ issueKey, commentId }) => run(() => client.apiDelete(`/issue/${encodeURIComponent(issueKey)}/comment/${encodeURIComponent(commentId)}`)));
|
|
168
|
+
server.registerTool("get_issue_worklogs", {
|
|
169
|
+
title: "Get issue worklogs",
|
|
170
|
+
description: "List time-tracking worklogs on an issue.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
173
|
+
maxResults: z.number().int().min(1).max(1000).default(50),
|
|
174
|
+
startAt: z.number().int().min(0).default(0),
|
|
175
|
+
orderBy: z.string().optional().describe("'started' or '-started'"),
|
|
176
|
+
},
|
|
177
|
+
}, async ({ issueKey, maxResults, startAt, orderBy }) => run(() => client.apiGet(`/issue/${encodeURIComponent(issueKey)}/worklog`, { maxResults, startAt, orderBy })));
|
|
178
|
+
server.registerTool("add_worklog", {
|
|
179
|
+
title: "Add worklog",
|
|
180
|
+
description: "Add a worklog (time spent) to an issue. timeSpent uses Jira format, e.g. '2h 30m' or '1d'.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
issueKey: z.string().describe("Issue key, e.g. PROJ-123"),
|
|
183
|
+
timeSpent: z.string().describe("Time spent, e.g. '1h 30m'"),
|
|
184
|
+
comment: z.string().optional(),
|
|
185
|
+
started: z.string().optional().describe("Start timestamp (ISO-8601)"),
|
|
186
|
+
},
|
|
187
|
+
}, async ({ issueKey, timeSpent, comment, started }) => run(() => client.apiPost(`/issue/${encodeURIComponent(issueKey)}/worklog`, { timeSpent, comment, started })));
|
|
188
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { run } from "../util.js";
|
|
2
|
+
export function registerMetaTools(server, client) {
|
|
3
|
+
server.registerTool("get_fields", {
|
|
4
|
+
title: "Get all fields",
|
|
5
|
+
description: "List all issue fields (including custom field IDs) in the Jira instance.",
|
|
6
|
+
inputSchema: {},
|
|
7
|
+
}, async () => run(() => client.apiGet("/field")));
|
|
8
|
+
server.registerTool("get_issue_types", {
|
|
9
|
+
title: "Get issue types",
|
|
10
|
+
description: "List all issue types (Story, Task, Bug, Epic, ...) in the Jira instance.",
|
|
11
|
+
inputSchema: {},
|
|
12
|
+
}, async () => run(() => client.apiGet("/issuetype")));
|
|
13
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { run } from "../util.js";
|
|
3
|
+
export function registerProjectTools(server, client) {
|
|
4
|
+
server.registerTool("list_projects", {
|
|
5
|
+
title: "List projects",
|
|
6
|
+
description: "List all Jira projects visible to the current user.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
maxResults: z.number().int().min(1).max(1000).default(100),
|
|
9
|
+
startAt: z.number().int().min(0).default(0),
|
|
10
|
+
},
|
|
11
|
+
}, async ({ maxResults, startAt }) => run(() => client.apiGet("/project", { maxResults, startAt })));
|
|
12
|
+
server.registerTool("get_project", {
|
|
13
|
+
title: "Get project",
|
|
14
|
+
description: "Get a single project by key or ID.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
projectKeyOrId: z.string().describe("Project key (e.g. PROJ) or numeric ID"),
|
|
17
|
+
expand: z.string().optional().describe("e.g. lead,urls,insight"),
|
|
18
|
+
},
|
|
19
|
+
}, async ({ projectKeyOrId, expand }) => run(() => client.apiGet(`/project/${encodeURIComponent(projectKeyOrId)}`, { expand })));
|
|
20
|
+
server.registerTool("get_project_components", {
|
|
21
|
+
title: "Get project components",
|
|
22
|
+
description: "List components of a project.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
projectKey: z.string().describe("Project key"),
|
|
25
|
+
},
|
|
26
|
+
}, async ({ projectKey }) => run(() => client.apiGet(`/project/${encodeURIComponent(projectKey)}/components`)));
|
|
27
|
+
server.registerTool("create_project_component", {
|
|
28
|
+
title: "Create project component",
|
|
29
|
+
description: "Create a component in a project.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
projectKey: z.string().describe("Project key"),
|
|
32
|
+
name: z.string().describe("Component name"),
|
|
33
|
+
description: z.string().optional(),
|
|
34
|
+
},
|
|
35
|
+
}, async ({ projectKey, name, description }) => run(() => client.apiPost(`/project/${encodeURIComponent(projectKey)}/components`, { name, description })));
|
|
36
|
+
server.registerTool("get_project_issue_types", {
|
|
37
|
+
title: "Get project issue types",
|
|
38
|
+
description: "List issue types available in a project.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
projectKey: z.string().describe("Project key"),
|
|
41
|
+
},
|
|
42
|
+
}, async ({ projectKey }) => run(() => client.apiGet(`/project/${encodeURIComponent(projectKey)}/issuetypes`)));
|
|
43
|
+
server.registerTool("get_project_roles", {
|
|
44
|
+
title: "Get project roles",
|
|
45
|
+
description: "List roles (and their actors) of a project.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
projectKey: z.string().describe("Project key"),
|
|
48
|
+
},
|
|
49
|
+
}, async ({ projectKey }) => run(() => client.apiGet(`/project/${encodeURIComponent(projectKey)}/role`)));
|
|
50
|
+
server.registerTool("get_project_versions", {
|
|
51
|
+
title: "Get project versions",
|
|
52
|
+
description: "List versions (releases) of a project.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
projectKey: z.string().describe("Project key"),
|
|
55
|
+
maxResults: z.number().int().min(1).max(1000).default(100),
|
|
56
|
+
startAt: z.number().int().min(0).default(0),
|
|
57
|
+
},
|
|
58
|
+
}, async ({ projectKey, maxResults, startAt }) => run(() => client.apiGet(`/project/${encodeURIComponent(projectKey)}/version`, { maxResults, startAt })));
|
|
59
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rapidViewId, run } from "../util.js";
|
|
3
|
+
const sprintState = z.enum(["active", "closed", "future"]).describe("Sprint state filter");
|
|
4
|
+
export function registerSprintTools(server, client) {
|
|
5
|
+
server.registerTool("list_sprints", {
|
|
6
|
+
title: "List sprints for a board",
|
|
7
|
+
description: "List sprints on a board, optionally filtered by state (active, closed, future).",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
boardId: z.number().int().describe("Board ID"),
|
|
10
|
+
state: sprintState.optional(),
|
|
11
|
+
maxResults: z.number().int().min(1).max(1000).default(50),
|
|
12
|
+
startAt: z.number().int().min(0).default(0),
|
|
13
|
+
},
|
|
14
|
+
}, async ({ boardId, state, maxResults, startAt }) => run(() => client.agileGet(`/board/${boardId}/sprint`, { state, maxResults, startAt })));
|
|
15
|
+
server.registerTool("get_sprint", {
|
|
16
|
+
title: "Get sprint",
|
|
17
|
+
description: "Get a single sprint by ID (name, state, start/end dates, goal).",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
sprintId: z.number().int().describe("Sprint ID"),
|
|
20
|
+
},
|
|
21
|
+
}, async ({ sprintId }) => run(() => client.agileGet(`/sprint/${sprintId}`)));
|
|
22
|
+
server.registerTool("create_sprint", {
|
|
23
|
+
title: "Create sprint",
|
|
24
|
+
description: "Create a new sprint on a board. Dates are ISO-8601 strings, e.g. 2026-09-01T09:00:00.000+0000.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
boardId: z.number().int().describe("Board ID"),
|
|
27
|
+
name: z.string().describe("Sprint name"),
|
|
28
|
+
startDate: z.string().optional().describe("Start date (ISO-8601)"),
|
|
29
|
+
endDate: z.string().optional().describe("End date (ISO-8601)"),
|
|
30
|
+
goal: z.string().optional().describe("Sprint goal"),
|
|
31
|
+
},
|
|
32
|
+
}, async ({ boardId, name, startDate, endDate, goal }) => run(() => client.agilePost(`/board/${boardId}/sprint`, { name, startDate, endDate, goal })));
|
|
33
|
+
server.registerTool("update_sprint", {
|
|
34
|
+
title: "Update sprint",
|
|
35
|
+
description: "Update a sprint's name, goal, dates, or state (active/closed/future). Only provided fields are changed.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
sprintId: z.number().int().describe("Sprint ID"),
|
|
38
|
+
name: z.string().optional(),
|
|
39
|
+
goal: z.string().optional(),
|
|
40
|
+
startDate: z.string().optional().describe("Start date (ISO-8601)"),
|
|
41
|
+
endDate: z.string().optional().describe("End date (ISO-8601)"),
|
|
42
|
+
state: sprintState.optional(),
|
|
43
|
+
},
|
|
44
|
+
}, async ({ sprintId, name, goal, startDate, endDate, state }) => run(() => client.agilePut(`/sprint/${sprintId}`, { name, goal, startDate, endDate, state })));
|
|
45
|
+
server.registerTool("close_sprint", {
|
|
46
|
+
title: "Close sprint",
|
|
47
|
+
description: "Close a sprint (sets its state to closed).",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
sprintId: z.number().int().describe("Sprint ID"),
|
|
50
|
+
},
|
|
51
|
+
}, async ({ sprintId }) => run(() => client.agilePut(`/sprint/${sprintId}`, { state: "closed" })));
|
|
52
|
+
server.registerTool("get_sprint_issues", {
|
|
53
|
+
title: "Get sprint issues",
|
|
54
|
+
description: "List the issues in a sprint.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
sprintId: z.number().int().describe("Sprint ID"),
|
|
57
|
+
fields: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Comma-separated issue fields to return (default: key, summary, status, assignee)"),
|
|
61
|
+
maxResults: z.number().int().min(1).max(1000).default(100),
|
|
62
|
+
startAt: z.number().int().min(0).default(0),
|
|
63
|
+
},
|
|
64
|
+
}, async ({ sprintId, fields, maxResults, startAt }) => run(() => client.agileGet(`/sprint/${sprintId}/issue`, {
|
|
65
|
+
fields: fields ?? "key,summary,status,assignee",
|
|
66
|
+
maxResults,
|
|
67
|
+
startAt,
|
|
68
|
+
})));
|
|
69
|
+
server.registerTool("get_sprint_view", {
|
|
70
|
+
title: "Get sprint view",
|
|
71
|
+
description: "Full sprint view like the Jira UI: board info, current sprint, and all its issues. Uses the agile rapid view API (rapidViewId = boardId * 10^13 + sprintId).",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
boardId: z.number().int().describe("Board ID"),
|
|
74
|
+
sprintId: z.number().int().describe("Sprint ID"),
|
|
75
|
+
fields: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("Comma-separated issue fields to return (default: key, summary, status, assignee)"),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ boardId, sprintId, fields }) => run(() => client.agileGet(`/rapid/${rapidViewId(boardId, sprintId)}`, {
|
|
81
|
+
fields: fields ?? "key,summary,status,assignee",
|
|
82
|
+
})));
|
|
83
|
+
server.registerTool("get_backlog", {
|
|
84
|
+
title: "Get board backlog",
|
|
85
|
+
description: "List backlog issues for a board (issues in the board's projects with no sprint), ordered by rank. Implemented via JQL: project in (<board projects>) AND sprint IS NONE ORDER BY rank.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
boardId: z.number().int().describe("Board ID"),
|
|
88
|
+
fields: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Comma-separated issue fields to return (default: key, summary, status, assignee)"),
|
|
92
|
+
maxResults: z.number().int().min(1).max(100).default(50),
|
|
93
|
+
startAt: z.number().int().min(0).default(0),
|
|
94
|
+
},
|
|
95
|
+
}, async ({ boardId, fields, maxResults, startAt }) => run(async () => {
|
|
96
|
+
const board = (await client.agileGet(`/board/${boardId}`));
|
|
97
|
+
const keys = (board.projects ?? []).map((p) => p.key);
|
|
98
|
+
const jql = keys.length > 0 ? `project in (${keys.join(", ")}) AND sprint IS NONE ORDER BY rank` : "sprint IS NONE ORDER BY rank";
|
|
99
|
+
return client.apiGet("/search", {
|
|
100
|
+
jql,
|
|
101
|
+
fields: fields ?? "key,summary,status,assignee",
|
|
102
|
+
maxResults,
|
|
103
|
+
startAt,
|
|
104
|
+
});
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { run } from "../util.js";
|
|
3
|
+
export function registerUserTools(server, client) {
|
|
4
|
+
server.registerTool("get_current_user", {
|
|
5
|
+
title: "Get current user",
|
|
6
|
+
description: "Get the profile of the user whose credentials are configured (also verifies auth works).",
|
|
7
|
+
inputSchema: {},
|
|
8
|
+
}, async () => run(() => client.apiGet("/myself")));
|
|
9
|
+
server.registerTool("find_users", {
|
|
10
|
+
title: "Find users",
|
|
11
|
+
description: "Search for users by name/email (Cloud) or username (Data Center). Returns account IDs / usernames usable for assignment.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
query: z.string().describe("Search text (name, email, or username)"),
|
|
14
|
+
maxResults: z.number().int().min(1).max(1000).default(25),
|
|
15
|
+
},
|
|
16
|
+
}, async ({ query, maxResults }) => run(() => client.apiGet("/user/search", client.apiVersion === "3" ? { query, maxResults } : { username: query, maxResults })));
|
|
17
|
+
server.registerTool("get_user", {
|
|
18
|
+
title: "Get user",
|
|
19
|
+
description: "Get a user by account ID (Jira Cloud / API v3) or username (Data Center / API v2).",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
accountId: z.string().optional().describe("Account ID (Jira Cloud / API v3)"),
|
|
22
|
+
username: z.string().optional().describe("Username (Data Center / API v2)"),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ accountId, username }) => run(() => {
|
|
25
|
+
if (!accountId && !username) {
|
|
26
|
+
throw new Error("Provide at least one of accountId or username.");
|
|
27
|
+
}
|
|
28
|
+
if (client.apiVersion === "3") {
|
|
29
|
+
if (!accountId) {
|
|
30
|
+
throw new Error("API v3 requires accountId for get_user. Use find_users to look up the account ID for a username.");
|
|
31
|
+
}
|
|
32
|
+
return client.apiGet(`/user/${encodeURIComponent(accountId)}`);
|
|
33
|
+
}
|
|
34
|
+
return client.apiGet("/user", { username: username ?? accountId });
|
|
35
|
+
}));
|
|
36
|
+
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { JiraApiError } from "./client.js";
|
|
2
|
+
export function jsonResult(data) {
|
|
3
|
+
const text = typeof data === "string" ? data : data === null ? "OK (no body)" : JSON.stringify(data, null, 2);
|
|
4
|
+
return { content: [{ type: "text", text }] };
|
|
5
|
+
}
|
|
6
|
+
export async function run(fn) {
|
|
7
|
+
try {
|
|
8
|
+
return jsonResult(await fn());
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
const message = err instanceof JiraApiError
|
|
12
|
+
? err.message
|
|
13
|
+
: err instanceof Error
|
|
14
|
+
? `${err.name}: ${err.message}`
|
|
15
|
+
: String(err);
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function rapidViewId(boardId, sprintId) {
|
|
23
|
+
return boardId * 10_000_000_000_000 + sprintId;
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thammarongg/jira-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing the Jira REST API (boards, sprints, issues, JQL, and a generic passthrough) for Jira Cloud and Data Center",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jira-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"skill"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"jira",
|
|
18
|
+
"atlassian",
|
|
19
|
+
"jira-cloud",
|
|
20
|
+
"jira-data-center",
|
|
21
|
+
"sprint",
|
|
22
|
+
"board",
|
|
23
|
+
"jql",
|
|
24
|
+
"ai",
|
|
25
|
+
"claude"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"start": "node dist/index.js",
|
|
31
|
+
"dev": "tsx src/index.ts",
|
|
32
|
+
"smoke": "node scripts/smoke.mjs",
|
|
33
|
+
"prepublishOnly": "npm run build && npm run smoke"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
|
+
"zod": "^4.4.3"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.4.0",
|
|
48
|
+
"tsx": "^4.23.12",
|
|
49
|
+
"typescript": "^7.0.2"
|
|
50
|
+
}
|
|
51
|
+
}
|