@raghulm/aegis-mcp 1.0.3 → 1.0.5

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  <div align="center">
2
+ <img src="docs/aegis-mcp-logo-v2.svg" width="100%" alt="Aegis MCP logo"/>
2
3
  <h1>🛡️ Aegis MCP Server</h1>
3
4
  <p><b>Aegis MCP is an open-source, DevSecOps-focused Model Context Protocol server that allows AI agents to safely interact with cloud infrastructure, CI/CD systems, and security tooling.</b></p>
4
5
 
@@ -134,12 +135,12 @@ MEDIUM: 7
134
135
 
135
136
  ## 🚀 Getting Started
136
137
 
137
- ### Prerequisites
138
-
139
- - **Python 3.12+**
140
- - **Node.js 18+** (only if you want to run via npm/npx)
141
- - **Semgrep** — `pip install semgrep` (for SAST scanning)
142
- - Optional: AWS CLI / `boto3`, `kubectl`, Trivy (for their respective tools)
138
+ ### Prerequisites
139
+
140
+ - **Python 3.12+**
141
+ - **Node.js 18+** (only if you want to run via npm/npx)
142
+ - **Semgrep** — `pip install semgrep` (for SAST scanning)
143
+ - Optional: AWS CLI / `boto3`, `kubectl`, Trivy (for their respective tools)
143
144
 
144
145
  ### Installation
145
146
 
@@ -157,20 +158,20 @@ source .venv/bin/activate
157
158
  .venv\Scripts\activate
158
159
 
159
160
  # Install dependencies
160
- pip install -r requirements.txt
161
- ```
162
-
163
- ### Install via npm (Public Package)
164
-
165
- ```bash
166
- npm install -g @raghulm/aegis-mcp
167
- # or run without installing globally:
168
- npx -y @raghulm/aegis-mcp
169
- ```
170
-
171
- On first run, the npm wrapper creates a local Python virtual environment and installs dependencies from `requirements.txt` automatically.
172
-
173
- ---
161
+ pip install -r requirements.txt
162
+ ```
163
+
164
+ ### Install via npm (Public Package)
165
+
166
+ ```bash
167
+ npm install -g @raghulm/aegis-mcp
168
+ # or run without installing globally:
169
+ npx -y @raghulm/aegis-mcp
170
+ ```
171
+
172
+ On first run, the npm wrapper creates a local Python virtual environment and installs dependencies from `requirements.txt` automatically.
173
+
174
+ ---
174
175
 
175
176
  ## 🤖 Usage with AI Agents
176
177
 
@@ -178,16 +179,16 @@ On first run, the npm wrapper creates a local Python virtual environment and ins
178
179
 
179
180
  Add to your MCP config (e.g., `mcp_config.json`):
180
181
 
181
- ```json
182
- {
183
- "mcpServers": {
184
- "aegis": {
185
- "command": "npx",
186
- "args": ["-y", "@raghulm/aegis-mcp"]
187
- }
188
- }
189
- }
190
- ```
182
+ ```json
183
+ {
184
+ "mcpServers": {
185
+ "aegis": {
186
+ "command": "npx",
187
+ "args": ["-y", "@raghulm/aegis-mcp"]
188
+ }
189
+ }
190
+ }
191
+ ```
191
192
 
192
193
  > ✅ **All 12 tools work**, including Semgrep SAST.
193
194
 
@@ -197,16 +198,16 @@ Add to `claude_desktop_config.json`:
197
198
  - **Windows**: `%LOCALAPPDATA%\Packages\Claude_...\LocalCache\Roaming\Claude\`
198
199
  - **Mac**: `~/Library/Application Support/Claude/`
199
200
 
200
- ```json
201
- {
202
- "mcpServers": {
203
- "aegis": {
204
- "command": "npx",
205
- "args": ["-y", "@raghulm/aegis-mcp"]
206
- }
207
- }
208
- }
209
- ```
201
+ ```json
202
+ {
203
+ "mcpServers": {
204
+ "aegis": {
205
+ "command": "npx",
206
+ "args": ["-y", "@raghulm/aegis-mcp"]
207
+ }
208
+ }
209
+ }
210
+ ```
210
211
 
211
212
  > ⚠️ **11 of 12 tools work.** Semgrep SAST does not work due to Windows pipe limitations.
212
213
 
@@ -301,41 +302,19 @@ The `@audit_tool_call` decorator emits structured JSON logs for every invocation
301
302
 
302
303
  ---
303
304
 
304
- ## 🛣️ Roadmap
305
-
306
- - [ ] Terraform security scanner
307
- - [ ] IAM policy risk detection
308
- - [ ] Kubernetes misconfiguration scanner (Basic `k8s_security_audit` implemented!)
309
- - [ ] GitHub Actions security audit
310
- - [ ] Cloud cost analysis tools
311
-
312
- ---
313
-
314
- ## 📦 Publish to npm
315
-
316
- ```bash
317
- # 1) Login to npm
318
- npm login
319
-
320
- # 2) Verify package contents
321
- npm run pack:check
322
-
323
- # 3) Publish publicly
324
- npm publish --access public
325
- ```
326
-
327
- For scoped packages like `@raghulm/aegis-mcp`, keep `--access public` in the publish command.
328
-
329
- ---
330
-
331
- ## 🤝 Contributing
332
-
333
- 1. Fork the project
334
- 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
335
- 3. Add your tool into `tools/<domain>/`
336
- 4. Register it via `@mcp.tool()` in `server/main.py` with `@audit_tool_call` and auth check
337
- 5. Add tests in `tests/`
338
- 6. Open a Pull Request
305
+ ## 🛣️ Roadmap
306
+
307
+ - [ ] Terraform security scanner
308
+ - [ ] IAM policy risk detection
309
+ - [ ] Kubernetes misconfiguration scanner (Basic `k8s_security_audit` implemented!)
310
+ - [ ] GitHub Actions security audit
311
+ - [ ] Cloud cost analysis tools
312
+
313
+ ---
314
+
315
+ ## 🤝 Contributing
316
+
317
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution and maintainer release workflows.
339
318
 
340
319
  ---
341
320
 
@@ -3,21 +3,42 @@
3
3
 
4
4
  const crypto = require("crypto");
5
5
  const fs = require("fs");
6
+ const os = require("os");
6
7
  const path = require("path");
7
8
  const { spawnSync } = require("child_process");
8
9
 
9
10
  const PROJECT_ROOT = path.resolve(__dirname, "..");
10
11
  const REQUIREMENTS_FILE = path.join(PROJECT_ROOT, "requirements.txt");
11
- const VENV_DIR = path.join(PROJECT_ROOT, ".venv");
12
- const REQUIREMENTS_STAMP = path.join(VENV_DIR, ".requirements.sha256");
12
+ const RUNTIME_ROOT = process.env.AEGIS_HOME || path.join(os.homedir(), ".aegis-mcp");
13
+ const VENV_DIR = path.join(RUNTIME_ROOT, "venv");
14
+ const REQUIREMENTS_STAMP = path.join(RUNTIME_ROOT, ".requirements.sha256");
13
15
  const IS_WINDOWS = process.platform === "win32";
16
+ const DEBUG = process.env.AEGIS_DEBUG === "1";
14
17
 
15
18
  function run(command, args, options = {}) {
16
- return spawnSync(command, args, {
19
+ const result = spawnSync(command, args, {
17
20
  cwd: PROJECT_ROOT,
18
- stdio: "inherit",
21
+ encoding: "utf8",
22
+ stdio: ["ignore", "pipe", "pipe"],
19
23
  ...options
20
24
  });
25
+
26
+ if (DEBUG && result.stdout) {
27
+ process.stderr.write(result.stdout);
28
+ }
29
+ if (DEBUG && result.stderr) {
30
+ process.stderr.write(result.stderr);
31
+ }
32
+
33
+ return result;
34
+ }
35
+
36
+ function throwCommandError(result, contextMessage) {
37
+ const output = `${result?.stdout || ""}\n${result?.stderr || ""}`.trim();
38
+ if (output) {
39
+ process.stderr.write(`${output}\n`);
40
+ }
41
+ throw new Error(contextMessage);
21
42
  }
22
43
 
23
44
  function candidatePythonCommands() {
@@ -43,10 +64,7 @@ function candidatePythonCommands() {
43
64
 
44
65
  function findWorkingPython() {
45
66
  for (const candidate of candidatePythonCommands()) {
46
- const versionCheck = run(candidate.command, [...candidate.prefixArgs, "--version"], {
47
- stdio: "pipe",
48
- encoding: "utf8"
49
- });
67
+ const versionCheck = run(candidate.command, [...candidate.prefixArgs, "--version"]);
50
68
  if (versionCheck.status === 0) {
51
69
  return candidate;
52
70
  }
@@ -77,29 +95,44 @@ function requirementsHash() {
77
95
  }
78
96
 
79
97
  function ensureVirtualEnvironment(python) {
98
+ fs.mkdirSync(RUNTIME_ROOT, { recursive: true });
99
+
80
100
  const pythonInVenv = venvPythonPath();
81
101
  if (fs.existsSync(pythonInVenv)) {
82
102
  return;
83
103
  }
84
104
 
85
- console.error("[aegis-mcp] Creating Python virtual environment...");
86
105
  const created = run(python.command, [...python.prefixArgs, "-m", "venv", VENV_DIR]);
87
106
  if (created.status !== 0) {
88
- throw new Error("Failed to create Python virtual environment.");
107
+ throwCommandError(created, "Failed to create Python virtual environment.");
89
108
  }
90
109
  }
91
110
 
92
111
  function installDependencies(pythonInVenv) {
93
- console.error("[aegis-mcp] Installing Python dependencies from requirements.txt...");
94
-
95
- const pipUpgrade = run(pythonInVenv, ["-m", "pip", "install", "--upgrade", "pip"]);
112
+ const pipUpgrade = run(pythonInVenv, [
113
+ "-m",
114
+ "pip",
115
+ "install",
116
+ "--disable-pip-version-check",
117
+ "--quiet",
118
+ "--upgrade",
119
+ "pip"
120
+ ]);
96
121
  if (pipUpgrade.status !== 0) {
97
- throw new Error("Failed to upgrade pip in virtual environment.");
122
+ throwCommandError(pipUpgrade, "Failed to upgrade pip in virtual environment.");
98
123
  }
99
124
 
100
- const pipInstall = run(pythonInVenv, ["-m", "pip", "install", "-r", REQUIREMENTS_FILE]);
125
+ const pipInstall = run(pythonInVenv, [
126
+ "-m",
127
+ "pip",
128
+ "install",
129
+ "--disable-pip-version-check",
130
+ "--quiet",
131
+ "-r",
132
+ REQUIREMENTS_FILE
133
+ ]);
101
134
  if (pipInstall.status !== 0) {
102
- throw new Error("Failed to install Python dependencies.");
135
+ throwCommandError(pipInstall, "Failed to install Python dependencies.");
103
136
  }
104
137
  }
105
138
 
package/package.json CHANGED
@@ -1,50 +1,54 @@
1
- {
2
- "name": "@raghulm/aegis-mcp",
3
- "version": "1.0.3",
4
- "description": "DevSecOps-focused MCP server for AWS, Kubernetes, CI/CD, and security tooling.",
5
- "license": "MIT",
6
- "author": "Raghul M",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/raghulvj01/aegis-mcp.git"
10
- },
11
- "bugs": {
12
- "url": "https://github.com/raghulvj01/aegis-mcp/issues"
13
- },
14
- "homepage": "https://github.com/raghulvj01/aegis-mcp#readme",
15
- "type": "commonjs",
16
- "bin": {
17
- "aegis-mcp": "bin/aegis-mcp.js"
18
- },
19
- "scripts": {
20
- "setup:python": "node ./bin/prepare-python-env.js",
21
- "start": "node ./bin/aegis-mcp.js",
22
- "pack:check": "npm pack --dry-run"
23
- },
24
- "files": [
25
- "audit/**/*.py",
26
- "bin/*.js",
27
- "policies/*.yaml",
28
- "server/**/*.py",
29
- "tools/**/*.py",
30
- "requirements.txt",
31
- "run_stdio.py",
32
- "README.md",
33
- "LICENSE"
34
- ],
35
- "keywords": [
36
- "mcp",
37
- "model-context-protocol",
38
- "devsecops",
39
- "security",
40
- "aws",
41
- "kubernetes",
42
- "claude"
43
- ],
44
- "publishConfig": {
45
- "access": "public"
46
- },
47
- "engines": {
48
- "node": ">=18"
49
- }
50
- }
1
+ {
2
+ "name": "@raghulm/aegis-mcp",
3
+ "version": "1.0.5",
4
+ "description": "DevSecOps-focused MCP server for AWS, Kubernetes, CI/CD, and security tooling.",
5
+ "license": "MIT",
6
+ "author": "Raghul M",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/raghulvj01/aegis-mcp.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/raghulvj01/aegis-mcp/issues"
13
+ },
14
+ "homepage": "https://github.com/raghulvj01/aegis-mcp#readme",
15
+ "type": "commonjs",
16
+ "bin": {
17
+ "aegis-mcp": "bin/aegis-mcp.js"
18
+ },
19
+ "scripts": {
20
+ "setup:python": "node ./bin/prepare-python-env.js",
21
+ "start": "node ./bin/aegis-mcp.js",
22
+ "pack:check": "npm pack --dry-run"
23
+ },
24
+ "files": [
25
+ "audit/**/*.py",
26
+ "bin/*.js",
27
+ "policies/*.yaml",
28
+ "server/**/*.py",
29
+ "tools/**/*.py",
30
+ "requirements.txt",
31
+ "run_stdio.py",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "keywords": [
36
+ "mcp",
37
+ "model-context-protocol",
38
+ "devsecops",
39
+ "security",
40
+ "aws",
41
+ "kubernetes",
42
+ "claude",
43
+ "jenkins"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "dependencies": {
52
+ "@raghulm/aegis-mcp": "^1.0.5"
53
+ }
54
+ }
package/requirements.txt CHANGED
@@ -5,3 +5,4 @@ fastapi>=0.111.0
5
5
  uvicorn>=0.30.0
6
6
  pyyaml>=6.0.1
7
7
  semgrep>=1.60.0
8
+ python-jenkins>=1.8.0
package/server/main.py CHANGED
@@ -10,6 +10,15 @@ from server.config import load_role_policies, load_scope_policies, load_settings
10
10
  from tools.aws.ec2 import list_ec2_instances
11
11
  from tools.aws.s3 import check_s3_public_access
12
12
  from tools.cicd.pipeline import pipeline_status
13
+ from tools.cicd.jenkins import (
14
+ jenkins_list_jobs as _jenkins_list_jobs,
15
+ jenkins_get_job_info as _jenkins_get_job_info,
16
+ jenkins_create_job as _jenkins_create_job,
17
+ jenkins_trigger_build as _jenkins_trigger_build,
18
+ jenkins_get_build_info as _jenkins_get_build_info,
19
+ jenkins_get_build_log as _jenkins_get_build_log,
20
+ jenkins_delete_job as _jenkins_delete_job,
21
+ )
13
22
  from tools.git.repo import get_recent_commits
14
23
  from tools.kubernetes.pods import list_pods
15
24
  from tools.kubernetes.audit import k8s_security_audit
@@ -140,5 +149,64 @@ def security_semgrep_scan(path: str, config: str = "auto", token: str = "") -> d
140
149
  return run_semgrep_scan(path, config)
141
150
 
142
151
 
152
+ # ── Jenkins CI/CD tools ────────────────────────────────────────────
153
+
154
+
155
+ @mcp.tool()
156
+ @audit_tool_call("jenkins_list_jobs")
157
+ def jenkins_list_jobs(url: str, username: str, api_token: str, token: str = "") -> list[dict]:
158
+ """List all jobs on a Jenkins server. Provide the Jenkins URL, username, and API token."""
159
+ _authorize(token, "jenkins_list_jobs")
160
+ return _jenkins_list_jobs(url, username, api_token)
161
+
162
+
163
+ @mcp.tool()
164
+ @audit_tool_call("jenkins_get_job_info")
165
+ def jenkins_get_job_info(url: str, username: str, api_token: str, job_name: str, token: str = "") -> dict:
166
+ """Get detailed information about a specific Jenkins job including build history and health."""
167
+ _authorize(token, "jenkins_get_job_info")
168
+ return _jenkins_get_job_info(url, username, api_token, job_name)
169
+
170
+
171
+ @mcp.tool()
172
+ @audit_tool_call("jenkins_create_job")
173
+ def jenkins_create_job(url: str, username: str, api_token: str, job_name: str, config_xml: str = "", token: str = "") -> dict:
174
+ """Create a new Jenkins job. Optionally provide config_xml for the job definition; otherwise a minimal freestyle project is created."""
175
+ _authorize(token, "jenkins_create_job")
176
+ return _jenkins_create_job(url, username, api_token, job_name, config_xml)
177
+
178
+
179
+ @mcp.tool()
180
+ @audit_tool_call("jenkins_trigger_build")
181
+ def jenkins_trigger_build(url: str, username: str, api_token: str, job_name: str, parameters: str = "", token: str = "") -> dict:
182
+ """Trigger a build for an existing Jenkins job. Optionally pass parameters as a JSON string, e.g. '{"BRANCH": "main"}'."""
183
+ _authorize(token, "jenkins_trigger_build")
184
+ return _jenkins_trigger_build(url, username, api_token, job_name, parameters)
185
+
186
+
187
+ @mcp.tool()
188
+ @audit_tool_call("jenkins_get_build_info")
189
+ def jenkins_get_build_info(url: str, username: str, api_token: str, job_name: str, build_number: int, token: str = "") -> dict:
190
+ """Get information about a specific Jenkins build including result, duration, and status."""
191
+ _authorize(token, "jenkins_get_build_info")
192
+ return _jenkins_get_build_info(url, username, api_token, job_name, build_number)
193
+
194
+
195
+ @mcp.tool()
196
+ @audit_tool_call("jenkins_get_build_log")
197
+ def jenkins_get_build_log(url: str, username: str, api_token: str, job_name: str, build_number: int, token: str = "") -> dict:
198
+ """Fetch the console output of a Jenkins build."""
199
+ _authorize(token, "jenkins_get_build_log")
200
+ return _jenkins_get_build_log(url, username, api_token, job_name, build_number)
201
+
202
+
203
+ @mcp.tool()
204
+ @audit_tool_call("jenkins_delete_job")
205
+ def jenkins_delete_job(url: str, username: str, api_token: str, job_name: str, token: str = "") -> dict:
206
+ """Delete a Jenkins job from the server."""
207
+ _authorize(token, "jenkins_delete_job")
208
+ return _jenkins_delete_job(url, username, api_token, job_name)
209
+
210
+
143
211
  if __name__ == "__main__":
144
212
  mcp.run(transport="streamable-http")
@@ -0,0 +1,256 @@
1
+ """Jenkins CI/CD integration tools for Aegis MCP.
2
+
3
+ Provides functions to manage Jenkins jobs and builds via the Jenkins REST API
4
+ using the ``python-jenkins`` library. Credentials (URL, username, API token)
5
+ are passed per-call so no global state or environment variables are required.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ import jenkins
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Helpers
18
+ # ---------------------------------------------------------------------------
19
+
20
+ def _client(url: str, username: str, api_token: str) -> jenkins.Jenkins:
21
+ """Return a configured Jenkins client, raising RuntimeError on failure."""
22
+ try:
23
+ server = jenkins.Jenkins(url, username=username, password=api_token)
24
+ # Verify credentials by fetching the server version header
25
+ server.get_whoami()
26
+ return server
27
+ except jenkins.JenkinsException as exc:
28
+ raise RuntimeError(
29
+ f"Cannot connect to Jenkins at '{url}': {exc}"
30
+ ) from exc
31
+ except Exception as exc:
32
+ raise RuntimeError(
33
+ f"Jenkins connection error: {exc}"
34
+ ) from exc
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Tool functions
39
+ # ---------------------------------------------------------------------------
40
+
41
+ def jenkins_list_jobs(url: str, username: str, api_token: str) -> list[dict]:
42
+ """List all jobs on a Jenkins server.
43
+
44
+ Returns a list of dicts with keys: name, url, color (build status indicator).
45
+ """
46
+ server = _client(url, username, api_token)
47
+ try:
48
+ jobs = server.get_all_jobs()
49
+ return [
50
+ {
51
+ "name": j.get("name", ""),
52
+ "url": j.get("url", ""),
53
+ "color": j.get("color", ""),
54
+ }
55
+ for j in jobs
56
+ ]
57
+ except jenkins.JenkinsException as exc:
58
+ raise RuntimeError(f"Failed to list Jenkins jobs: {exc}") from exc
59
+
60
+
61
+ def jenkins_get_job_info(
62
+ url: str, username: str, api_token: str, job_name: str
63
+ ) -> dict:
64
+ """Get detailed information about a Jenkins job.
65
+
66
+ Returns build history, health reports, and configuration details.
67
+ """
68
+ server = _client(url, username, api_token)
69
+ try:
70
+ info = server.get_job_info(job_name)
71
+ return {
72
+ "name": info.get("name", ""),
73
+ "url": info.get("url", ""),
74
+ "description": info.get("description", ""),
75
+ "buildable": info.get("buildable", False),
76
+ "color": info.get("color", ""),
77
+ "last_build": info.get("lastBuild"),
78
+ "last_successful_build": info.get("lastSuccessfulBuild"),
79
+ "last_failed_build": info.get("lastFailedBuild"),
80
+ "health_report": info.get("healthReport", []),
81
+ "in_queue": info.get("inQueue", False),
82
+ }
83
+ except jenkins.NotFoundException:
84
+ raise RuntimeError(f"Jenkins job '{job_name}' not found")
85
+ except jenkins.JenkinsException as exc:
86
+ raise RuntimeError(
87
+ f"Failed to get info for job '{job_name}': {exc}"
88
+ ) from exc
89
+
90
+
91
+ def jenkins_create_job(
92
+ url: str,
93
+ username: str,
94
+ api_token: str,
95
+ job_name: str,
96
+ config_xml: str = "",
97
+ ) -> dict:
98
+ """Create a new Jenkins job.
99
+
100
+ Args:
101
+ url: Jenkins server URL.
102
+ username: Jenkins username.
103
+ api_token: Jenkins API token.
104
+ job_name: Name for the new job.
105
+ config_xml: Jenkins job configuration XML. If empty, a minimal
106
+ freestyle project config is used.
107
+
108
+ Returns:
109
+ Dict with the created job name and URL.
110
+ """
111
+ if not config_xml:
112
+ config_xml = jenkins.EMPTY_CONFIG_XML
113
+
114
+ server = _client(url, username, api_token)
115
+ try:
116
+ server.create_job(job_name, config_xml)
117
+ return {
118
+ "status": "created",
119
+ "job_name": job_name,
120
+ "url": f"{url.rstrip('/')}/job/{job_name}/",
121
+ }
122
+ except jenkins.JenkinsException as exc:
123
+ raise RuntimeError(
124
+ f"Failed to create job '{job_name}': {exc}"
125
+ ) from exc
126
+
127
+
128
+ def jenkins_trigger_build(
129
+ url: str,
130
+ username: str,
131
+ api_token: str,
132
+ job_name: str,
133
+ parameters: str = "",
134
+ ) -> dict:
135
+ """Trigger a build for a Jenkins job.
136
+
137
+ Args:
138
+ url: Jenkins server URL.
139
+ username: Jenkins username.
140
+ api_token: Jenkins API token.
141
+ job_name: Name of the job to build.
142
+ parameters: Optional JSON string of build parameters,
143
+ e.g. '{"BRANCH": "main"}'.
144
+
145
+ Returns:
146
+ Dict with the queue item number.
147
+ """
148
+ server = _client(url, username, api_token)
149
+ try:
150
+ params: dict[str, Any] | None = None
151
+ if parameters:
152
+ params = json.loads(parameters)
153
+
154
+ queue_item = server.build_job(job_name, parameters=params)
155
+ return {
156
+ "status": "triggered",
157
+ "job_name": job_name,
158
+ "queue_item": queue_item,
159
+ }
160
+ except json.JSONDecodeError as exc:
161
+ raise RuntimeError(
162
+ f"Invalid parameters JSON: {exc}"
163
+ ) from exc
164
+ except jenkins.JenkinsException as exc:
165
+ raise RuntimeError(
166
+ f"Failed to trigger build for '{job_name}': {exc}"
167
+ ) from exc
168
+
169
+
170
+ def jenkins_get_build_info(
171
+ url: str,
172
+ username: str,
173
+ api_token: str,
174
+ job_name: str,
175
+ build_number: int,
176
+ ) -> dict:
177
+ """Get information about a specific build.
178
+
179
+ Returns build result, duration, timestamp, and other metadata.
180
+ """
181
+ server = _client(url, username, api_token)
182
+ try:
183
+ info = server.get_build_info(job_name, build_number)
184
+ return {
185
+ "job_name": job_name,
186
+ "build_number": info.get("number"),
187
+ "result": info.get("result"),
188
+ "duration_ms": info.get("duration"),
189
+ "timestamp": info.get("timestamp"),
190
+ "building": info.get("building", False),
191
+ "url": info.get("url", ""),
192
+ "display_name": info.get("displayName", ""),
193
+ }
194
+ except jenkins.NotFoundException:
195
+ raise RuntimeError(
196
+ f"Build #{build_number} not found for job '{job_name}'"
197
+ )
198
+ except jenkins.JenkinsException as exc:
199
+ raise RuntimeError(
200
+ f"Failed to get build info for '{job_name}' #{build_number}: {exc}"
201
+ ) from exc
202
+
203
+
204
+ def jenkins_get_build_log(
205
+ url: str,
206
+ username: str,
207
+ api_token: str,
208
+ job_name: str,
209
+ build_number: int,
210
+ ) -> dict:
211
+ """Fetch the console output of a Jenkins build.
212
+
213
+ Returns the full console log as a string (truncated to 50 000 chars to
214
+ keep MCP responses manageable).
215
+ """
216
+ server = _client(url, username, api_token)
217
+ try:
218
+ output = server.get_build_console_output(job_name, build_number)
219
+ max_len = 50_000
220
+ truncated = len(output) > max_len
221
+ return {
222
+ "job_name": job_name,
223
+ "build_number": build_number,
224
+ "log": output[:max_len],
225
+ "truncated": truncated,
226
+ }
227
+ except jenkins.NotFoundException:
228
+ raise RuntimeError(
229
+ f"Build #{build_number} not found for job '{job_name}'"
230
+ )
231
+ except jenkins.JenkinsException as exc:
232
+ raise RuntimeError(
233
+ f"Failed to get build log for '{job_name}' #{build_number}: {exc}"
234
+ ) from exc
235
+
236
+
237
+ def jenkins_delete_job(
238
+ url: str, username: str, api_token: str, job_name: str
239
+ ) -> dict:
240
+ """Delete a Jenkins job.
241
+
242
+ Returns confirmation dict on success.
243
+ """
244
+ server = _client(url, username, api_token)
245
+ try:
246
+ server.delete_job(job_name)
247
+ return {
248
+ "status": "deleted",
249
+ "job_name": job_name,
250
+ }
251
+ except jenkins.NotFoundException:
252
+ raise RuntimeError(f"Jenkins job '{job_name}' not found")
253
+ except jenkins.JenkinsException as exc:
254
+ raise RuntimeError(
255
+ f"Failed to delete job '{job_name}': {exc}"
256
+ ) from exc