@neosamon/jira-mcp-server 0.1.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/README.md +182 -0
- package/build/index.js +246 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Jira MCP Server
|
|
2
|
+
|
|
3
|
+
一个基于 TypeScript/Node.js 的 Jira Model Context Protocol (MCP) Server,允许 AI 助手直接与 Jira 进行交互。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- **查询 Issue**: 通过 Issue Key 获取 Issue 的详细信息,包括类型、状态、优先级、描述和评论
|
|
8
|
+
- **创建评论**: 为指定的 Issue 添加评论
|
|
9
|
+
|
|
10
|
+
## 环境变量配置
|
|
11
|
+
|
|
12
|
+
在使用之前,需要设置以下环境变量:
|
|
13
|
+
|
|
14
|
+
| 环境变量 | 必需 | 说明 | 示例 |
|
|
15
|
+
|----------|------|------|------|
|
|
16
|
+
| `JIRA_BASE_URL` | 是 | Jira 服务器地址(不要以斜杠结尾) | `https://your-jira.example.com` |
|
|
17
|
+
| `JIRA_USERNAME` | 是 | Jira 用户名或邮箱 | `user@example.com` |
|
|
18
|
+
| `JIRA_API_TOKEN` | 条件必需* | Jira API Token | `your-api-token-here` |
|
|
19
|
+
| `JIRA_PASSWORD` | 条件必需* | Jira 密码 | `your-password` |
|
|
20
|
+
| `JIRA_AUTH_TYPE` | 否 | 认证方式:`auto`(默认)、`token`、`password` | `auto` |
|
|
21
|
+
|
|
22
|
+
**至少需要设置 `JIRA_API_TOKEN` 或 `JIRA_PASSWORD` 之一**
|
|
23
|
+
|
|
24
|
+
### 认证方式说明
|
|
25
|
+
|
|
26
|
+
本 MCP Server 支持两种认证方式:
|
|
27
|
+
|
|
28
|
+
1. **API Token 认证(推荐)**: 更安全,可以随时撤销
|
|
29
|
+
2. **密码认证**: 适用于不支持 API Token 的旧版本 Jira
|
|
30
|
+
|
|
31
|
+
#### 认证方式选择逻辑
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
JIRA_AUTH_TYPE 环境变量
|
|
35
|
+
│
|
|
36
|
+
├─ "token" → 强制使用 API Token(忽略 JIRA_PASSWORD)
|
|
37
|
+
├─ "password" → 强制使用密码(忽略 JIRA_API_TOKEN)
|
|
38
|
+
└─ "auto" 或未设置
|
|
39
|
+
│
|
|
40
|
+
├─ 如果设置了 JIRA_PASSWORD → 使用密码(优先)
|
|
41
|
+
└─ 否则 → 使用 API Token
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### 获取 Jira API Token
|
|
45
|
+
|
|
46
|
+
1. 登录到您的 Jira 实例
|
|
47
|
+
2. 进入 **Account Settings** > **Security** > **API Tokens**
|
|
48
|
+
3. 点击 **Create API token**
|
|
49
|
+
4. 复制生成的 Token 并设置为 `JIRA_API_TOKEN` 环境变量
|
|
50
|
+
|
|
51
|
+
## 安装
|
|
52
|
+
|
|
53
|
+
### 通过 npm 全局安装
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install -g jira-mcp-server
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 使用 npx 运行(无需安装)
|
|
60
|
+
|
|
61
|
+
```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
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 使用方法
|
|
83
|
+
|
|
84
|
+
### 设置环境变量
|
|
85
|
+
|
|
86
|
+
#### 使用 API Token 认证(推荐)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
export JIRA_BASE_URL="https://your-jira.example.com"
|
|
90
|
+
export JIRA_USERNAME="your-email@example.com"
|
|
91
|
+
export JIRA_API_TOKEN="your-api-token"
|
|
92
|
+
|
|
93
|
+
npx jira-mcp-server
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### 使用密码认证
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export JIRA_BASE_URL="https://your-jira.example.com"
|
|
100
|
+
export JIRA_USERNAME="your-email@example.com"
|
|
101
|
+
export JIRA_PASSWORD="your-password"
|
|
102
|
+
|
|
103
|
+
npx jira-mcp-server
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### 显式指定认证方式
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 强制使用 Token 认证
|
|
110
|
+
export JIRA_AUTH_TYPE="token"
|
|
111
|
+
export JIRA_API_TOKEN="your-api-token"
|
|
112
|
+
|
|
113
|
+
# 强制使用密码认证
|
|
114
|
+
export JIRA_AUTH_TYPE="password"
|
|
115
|
+
export JIRA_PASSWORD="your-password"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Claude Desktop 配置
|
|
119
|
+
|
|
120
|
+
在 Claude Desktop 的配置文件中添加以下内容:
|
|
121
|
+
|
|
122
|
+
### macOS
|
|
123
|
+
|
|
124
|
+
配置文件路径: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
125
|
+
|
|
126
|
+
### Windows
|
|
127
|
+
|
|
128
|
+
配置文件路径: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
129
|
+
|
|
130
|
+
### 配置内容
|
|
131
|
+
|
|
132
|
+
#### 使用 API Token 认证(推荐)
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"jira": {
|
|
138
|
+
"command": "jira-mcp-server",
|
|
139
|
+
"env": {
|
|
140
|
+
"JIRA_BASE_URL": "https://your-jira.example.com",
|
|
141
|
+
"JIRA_USERNAME": "your-email@example.com",
|
|
142
|
+
"JIRA_API_TOKEN": "your-api-token"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### 使用密码认证
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcpServers": {
|
|
154
|
+
"jira": {
|
|
155
|
+
"command": "jira-mcp-server",
|
|
156
|
+
"env": {
|
|
157
|
+
"JIRA_BASE_URL": "https://your-jira.example.com",
|
|
158
|
+
"JIRA_USERNAME": "your-email@example.com",
|
|
159
|
+
"JIRA_PASSWORD": "your-password"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
如果使用 `npx`,可以将 `command` 改为 `"npx"`,并添加 `"args": ["jira-mcp-server"]`。
|
|
167
|
+
|
|
168
|
+
## 使用示例
|
|
169
|
+
|
|
170
|
+
配置完成后,您可以在 Claude 中使用以下命令:
|
|
171
|
+
|
|
172
|
+
- "查询 Jira Issue PROJ-123 的详情"
|
|
173
|
+
- "为 PROJ-123 添加评论:已完成代码审查"
|
|
174
|
+
|
|
175
|
+
## 兼容性
|
|
176
|
+
|
|
177
|
+
- **Node.js 版本**: 18 或更高版本
|
|
178
|
+
- **Jira 版本**: 7.0.9+ (使用 REST API v2)
|
|
179
|
+
|
|
180
|
+
## 许可证
|
|
181
|
+
|
|
182
|
+
MIT License
|
package/build/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
function loadConfig() {
|
|
5
|
+
const baseURL = process.env.JIRA_BASE_URL;
|
|
6
|
+
const username = process.env.JIRA_USERNAME;
|
|
7
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
8
|
+
const password = process.env.JIRA_PASSWORD;
|
|
9
|
+
const authType = (process.env.JIRA_AUTH_TYPE || "auto");
|
|
10
|
+
// 验证必需的环境变量
|
|
11
|
+
if (!baseURL) {
|
|
12
|
+
console.error("错误: JIRA_BASE_URL 环境变量是必需的");
|
|
13
|
+
console.error("请设置 Jira 服务器地址(如 https://your-jira.example.com)");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (!username) {
|
|
17
|
+
console.error("错误: JIRA_USERNAME 环境变量是必需的");
|
|
18
|
+
console.error("请设置 Jira 用户名或邮箱");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
// 验证认证凭据
|
|
22
|
+
if (!apiToken && !password) {
|
|
23
|
+
console.error("错误: 至少需要设置 JIRA_API_TOKEN 或 JIRA_PASSWORD 之一");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// 显式指定 Token 认证但未提供
|
|
27
|
+
if (authType === "token" && !apiToken) {
|
|
28
|
+
console.error("错误: 使用 Token 认证时 JIRA_API_TOKEN 是必需的");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// 显式指定密码认证但未提供
|
|
32
|
+
if (authType === "password" && !password) {
|
|
33
|
+
console.error("错误: 使用密码认证时 JIRA_PASSWORD 是必需的");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return { baseURL, username, apiToken, password, authType };
|
|
37
|
+
}
|
|
38
|
+
// ==================== HTTP 客户端 ====================
|
|
39
|
+
// 根据 authType 确定使用哪种认证凭据
|
|
40
|
+
function getAuthCredential(config) {
|
|
41
|
+
if (config.authType === "token") {
|
|
42
|
+
return config.apiToken;
|
|
43
|
+
}
|
|
44
|
+
if (config.authType === "password") {
|
|
45
|
+
return config.password;
|
|
46
|
+
}
|
|
47
|
+
// auto 模式:密码优先
|
|
48
|
+
return config.password || config.apiToken;
|
|
49
|
+
}
|
|
50
|
+
// 生成 Basic Auth 头
|
|
51
|
+
function getBasicAuthHeader(username, credential) {
|
|
52
|
+
const credentials = Buffer.from(`${username}:${credential}`).toString("base64");
|
|
53
|
+
return `Basic ${credentials}`;
|
|
54
|
+
}
|
|
55
|
+
// HTTP 请求辅助函数
|
|
56
|
+
async function makeJiraRequest(url, config, options) {
|
|
57
|
+
const credential = getAuthCredential(config);
|
|
58
|
+
const authHeader = getBasicAuthHeader(config.username, credential);
|
|
59
|
+
const headers = {
|
|
60
|
+
Authorization: authHeader,
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
Accept: "application/json",
|
|
63
|
+
...options?.headers,
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`${config.baseURL}${url}`, {
|
|
67
|
+
...options,
|
|
68
|
+
headers,
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const errorText = await response.text().catch(() => "");
|
|
72
|
+
console.error(`Jira API error: ${response.status} - ${errorText}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return (await response.json());
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error("Error making Jira request:", error);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ==================== Jira API 调用方法 ====================
|
|
83
|
+
async function getIssue(config, issueKey) {
|
|
84
|
+
return makeJiraRequest(`/rest/api/2/issue/${issueKey}`, config);
|
|
85
|
+
}
|
|
86
|
+
async function addComment(config, issueKey, body) {
|
|
87
|
+
return makeJiraRequest(`/rest/api/2/issue/${issueKey}/comment`, config, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body: JSON.stringify({ body }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// ==================== 输出格式化函数 ====================
|
|
93
|
+
function formatComment(comment) {
|
|
94
|
+
const author = comment.author.displayName;
|
|
95
|
+
const created = comment.created;
|
|
96
|
+
const commentBody = comment.body || "(无内容)";
|
|
97
|
+
return `**${author}** - ${created}\n${commentBody}\n`;
|
|
98
|
+
}
|
|
99
|
+
function formatIssue(issue) {
|
|
100
|
+
const output = [];
|
|
101
|
+
// 标题
|
|
102
|
+
output.push(`## Issue: ${issue.key}\n`);
|
|
103
|
+
// 类型、状态、优先级
|
|
104
|
+
const typeName = issue.fields.issuetype?.name || "未知";
|
|
105
|
+
const statusName = issue.fields.status?.name || "未知";
|
|
106
|
+
const priorityName = issue.fields.priority?.name || "未设置";
|
|
107
|
+
output.push(`**类型**: ${typeName} | **状态**: ${statusName} | **优先级**: ${priorityName}\n`);
|
|
108
|
+
// 摘要
|
|
109
|
+
if (issue.fields.summary) {
|
|
110
|
+
output.push(`**摘要**: ${issue.fields.summary}\n`);
|
|
111
|
+
}
|
|
112
|
+
// 描述
|
|
113
|
+
if (issue.fields.description) {
|
|
114
|
+
let descText;
|
|
115
|
+
if (typeof issue.fields.description === "string") {
|
|
116
|
+
descText = issue.fields.description;
|
|
117
|
+
}
|
|
118
|
+
else if (issue.fields.description.__html) {
|
|
119
|
+
descText = issue.fields.description.__html;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
descText = String(issue.fields.description);
|
|
123
|
+
}
|
|
124
|
+
if (descText.trim()) {
|
|
125
|
+
output.push(`**描述**:\n${descText}\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 评论
|
|
129
|
+
if (issue.fields.comment?.comments && issue.fields.comment.comments.length > 0) {
|
|
130
|
+
const allComments = issue.fields.comment.comments;
|
|
131
|
+
const commentCount = allComments.length;
|
|
132
|
+
const commentsToShow = allComments.slice().reverse().slice(0, 50);
|
|
133
|
+
output.push(`**评论** (${commentCount} 条):\n`);
|
|
134
|
+
for (const comment of commentsToShow) {
|
|
135
|
+
output.push(formatComment(comment));
|
|
136
|
+
}
|
|
137
|
+
if (commentCount > 50) {
|
|
138
|
+
output.push(`*(还有 ${commentCount - 50} 条评论未显示)*\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return output.join("");
|
|
142
|
+
}
|
|
143
|
+
function formatCreatedComment(issueKey, comment) {
|
|
144
|
+
const author = comment.author.displayName;
|
|
145
|
+
const created = comment.created;
|
|
146
|
+
const body = comment.body || "(无内容)";
|
|
147
|
+
// 预览(前 100 字符)
|
|
148
|
+
const preview = body.length > 100 ? `${body.substring(0, 100)}...` : body;
|
|
149
|
+
return `✓ 评论已创建\n\n` +
|
|
150
|
+
`**Issue**: ${issueKey}\n` +
|
|
151
|
+
`**评论 ID**: ${comment.id}\n` +
|
|
152
|
+
`**作者**: ${author}\n` +
|
|
153
|
+
`**创建时间**: ${created}\n` +
|
|
154
|
+
`**内容**: ${preview}`;
|
|
155
|
+
}
|
|
156
|
+
// ==================== MCP Server ====================
|
|
157
|
+
// 创建 server 实例
|
|
158
|
+
const server = new McpServer({
|
|
159
|
+
name: "jira",
|
|
160
|
+
version: "0.1.0",
|
|
161
|
+
});
|
|
162
|
+
// 注册 get_issue 工具
|
|
163
|
+
server.registerTool("get_issue", {
|
|
164
|
+
title: "Get Jira Issue",
|
|
165
|
+
description: "Get detailed information about a Jira Issue including type, status, priority, description, and comments.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
issueKey: z.string().describe("Jira Issue Key (e.g., PROJ-123)"),
|
|
168
|
+
},
|
|
169
|
+
}, async ({ issueKey }) => {
|
|
170
|
+
const config = loadConfig();
|
|
171
|
+
const issue = await getIssue(config, issueKey);
|
|
172
|
+
if (!issue) {
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: `错误: 无法连接到 Jira 服务器或 Issue '${issueKey}' 不存在`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: formatIssue(issue),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
// 注册 add_comment 工具
|
|
192
|
+
server.registerTool("add_comment", {
|
|
193
|
+
title: "Add Comment to Issue",
|
|
194
|
+
description: "Add a comment to a Jira Issue.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
issueKey: z.string().describe("Jira Issue Key (e.g., PROJ-123)"),
|
|
197
|
+
body: z.string().describe("Comment content"),
|
|
198
|
+
},
|
|
199
|
+
}, async ({ issueKey, body }) => {
|
|
200
|
+
// 验证 body 非空
|
|
201
|
+
if (!body || body.trim().length === 0) {
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: "错误: 评论内容不能为空",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const config = loadConfig();
|
|
212
|
+
const result = await addComment(config, issueKey, body);
|
|
213
|
+
if (!result) {
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: `错误: 无法添加评论。请检查 Issue Key '${issueKey}' 是否存在,以及您是否有权限添加评论。`,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text",
|
|
227
|
+
text: formatCreatedComment(issueKey, result),
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
// ==================== 服务器启动 ====================
|
|
233
|
+
async function main() {
|
|
234
|
+
// 加载并验证配置
|
|
235
|
+
const config = loadConfig();
|
|
236
|
+
console.error(`Jira MCP Server 配置加载成功: ${config.baseURL} (认证方式: ${config.authType})`);
|
|
237
|
+
// 创建 stdio 传输
|
|
238
|
+
const transport = new StdioServerTransport();
|
|
239
|
+
// 连接服务器
|
|
240
|
+
await server.connect(transport);
|
|
241
|
+
console.error("Jira MCP Server running on stdio");
|
|
242
|
+
}
|
|
243
|
+
main().catch((error) => {
|
|
244
|
+
console.error("Fatal error in main():", error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neosamon/jira-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Jira MCP Server - Model Context Protocol server for Jira integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jira-mcp-server": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["build"],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["mcp", "jira", "model-context-protocol"],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.24.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.19.2",
|
|
26
|
+
"typescript": "^5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|