@zythegit/jenkins-config-mcp 1.6.0-rc.3
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 +46 -0
- package/bin/jenkins-config-mcp.js +322 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @zythegit/jenkins-config-mcp
|
|
2
|
+
|
|
3
|
+
`jenkins-config` MCP Server 的一键启动器。**目标机器只需要 Node.js 18+,不需要 Python。**
|
|
4
|
+
|
|
5
|
+
MCP Server 本体是 Python 实现,但发布物是 PyInstaller 打出的单文件二进制(自带 Python 运行时)。
|
|
6
|
+
本包是一层薄启动器:首次运行时从 GitHub Release 下载当前平台的二进制、校验 sha256 并缓存,
|
|
7
|
+
之后直接复用;启动后以 stdio 直通方式转发 MCP 协议。
|
|
8
|
+
|
|
9
|
+
## 用法
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"jenkins-build": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@zythegit/jenkins-config-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
默认为只读模式,`trigger_build` / `rebuild_last` / `save_config` 会被拒绝;
|
|
23
|
+
确认要让 AI 代为触发构建时再加 `"env": { "JENKINS_MCP_ALLOW_WRITE": "1" }`。
|
|
24
|
+
|
|
25
|
+
支持的平台:Windows x64、macOS x64 / arm64、Linux x64 / arm64。
|
|
26
|
+
|
|
27
|
+
## 解析优先级
|
|
28
|
+
|
|
29
|
+
1. `JENKINS_MCP_BINARY` — 直接指定二进制路径
|
|
30
|
+
2. `JENKINS_MCP_PYTHON` — 指定解释器,执行 `-m jenkins_config.mcp.server`(开发用)
|
|
31
|
+
3. 缓存中已下载的二进制
|
|
32
|
+
4. 从 Release 下载二进制并校验 sha256
|
|
33
|
+
5. 兜底:PATH 上的 `jenkins-config-mcp` / `uvx` / `python`
|
|
34
|
+
|
|
35
|
+
日志一律走 stderr,stdout 只用于 MCP 的 JSON-RPC 通道。
|
|
36
|
+
|
|
37
|
+
## 环境变量
|
|
38
|
+
|
|
39
|
+
- `JENKINS_MCP_VERSION` — 要下载的 Release tag,默认 `v<npm 包版本>`
|
|
40
|
+
- `JENKINS_MCP_RELEASE_BASE` — 下载地址前缀,默认 GitHub Release,可指向内网镜像
|
|
41
|
+
- `JENKINS_MCP_CACHE_DIR` — 缓存根目录,默认 `~/.cache`(Windows 为 `%LOCALAPPDATA%`)
|
|
42
|
+
- `JENKINS_MCP_SKIP_CHECKSUM=1` — 跳过 sha256 校验(不建议)
|
|
43
|
+
- `JENKINS_MCP_LAUNCHER_DRYRUN=1` — 只打印解析出的命令后退出,用于排查启动问题
|
|
44
|
+
|
|
45
|
+
Server 自身的环境变量(`JENKINS_MCP_ALLOW_WRITE`、`JENKINS_MCP_ALLOWED_HOSTS`、
|
|
46
|
+
`JENKINS_MCP_CONFIG_ROOTS`)会原样透传,说明见仓库内 `docs/mcp/README.md`。
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* jenkins-config MCP Server 的 npx 启动器。
|
|
4
|
+
*
|
|
5
|
+
* MCP Server 本体是 Python 实现,但发布物是 PyInstaller 打出的单文件二进制
|
|
6
|
+
* (自带 Python 运行时),因此使用方机器上只需要 Node,不需要 Python。
|
|
7
|
+
*
|
|
8
|
+
* 首次运行时从 GitHub Release 下载当前平台的二进制并缓存,之后直接复用。
|
|
9
|
+
* 所有日志一律写 stderr —— stdout 是 MCP 的 JSON-RPC 通道,不能污染。
|
|
10
|
+
*
|
|
11
|
+
* 解析优先级:
|
|
12
|
+
* 1. JENKINS_MCP_BINARY 直接指定二进制路径
|
|
13
|
+
* 2. JENKINS_MCP_PYTHON 指定解释器,运行 -m jenkins_config.mcp.server(开发用)
|
|
14
|
+
* 3. 缓存中已下载的二进制
|
|
15
|
+
* 4. 从 Release 下载二进制(校验 sha256)
|
|
16
|
+
* 5. 兜底:PATH 上的 jenkins-config-mcp / uvx / python
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const { spawn } = require('node:child_process');
|
|
22
|
+
const crypto = require('node:crypto');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const { version } = require('../package.json');
|
|
28
|
+
|
|
29
|
+
const REPO = 'zyTheGit/jenkins-config';
|
|
30
|
+
const CHECKSUMS = 'checksums.txt';
|
|
31
|
+
const MODULE_ARGS = ['-m', 'jenkins_config.mcp.server'];
|
|
32
|
+
const DEFAULT_PACKAGE =
|
|
33
|
+
'jenkins-config[mcp] @ git+https://github.com/zyTheGit/jenkins-config.git';
|
|
34
|
+
|
|
35
|
+
// 平台 → Release 资产名,与 .github/workflows/build.yml 的产物命名保持一致
|
|
36
|
+
const ASSETS = {
|
|
37
|
+
'win32-x64': 'jenkins-config-mcp-win-x64.exe',
|
|
38
|
+
'darwin-x64': 'jenkins-config-mcp-macos-x64',
|
|
39
|
+
'darwin-arm64': 'jenkins-config-mcp-macos-arm64',
|
|
40
|
+
'linux-x64': 'jenkins-config-mcp-linux-x64',
|
|
41
|
+
'linux-arm64': 'jenkins-config-mcp-linux-arm64',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 输出一行日志到 stderr。
|
|
46
|
+
*
|
|
47
|
+
* @param {string} message 日志内容
|
|
48
|
+
*/
|
|
49
|
+
function log(message) {
|
|
50
|
+
process.stderr.write(`[jenkins-config-mcp] ${message}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 在 PATH 中查找可执行文件(Windows 下按 PATHEXT 补全后缀)。
|
|
55
|
+
*
|
|
56
|
+
* 不走 shell,避免把用户参数交给命令行解析器。
|
|
57
|
+
*
|
|
58
|
+
* @param {string} name 可执行文件名
|
|
59
|
+
* @returns {string|null} 命中的绝对路径,未命中返回 null
|
|
60
|
+
*/
|
|
61
|
+
function which(name) {
|
|
62
|
+
if (name.includes(path.sep) || name.includes('/')) {
|
|
63
|
+
return fs.existsSync(name) ? name : null;
|
|
64
|
+
}
|
|
65
|
+
const exts =
|
|
66
|
+
process.platform === 'win32'
|
|
67
|
+
? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
68
|
+
: [''];
|
|
69
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
70
|
+
if (!dir) continue;
|
|
71
|
+
for (const ext of exts) {
|
|
72
|
+
const candidate = path.join(dir, name + ext);
|
|
73
|
+
try {
|
|
74
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
75
|
+
} catch {
|
|
76
|
+
// 不存在或不可读,继续尝试下一个候选
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 当前平台对应的 Release 资产名。
|
|
85
|
+
*
|
|
86
|
+
* @returns {string} 资产文件名
|
|
87
|
+
* @throws {Error} 平台/架构不在支持列表中
|
|
88
|
+
*/
|
|
89
|
+
function assetName() {
|
|
90
|
+
const key = `${process.platform}-${process.arch}`;
|
|
91
|
+
const name = ASSETS[key];
|
|
92
|
+
if (!name) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`暂无 ${key} 的预编译二进制,可设置 JENKINS_MCP_PYTHON 指向本地 Python 解释器`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 二进制缓存目录:<缓存根>/jenkins-config-mcp/<version>。
|
|
102
|
+
*
|
|
103
|
+
* @returns {string} 目录绝对路径
|
|
104
|
+
*/
|
|
105
|
+
function cacheDir() {
|
|
106
|
+
const override = process.env.JENKINS_MCP_CACHE_DIR;
|
|
107
|
+
const base =
|
|
108
|
+
override ||
|
|
109
|
+
(process.platform === 'win32'
|
|
110
|
+
? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
111
|
+
: path.join(os.homedir(), '.cache'));
|
|
112
|
+
return path.join(base, 'jenkins-config-mcp', releaseTag());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 要下载的 Release tag,默认与 npm 包版本一致。
|
|
117
|
+
*
|
|
118
|
+
* @returns {string} 形如 v1.0.0
|
|
119
|
+
*/
|
|
120
|
+
function releaseTag() {
|
|
121
|
+
return process.env.JENKINS_MCP_VERSION || `v${version}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Release 资产下载地址前缀。
|
|
126
|
+
*
|
|
127
|
+
* @returns {string} 不含结尾斜杠的 URL 前缀
|
|
128
|
+
*/
|
|
129
|
+
function releaseBase() {
|
|
130
|
+
const base =
|
|
131
|
+
process.env.JENKINS_MCP_RELEASE_BASE || `https://github.com/${REPO}/releases/download`;
|
|
132
|
+
return `${base.replace(/\/+$/, '')}/${releaseTag()}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 下载 URL 内容到内存。
|
|
137
|
+
*
|
|
138
|
+
* @param {string} url 目标地址
|
|
139
|
+
* @returns {Promise<Buffer>} 响应体
|
|
140
|
+
* @throws {Error} 非 2xx 响应
|
|
141
|
+
*/
|
|
142
|
+
async function fetchBuffer(url) {
|
|
143
|
+
const response = await fetch(url, { redirect: 'follow' });
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`下载失败 ${response.status} ${response.statusText}: ${url}`);
|
|
146
|
+
}
|
|
147
|
+
return Buffer.from(await response.arrayBuffer());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 从 Release 的 checksums.txt 中取出指定资产的 sha256。
|
|
152
|
+
*
|
|
153
|
+
* @param {string} name 资产文件名
|
|
154
|
+
* @returns {Promise<string>} 小写 sha256
|
|
155
|
+
* @throws {Error} 清单缺失或清单中没有该资产
|
|
156
|
+
*/
|
|
157
|
+
async function expectedSha256(name) {
|
|
158
|
+
const text = (await fetchBuffer(`${releaseBase()}/${CHECKSUMS}`)).toString('utf8');
|
|
159
|
+
for (const line of text.split(/\r?\n/)) {
|
|
160
|
+
const [hash, file] = line.trim().split(/\s+/);
|
|
161
|
+
if (file === name && hash) return hash.toLowerCase();
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`${CHECKSUMS} 中缺少 ${name} 的记录`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 确保当前平台的二进制已就绪,必要时下载并校验。
|
|
168
|
+
*
|
|
169
|
+
* @returns {Promise<string>} 二进制绝对路径
|
|
170
|
+
*/
|
|
171
|
+
async function ensureBinary() {
|
|
172
|
+
const name = assetName();
|
|
173
|
+
const dir = cacheDir();
|
|
174
|
+
const target = path.join(dir, name);
|
|
175
|
+
if (fs.existsSync(target)) return target;
|
|
176
|
+
|
|
177
|
+
const url = `${releaseBase()}/${name}`;
|
|
178
|
+
log(`首次运行,正在下载 ${releaseTag()} 的 ${name} ...`);
|
|
179
|
+
const payload = await fetchBuffer(url);
|
|
180
|
+
|
|
181
|
+
if (process.env.JENKINS_MCP_SKIP_CHECKSUM !== '1') {
|
|
182
|
+
const expected = await expectedSha256(name);
|
|
183
|
+
const actual = crypto.createHash('sha256').update(payload).digest('hex');
|
|
184
|
+
if (actual !== expected) {
|
|
185
|
+
throw new Error(`sha256 校验失败:期望 ${expected},实际 ${actual}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
// 先写临时文件再 rename,避免并发启动时读到半个文件
|
|
191
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
192
|
+
fs.writeFileSync(tmp, payload, { mode: 0o755 });
|
|
193
|
+
fs.renameSync(tmp, target);
|
|
194
|
+
log(`已缓存到 ${target}`);
|
|
195
|
+
return target;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 判断 PATH 上命中的 jenkins-config-mcp 是否就是本 npm 包生成的 shim。
|
|
200
|
+
*
|
|
201
|
+
* @param {string} candidate which() 命中的路径
|
|
202
|
+
* @returns {boolean} 是自身则 true
|
|
203
|
+
*/
|
|
204
|
+
function isSelf(candidate) {
|
|
205
|
+
try {
|
|
206
|
+
const dir = path.dirname(fs.realpathSync(candidate));
|
|
207
|
+
return dir === path.dirname(fs.realpathSync(__filename));
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 下载不可用时的兜底解析(面向已有 Python 环境的开发者)。
|
|
215
|
+
*
|
|
216
|
+
* @returns {{command: string, args: string[], source: string}|null}
|
|
217
|
+
*/
|
|
218
|
+
function resolveFallback() {
|
|
219
|
+
const installed = which('jenkins-config-mcp');
|
|
220
|
+
if (installed && !isSelf(installed)) {
|
|
221
|
+
return { command: installed, args: [], source: 'console-script' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const uvx = which('uvx');
|
|
225
|
+
if (uvx) {
|
|
226
|
+
const spec = process.env.JENKINS_MCP_PACKAGE || DEFAULT_PACKAGE;
|
|
227
|
+
return { command: uvx, args: ['--from', spec, 'jenkins-config-mcp'], source: 'uvx' };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const py of ['python3', 'python']) {
|
|
231
|
+
const found = which(py);
|
|
232
|
+
if (found) return { command: found, args: MODULE_ARGS, source: py };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 解析最终要执行的命令。
|
|
240
|
+
*
|
|
241
|
+
* @returns {Promise<{command: string, args: string[], source: string}>}
|
|
242
|
+
* @throws {Error} 二进制下载失败且无任何兜底方案
|
|
243
|
+
*/
|
|
244
|
+
async function resolveCommand() {
|
|
245
|
+
const explicitBinary = process.env.JENKINS_MCP_BINARY;
|
|
246
|
+
if (explicitBinary) {
|
|
247
|
+
return { command: explicitBinary, args: [], source: 'JENKINS_MCP_BINARY' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const explicitPython = process.env.JENKINS_MCP_PYTHON;
|
|
251
|
+
if (explicitPython) {
|
|
252
|
+
return { command: explicitPython, args: MODULE_ARGS, source: 'JENKINS_MCP_PYTHON' };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
return { command: await ensureBinary(), args: [], source: 'release-binary' };
|
|
257
|
+
} catch (err) {
|
|
258
|
+
log(`预编译二进制不可用:${err.message}`);
|
|
259
|
+
const fallback = resolveFallback();
|
|
260
|
+
if (fallback) {
|
|
261
|
+
log(`回退到本地 Python 环境(${fallback.source})`);
|
|
262
|
+
return fallback;
|
|
263
|
+
}
|
|
264
|
+
throw new Error(
|
|
265
|
+
[
|
|
266
|
+
'无法启动 MCP Server。可任选一种方式:',
|
|
267
|
+
` 1) 确认能访问 ${releaseBase()},或用 JENKINS_MCP_RELEASE_BASE 指定镜像地址`,
|
|
268
|
+
' 2) 用 JENKINS_MCP_BINARY 指向手动下载的二进制',
|
|
269
|
+
' 3) 安装 uv 或 pip install "jenkins-config[mcp]" 后重试',
|
|
270
|
+
].join('\n')
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function main() {
|
|
276
|
+
let resolved;
|
|
277
|
+
try {
|
|
278
|
+
resolved = await resolveCommand();
|
|
279
|
+
} catch (err) {
|
|
280
|
+
log(err.message);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const args = resolved.args.concat(process.argv.slice(2));
|
|
285
|
+
|
|
286
|
+
if (process.env.JENKINS_MCP_LAUNCHER_DRYRUN) {
|
|
287
|
+
process.stdout.write(
|
|
288
|
+
JSON.stringify({ source: resolved.source, command: resolved.command, args }) + '\n'
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const child = spawn(resolved.command, args, {
|
|
294
|
+
stdio: 'inherit',
|
|
295
|
+
shell: false,
|
|
296
|
+
windowsHide: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
child.on('error', (err) => {
|
|
300
|
+
log(`启动失败 (${resolved.command}): ${err.message}`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
305
|
+
process.on(sig, () => {
|
|
306
|
+
if (!child.killed) child.kill(sig);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
child.on('exit', (code, signal) => {
|
|
311
|
+
if (signal) {
|
|
312
|
+
process.kill(process.pid, signal);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
process.exit(code ?? 0);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main();
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zythegit/jenkins-config-mcp",
|
|
3
|
+
"version": "1.6.0-rc.3",
|
|
4
|
+
"description": "npx launcher for the jenkins-config MCP Server (Python)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"jenkins-config-mcp": "bin/jenkins-config-mcp.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"jenkins",
|
|
18
|
+
"modelcontextprotocol"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/zyTheGit/jenkins-config.git",
|
|
23
|
+
"directory": "npm"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|