@stubbedev/atlassian-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/jira.js ADDED
@@ -0,0 +1,189 @@
1
+ function text(t) {
2
+ return { content: [{ type: 'text', text: t }] };
3
+ }
4
+ function pagination(total, startAt, count) {
5
+ const end = startAt + count;
6
+ return total > end ? ` (showing ${startAt + 1}–${end} of ${total}, use startAt=${end} for next page)` : '';
7
+ }
8
+ function buildJQL(args) {
9
+ if (args.jql)
10
+ return args.jql;
11
+ const clauses = [];
12
+ if (args.query)
13
+ clauses.push(`text ~ ${JSON.stringify(args.query)}`);
14
+ if (args.project)
15
+ clauses.push(`project = "${args.project}"`);
16
+ if (args.status)
17
+ clauses.push(`status = "${args.status}"`);
18
+ if (args.assignee)
19
+ clauses.push(`assignee = "${args.assignee}"`);
20
+ if (args.issueType)
21
+ clauses.push(`issuetype = "${args.issueType}"`);
22
+ if (clauses.length === 0)
23
+ throw new Error('Provide at least one of: query, jql, project, status, assignee, issueType');
24
+ return clauses.join(' AND ') + ' ORDER BY updated DESC';
25
+ }
26
+ export class JiraClient {
27
+ baseUrl;
28
+ headers;
29
+ constructor(baseUrl, token) {
30
+ this.baseUrl = baseUrl.replace(/\/$/, '');
31
+ this.headers = {
32
+ Authorization: `Bearer ${token}`,
33
+ 'Content-Type': 'application/json',
34
+ Accept: 'application/json',
35
+ };
36
+ }
37
+ async request(method, path, body) {
38
+ const url = `${this.baseUrl}/rest/api/2${path}`;
39
+ const opts = { method, headers: this.headers };
40
+ if (body !== undefined)
41
+ opts.body = JSON.stringify(body);
42
+ const res = await fetch(url, opts);
43
+ if (!res.ok) {
44
+ const errText = await res.text();
45
+ throw new Error(`Jira ${res.status} ${method} ${path}: ${errText}`);
46
+ }
47
+ return res.status === 204 ? null : res.json();
48
+ }
49
+ async searchIssues(args) {
50
+ const { maxResults = 20, startAt = 0 } = args;
51
+ const jql = buildJQL(args);
52
+ const params = new URLSearchParams({
53
+ jql,
54
+ maxResults: String(maxResults),
55
+ startAt: String(startAt),
56
+ fields: 'summary,status,assignee,priority,issuetype',
57
+ });
58
+ const data = await this.request('GET', `/search?${params}`);
59
+ if (!data)
60
+ return text('No results.');
61
+ const lines = data.issues.map((i, idx) => {
62
+ const assignee = i.fields.assignee?.displayName ?? 'Unassigned';
63
+ return `${startAt + idx + 1}. [${i.key}] ${i.fields.summary} | ${i.fields.status.name} | ${assignee}`;
64
+ });
65
+ const page = pagination(data.total, startAt, data.issues.length);
66
+ return text(`Found ${data.total} issues${page}:\n${lines.join('\n')}`);
67
+ }
68
+ async myIssues(args) {
69
+ return this.searchIssues({
70
+ jql: 'assignee = currentUser() ORDER BY updated DESC',
71
+ maxResults: args.maxResults,
72
+ startAt: args.startAt,
73
+ });
74
+ }
75
+ async getProjects(args) {
76
+ const limit = args.maxResults ?? 50;
77
+ const data = await this.request('GET', `/project?maxResults=${limit}`);
78
+ if (!data || data.length === 0)
79
+ return text('No projects found.');
80
+ const lines = data.map((p, i) => `${i + 1}. [${p.key}] ${p.name} (${p.projectTypeKey})`);
81
+ return text(`${data.length} project(s):\n${lines.join('\n')}`);
82
+ }
83
+ async getIssueTypes(args) {
84
+ const { projectKey } = args;
85
+ const data = await this.request('GET', `/project/${projectKey}/statuses`);
86
+ if (!data || data.length === 0)
87
+ return text('No issue types found.');
88
+ const lines = data.map((t) => {
89
+ const statuses = t.statuses.map((s) => s.name).join(', ');
90
+ return `${t.name}: ${statuses}`;
91
+ });
92
+ return text(`Issue types and statuses for ${projectKey}:\n${lines.join('\n')}`);
93
+ }
94
+ async searchUsers(args) {
95
+ const params = new URLSearchParams({
96
+ username: args.query,
97
+ maxResults: String(args.maxResults ?? 10),
98
+ });
99
+ const data = await this.request('GET', `/user/search?${params}`);
100
+ if (!data || data.length === 0)
101
+ return text('No users found.');
102
+ const lines = data
103
+ .filter((u) => u.active)
104
+ .map((u, i) => `${i + 1}. ${u.displayName} (${u.name}) — ${u.emailAddress}`);
105
+ return text(`${lines.length} user(s) found:\n${lines.join('\n')}`);
106
+ }
107
+ async getIssue(args) {
108
+ const fields = 'summary,description,status,assignee,priority,issuetype,labels,components';
109
+ const data = await this.request('GET', `/issue/${args.issueKey}?fields=${fields}`);
110
+ if (!data)
111
+ return text('Issue not found.');
112
+ const f = data.fields;
113
+ const lines = [
114
+ `Issue: ${data.key} — ${f.summary}`,
115
+ `Status: ${f.status.name}`,
116
+ `Type: ${f.issuetype.name}`,
117
+ `Priority: ${f.priority?.name ?? 'None'}`,
118
+ `Assignee: ${f.assignee?.displayName ?? 'Unassigned'}`,
119
+ `Labels: ${f.labels?.join(', ') || 'None'}`,
120
+ `Components: ${f.components?.map((c) => c.name).join(', ') || 'None'}`,
121
+ '',
122
+ 'Description:',
123
+ f.description ?? '(no description)',
124
+ ];
125
+ return text(lines.join('\n'));
126
+ }
127
+ async createIssue(args) {
128
+ const { projectKey } = args;
129
+ const fields = {
130
+ project: { key: projectKey },
131
+ issuetype: { name: args.issueType },
132
+ summary: args.summary,
133
+ };
134
+ if (args.description)
135
+ fields.description = args.description;
136
+ if (args.assignee)
137
+ fields.assignee = { name: args.assignee };
138
+ if (args.priority)
139
+ fields.priority = { name: args.priority };
140
+ const data = await this.request('POST', '/issue', { fields });
141
+ if (!data)
142
+ return text('Issue created.');
143
+ return text(`Created ${data.key}.`);
144
+ }
145
+ async updateIssue(args) {
146
+ const fields = {};
147
+ if (args.summary !== undefined)
148
+ fields.summary = args.summary;
149
+ if (args.description !== undefined)
150
+ fields.description = args.description;
151
+ if (args.assignee !== undefined)
152
+ fields.assignee = { name: args.assignee };
153
+ if (args.priority !== undefined)
154
+ fields.priority = { name: args.priority };
155
+ if (Object.keys(fields).length === 0)
156
+ return text('Nothing to update.');
157
+ await this.request('PUT', `/issue/${args.issueKey}`, { fields });
158
+ return text(`Updated ${args.issueKey}.`);
159
+ }
160
+ async getComments(args) {
161
+ const { issueKey, maxResults = 50, startAt = 0 } = args;
162
+ const data = await this.request('GET', `/issue/${issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}`);
163
+ if (!data || data.comments.length === 0)
164
+ return text('No comments found.');
165
+ const blocks = data.comments.map((c) => {
166
+ const date = c.created.slice(0, 10);
167
+ return `--- ${c.author.displayName} (${date}) ---\n${c.body}`;
168
+ });
169
+ const page = pagination(data.total, startAt, data.comments.length);
170
+ return text(`${data.total} comment(s) on ${issueKey}${page}:\n\n${blocks.join('\n\n')}`);
171
+ }
172
+ async addComment(args) {
173
+ await this.request('POST', `/issue/${args.issueKey}/comment`, { body: args.body });
174
+ return text(`Comment added to ${args.issueKey}.`);
175
+ }
176
+ async getTransitions(args) {
177
+ const data = await this.request('GET', `/issue/${args.issueKey}/transitions`);
178
+ if (!data || data.transitions.length === 0)
179
+ return text('No transitions available.');
180
+ const lines = data.transitions.map((t) => `${t.id}: ${t.name} → ${t.to.name}`);
181
+ return text(`Available transitions for ${args.issueKey}:\n${lines.join('\n')}`);
182
+ }
183
+ async transitionIssue(args) {
184
+ await this.request('POST', `/issue/${args.issueKey}/transitions`, {
185
+ transition: { id: args.transitionId },
186
+ });
187
+ return text(`Transitioned ${args.issueKey} using transition ${args.transitionId}.`);
188
+ }
189
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@stubbedev/atlassian-mcp",
3
+ "version": "0.0.1",
4
+ "description": "MCP server for self-hosted Jira and Bitbucket",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "atlassian-mcp.schema.json",
10
+ "README.md"
11
+ ],
12
+ "bin": {
13
+ "atlassian-mcp": "dist/index.js"
14
+ },
15
+ "scripts": {
16
+ "prepare": "tsc",
17
+ "build": "tsc",
18
+ "start": "node dist/index.js",
19
+ "dev": "tsc --watch"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.29.0",
23
+ "dotenv": "^16.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.0.0",
27
+ "@types/node": "^22.0.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/stubbedev/atlassian-mcp.git"
38
+ },
39
+ "homepage": "https://github.com/stubbedev/atlassian-mcp#readme"
40
+ }