@rishibhushan/jenkins-mcp-server 1.1.6 → 1.1.7
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/bin/jenkins-mcp.js +50 -697
- package/package.json +1 -1
- package/requirements.txt +1 -1
package/bin/jenkins-mcp.js
CHANGED
|
@@ -1,727 +1,80 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Jenkins MCP Server - Node.js Wrapper
|
|
4
|
-
*
|
|
5
|
-
* This wrapper handles:
|
|
6
|
-
* - Python version detection (cross-platform)
|
|
7
|
-
* - Virtual environment creation
|
|
8
|
-
* - Smart proxy detection and handling (auto-detects corporate vs public networks)
|
|
9
|
-
* - Dependency installation with retry logic
|
|
10
|
-
* - Server execution
|
|
11
|
-
*
|
|
12
|
-
* Supports: Windows, macOS, Linux
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const { spawn, spawnSync } = require('child_process');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
|
|
19
|
-
// Configuration
|
|
20
|
-
const projectRoot = path.join(__dirname, '..');
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
const isWindows = process.platform === 'win32';
|
|
23
|
-
|
|
24
|
-
// Color codes for terminal output (if supported)
|
|
25
|
-
const colors = {
|
|
26
|
-
reset: '\x1b[0m',
|
|
27
|
-
green: '\x1b[32m',
|
|
28
|
-
yellow: '\x1b[33m',
|
|
29
|
-
red: '\x1b[31m',
|
|
30
|
-
blue: '\x1b[34m',
|
|
31
|
-
cyan: '\x1b[36m',
|
|
32
|
-
bold: '\x1b[1m'
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function log(message, color = 'reset') {
|
|
36
|
-
const colorCode = process.stderr.isTTY ? colors[color] : '';
|
|
37
|
-
const resetCode = process.stderr.isTTY ? colors.reset : '';
|
|
38
|
-
// CRITICAL: Use stderr for all output, stdout is for JSON-RPC only
|
|
39
|
-
console.error(`${colorCode}${message}${resetCode}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function error(message) {
|
|
43
|
-
console.error(`${colors.red}ERROR: ${message}${colors.reset}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function warning(message) {
|
|
47
|
-
console.error(`${colors.yellow}WARNING: ${message}${colors.reset}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function info(message) {
|
|
51
|
-
console.error(`${colors.cyan}ℹ ${message}${colors.reset}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Detect and display proxy configuration
|
|
56
|
-
* @returns {Object} Proxy configuration details
|
|
57
|
-
*/
|
|
58
|
-
function detectProxyConfig() {
|
|
59
|
-
const proxyVars = [
|
|
60
|
-
'HTTP_PROXY', 'http_proxy',
|
|
61
|
-
'HTTPS_PROXY', 'https_proxy',
|
|
62
|
-
'ALL_PROXY', 'all_proxy',
|
|
63
|
-
'NO_PROXY', 'no_proxy'
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
const activeProxies = {};
|
|
67
|
-
for (const varName of proxyVars) {
|
|
68
|
-
if (process.env[varName]) {
|
|
69
|
-
activeProxies[varName] = process.env[varName];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
2
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* Check npm config for proxy settings
|
|
78
|
-
* @returns {Object} npm proxy configuration
|
|
79
|
-
*/
|
|
80
|
-
function checkNpmProxy() {
|
|
81
|
-
try {
|
|
82
|
-
const result = spawnSync('npm', ['config', 'list'], {
|
|
83
|
-
stdio: 'pipe',
|
|
84
|
-
encoding: 'utf-8'
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (result.status === 0) {
|
|
88
|
-
const output = result.stdout || '';
|
|
89
|
-
const proxyLines = output.split('\n').filter(line =>
|
|
90
|
-
line.toLowerCase().includes('proxy') && !line.includes('; ///')
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
if (proxyLines.length > 0) {
|
|
94
|
-
return {
|
|
95
|
-
found: true,
|
|
96
|
-
config: proxyLines.join('\n')
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// npm not available or error
|
|
102
|
-
}
|
|
103
|
-
return { found: false };
|
|
104
|
-
}
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
105
7
|
|
|
106
8
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
9
|
+
* Resolve paths relative to the *installed package*,
|
|
10
|
+
* NOT the caller's current working directory.
|
|
109
11
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
stdio: 'pipe',
|
|
114
|
-
encoding: 'utf-8'
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
if (result.status === 0) {
|
|
118
|
-
const output = result.stdout || '';
|
|
119
|
-
if (output.toLowerCase().includes('proxy')) {
|
|
120
|
-
return {
|
|
121
|
-
found: true,
|
|
122
|
-
config: output
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
} catch (e) {
|
|
127
|
-
// pip not available or error
|
|
128
|
-
}
|
|
129
|
-
return { found: false };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Test if proxy is reachable
|
|
134
|
-
* @param {string} proxyUrl - Proxy URL to test
|
|
135
|
-
* @returns {boolean} True if proxy is reachable
|
|
136
|
-
*/
|
|
137
|
-
function testProxyConnectivity(proxyUrl) {
|
|
138
|
-
try {
|
|
139
|
-
// Try to parse proxy URL
|
|
140
|
-
const url = new URL(proxyUrl);
|
|
141
|
-
|
|
142
|
-
// Use curl to test proxy connectivity (cross-platform)
|
|
143
|
-
const testCmd = isWindows
|
|
144
|
-
? `curl -s -o NUL -w "%{http_code}" --proxy ${proxyUrl} --max-time 5 https://pypi.org/simple/`
|
|
145
|
-
: `curl -s -o /dev/null -w "%{http_code}" --proxy ${proxyUrl} --max-time 5 https://pypi.org/simple/`;
|
|
146
|
-
|
|
147
|
-
const result = spawnSync(isWindows ? 'cmd' : 'sh',
|
|
148
|
-
isWindows ? ['/c', testCmd] : ['-c', testCmd],
|
|
149
|
-
{
|
|
150
|
-
stdio: 'pipe',
|
|
151
|
-
encoding: 'utf-8',
|
|
152
|
-
timeout: 6000
|
|
153
|
-
}
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const httpCode = result.stdout?.trim();
|
|
157
|
-
return httpCode === '200' || httpCode === '301' || httpCode === '302';
|
|
158
|
-
} catch (e) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
162
15
|
|
|
163
16
|
/**
|
|
164
|
-
*
|
|
165
|
-
* @returns {boolean} True if PyPI is accessible
|
|
17
|
+
* Debug (stderr only – safe for MCP)
|
|
166
18
|
*/
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const testCmd = isWindows
|
|
170
|
-
? 'curl -s -o NUL -w "%{http_code}" --max-time 5 https://pypi.org/simple/'
|
|
171
|
-
: 'curl -s -o /dev/null -w "%{http_code}" --max-time 5 https://pypi.org/simple/';
|
|
172
|
-
|
|
173
|
-
const result = spawnSync(isWindows ? 'cmd' : 'sh',
|
|
174
|
-
isWindows ? ['/c', testCmd] : ['-c', testCmd],
|
|
175
|
-
{
|
|
176
|
-
stdio: 'pipe',
|
|
177
|
-
encoding: 'utf-8',
|
|
178
|
-
timeout: 6000
|
|
179
|
-
}
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const httpCode = result.stdout?.trim();
|
|
183
|
-
return httpCode === '200' || httpCode === '301' || httpCode === '302';
|
|
184
|
-
} catch (e) {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
19
|
+
console.error("[jenkins-mcp] package root:", PACKAGE_ROOT);
|
|
188
20
|
|
|
189
21
|
/**
|
|
190
|
-
*
|
|
191
|
-
* @returns {string} Python command name
|
|
22
|
+
* Resolve Python executable inside packaged venv
|
|
192
23
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const result = spawnSync(cmd, ['--version'], {
|
|
203
|
-
stdio: 'pipe',
|
|
204
|
-
encoding: 'utf-8'
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (result.status === 0) {
|
|
208
|
-
const output = result.stdout || result.stderr;
|
|
209
|
-
if (output.includes('Python 3')) {
|
|
210
|
-
const version = output.trim();
|
|
211
|
-
log(`✓ Found ${version}`, 'green');
|
|
212
|
-
return cmd;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
} catch (e) {
|
|
216
|
-
// Command not found, try next
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
error('Python 3 is required but not found.');
|
|
222
|
-
console.error('\nPlease install Python 3.8 or higher from:');
|
|
223
|
-
console.error(' https://www.python.org/downloads/');
|
|
224
|
-
console.error('\nAfter installation, restart your terminal and try again.');
|
|
24
|
+
const pythonPath = path.join(
|
|
25
|
+
PACKAGE_ROOT,
|
|
26
|
+
".venv",
|
|
27
|
+
"bin",
|
|
28
|
+
"python"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(pythonPath)) {
|
|
32
|
+
console.error("[jenkins-mcp] ERROR: Python venv not found at:", pythonPath);
|
|
225
33
|
process.exit(1);
|
|
226
34
|
}
|
|
227
35
|
|
|
228
36
|
/**
|
|
229
|
-
*
|
|
230
|
-
* @param {string} venvPath - Path to virtual environment
|
|
231
|
-
* @returns {Object} Paths to python and pip executables
|
|
232
|
-
*/
|
|
233
|
-
function getVenvPaths(venvPath) {
|
|
234
|
-
if (isWindows) {
|
|
235
|
-
return {
|
|
236
|
-
python: path.join(venvPath, 'Scripts', 'python.exe'),
|
|
237
|
-
pip: path.join(venvPath, 'Scripts', 'pip.exe')
|
|
238
|
-
};
|
|
239
|
-
} else {
|
|
240
|
-
return {
|
|
241
|
-
python: path.join(venvPath, 'bin', 'python'),
|
|
242
|
-
pip: path.join(venvPath, 'bin', 'pip')
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Create virtual environment if it doesn't exist
|
|
249
|
-
* @param {string} pythonCmd - Python command to use
|
|
250
|
-
* @returns {string} Path to virtual environment
|
|
251
|
-
*/
|
|
252
|
-
function ensureVenv(pythonCmd) {
|
|
253
|
-
const venvPath = path.join(projectRoot, '.venv');
|
|
254
|
-
|
|
255
|
-
if (fs.existsSync(venvPath)) {
|
|
256
|
-
log('✓ Virtual environment exists', 'green');
|
|
257
|
-
return venvPath;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
log('Creating Python virtual environment...', 'yellow');
|
|
261
|
-
|
|
262
|
-
const result = spawnSync(pythonCmd, ['-m', 'venv', venvPath], {
|
|
263
|
-
cwd: projectRoot,
|
|
264
|
-
stdio: ['ignore', 'pipe', 'inherit']
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
if (result.status !== 0) {
|
|
268
|
-
error('Failed to create virtual environment');
|
|
269
|
-
console.error('\nTroubleshooting:');
|
|
270
|
-
console.error(' 1. Ensure Python venv module is installed');
|
|
271
|
-
console.error(' 2. Check disk space and permissions');
|
|
272
|
-
console.error(' 3. Try manually: python3 -m venv .venv');
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
log('✓ Virtual environment created', 'green');
|
|
277
|
-
return venvPath;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Check if dependencies are installed
|
|
282
|
-
* @param {string} pipPath - Path to pip executable
|
|
283
|
-
* @returns {boolean} True if dependencies are installed
|
|
37
|
+
* Forward CLI arguments (e.g. --env-file ...)
|
|
284
38
|
*/
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
const result = spawnSync(pipPath, ['list'], {
|
|
288
|
-
stdio: 'pipe',
|
|
289
|
-
encoding: 'utf-8'
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (result.status === 0) {
|
|
293
|
-
const output = result.stdout || '';
|
|
294
|
-
// Check for key dependencies
|
|
295
|
-
return output.includes('python-jenkins') &&
|
|
296
|
-
output.includes('mcp') &&
|
|
297
|
-
output.includes('requests');
|
|
298
|
-
}
|
|
299
|
-
} catch (e) {
|
|
300
|
-
// If we can't check, assume not installed
|
|
301
|
-
}
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Show helpful guidance for proxy-related installation failures
|
|
307
|
-
*/
|
|
308
|
-
function showProxyTroubleshooting(activeProxies, canAccessDirectly, proxyWorks) {
|
|
309
|
-
console.error('\n' + '='.repeat(70));
|
|
310
|
-
console.error(colors.bold + colors.yellow + 'INSTALLATION FAILED - NETWORK CONFIGURATION ISSUE' + colors.reset);
|
|
311
|
-
console.error('='.repeat(70));
|
|
312
|
-
|
|
313
|
-
// Check npm and pip config
|
|
314
|
-
const npmProxy = checkNpmProxy();
|
|
315
|
-
const pipProxy = checkPipProxy();
|
|
316
|
-
|
|
317
|
-
if (npmProxy.found) {
|
|
318
|
-
console.error('\n' + colors.red + '⚠️ FOUND PROXY IN NPM CONFIG!' + colors.reset);
|
|
319
|
-
console.error(colors.cyan + npmProxy.config + colors.reset);
|
|
320
|
-
console.error('\n' + colors.yellow + 'This is likely the cause of your issue!' + colors.reset);
|
|
321
|
-
console.error('\n' + colors.bold + 'FIX (run these commands):' + colors.reset);
|
|
322
|
-
console.error(colors.cyan + ' npm config delete proxy' + colors.reset);
|
|
323
|
-
console.error(colors.cyan + ' npm config delete https-proxy' + colors.reset);
|
|
324
|
-
console.error(colors.cyan + ' npm config delete http-proxy' + colors.reset);
|
|
325
|
-
console.error(colors.cyan + ' npm config --global delete proxy' + colors.reset);
|
|
326
|
-
console.error(colors.cyan + ' npm config --global delete https-proxy' + colors.reset);
|
|
327
|
-
console.error('\nThen run your command again.\n');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (pipProxy.found) {
|
|
331
|
-
console.error('\n' + colors.red + '⚠️ FOUND PROXY IN PIP CONFIG!' + colors.reset);
|
|
332
|
-
console.error(colors.cyan + pipProxy.config + colors.reset);
|
|
333
|
-
console.error('\n' + colors.bold + 'FIX (run these commands):' + colors.reset);
|
|
334
|
-
console.error(colors.cyan + ' pip3 config unset global.proxy' + colors.reset);
|
|
335
|
-
console.error(colors.cyan + ' pip3 config unset user.proxy' + colors.reset);
|
|
336
|
-
console.error('\nOr edit/delete: ~/.config/pip/pip.conf\n');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (Object.keys(activeProxies).length > 0) {
|
|
340
|
-
console.error('\n📡 Active proxy environment variables found:');
|
|
341
|
-
for (const [key, value] of Object.entries(activeProxies)) {
|
|
342
|
-
console.error(` ${colors.cyan}${key}${colors.reset} = ${value}`);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (!proxyWorks && canAccessDirectly) {
|
|
346
|
-
// Proxy is set but doesn't work, and direct access works
|
|
347
|
-
console.error('\n' + colors.red + '❌ Proxy is NOT reachable' + colors.reset);
|
|
348
|
-
console.error(colors.green + '✓ Direct internet access IS available' + colors.reset);
|
|
349
|
-
console.error('\n' + colors.yellow + '⚠️ You\'re on a PUBLIC network but have proxy settings from a corporate/VPN network!' + colors.reset);
|
|
350
|
-
console.error('\n💡 SOLUTION:\n');
|
|
351
|
-
console.error(colors.bold + 'Remove the proxy settings:' + colors.reset);
|
|
352
|
-
console.error(colors.cyan + ' unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy ALL_PROXY all_proxy' + colors.reset);
|
|
353
|
-
console.error(' Then run the command again.\n');
|
|
354
|
-
|
|
355
|
-
} else if (proxyWorks && !canAccessDirectly) {
|
|
356
|
-
// Proxy works, direct access doesn't
|
|
357
|
-
console.error('\n' + colors.green + '✓ Proxy IS reachable' + colors.reset);
|
|
358
|
-
console.error(colors.red + '❌ Direct internet access is NOT available' + colors.reset);
|
|
359
|
-
console.error('\n' + colors.blue + 'You\'re on a CORPORATE network - proxy is required.' + colors.reset);
|
|
360
|
-
console.error('\n💡 SOLUTION:\n');
|
|
361
|
-
console.error('The proxy should work. The error may be due to:');
|
|
362
|
-
console.error('1. SSL certificate issues - contact your IT department');
|
|
363
|
-
console.error('2. Authentication required - check if proxy needs username/password');
|
|
364
|
-
console.error('3. Specific packages blocked - contact your IT department\n');
|
|
365
|
-
|
|
366
|
-
} else if (!proxyWorks && !canAccessDirectly) {
|
|
367
|
-
// Neither works
|
|
368
|
-
console.error('\n' + colors.red + '❌ Proxy is NOT reachable' + colors.reset);
|
|
369
|
-
console.error(colors.red + '❌ Direct internet access is NOT available' + colors.reset);
|
|
370
|
-
console.error('\n💡 SOLUTIONS:\n');
|
|
371
|
-
console.error('1. Check your internet connection');
|
|
372
|
-
console.error('2. If on corporate network, verify proxy settings with IT');
|
|
373
|
-
console.error('3. Try a different network (mobile hotspot, home WiFi)');
|
|
374
|
-
console.error('4. Check firewall/antivirus settings\n');
|
|
375
|
-
|
|
376
|
-
} else {
|
|
377
|
-
// Both work (unusual case)
|
|
378
|
-
console.error('\n' + colors.green + '✓ Proxy IS reachable' + colors.reset);
|
|
379
|
-
console.error(colors.green + '✓ Direct internet access IS available' + colors.reset);
|
|
380
|
-
console.error('\nThe issue may be:');
|
|
381
|
-
console.error('1. SSL certificate problems');
|
|
382
|
-
console.error('2. Intermittent connectivity');
|
|
383
|
-
console.error('3. Package-specific blocking\n');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
console.error(colors.bold + 'Additional options:' + colors.reset);
|
|
387
|
-
console.error('• Check system proxy settings:');
|
|
388
|
-
if (!isWindows) {
|
|
389
|
-
console.error(' macOS: System Settings → Network → Advanced → Proxies');
|
|
390
|
-
} else {
|
|
391
|
-
console.error(' Windows: Settings → Network & Internet → Proxy');
|
|
392
|
-
}
|
|
393
|
-
console.error('• Try manual installation (see TROUBLESHOOTING.md)');
|
|
394
|
-
|
|
395
|
-
} else {
|
|
396
|
-
console.error('\n💡 No proxy environment variables detected.\n');
|
|
397
|
-
console.error('POSSIBLE CAUSES:');
|
|
398
|
-
console.error('• Network connectivity issues');
|
|
399
|
-
console.error('• Firewall blocking PyPI access');
|
|
400
|
-
console.error('• DNS resolution problems');
|
|
401
|
-
console.error('• System-level proxy (not in environment variables)\n');
|
|
402
|
-
|
|
403
|
-
console.error('SOLUTIONS TO TRY:');
|
|
404
|
-
console.error('1. Check your internet connection');
|
|
405
|
-
console.error('2. Try: curl https://pypi.org/simple/');
|
|
406
|
-
console.error('3. Check firewall/antivirus settings');
|
|
407
|
-
console.error('4. Check system proxy settings (not environment variables)\n');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
console.error('='.repeat(70) + '\n');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Install Python dependencies with smart proxy detection
|
|
415
|
-
* @param {string} venvPath - Path to virtual environment
|
|
416
|
-
*/
|
|
417
|
-
function installDependencies(venvPath) {
|
|
418
|
-
const { pip } = getVenvPaths(venvPath);
|
|
419
|
-
const wheelsPath = path.join(projectRoot, 'wheels');
|
|
420
|
-
const requirementsPath = path.join(projectRoot, 'requirements.txt');
|
|
421
|
-
|
|
422
|
-
console.error('Installing Python dependencies...');
|
|
423
|
-
|
|
424
|
-
// Check if wheels directory exists (pre-packaged wheels)
|
|
425
|
-
if (fs.existsSync(wheelsPath)) {
|
|
426
|
-
console.error('Using pre-packaged wheels (no internet required)...');
|
|
427
|
-
|
|
428
|
-
const installReqs = spawnSync(pip, [
|
|
429
|
-
'install',
|
|
430
|
-
'--no-index',
|
|
431
|
-
'--find-links', wheelsPath,
|
|
432
|
-
'-r', requirementsPath
|
|
433
|
-
], {
|
|
434
|
-
cwd: projectRoot,
|
|
435
|
-
stdio: ['ignore', 'pipe', 'inherit']
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
if (installReqs.status !== 0) {
|
|
439
|
-
error('Failed to install from wheels');
|
|
440
|
-
process.exit(1);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
log('✓ Requirements installed', 'green');
|
|
444
|
-
} else {
|
|
445
|
-
// Need network - detect proxy configuration intelligently
|
|
446
|
-
console.error('Downloading from PyPI...');
|
|
447
|
-
|
|
448
|
-
const activeProxies = detectProxyConfig();
|
|
449
|
-
const hasProxyVars = Object.keys(activeProxies).length > 0;
|
|
450
|
-
|
|
451
|
-
let proxyToUse = null;
|
|
452
|
-
let useNoProxy = false;
|
|
453
|
-
|
|
454
|
-
if (hasProxyVars) {
|
|
455
|
-
const proxyUrl = process.env.HTTP_PROXY || process.env.http_proxy ||
|
|
456
|
-
process.env.HTTPS_PROXY || process.env.https_proxy;
|
|
457
|
-
|
|
458
|
-
info('Proxy environment variables detected. Testing connectivity...');
|
|
459
|
-
|
|
460
|
-
// Test proxy connectivity
|
|
461
|
-
const proxyWorks = proxyUrl && proxyUrl.startsWith('http') && testProxyConnectivity(proxyUrl);
|
|
462
|
-
const directWorks = testDirectPyPIAccess();
|
|
463
|
-
|
|
464
|
-
if (proxyWorks && !directWorks) {
|
|
465
|
-
// Corporate network - use proxy
|
|
466
|
-
info('Corporate network detected. Using proxy.');
|
|
467
|
-
proxyToUse = proxyUrl;
|
|
468
|
-
} else if (!proxyWorks && directWorks) {
|
|
469
|
-
// Public network with stale proxy vars - ignore proxy
|
|
470
|
-
warning('Proxy unreachable but direct access available. Ignoring proxy settings.');
|
|
471
|
-
useNoProxy = true;
|
|
472
|
-
} else if (proxyWorks && directWorks) {
|
|
473
|
-
// Both work - prefer direct
|
|
474
|
-
info('Both proxy and direct access available. Using direct connection.');
|
|
475
|
-
useNoProxy = true;
|
|
476
|
-
} else {
|
|
477
|
-
// Neither works - will fail but try direct
|
|
478
|
-
warning('Neither proxy nor direct access working. Attempting direct connection...');
|
|
479
|
-
useNoProxy = true;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Build pip arguments
|
|
484
|
-
const pipArgs = ['install', '-r', requirementsPath];
|
|
485
|
-
|
|
486
|
-
// Trusted hosts for SSL-friendly installation
|
|
487
|
-
const trustedHostArgs = [
|
|
488
|
-
'--trusted-host', 'pypi.org',
|
|
489
|
-
'--trusted-host', 'pypi.python.org',
|
|
490
|
-
'--trusted-host', 'files.pythonhosted.org'
|
|
491
|
-
];
|
|
492
|
-
pipArgs.push(...trustedHostArgs);
|
|
493
|
-
|
|
494
|
-
// Add proxy if needed
|
|
495
|
-
if (proxyToUse && !useNoProxy) {
|
|
496
|
-
info(`Using proxy: ${proxyToUse}`);
|
|
497
|
-
pipArgs.push('--proxy', proxyToUse);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Set up environment (potentially removing proxy vars)
|
|
501
|
-
const pipEnv = { ...process.env };
|
|
502
|
-
if (useNoProxy) {
|
|
503
|
-
// Remove proxy environment variables for this pip call
|
|
504
|
-
delete pipEnv.HTTP_PROXY;
|
|
505
|
-
delete pipEnv.HTTPS_PROXY;
|
|
506
|
-
delete pipEnv.http_proxy;
|
|
507
|
-
delete pipEnv.https_proxy;
|
|
508
|
-
delete pipEnv.ALL_PROXY;
|
|
509
|
-
delete pipEnv.all_proxy;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const installReqs = spawnSync(pip, pipArgs, {
|
|
513
|
-
cwd: projectRoot,
|
|
514
|
-
stdio: ['ignore', 'pipe', 'inherit'],
|
|
515
|
-
env: pipEnv
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
if (installReqs.status !== 0) {
|
|
519
|
-
error('Failed to install dependencies from PyPI');
|
|
520
|
-
|
|
521
|
-
// Show intelligent troubleshooting
|
|
522
|
-
const proxyUrl = process.env.HTTP_PROXY || process.env.http_proxy;
|
|
523
|
-
const proxyWorks = proxyUrl ? testProxyConnectivity(proxyUrl) : false;
|
|
524
|
-
const directWorks = testDirectPyPIAccess();
|
|
525
|
-
|
|
526
|
-
showProxyTroubleshooting(activeProxies, directWorks, proxyWorks);
|
|
527
|
-
process.exit(1);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
log('✓ Requirements installed', 'green');
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Install package itself
|
|
534
|
-
console.error('Installing jenkins-mcp-server package...');
|
|
535
|
-
|
|
536
|
-
// Verify pyproject.toml exists
|
|
537
|
-
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
538
|
-
if (!fs.existsSync(pyprojectPath)) {
|
|
539
|
-
error('pyproject.toml not found in project root');
|
|
540
|
-
console.error('\nProject root:', projectRoot);
|
|
541
|
-
console.error('Expected file:', pyprojectPath);
|
|
542
|
-
console.error('\nThis may be due to npx cache issues with special characters.');
|
|
543
|
-
console.error('Try clearing npx cache: npx clear-npx-cache');
|
|
544
|
-
process.exit(1);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const packageArgs = ['install', '-e', '.'];
|
|
548
|
-
|
|
549
|
-
const installPkg = spawnSync(pip, packageArgs, {
|
|
550
|
-
cwd: projectRoot,
|
|
551
|
-
stdio: ['ignore', 'pipe', 'inherit']
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
if (installPkg.status !== 0) {
|
|
555
|
-
error('Failed to install package');
|
|
556
|
-
console.error('\nProject root:', projectRoot);
|
|
557
|
-
console.error('Files in project root:');
|
|
558
|
-
try {
|
|
559
|
-
const files = fs.readdirSync(projectRoot);
|
|
560
|
-
console.error(files.join(', '));
|
|
561
|
-
} catch (e) {
|
|
562
|
-
console.error('Could not list files');
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
console.error('\n💡 TROUBLESHOOTING:');
|
|
566
|
-
console.error('1. The npx cache may have issues with the @ symbol in package name');
|
|
567
|
-
console.error('2. Try: npx clear-npx-cache && rm -rf ~/.npm/_npx');
|
|
568
|
-
console.error('3. Or install globally: npm install -g @rishibhushan/jenkins-mcp-server');
|
|
569
|
-
console.error(' Then run: jenkins-mcp-server --env-file /path/to/.env');
|
|
570
|
-
process.exit(1);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
log('✓ Package installed successfully', 'green');
|
|
574
|
-
}
|
|
39
|
+
const args = process.argv.slice(2);
|
|
575
40
|
|
|
576
41
|
/**
|
|
577
|
-
*
|
|
578
|
-
*
|
|
42
|
+
* Final Python command:
|
|
43
|
+
* python -m jenkins_mcp_server <args>
|
|
579
44
|
*/
|
|
580
|
-
|
|
581
|
-
|
|
45
|
+
const pythonArgs = [
|
|
46
|
+
"-m",
|
|
47
|
+
"jenkins_mcp_server",
|
|
48
|
+
...args,
|
|
49
|
+
];
|
|
582
50
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
type: 'module',
|
|
586
|
-
args: ['-m', 'jenkins_mcp_server']
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Fallback options
|
|
591
|
-
const fallbacks = [
|
|
592
|
-
path.join(projectRoot, 'src', 'jenkins_mcp_server', 'server.py'),
|
|
593
|
-
path.join(projectRoot, 'src', 'main.py'),
|
|
594
|
-
path.join(projectRoot, 'main.py')
|
|
595
|
-
];
|
|
596
|
-
|
|
597
|
-
for (const filePath of fallbacks) {
|
|
598
|
-
if (fs.existsSync(filePath)) {
|
|
599
|
-
return {
|
|
600
|
-
type: 'script',
|
|
601
|
-
args: [filePath]
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
error('Could not find Python entry point');
|
|
607
|
-
console.error('\nExpected one of:');
|
|
608
|
-
console.error(' - src/jenkins_mcp_server/__main__.py (preferred)');
|
|
609
|
-
console.error(' - src/jenkins_mcp_server/server.py');
|
|
610
|
-
console.error(' - src/main.py');
|
|
611
|
-
process.exit(1);
|
|
612
|
-
}
|
|
51
|
+
console.error("[jenkins-mcp] Python:", pythonPath);
|
|
52
|
+
console.error("[jenkins-mcp] Args:", pythonArgs.join(" "));
|
|
613
53
|
|
|
614
54
|
/**
|
|
615
|
-
*
|
|
616
|
-
* @param {string} venvPath - Path to virtual environment
|
|
55
|
+
* Spawn Python MCP server
|
|
617
56
|
*/
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// Set up environment
|
|
623
|
-
const env = {
|
|
57
|
+
const child = spawn(pythonPath, pythonArgs, {
|
|
58
|
+
cwd: PACKAGE_ROOT, // 🔑 critical
|
|
59
|
+
env: {
|
|
624
60
|
...process.env,
|
|
625
|
-
PYTHONPATH: path.join(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const serverArgs = [...entryPoint.args, ...args];
|
|
630
|
-
|
|
631
|
-
// All logging to stderr (stdout reserved for JSON-RPC)
|
|
632
|
-
console.error('=== NODE WRAPPER DEBUG ===');
|
|
633
|
-
console.error('Project root:', projectRoot);
|
|
634
|
-
console.error('Python path:', python);
|
|
635
|
-
console.error('Entry point:', entryPoint);
|
|
636
|
-
console.error('Server args:', serverArgs);
|
|
637
|
-
console.error('PYTHONPATH:', env.PYTHONPATH);
|
|
638
|
-
console.error('=== ATTEMPTING TO START PYTHON ===');
|
|
639
|
-
|
|
640
|
-
log('Starting Jenkins MCP Server...', 'green');
|
|
641
|
-
log(`Command: ${python} ${serverArgs.join(' ')}`, 'blue');
|
|
642
|
-
|
|
643
|
-
// CRITICAL: stdin=pipe, stdout=inherit (for JSON-RPC), stderr=inherit (for logs)
|
|
644
|
-
const server = spawn(python, serverArgs, {
|
|
645
|
-
cwd: projectRoot,
|
|
646
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
647
|
-
env: env,
|
|
648
|
-
shell: isWindows
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
server.on('error', (err) => {
|
|
652
|
-
console.error('=== SPAWN ERROR ===', err);
|
|
653
|
-
error(`Failed to start server: ${err.message}`);
|
|
654
|
-
process.exit(1);
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
server.on('spawn', () => {
|
|
658
|
-
console.error('=== PYTHON PROCESS SPAWNED ===');
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
server.on('close', (code, signal) => {
|
|
662
|
-
console.error(`=== PYTHON PROCESS CLOSED: code=${code}, signal=${signal} ===`);
|
|
663
|
-
if (code !== 0 && code !== null) {
|
|
664
|
-
log(`Server exited with code ${code}`, 'yellow');
|
|
665
|
-
}
|
|
666
|
-
process.exit(code || 0);
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
// Handle graceful shutdown
|
|
670
|
-
const cleanup = () => {
|
|
671
|
-
log('\nShutting down...', 'yellow');
|
|
672
|
-
server.kill('SIGTERM');
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
process.on('SIGINT', cleanup);
|
|
676
|
-
process.on('SIGTERM', cleanup);
|
|
677
|
-
|
|
678
|
-
// Windows doesn't support SIGINT/SIGTERM the same way
|
|
679
|
-
if (isWindows) {
|
|
680
|
-
process.on('SIGBREAK', cleanup);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
61
|
+
PYTHONPATH: path.join(PACKAGE_ROOT, "src"), // 🔑 critical
|
|
62
|
+
},
|
|
63
|
+
stdio: ["inherit", "inherit", "inherit"], // MCP stdio
|
|
64
|
+
});
|
|
683
65
|
|
|
684
66
|
/**
|
|
685
|
-
*
|
|
67
|
+
* Propagate exit code
|
|
686
68
|
*/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
691
|
-
console.error('Jenkins MCP Server - Node.js Wrapper');
|
|
692
|
-
console.error('\nUsage: jenkins-mcp-server [options]');
|
|
693
|
-
console.error('\nOptions:');
|
|
694
|
-
console.error(' --env-file PATH Path to custom .env file');
|
|
695
|
-
console.error(' --verbose, -v Enable verbose logging');
|
|
696
|
-
console.error(' --no-vscode Skip loading VS Code settings');
|
|
697
|
-
console.error(' --version Show version');
|
|
698
|
-
console.error(' --help, -h Show this help message');
|
|
699
|
-
process.exit(0);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Check Python availability
|
|
703
|
-
const pythonCmd = checkPython();
|
|
704
|
-
|
|
705
|
-
// Ensure virtual environment exists
|
|
706
|
-
const venvPath = ensureVenv(pythonCmd);
|
|
707
|
-
|
|
708
|
-
// Check and install dependencies if needed
|
|
709
|
-
const { pip } = getVenvPaths(venvPath);
|
|
710
|
-
if (!dependenciesInstalled(pip)) {
|
|
711
|
-
installDependencies(venvPath);
|
|
712
|
-
} else {
|
|
713
|
-
log('✓ Dependencies already installed', 'green');
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Run the server
|
|
717
|
-
runServer(venvPath);
|
|
718
|
-
|
|
719
|
-
} catch (err) {
|
|
720
|
-
error(`Unexpected error: ${err.message}`);
|
|
721
|
-
console.error(err.stack);
|
|
69
|
+
child.on("exit", (code, signal) => {
|
|
70
|
+
if (signal) {
|
|
71
|
+
console.error("[jenkins-mcp] exited due to signal:", signal);
|
|
722
72
|
process.exit(1);
|
|
723
73
|
}
|
|
724
|
-
|
|
74
|
+
process.exit(code ?? 0);
|
|
75
|
+
});
|
|
725
76
|
|
|
726
|
-
|
|
727
|
-
|
|
77
|
+
child.on("error", (err) => {
|
|
78
|
+
console.error("[jenkins-mcp] failed to start:", err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
package/package.json
CHANGED
package/requirements.txt
CHANGED