@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 +54 -75
- package/bin/prepare-python-env.js +49 -16
- package/package.json +54 -50
- package/requirements.txt +1 -0
- package/server/main.py +68 -0
- package/tools/cicd/jenkins.py +256 -0
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
|
-
##
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
12
|
-
const
|
|
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
|
-
|
|
19
|
+
const result = spawnSync(command, args, {
|
|
17
20
|
cwd: PROJECT_ROOT,
|
|
18
|
-
|
|
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
|
-
|
|
107
|
+
throwCommandError(created, "Failed to create Python virtual environment.");
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
function installDependencies(pythonInVenv) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
122
|
+
throwCommandError(pipUpgrade, "Failed to upgrade pip in virtual environment.");
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
const pipInstall = run(pythonInVenv, [
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
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
|