@neosamon/jira-mcp-server 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wuyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,10 @@
5
5
  ## 功能
6
6
 
7
7
  - **查询 Issue**: 通过 Issue Key 获取 Issue 的详细信息,包括类型、状态、优先级、描述和评论
8
+ - **搜索 Issues**: 使用 JQL (Jira Query Language) 批量搜索 Issues
8
9
  - **创建评论**: 为指定的 Issue 添加评论
10
+ - **字段发现**: 探测 Issue 的所有可用字段,用于配置自定义字段映射
11
+ - **自定义字段**: 支持通过环境变量配置,提取 Jira 自定义字段
9
12
 
10
13
  ## 环境变量配置
11
14
 
@@ -48,35 +51,48 @@ JIRA_AUTH_TYPE 环境变量
48
51
  3. 点击 **Create API token**
49
52
  4. 复制生成的 Token 并设置为 `JIRA_API_TOKEN` 环境变量
50
53
 
54
+ ### 自定义字段配置(可选)
55
+
56
+ Jira 实例通常包含自定义字段(如测试步骤、预期结果、代码分支等)。要提取这些字段,需要配置字段映射。
57
+
58
+ #### 配置步骤
59
+
60
+ 1. **使用字段发现工具**找出目标字段的 ID:
61
+
62
+ ```
63
+ "使用 get_issue_fields 工具查看 Issue PROJ-123 的所有字段"
64
+ ```
65
+
66
+ 2. **配置环境变量**将字段名映射到字段 ID:
67
+
68
+ ```bash
69
+ export JIRA_FIELD_TEST_STEP="customfield_10206"
70
+ export JIRA_FIELD_EXPECTED_RESULT="customfield_10208"
71
+ export JIRA_FIELD_ACTUAL_RESULT="customfield_10210"
72
+ export JIRA_FIELD_CODE_BRANCH="customfield_10300"
73
+ ```
74
+
75
+ 3. **重启服务器**,`get_issue` 工具将自动包含配置的自定义字段
76
+
77
+ #### 字段映射规则
78
+
79
+ - 环境变量格式:`JIRA_FIELD_<NAME>=<field_id>`
80
+ - 字段名会自动转换为驼峰命名(如 `JIRA_FIELD_TEST_STEP` → `testStep`)
81
+ - 未配置的字段不会出现在输出中
82
+ - 字段不存在或为空时显示 `N/A`
83
+
51
84
  ## 安装
52
85
 
53
86
  ### 通过 npm 全局安装
54
87
 
55
88
  ```bash
56
- npm install -g jira-mcp-server
89
+ npm install -g @neosamon/jira-mcp-server
57
90
  ```
58
91
 
59
92
  ### 使用 npx 运行(无需安装)
60
93
 
61
94
  ```bash
62
- npx jira-mcp-server
63
- ```
64
-
65
- ### 从源码构建
66
-
67
- ```bash
68
- # 克隆仓库
69
- git clone <repository-url>
70
- cd jira-mcp-server-ts
71
-
72
- # 安装依赖
73
- npm install
74
-
75
- # 构建
76
- npm run build
77
-
78
- # 运行
79
- node build/index.js
95
+ npx @neosamon/jira-mcp-server
80
96
  ```
81
97
 
82
98
  ## 使用方法
@@ -90,7 +106,7 @@ export JIRA_BASE_URL="https://your-jira.example.com"
90
106
  export JIRA_USERNAME="your-email@example.com"
91
107
  export JIRA_API_TOKEN="your-api-token"
92
108
 
93
- npx jira-mcp-server
109
+ npx @neosamon/jira-mcp-server
94
110
  ```
95
111
 
96
112
  #### 使用密码认证
@@ -100,7 +116,7 @@ export JIRA_BASE_URL="https://your-jira.example.com"
100
116
  export JIRA_USERNAME="your-email@example.com"
101
117
  export JIRA_PASSWORD="your-password"
102
118
 
103
- npx jira-mcp-server
119
+ npx @neosamon/jira-mcp-server
104
120
  ```
105
121
 
106
122
  #### 显式指定认证方式
@@ -135,7 +151,8 @@ export JIRA_PASSWORD="your-password"
135
151
  {
136
152
  "mcpServers": {
137
153
  "jira": {
138
- "command": "jira-mcp-server",
154
+ "command": "npx",
155
+ "args": ["@neosamon/jira-mcp-server"],
139
156
  "env": {
140
157
  "JIRA_BASE_URL": "https://your-jira.example.com",
141
158
  "JIRA_USERNAME": "your-email@example.com",
@@ -152,7 +169,8 @@ export JIRA_PASSWORD="your-password"
152
169
  {
153
170
  "mcpServers": {
154
171
  "jira": {
155
- "command": "jira-mcp-server",
172
+ "command": "npx",
173
+ "args": ["@neosamon/jira-mcp-server"],
156
174
  "env": {
157
175
  "JIRA_BASE_URL": "https://your-jira.example.com",
158
176
  "JIRA_USERNAME": "your-email@example.com",
@@ -163,15 +181,47 @@ export JIRA_PASSWORD="your-password"
163
181
  }
164
182
  ```
165
183
 
166
- 如果使用 `npx`,可以将 `command` 改为 `"npx"`,并添加 `"args": ["jira-mcp-server"]`。
184
+ ## Claude CLI 配置
185
+
186
+ ### 基于项目添加
187
+ ```bash
188
+ claude mcp add jira --transport stdio \
189
+ --env JIRA_BASE_URL="https://your-jira.example.com" \
190
+ --env JIRA_USERNAME="your-email@example.com" \
191
+ --env JIRA_PASSWORD="your-password" \
192
+ -- npx @neosamon/jira-mcp-server
193
+ ```
194
+
195
+ ### 全局添加
196
+ ```bash
197
+ claude mcp add jira --scope user --transport stdio \
198
+ --env JIRA_BASE_URL="https://your-jira.example.com" \
199
+ --env JIRA_USERNAME="your-email@example.com" \
200
+ --env JIRA_PASSWORD="your-password" \
201
+ -- npx @neosamon/jira-mcp-server
202
+ ```
167
203
 
168
204
  ## 使用示例
169
205
 
170
206
  配置完成后,您可以在 Claude 中使用以下命令:
171
207
 
208
+ ### 基本功能
209
+
172
210
  - "查询 Jira Issue PROJ-123 的详情"
173
211
  - "为 PROJ-123 添加评论:已完成代码审查"
174
212
 
213
+ ### 字段发现
214
+
215
+ - "使用 get_issue_fields 工具查看 Issue PROJ-123 的所有可用字段"
216
+ - "列出 PROJ-123 的字段,帮我找出测试步骤对应的字段 ID"
217
+
218
+ ### JQL 搜索
219
+
220
+ - "搜索分配给我的所有进行中任务"
221
+ - "查找项目 PROJ 中所有高优先级的 Bug"
222
+ - "列出本周创建的所有 Issues"
223
+ - "搜索状态为 Open 且优先级为 High 的 Issues"
224
+
175
225
  ## 兼容性
176
226
 
177
227
  - **Node.js 版本**: 18 或更高版本
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import { z } from "zod";
@@ -35,6 +36,21 @@ function loadConfig() {
35
36
  }
36
37
  return { baseURL, username, apiToken, password, authType };
37
38
  }
39
+ // 加载自定义字段映射配置
40
+ function loadFieldMapping() {
41
+ const mapping = {};
42
+ const prefix = "JIRA_FIELD_";
43
+ for (const [key, value] of Object.entries(process.env)) {
44
+ if (key.startsWith(prefix) && value) {
45
+ // 将 JIRA_FIELD_TEST_STEP 转换为 testStep (驼峰命名)
46
+ const fieldName = key.substring(prefix.length)
47
+ .toLowerCase()
48
+ .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
49
+ mapping[fieldName] = value;
50
+ }
51
+ }
52
+ return mapping;
53
+ }
38
54
  // ==================== HTTP 客户端 ====================
39
55
  // 根据 authType 确定使用哪种认证凭据
40
56
  function getAuthCredential(config) {
@@ -90,13 +106,58 @@ async function addComment(config, issueKey, body) {
90
106
  });
91
107
  }
92
108
  // ==================== 输出格式化函数 ====================
109
+ /**
110
+ * 格式化字段值用于显示
111
+ * 根据值的类型采用不同的展示策略
112
+ */
113
+ function formatFieldValue(value) {
114
+ // null 或 undefined
115
+ if (value === null || value === undefined) {
116
+ return "N/A";
117
+ }
118
+ // 字符串类型 - 截断过长内容
119
+ if (typeof value === "string") {
120
+ if (value.length <= 50) {
121
+ return value;
122
+ }
123
+ return `${value.substring(0, 50)}...`;
124
+ }
125
+ // 数组类型
126
+ if (Array.isArray(value)) {
127
+ return `[Array] (${value.length} items)`;
128
+ }
129
+ // 对象类型
130
+ if (typeof value === "object") {
131
+ // 提取 name 属性(如用户、状态等)
132
+ if ("name" in value && typeof value.name === "string") {
133
+ return value.name;
134
+ }
135
+ // 提取 value 属性(如选项字段)
136
+ if ("value" in value && typeof value.value === "string") {
137
+ return value.value;
138
+ }
139
+ // 其他复杂对象 - JSON 序列化
140
+ try {
141
+ const jsonStr = JSON.stringify(value);
142
+ if (jsonStr.length <= 50) {
143
+ return jsonStr;
144
+ }
145
+ return `${jsonStr.substring(0, 50)}...`;
146
+ }
147
+ catch {
148
+ return "[Object]";
149
+ }
150
+ }
151
+ // 数字、布尔等基本类型
152
+ return String(value);
153
+ }
93
154
  function formatComment(comment) {
94
155
  const author = comment.author.displayName;
95
156
  const created = comment.created;
96
157
  const commentBody = comment.body || "(无内容)";
97
158
  return `**${author}** - ${created}\n${commentBody}\n`;
98
159
  }
99
- function formatIssue(issue) {
160
+ function formatIssue(issue, fieldMapping) {
100
161
  const output = [];
101
162
  // 标题
102
163
  output.push(`## Issue: ${issue.key}\n`);
@@ -125,6 +186,16 @@ function formatIssue(issue) {
125
186
  output.push(`**描述**:\n${descText}\n`);
126
187
  }
127
188
  }
189
+ // 自定义字段
190
+ if (fieldMapping && Object.keys(fieldMapping).length > 0) {
191
+ output.push(`**自定义字段**:\n`);
192
+ for (const [fieldName, fieldId] of Object.entries(fieldMapping)) {
193
+ const fieldValue = issue.fields[fieldId];
194
+ const displayValue = formatFieldValue(fieldValue);
195
+ output.push(`- **${fieldName}**: ${displayValue}\n`);
196
+ }
197
+ output.push(`\n`);
198
+ }
128
199
  // 评论
129
200
  if (issue.fields.comment?.comments && issue.fields.comment.comments.length > 0) {
130
201
  const allComments = issue.fields.comment.comments;
@@ -168,6 +239,7 @@ server.registerTool("get_issue", {
168
239
  },
169
240
  }, async ({ issueKey }) => {
170
241
  const config = loadConfig();
242
+ const fieldMapping = loadFieldMapping();
171
243
  const issue = await getIssue(config, issueKey);
172
244
  if (!issue) {
173
245
  return {
@@ -183,7 +255,7 @@ server.registerTool("get_issue", {
183
255
  content: [
184
256
  {
185
257
  type: "text",
186
- text: formatIssue(issue),
258
+ text: formatIssue(issue, fieldMapping),
187
259
  },
188
260
  ],
189
261
  };
@@ -229,6 +301,150 @@ server.registerTool("add_comment", {
229
301
  ],
230
302
  };
231
303
  });
304
+ // 注册 get_issue_fields 工具
305
+ server.registerTool("get_issue_fields", {
306
+ title: "Get Issue Fields",
307
+ description: "Get all available fields for a Jira Issue. This helps discover custom field IDs for configuration.",
308
+ inputSchema: {
309
+ issueKey: z.string().describe("Jira Issue Key (e.g., PROJ-123)"),
310
+ },
311
+ }, async ({ issueKey }) => {
312
+ const config = loadConfig();
313
+ const issue = await getIssue(config, issueKey);
314
+ if (!issue) {
315
+ return {
316
+ content: [
317
+ {
318
+ type: "text",
319
+ text: `错误: 无法连接到 Jira 服务器或 Issue '${issueKey}' 不存在`,
320
+ },
321
+ ],
322
+ };
323
+ }
324
+ // 遍历所有字段并格式化
325
+ const fieldsList = [];
326
+ for (const [fieldId, fieldValue] of Object.entries(issue.fields)) {
327
+ fieldsList.push({
328
+ fieldId,
329
+ valuePreview: formatFieldValue(fieldValue),
330
+ });
331
+ }
332
+ // 格式化输出
333
+ const output = `## Issue ${issueKey} - 字段列表\n\n` +
334
+ `共找到 ${fieldsList.length} 个字段:\n\n` +
335
+ fieldsList.map(f => `- **${f.fieldId}**: ${f.valuePreview}`).join("\n");
336
+ return {
337
+ content: [
338
+ {
339
+ type: "text",
340
+ text: output,
341
+ },
342
+ ],
343
+ };
344
+ });
345
+ // 注册 search_issues 工具
346
+ server.registerTool("search_issues", {
347
+ title: "Search Issues with JQL",
348
+ description: "Search for Jira Issues using JQL (Jira Query Language). Returns a list of matching Issues with Key, Summary, and Status.",
349
+ inputSchema: {
350
+ jql: z.string().describe("JQL query string (e.g., project = PROJ AND status = 'In Progress')"),
351
+ maxResults: z.number().optional().describe("Maximum number of results to return (default: 50, max: 100)"),
352
+ },
353
+ }, async ({ jql, maxResults = 50 }) => {
354
+ // 参数验证
355
+ if (!jql || jql.trim().length === 0) {
356
+ return {
357
+ content: [
358
+ {
359
+ type: "text",
360
+ text: "错误: JQL 查询不能为空",
361
+ },
362
+ ],
363
+ };
364
+ }
365
+ // 验证 maxResults
366
+ const limit = Math.min(Math.max(1, Math.floor(maxResults)), 100);
367
+ if (limit !== maxResults) {
368
+ return {
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: `错误: maxResults 必须是 1-100 之间的整数,已请求: ${maxResults}`,
373
+ },
374
+ ],
375
+ };
376
+ }
377
+ const config = loadConfig();
378
+ // 调用 Jira API 搜索
379
+ const credential = getAuthCredential(config);
380
+ const authHeader = getBasicAuthHeader(config.username, credential);
381
+ try {
382
+ const url = new URL(`${config.baseURL}/rest/api/2/search`);
383
+ url.searchParams.set("jql", jql);
384
+ url.searchParams.set("maxResults", String(limit));
385
+ url.searchParams.set("fields", "key,summary,status");
386
+ const searchResponse = await fetch(url.toString(), {
387
+ headers: {
388
+ Authorization: authHeader,
389
+ Accept: "application/json",
390
+ },
391
+ });
392
+ if (!searchResponse.ok) {
393
+ const errorText = await searchResponse.text();
394
+ return {
395
+ content: [
396
+ {
397
+ type: "text",
398
+ text: `Jira 查询失败: ${searchResponse.status}\n${errorText}`,
399
+ },
400
+ ],
401
+ isError: true,
402
+ };
403
+ }
404
+ const data = await searchResponse.json();
405
+ const issues = data.issues || [];
406
+ // 格式化结果
407
+ if (issues.length === 0) {
408
+ return {
409
+ content: [
410
+ {
411
+ type: "text",
412
+ text: `未找到匹配的 Issues\n\nJQL: ${jql}`,
413
+ },
414
+ ],
415
+ };
416
+ }
417
+ const total = data.total || issues.length;
418
+ const output = `## 搜索结果\n\n` +
419
+ `**JQL**: ${jql}\n` +
420
+ `**找到**: ${total} 个 Issues (显示 ${issues.length} 个)\n\n` +
421
+ issues.map((issue) => {
422
+ const key = issue.key;
423
+ const summary = issue.fields?.summary || "无摘要";
424
+ const status = issue.fields?.status?.name || "未知";
425
+ return `- **${key}**: ${summary} [${status}]`;
426
+ }).join("\n");
427
+ return {
428
+ content: [
429
+ {
430
+ type: "text",
431
+ text: output,
432
+ },
433
+ ],
434
+ };
435
+ }
436
+ catch (error) {
437
+ return {
438
+ content: [
439
+ {
440
+ type: "text",
441
+ text: `搜索失败: ${error instanceof Error ? error.message : String(error)}`,
442
+ },
443
+ ],
444
+ isError: true,
445
+ };
446
+ }
447
+ });
232
448
  // ==================== 服务器启动 ====================
233
449
  async function main() {
234
450
  // 加载并验证配置
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@neosamon/jira-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Jira MCP Server - Model Context Protocol server for Jira integration",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
7
7
  "bin": {
8
8
  "jira-mcp-server": "./build/index.js"
9
9
  },
10
- "files": ["build"],
10
+ "files": [
11
+ "build"
12
+ ],
11
13
  "engines": {
12
14
  "node": ">=18"
13
15
  },
@@ -15,7 +17,11 @@
15
17
  "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
16
18
  "prepublishOnly": "npm run build"
17
19
  },
18
- "keywords": ["mcp", "jira", "model-context-protocol"],
20
+ "keywords": [
21
+ "mcp",
22
+ "jira",
23
+ "model-context-protocol"
24
+ ],
19
25
  "author": "",
20
26
  "license": "MIT",
21
27
  "dependencies": {