@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.
@@ -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 or name
180
- fields.assignee = params.assignee ? { id: params.assignee } : null;
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 = { name: params.assignee };
699
+ fields.assignee = { accountId: params.assignee };
700
700
  if (params.labels)
701
701
  fields.labels = params.labels;
702
702
  if (params.customFields) {
@@ -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.4",
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": "^5.2.2",
49
+ "jira.js": "5.2.2",
50
50
  "jsdom": "^27.3.0",
51
51
  "markdown-it": "^14.1.0"
52
52
  },