@rishibhushan/jenkins-mcp-server 1.0.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 +575 -0
- package/bin/jenkins-mcp.js +327 -0
- package/package.json +51 -0
- package/requirements.txt +12 -0
- package/src/jenkins_mcp_server/__init__.py +133 -0
- package/src/jenkins_mcp_server/__main__.py +19 -0
- package/src/jenkins_mcp_server/config.py +278 -0
- package/src/jenkins_mcp_server/jenkins_client.py +516 -0
- package/src/jenkins_mcp_server/server.py +1037 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jenkins MCP Server Configuration Module
|
|
3
|
+
|
|
4
|
+
Handles loading Jenkins connection settings from multiple sources:
|
|
5
|
+
1. VS Code settings.json (highest priority)
|
|
6
|
+
2. Environment variables / .env file
|
|
7
|
+
3. Direct instantiation with parameters
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
from pydantic import Field, field_validator
|
|
17
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
18
|
+
|
|
19
|
+
# Configure logging
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JenkinsSettings(BaseSettings):
|
|
24
|
+
"""
|
|
25
|
+
Jenkins connection settings with support for multiple configuration sources.
|
|
26
|
+
|
|
27
|
+
Priority order:
|
|
28
|
+
1. Directly passed parameters
|
|
29
|
+
2. VS Code settings
|
|
30
|
+
3. Environment variables
|
|
31
|
+
4. .env file
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Use 'url' as the primary field name, but accept 'jenkins_url' as alias
|
|
35
|
+
url: Optional[str] = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
alias="jenkins_url",
|
|
38
|
+
description="Jenkins server URL (e.g., http://localhost:8080)"
|
|
39
|
+
)
|
|
40
|
+
username: Optional[str] = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description="Jenkins username"
|
|
43
|
+
)
|
|
44
|
+
password: Optional[str] = Field(
|
|
45
|
+
default=None,
|
|
46
|
+
description="Jenkins password"
|
|
47
|
+
)
|
|
48
|
+
token: Optional[str] = Field(
|
|
49
|
+
default=None,
|
|
50
|
+
description="Jenkins API token (preferred over password)"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
model_config = SettingsConfigDict(
|
|
54
|
+
env_prefix="JENKINS_",
|
|
55
|
+
env_file_encoding="utf-8",
|
|
56
|
+
case_sensitive=False,
|
|
57
|
+
populate_by_name=True, # Allow both 'url' and 'jenkins_url'
|
|
58
|
+
extra="ignore"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@field_validator('url')
|
|
62
|
+
@classmethod
|
|
63
|
+
def strip_trailing_slash(cls, v: Optional[str]) -> Optional[str]:
|
|
64
|
+
"""Remove trailing slash from URL"""
|
|
65
|
+
if v:
|
|
66
|
+
return v.rstrip('/')
|
|
67
|
+
return v
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_configured(self) -> bool:
|
|
71
|
+
"""Check if minimum required settings are present"""
|
|
72
|
+
return bool(self.url and self.username and (self.token or self.password))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def auth_method(self) -> str:
|
|
76
|
+
"""Return the authentication method being used"""
|
|
77
|
+
if self.token:
|
|
78
|
+
return "API Token"
|
|
79
|
+
elif self.password:
|
|
80
|
+
return "Password"
|
|
81
|
+
return "None"
|
|
82
|
+
|
|
83
|
+
def get_credentials(self) -> tuple[Optional[str], Optional[str]]:
|
|
84
|
+
"""Return (username, password/token) tuple for authentication"""
|
|
85
|
+
if self.username:
|
|
86
|
+
auth_value = self.token if self.token else self.password
|
|
87
|
+
return (self.username, auth_value)
|
|
88
|
+
return (None, None)
|
|
89
|
+
|
|
90
|
+
def log_config(self, hide_sensitive: bool = True) -> None:
|
|
91
|
+
"""Log current configuration (with optional masking of sensitive data)"""
|
|
92
|
+
logger.info("Jenkins Configuration:")
|
|
93
|
+
logger.info(f" URL: {self.url or 'Not configured'}")
|
|
94
|
+
logger.info(f" Username: {self.username or 'Not configured'}")
|
|
95
|
+
|
|
96
|
+
if hide_sensitive:
|
|
97
|
+
logger.info(f" Authentication: {self.auth_method}")
|
|
98
|
+
else:
|
|
99
|
+
logger.info(f" Token: {self.token or 'Not set'}")
|
|
100
|
+
logger.info(f" Password: {self.password or 'Not set'}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class VSCodeSettingsLoader:
|
|
104
|
+
"""Handles loading Jenkins settings from VS Code settings.json files"""
|
|
105
|
+
|
|
106
|
+
# Standard VS Code settings paths by platform
|
|
107
|
+
VSCODE_PATHS = [
|
|
108
|
+
# Workspace settings (highest priority if exists)
|
|
109
|
+
Path.cwd() / ".vscode/settings.json",
|
|
110
|
+
# User settings (platform-specific)
|
|
111
|
+
Path.home() / "Library/Application Support/Code/User/settings.json", # macOS
|
|
112
|
+
Path.home() / "Library/Application Support/Code - Insiders/User/settings.json",
|
|
113
|
+
Path.home() / ".config/Code/User/settings.json", # Linux
|
|
114
|
+
Path.home() / ".config/Code - Insiders/User/settings.json",
|
|
115
|
+
Path.home() / "AppData/Roaming/Code/User/settings.json", # Windows
|
|
116
|
+
Path.home() / "AppData/Roaming/Code - Insiders/User/settings.json",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def parse_jsonc(content: str) -> Dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Parse JSON with comments (JSONC).
|
|
123
|
+
|
|
124
|
+
Removes single-line (//) and multi-line (/* */) comments before parsing.
|
|
125
|
+
"""
|
|
126
|
+
if not content or not content.strip():
|
|
127
|
+
return {}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Try direct JSON parse first (fastest path)
|
|
131
|
+
return json.loads(content)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# Remove comments
|
|
136
|
+
# Remove single-line comments
|
|
137
|
+
content = re.sub(r'//.*$', '', content, flags=re.MULTILINE)
|
|
138
|
+
# Remove multi-line comments
|
|
139
|
+
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
|
|
140
|
+
|
|
141
|
+
# Clean up trailing commas (common JSONC pattern)
|
|
142
|
+
content = re.sub(r',(\s*[}\]])', r'\1', content)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
return json.loads(content)
|
|
146
|
+
except json.JSONDecodeError as e:
|
|
147
|
+
logger.warning(f"Failed to parse JSONC: {e}")
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def find_jenkins_settings(cls, settings: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
152
|
+
"""
|
|
153
|
+
Extract Jenkins settings from VS Code settings dictionary.
|
|
154
|
+
|
|
155
|
+
Supports two configuration patterns:
|
|
156
|
+
1. "jenkins-mcp-server": { "jenkins": {...} }
|
|
157
|
+
2. "mcp": { "servers": { "jenkins-mcp-server": { "jenkinsConfig": {...} } } }
|
|
158
|
+
"""
|
|
159
|
+
# Pattern 1: Direct jenkins-mcp-server.jenkins
|
|
160
|
+
jenkins_config = settings.get("jenkins-mcp-server", {}).get("jenkins", {})
|
|
161
|
+
if jenkins_config:
|
|
162
|
+
return jenkins_config
|
|
163
|
+
|
|
164
|
+
# Pattern 2: MCP servers configuration
|
|
165
|
+
mcp_config = (
|
|
166
|
+
settings.get("mcp", {})
|
|
167
|
+
.get("servers", {})
|
|
168
|
+
.get("jenkins-mcp-server", {})
|
|
169
|
+
.get("jenkinsConfig", {})
|
|
170
|
+
)
|
|
171
|
+
if mcp_config:
|
|
172
|
+
return mcp_config
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def load(cls) -> Optional[Dict[str, Any]]:
|
|
178
|
+
"""
|
|
179
|
+
Load Jenkins settings from VS Code settings files.
|
|
180
|
+
|
|
181
|
+
Returns the first valid configuration found, or None.
|
|
182
|
+
"""
|
|
183
|
+
for settings_path in cls.VSCODE_PATHS:
|
|
184
|
+
if not settings_path.exists():
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
logger.debug(f"Checking VS Code settings: {settings_path}")
|
|
189
|
+
content = settings_path.read_text(encoding='utf-8')
|
|
190
|
+
settings = cls.parse_jsonc(content)
|
|
191
|
+
|
|
192
|
+
jenkins_settings = cls.find_jenkins_settings(settings)
|
|
193
|
+
if jenkins_settings:
|
|
194
|
+
logger.info(f"Loaded Jenkins settings from: {settings_path}")
|
|
195
|
+
return jenkins_settings
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.debug(f"Error reading {settings_path}: {e}")
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
logger.debug("No Jenkins settings found in VS Code configuration")
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_settings(
|
|
206
|
+
env_file: Optional[str] = None,
|
|
207
|
+
load_vscode: bool = True,
|
|
208
|
+
**override_values
|
|
209
|
+
) -> JenkinsSettings:
|
|
210
|
+
"""
|
|
211
|
+
Load Jenkins settings from all available sources.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
env_file: Optional path to .env file (overrides default .env)
|
|
215
|
+
load_vscode: Whether to load from VS Code settings (default: True)
|
|
216
|
+
**override_values: Direct override values (highest priority)
|
|
217
|
+
|
|
218
|
+
Priority order:
|
|
219
|
+
1. override_values (passed as kwargs)
|
|
220
|
+
2. VS Code settings (if load_vscode=True)
|
|
221
|
+
3. Environment variables / .env file
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
JenkinsSettings instance with merged configuration
|
|
225
|
+
"""
|
|
226
|
+
# Start with environment variables and .env file
|
|
227
|
+
if env_file:
|
|
228
|
+
settings = JenkinsSettings(_env_file=env_file)
|
|
229
|
+
else:
|
|
230
|
+
settings = JenkinsSettings()
|
|
231
|
+
|
|
232
|
+
# Override with VS Code settings if requested
|
|
233
|
+
if load_vscode:
|
|
234
|
+
vscode_settings = VSCodeSettingsLoader.load()
|
|
235
|
+
if vscode_settings:
|
|
236
|
+
# Merge VS Code settings into our settings object
|
|
237
|
+
for key in ['url', 'username', 'password', 'token']:
|
|
238
|
+
vscode_value = vscode_settings.get(key)
|
|
239
|
+
if vscode_value is not None:
|
|
240
|
+
setattr(settings, key, vscode_value)
|
|
241
|
+
|
|
242
|
+
# Apply direct overrides (highest priority)
|
|
243
|
+
for key, value in override_values.items():
|
|
244
|
+
if value is not None and hasattr(settings, key):
|
|
245
|
+
setattr(settings, key, value)
|
|
246
|
+
|
|
247
|
+
# Log final configuration
|
|
248
|
+
settings.log_config()
|
|
249
|
+
|
|
250
|
+
return settings
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Factory function for backward compatibility
|
|
254
|
+
def get_settings(
|
|
255
|
+
env_file: Optional[str] = None,
|
|
256
|
+
load_vscode: bool = True,
|
|
257
|
+
**kwargs
|
|
258
|
+
) -> JenkinsSettings:
|
|
259
|
+
"""
|
|
260
|
+
Get Jenkins settings instance.
|
|
261
|
+
|
|
262
|
+
This is the recommended way to obtain settings in the application.
|
|
263
|
+
Each call returns a fresh instance with current configuration.
|
|
264
|
+
"""
|
|
265
|
+
return load_settings(env_file=env_file, load_vscode=load_vscode, **kwargs)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# For backward compatibility with code that expects a global settings object
|
|
269
|
+
# Note: This is lazily evaluated when first accessed
|
|
270
|
+
_default_settings: Optional[JenkinsSettings] = None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def get_default_settings() -> JenkinsSettings:
|
|
274
|
+
"""Get or create the default settings instance (singleton pattern)"""
|
|
275
|
+
global _default_settings
|
|
276
|
+
if _default_settings is None:
|
|
277
|
+
_default_settings = load_settings()
|
|
278
|
+
return _default_settings
|