@aaronsb/jira-cloud-mcp 0.2.4 → 0.2.6
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/build/client/jira-client.js +3 -3
- package/build/worker.js +200 -0
- package/package.json +2 -2
|
@@ -176,8 +176,8 @@ export class JiraClient {
|
|
|
176
176
|
fields.parent = params.parentKey ? { key: params.parentKey } : null;
|
|
177
177
|
}
|
|
178
178
|
if (params.assignee !== undefined) {
|
|
179
|
-
// null unassigns, string assigns by account ID
|
|
180
|
-
fields.assignee = params.assignee ? {
|
|
179
|
+
// null unassigns, string assigns by account ID
|
|
180
|
+
fields.assignee = params.assignee ? { accountId: params.assignee } : null;
|
|
181
181
|
}
|
|
182
182
|
if (params.priority)
|
|
183
183
|
fields.priority = { id: params.priority };
|
|
@@ -696,7 +696,7 @@ export class JiraClient {
|
|
|
696
696
|
if (params.priority)
|
|
697
697
|
fields.priority = { id: params.priority };
|
|
698
698
|
if (params.assignee)
|
|
699
|
-
fields.assignee = {
|
|
699
|
+
fields.assignee = { accountId: params.assignee };
|
|
700
700
|
if (params.labels)
|
|
701
701
|
fields.labels = params.labels;
|
|
702
702
|
if (params.customFields) {
|
package/build/worker.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { McpAgent } from 'agents/mcp';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { JiraClient } from './client/jira-client.js';
|
|
4
|
+
// Atlassian OAuth handler
|
|
5
|
+
async function atlassianAuthHandler(request, env) {
|
|
6
|
+
const url = new URL(request.url);
|
|
7
|
+
if (url.pathname === '/callback') {
|
|
8
|
+
const code = url.searchParams.get('code');
|
|
9
|
+
const state = url.searchParams.get('state');
|
|
10
|
+
if (!code) {
|
|
11
|
+
return new Response('Missing authorization code', { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
// Exchange code for tokens
|
|
14
|
+
const tokenResponse = await fetch('https://auth.atlassian.com/oauth/token', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
grant_type: 'authorization_code',
|
|
21
|
+
client_id: env.ATLASSIAN_CLIENT_ID,
|
|
22
|
+
client_secret: env.ATLASSIAN_CLIENT_SECRET,
|
|
23
|
+
code,
|
|
24
|
+
redirect_uri: `${url.origin}/callback`,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (!tokenResponse.ok) {
|
|
28
|
+
const error = await tokenResponse.text();
|
|
29
|
+
return new Response(`Token exchange failed: ${error}`, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
const tokens = await tokenResponse.json();
|
|
32
|
+
// Get accessible resources (cloud instances)
|
|
33
|
+
const resourcesResponse = await fetch('https://api.atlassian.com/oauth/token/accessible-resources', {
|
|
34
|
+
headers: {
|
|
35
|
+
'Authorization': `Bearer ${tokens.access_token}`,
|
|
36
|
+
'Accept': 'application/json',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!resourcesResponse.ok) {
|
|
40
|
+
return new Response('Failed to get accessible resources', { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
const resources = await resourcesResponse.json();
|
|
43
|
+
if (resources.length === 0) {
|
|
44
|
+
return new Response('No accessible Jira instances found', { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
// Use first available resource (could add selection UI later)
|
|
47
|
+
const cloudId = resources[0].id;
|
|
48
|
+
// Get user info
|
|
49
|
+
const userResponse = await fetch(`https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/myself`, {
|
|
50
|
+
headers: {
|
|
51
|
+
'Authorization': `Bearer ${tokens.access_token}`,
|
|
52
|
+
'Accept': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const user = await userResponse.json();
|
|
56
|
+
// Store session in KV
|
|
57
|
+
const sessionId = crypto.randomUUID();
|
|
58
|
+
await env.OAUTH_KV.put(`session:${sessionId}`, JSON.stringify({
|
|
59
|
+
accessToken: tokens.access_token,
|
|
60
|
+
refreshToken: tokens.refresh_token,
|
|
61
|
+
cloudId,
|
|
62
|
+
email: user.emailAddress,
|
|
63
|
+
displayName: user.displayName,
|
|
64
|
+
}), { expirationTtl: 3600 * 24 * 7 }); // 7 days
|
|
65
|
+
// Redirect back to original state or home
|
|
66
|
+
const redirectUrl = state || '/';
|
|
67
|
+
return new Response(null, {
|
|
68
|
+
status: 302,
|
|
69
|
+
headers: {
|
|
70
|
+
'Location': redirectUrl,
|
|
71
|
+
'Set-Cookie': `session=${sessionId}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${3600 * 24 * 7}`,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (url.pathname === '/authorize') {
|
|
76
|
+
const state = url.searchParams.get('state') || '/';
|
|
77
|
+
const scopes = [
|
|
78
|
+
'read:jira-work',
|
|
79
|
+
'write:jira-work',
|
|
80
|
+
'read:jira-user',
|
|
81
|
+
'offline_access', // For refresh tokens
|
|
82
|
+
].join(' ');
|
|
83
|
+
const authUrl = new URL('https://auth.atlassian.com/authorize');
|
|
84
|
+
authUrl.searchParams.set('audience', 'api.atlassian.com');
|
|
85
|
+
authUrl.searchParams.set('client_id', env.ATLASSIAN_CLIENT_ID);
|
|
86
|
+
authUrl.searchParams.set('scope', scopes);
|
|
87
|
+
authUrl.searchParams.set('redirect_uri', `${url.origin}/callback`);
|
|
88
|
+
authUrl.searchParams.set('state', state);
|
|
89
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
90
|
+
authUrl.searchParams.set('prompt', 'consent');
|
|
91
|
+
return Response.redirect(authUrl.toString(), 302);
|
|
92
|
+
}
|
|
93
|
+
return new Response('Not found', { status: 404 });
|
|
94
|
+
}
|
|
95
|
+
// MCP Agent for Jira
|
|
96
|
+
export class JiraMCP extends McpAgent {
|
|
97
|
+
server = new McpServer({
|
|
98
|
+
name: 'jira-cloud-mcp',
|
|
99
|
+
version: '0.2.4',
|
|
100
|
+
});
|
|
101
|
+
jiraClient = null;
|
|
102
|
+
async init() {
|
|
103
|
+
// Initialize Jira client with OAuth token
|
|
104
|
+
if (this.props?.accessToken && this.props?.cloudId) {
|
|
105
|
+
this.jiraClient = new JiraClient({
|
|
106
|
+
host: `api.atlassian.com/ex/jira/${this.props.cloudId}`,
|
|
107
|
+
email: this.props.email,
|
|
108
|
+
apiToken: this.props.accessToken, // OAuth token used as bearer
|
|
109
|
+
// Note: We'll need to modify JiraClient to support OAuth bearer tokens
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Register tools
|
|
113
|
+
this.registerTools();
|
|
114
|
+
}
|
|
115
|
+
registerTools() {
|
|
116
|
+
// For now, register a simple test tool
|
|
117
|
+
// Full tool registration will come in next iteration
|
|
118
|
+
this.server.tool('test_connection', 'Test the Jira connection', {}, async () => {
|
|
119
|
+
if (!this.jiraClient) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: 'text', text: 'Not authenticated. Please authorize first.' }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'text',
|
|
127
|
+
text: `Connected as ${this.props?.displayName} (${this.props?.email})`
|
|
128
|
+
}],
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
// TODO: Register full Jira tools from existing handlers
|
|
132
|
+
// this.registerIssueTools();
|
|
133
|
+
// this.registerProjectTools();
|
|
134
|
+
// this.registerBoardTools();
|
|
135
|
+
// this.registerSprintTools();
|
|
136
|
+
// this.registerFilterTools();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Main Worker export
|
|
140
|
+
export default {
|
|
141
|
+
async fetch(request, env, ctx) {
|
|
142
|
+
const url = new URL(request.url);
|
|
143
|
+
// Handle OAuth endpoints
|
|
144
|
+
if (url.pathname === '/authorize' || url.pathname === '/callback') {
|
|
145
|
+
return atlassianAuthHandler(request, env);
|
|
146
|
+
}
|
|
147
|
+
// Handle MCP endpoints (SSE and streamable-http)
|
|
148
|
+
if (url.pathname === '/sse' || url.pathname === '/mcp') {
|
|
149
|
+
// Get session from cookie
|
|
150
|
+
const cookies = request.headers.get('Cookie') || '';
|
|
151
|
+
const sessionMatch = cookies.match(/session=([^;]+)/);
|
|
152
|
+
const sessionId = sessionMatch?.[1];
|
|
153
|
+
let props;
|
|
154
|
+
if (sessionId) {
|
|
155
|
+
const sessionData = await env.OAUTH_KV.get(`session:${sessionId}`);
|
|
156
|
+
if (sessionData) {
|
|
157
|
+
props = JSON.parse(sessionData);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Route to Durable Object
|
|
161
|
+
const id = env.MCP_OBJECT.idFromName('jira-mcp');
|
|
162
|
+
const stub = env.MCP_OBJECT.get(id);
|
|
163
|
+
// Pass props to the Durable Object
|
|
164
|
+
const newRequest = new Request(request.url, {
|
|
165
|
+
method: request.method,
|
|
166
|
+
headers: request.headers,
|
|
167
|
+
body: request.body,
|
|
168
|
+
});
|
|
169
|
+
// Add props as header for the Durable Object
|
|
170
|
+
if (props) {
|
|
171
|
+
newRequest.headers.set('X-MCP-Props', JSON.stringify(props));
|
|
172
|
+
}
|
|
173
|
+
return stub.fetch(newRequest);
|
|
174
|
+
}
|
|
175
|
+
// Home page with auth status
|
|
176
|
+
const cookies = request.headers.get('Cookie') || '';
|
|
177
|
+
const sessionMatch = cookies.match(/session=([^;]+)/);
|
|
178
|
+
const sessionId = sessionMatch?.[1];
|
|
179
|
+
if (sessionId) {
|
|
180
|
+
const sessionData = await env.OAUTH_KV.get(`session:${sessionId}`);
|
|
181
|
+
if (sessionData) {
|
|
182
|
+
const session = JSON.parse(sessionData);
|
|
183
|
+
return new Response(`
|
|
184
|
+
<h1>Jira Cloud MCP Server</h1>
|
|
185
|
+
<p>Logged in as: ${session.displayName} (${session.email})</p>
|
|
186
|
+
<p>MCP Endpoint: <code>${url.origin}/mcp</code></p>
|
|
187
|
+
<p>SSE Endpoint: <code>${url.origin}/sse</code></p>
|
|
188
|
+
`, {
|
|
189
|
+
headers: { 'Content-Type': 'text/html' },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return new Response(`
|
|
194
|
+
<h1>Jira Cloud MCP Server</h1>
|
|
195
|
+
<p><a href="/authorize">Authorize with Atlassian</a></p>
|
|
196
|
+
`, {
|
|
197
|
+
headers: { 'Content-Type': 'text/html' },
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaronsb/jira-cloud-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Jira Cloud - enables AI assistants to interact with Jira",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
49
|
-
"jira.js": "
|
|
49
|
+
"jira.js": "5.2.2",
|
|
50
50
|
"jsdom": "^27.3.0",
|
|
51
51
|
"markdown-it": "^14.1.0"
|
|
52
52
|
},
|