@jira-deploy/mcp 1.0.0 → 1.0.13

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/.env.example CHANGED
@@ -11,7 +11,10 @@ JIRA_USER_EMAIL=your-email@company.com
11
11
  JIRA_API_TOKEN=your_jira_api_token
12
12
 
13
13
  # 預設專案 key(可在開單時 override)
14
- JIRA_PROJECT_KEY=OPS
14
+ JIRA_PROJECT_KEY=PROJ
15
+
16
+ # Tenant-specific workflow config. Prefer a local JSON file for large configs.
17
+ JIRA_DEPLOY_CONFIG_PATH=
15
18
 
16
19
  # 輪詢設定
17
20
  POLL_INTERVAL_MS=30000
@@ -28,11 +31,10 @@ CONF_TOKEN=your-confluence-token
28
31
  # Jabber (XMPP)
29
32
  # Prefer storing the password in macOS Keychain and leave JABBER_PASSWORD empty.
30
33
  # Set JABBER_PASSWORD only for CI or quick overrides.
31
- JABBER_SERVER=10.244.192.137
32
- JABBER_USER=your_jabber_account_number # e.g., BK00619
33
- JABBER_DOMAIN=linebank.com.tw
34
+ JABBER_SERVER=xmpp.example.internal
35
+ JABBER_USER=your_jabber_user
36
+ JABBER_DOMAIN=example.internal
34
37
  JABBER_PASSWORD=
35
38
  JABBER_KEYCHAIN_SERVICE=jabber-copilot
36
- JABBER_KEYCHAIN_ACCOUNT=your_jabber_account_number # e.g., BK00619
39
+ JABBER_KEYCHAIN_ACCOUNT=your_jabber_user
37
40
  JABBER_TO=
38
- JABBER_NOTIFY_SCRIPT=src/scripts/jabber_notify.py
package/README.md CHANGED
@@ -29,7 +29,7 @@ npm install -g @jira-deploy/mcp
29
29
  npx -y @jira-deploy/mcp
30
30
  ```
31
31
 
32
- MCP app 維持走 npm / npx 發布與安裝,不提供 binary / Homebrew 安裝。
32
+ MCP app 維持走 npm / npx 發布與安裝,不提供 standalone binary 安裝。
33
33
 
34
34
  ### 本地開發 / 從 monorepo 啟動
35
35
 
@@ -60,7 +60,8 @@ cp .env.example .env
60
60
  "BITBUCKET_URL": "https://bitbucket.example.com",
61
61
  "BITBUCKET_API_TOKEN": "your_bitbucket_api_token",
62
62
  "CONF_BASE_URL": "https://confluence.example.com",
63
- "CONF_TOKEN": "your_confluence_token"
63
+ "CONF_TOKEN": "your_confluence_token",
64
+ "JIRA_DEPLOY_CONFIG_PATH": "/path/to/jira-deploy-config.json"
64
65
  }
65
66
  }
66
67
  }
@@ -75,12 +76,12 @@ cp .env.example .env
75
76
  安裝好並接入 MCP client 後,可以直接在 Chat 裡說:
76
77
 
77
78
  ```text
78
- 幫我開一張 IBK STG 上版單
79
- CID-1718 切到 Pending Approval
80
- CID-1718 被 approve
81
- CID-1718 切到 Approved
82
- CID-1718 的狀態跟留言
83
- tw-bank-webapp 這個 PR 的 diff
79
+ 幫我開一張 APP STG 上版單
80
+ PROJ-123 切到 Pending Approval
81
+ PROJ-123 被 approve
82
+ PROJ-123 切到 Approved
83
+ PROJ-123 的狀態跟留言
84
+ demo-web 這個 PR 的 diff
84
85
  查 Confluence Release Manager 值班表
85
86
  ```
86
87
 
@@ -132,17 +133,17 @@ Confluence:
132
133
  開一張 A平台 STG 上版單,版號 v1.2.3
133
134
 
134
135
  # 2. 切狀態等待審核
135
- OPS-123 切到 Pending Approval
136
+ PROJ-123 切到 Pending Approval
136
137
 
137
138
  # 3. 等待 Approve(Copilot 會輪詢)
138
- OPS-123 被 approve
139
+ PROJ-123 被 approve
139
140
 
140
141
  # 4. Approve 後切狀態(觸發 Jira Automation → Jenkins)
141
- OPS-123 切到 Approved
142
+ PROJ-123 切到 Approved
142
143
 
143
144
  # 5. 等部署完成後收尾
144
- OPS-123 加留言「STG 部署完成,build: https://jenkins/...」
145
- OPS-123 切到 Done
145
+ PROJ-123 加留言「STG 部署完成,build: https://jenkins/...」
146
+ PROJ-123 切到 Done
146
147
  ```
147
148
 
148
149
  ## 平台設定
@@ -152,7 +153,7 @@ Confluence:
152
153
  ```js
153
154
  export const PLATFORMS = {
154
155
  'a平台': {
155
- projectKey: 'OPS',
156
+ projectKey: 'PROJ',
156
157
  issueType: 'Task',
157
158
  components: ['A Platform'],
158
159
  labels: ['deploy', 'stg'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jira-deploy/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.13",
4
4
  "description": "MCP Server for Jira deploy ticket workflow",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -13,7 +13,10 @@
13
13
  "jira-deploy-mcp": "./src/index.js"
14
14
  },
15
15
  "files": [
16
- "src/**/*.js",
16
+ "src/env.js",
17
+ "src/index.js",
18
+ "src/server.js",
19
+ "src/utility-tools.js",
17
20
  "README.md",
18
21
  ".env.example"
19
22
  ],
@@ -24,10 +27,11 @@
24
27
  "dependencies": {
25
28
  "@modelcontextprotocol/sdk": "^1.0.0",
26
29
  "dotenv": "^16.3.0",
27
- "@jira-deploy/core": "1.0.0"
30
+ "@jira-deploy/core": "1.0.13"
28
31
  },
29
32
  "scripts": {
30
33
  "start": "node src/index.js",
31
- "dev": "node --watch src/index.js"
34
+ "dev": "node --watch src/index.js",
35
+ "test": "node --test src/*.test.js"
32
36
  }
33
37
  }
package/src/env.js ADDED
@@ -0,0 +1,11 @@
1
+ import {config} from 'dotenv';
2
+
3
+ export function loadMcpEnv({
4
+ packageEnvPath,
5
+ configImpl = config,
6
+ } = {}) {
7
+ configImpl();
8
+ if (packageEnvPath) {
9
+ configImpl({path: packageEnvPath});
10
+ }
11
+ }
package/src/index.js CHANGED
@@ -1,51 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import {config} from 'dotenv';
3
2
  import {fileURLToPath} from 'url';
4
3
  import {dirname, join} from 'path';
5
- import {Server} from '@modelcontextprotocol/sdk/server/index.js';
6
- import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import {
8
- CallToolRequestSchema,
9
- ListToolsRequestSchema,
10
- } from '@modelcontextprotocol/sdk/types.js';
11
- import {JiraClient, Notifier, getToolDefinitions, executeTool} from '@jira-deploy/core';
12
- import {getUtilityToolDefinitions, executeUtilityTool} from './utility-tools.js';
4
+ import {loadMcpEnv} from './env.js';
13
5
 
14
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
- config({path: join(__dirname, '..', '.env')});
7
+ loadMcpEnv({packageEnvPath: join(__dirname, '..', '.env')});
16
8
 
17
- const jira = new JiraClient();
18
- const notifier = new Notifier(jira);
19
-
20
- const server = new Server(
21
- {name: '@jira-deploy/mcp', version: '1.0.0'},
22
- {capabilities: {tools: {}}}
23
- );
24
-
25
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
26
- tools: [...getToolDefinitions(), ...getUtilityToolDefinitions()],
27
- }));
28
-
29
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
30
- const {name, arguments: args} = request.params;
31
-
32
- try {
33
- const utilityResult = await executeUtilityTool(name, args ?? {});
34
- if (utilityResult) return utilityResult;
35
- return await executeTool(name, args ?? {}, {jira, notifier});
36
- } catch (err) {
37
- return {
38
- content: [
39
- {
40
- type: 'text',
41
- text: JSON.stringify({error: err.message}),
42
- },
43
- ],
44
- isError: true,
45
- };
46
- }
47
- });
9
+ const {StdioServerTransport} = await import('@modelcontextprotocol/sdk/server/stdio.js');
10
+ const {createMcpServer} = await import('./server.js');
48
11
 
12
+ const server = createMcpServer();
49
13
  const transport = new StdioServerTransport();
50
14
  await server.connect(transport);
51
15
  console.error('[@jira-deploy/mcp] MCP Server ready');
package/src/server.js ADDED
@@ -0,0 +1,64 @@
1
+ import {Server} from '@modelcontextprotocol/sdk/server/index.js';
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema,
5
+ } from '@modelcontextprotocol/sdk/types.js';
6
+ import {JiraClient, Notifier, getToolDefinitions, executeTool} from '@jira-deploy/core';
7
+ import {getUtilityToolDefinitions, executeUtilityTool} from './utility-tools.js';
8
+
9
+ const NON_JIRA_CORE_TOOL_NAMES = new Set([
10
+ 'get_release_manager',
11
+ 'send_jabber_message',
12
+ ]);
13
+
14
+ export function createJiraDeps() {
15
+ const jira = new JiraClient();
16
+ return {jira, notifier: new Notifier(jira)};
17
+ }
18
+
19
+ export async function listTools() {
20
+ return {
21
+ tools: [...getToolDefinitions(), ...getUtilityToolDefinitions()],
22
+ };
23
+ }
24
+
25
+ export async function callTool(
26
+ request,
27
+ {
28
+ createDeps = createJiraDeps,
29
+ executeToolImpl = executeTool,
30
+ executeUtilityToolImpl = executeUtilityTool,
31
+ } = {},
32
+ ) {
33
+ const {name, arguments: args} = request.params;
34
+
35
+ try {
36
+ const utilityResult = await executeUtilityToolImpl(name, args ?? {});
37
+ if (utilityResult) return utilityResult;
38
+
39
+ const deps = NON_JIRA_CORE_TOOL_NAMES.has(name) ? {} : createDeps();
40
+ return await executeToolImpl(name, args ?? {}, deps);
41
+ } catch (err) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text',
46
+ text: JSON.stringify({error: err.message}),
47
+ },
48
+ ],
49
+ isError: true,
50
+ };
51
+ }
52
+ }
53
+
54
+ export function createMcpServer(options = {}) {
55
+ const server = new Server(
56
+ {name: '@jira-deploy/mcp', version: '1.0.3'},
57
+ {capabilities: {tools: {}}},
58
+ );
59
+
60
+ server.setRequestHandler(ListToolsRequestSchema, async () => listTools());
61
+ server.setRequestHandler(CallToolRequestSchema, async (request) => callTool(request, options));
62
+
63
+ return server;
64
+ }
@@ -143,19 +143,29 @@ function stripHtml(html) {
143
143
  .trim();
144
144
  }
145
145
 
146
+ function readOnlyTool(tool) {
147
+ return {
148
+ ...tool,
149
+ annotations: {
150
+ ...tool.annotations,
151
+ readOnlyHint: true,
152
+ },
153
+ };
154
+ }
155
+
146
156
  export function getUtilityToolDefinitions() {
147
157
  return [
148
- {
158
+ readOnlyTool({
149
159
  name: 'jira_get_issue',
150
160
  description:
151
161
  'Get a Jira issue by key. Returns summary, status, type, priority, assignee, description, comments, linked issues, and attachments.',
152
162
  inputSchema: {
153
163
  type: 'object',
154
164
  required: ['issueKey'],
155
- properties: {issueKey: {type: 'string', description: 'Jira issue key, e.g. LBPRJ-10949'}},
165
+ properties: {issueKey: {type: 'string', description: 'Jira issue key, e.g. PROJ-123'}},
156
166
  },
157
- },
158
- {
167
+ }),
168
+ readOnlyTool({
159
169
  name: 'jira_search_issues',
160
170
  description: 'Search Jira issues using JQL. Returns key, summary, status, type, priority, and assignee.',
161
171
  inputSchema: {
@@ -166,8 +176,8 @@ export function getUtilityToolDefinitions() {
166
176
  maxResults: {type: 'number', default: 20, description: 'Max results to return'},
167
177
  },
168
178
  },
169
- },
170
- {
179
+ }),
180
+ readOnlyTool({
171
181
  name: 'bitbucket_list_prs',
172
182
  description: 'List pull requests for a Bitbucket repository.',
173
183
  inputSchema: {
@@ -180,8 +190,8 @@ export function getUtilityToolDefinitions() {
180
190
  limit: {type: 'number', default: 25},
181
191
  },
182
192
  },
183
- },
184
- {
193
+ }),
194
+ readOnlyTool({
185
195
  name: 'bitbucket_get_pr',
186
196
  description: 'Get Bitbucket pull request details and recent activity.',
187
197
  inputSchema: {
@@ -193,8 +203,8 @@ export function getUtilityToolDefinitions() {
193
203
  prId: {type: 'number'},
194
204
  },
195
205
  },
196
- },
197
- {
206
+ }),
207
+ readOnlyTool({
198
208
  name: 'bitbucket_get_pr_changes',
199
209
  description: 'Get the list of changed files in a Bitbucket pull request.',
200
210
  inputSchema: {
@@ -206,8 +216,8 @@ export function getUtilityToolDefinitions() {
206
216
  prId: {type: 'number'},
207
217
  },
208
218
  },
209
- },
210
- {
219
+ }),
220
+ readOnlyTool({
211
221
  name: 'bitbucket_get_pr_diff',
212
222
  description: 'Get Bitbucket pull request diff content for code review.',
213
223
  inputSchema: {
@@ -226,8 +236,8 @@ export function getUtilityToolDefinitions() {
226
236
  },
227
237
  },
228
238
  },
229
- },
230
- {
239
+ }),
240
+ readOnlyTool({
231
241
  name: 'bitbucket_get_pr_comments',
232
242
  description: 'Get all comments on a Bitbucket pull request, including inline anchors.',
233
243
  inputSchema: {
@@ -239,7 +249,7 @@ export function getUtilityToolDefinitions() {
239
249
  prId: {type: 'number'},
240
250
  },
241
251
  },
242
- },
252
+ }),
243
253
  {
244
254
  name: 'bitbucket_add_pr_comment',
245
255
  description: 'Add a general comment to a Bitbucket pull request.',
@@ -273,7 +283,7 @@ export function getUtilityToolDefinitions() {
273
283
  },
274
284
  },
275
285
  },
276
- {
286
+ readOnlyTool({
277
287
  name: 'bitbucket_get_file',
278
288
  description: 'Get raw file content from a Bitbucket repository.',
279
289
  inputSchema: {
@@ -286,8 +296,8 @@ export function getUtilityToolDefinitions() {
286
296
  branch: {type: 'string', default: 'master'},
287
297
  },
288
298
  },
289
- },
290
- {
299
+ }),
300
+ readOnlyTool({
291
301
  name: 'confluence_search',
292
302
  description: 'Search Confluence pages using CQL.',
293
303
  inputSchema: {
@@ -298,8 +308,8 @@ export function getUtilityToolDefinitions() {
298
308
  limit: {type: 'number', default: 10},
299
309
  },
300
310
  },
301
- },
302
- {
311
+ }),
312
+ readOnlyTool({
303
313
  name: 'confluence_get_page',
304
314
  description: 'Get full readable content of a Confluence page by ID.',
305
315
  inputSchema: {
@@ -307,8 +317,8 @@ export function getUtilityToolDefinitions() {
307
317
  required: ['pageId'],
308
318
  properties: {pageId: {type: 'string'}},
309
319
  },
310
- },
311
- {
320
+ }),
321
+ readOnlyTool({
312
322
  name: 'confluence_get_children',
313
323
  description: 'Get child pages of a Confluence page.',
314
324
  inputSchema: {
@@ -316,8 +326,8 @@ export function getUtilityToolDefinitions() {
316
326
  required: ['pageId'],
317
327
  properties: {pageId: {type: 'string'}, limit: {type: 'number', default: 25}},
318
328
  },
319
- },
320
- {
329
+ }),
330
+ readOnlyTool({
321
331
  name: 'confluence_list_spaces',
322
332
  description: 'List available Confluence spaces.',
323
333
  inputSchema: {
@@ -325,8 +335,8 @@ export function getUtilityToolDefinitions() {
325
335
  required: [],
326
336
  properties: {limit: {type: 'number', default: 20}},
327
337
  },
328
- },
329
- {
338
+ }),
339
+ readOnlyTool({
330
340
  name: 'confluence_get_page_storage',
331
341
  description: 'Get raw Confluence storage-format HTML for a page.',
332
342
  inputSchema: {
@@ -334,7 +344,7 @@ export function getUtilityToolDefinitions() {
334
344
  required: ['pageId'],
335
345
  properties: {pageId: {type: 'string'}},
336
346
  },
337
- },
347
+ }),
338
348
  {
339
349
  name: 'confluence_create_page',
340
350
  description: 'Create a new Confluence page under a parent page. Body must be storage-format HTML.',
@@ -363,7 +373,7 @@ export function getUtilityToolDefinitions() {
363
373
  },
364
374
  },
365
375
  },
366
- {
376
+ readOnlyTool({
367
377
  name: 'confluence_list_versions',
368
378
  description: 'List historical versions of a Confluence page.',
369
379
  inputSchema: {
@@ -375,8 +385,8 @@ export function getUtilityToolDefinitions() {
375
385
  start: {type: 'number', default: 0},
376
386
  },
377
387
  },
378
- },
379
- {
388
+ }),
389
+ readOnlyTool({
380
390
  name: 'confluence_get_page_version',
381
391
  description: 'Get readable body and metadata of a historical Confluence page version.',
382
392
  inputSchema: {
@@ -384,8 +394,8 @@ export function getUtilityToolDefinitions() {
384
394
  required: ['pageId', 'version'],
385
395
  properties: {pageId: {type: 'string'}, version: {type: 'number'}},
386
396
  },
387
- },
388
- {
397
+ }),
398
+ readOnlyTool({
389
399
  name: 'confluence_get_calendar_events',
390
400
  description:
391
401
  'Query Confluence Team Calendar events. Useful for finding Release Manager / Sign off staff.',
@@ -397,7 +407,7 @@ export function getUtilityToolDefinitions() {
397
407
  date: {type: 'string', description: 'YYYY-MM-DD'},
398
408
  },
399
409
  },
400
- },
410
+ }),
401
411
  ];
402
412
  }
403
413
 
@@ -875,8 +885,8 @@ async function confluenceGetCalendarEvents({subCalendarId, date}) {
875
885
  return ok({
876
886
  date,
877
887
  total: 1,
878
- events: [{what: 'Sign off staff', who: 'Alvin Wang (BK00236)'}],
879
- signOffStaff: {name: 'Alvin Wang (BK00236)'},
888
+ events: [{what: 'Sign off staff', who: 'Demo User (demo-user)'}],
889
+ signOffStaff: {name: 'Demo User (demo-user)'},
880
890
  dryRun: true,
881
891
  });
882
892
  }