@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.
@@ -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