@raghulm/aegis-mcp 1.0.2 → 1.0.3
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/LICENSE +21 -0
- package/README.md +69 -77
- package/audit/__init__.py +0 -0
- package/audit/audit_logger.py +62 -0
- package/bin/aegis-mcp.js +44 -164
- package/bin/prepare-python-env.js +143 -0
- package/package.json +50 -39
- package/policies/roles.yaml +34 -0
- package/policies/scope_rules.yaml +16 -0
- package/requirements.txt +7 -0
- package/run_stdio.py +22 -0
- package/server/__init__.py +0 -0
- package/server/auth.py +69 -0
- package/server/config.py +82 -0
- package/server/health.py +19 -0
- package/server/logging.py +33 -0
- package/server/main.py +144 -0
- package/server/stdio.py +7 -0
- package/tools/__init__.py +0 -0
- package/tools/aws/__init__.py +0 -0
- package/tools/aws/ec2.py +26 -0
- package/tools/aws/s3.py +54 -0
- package/tools/cicd/__init__.py +0 -0
- package/tools/cicd/pipeline.py +33 -0
- package/tools/git/__init__.py +0 -0
- package/tools/git/repo.py +22 -0
- package/tools/kubernetes/__init__.py +0 -0
- package/tools/kubernetes/audit.py +108 -0
- package/tools/kubernetes/pods.py +27 -0
- package/tools/network/__init__.py +0 -0
- package/tools/network/headers.py +99 -0
- package/tools/network/port_scanner.py +66 -0
- package/tools/network/ssl_checker.py +65 -0
- package/tools/security/__init__.py +0 -0
- package/tools/security/deps.py +103 -0
- package/tools/security/secrets.py +91 -0
- package/tools/security/semgrep.py +261 -0
- package/tools/security/trivy.py +19 -0
- package/bin/aegis-mcp.cmd +0 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raghul M
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,22 +6,12 @@
|
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
7
|
[](https://modelcontextprotocol.io/)
|
|
8
8
|
[](https://www.docker.com/)
|
|
9
|
-
[](https://www.npmjs.com/package/@raghulm/aegis-mcp)
|
|
10
|
-
[](https://www.npmjs.com/package/@raghulm/aegis-mcp)
|
|
11
9
|
</div>
|
|
12
10
|
|
|
13
11
|
---
|
|
14
12
|
|
|
15
13
|
**Aegis MCP Server** empowers AI assistants (like Claude, Cursor, and GitHub Copilot) to perform cloud architecture administration, security scanning, and network analyses directly from their execution environments. It wraps powerful underlying tools and SDKs into secure, audited MCP tool sets.
|
|
16
14
|
|
|
17
|
-
### Quick Start
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npx @raghulm/aegis-mcp
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
That's it! The installer will check Python, clone the repo, set up dependencies, and print your MCP config.
|
|
24
|
-
|
|
25
15
|
---
|
|
26
16
|
|
|
27
17
|
## 📸 Demo in Action
|
|
@@ -144,39 +134,14 @@ MEDIUM: 7
|
|
|
144
134
|
|
|
145
135
|
## 🚀 Getting Started
|
|
146
136
|
|
|
147
|
-
###
|
|
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)
|
|
148
143
|
|
|
149
|
-
|
|
150
|
-
npx @raghulm/aegis-mcp
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
That's it. Aegis will:
|
|
154
|
-
1. ✅ Check your Python version (3.12+ required)
|
|
155
|
-
2. 📥 Clone and install the server into `~/.aegis-mcp`
|
|
156
|
-
3. 🖨️ Print your ready-to-paste MCP config
|
|
157
|
-
4. 🚀 Start the server
|
|
158
|
-
|
|
159
|
-
**Additional commands:**
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
npx @raghulm/aegis-mcp --install # Force reinstall / update
|
|
163
|
-
npx @raghulm/aegis-mcp --config # Print MCP config JSON only
|
|
164
|
-
npx @raghulm/aegis-mcp --help # Show all options
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
> 💡 **Tip:** After running `npx @raghulm/aegis-mcp`, copy the printed MCP config into your AI agent's config file (Claude Desktop, Cursor, Windsurf, etc.)
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
### Prerequisites
|
|
172
|
-
|
|
173
|
-
- **Node.js 16+** (for NPX install)
|
|
174
|
-
- **Python 3.12+**
|
|
175
|
-
- **Git**
|
|
176
|
-
- **Semgrep** — `pip install semgrep` (for SAST scanning)
|
|
177
|
-
- Optional: AWS CLI / `boto3`, `kubectl`, Trivy (for their respective tools)
|
|
178
|
-
|
|
179
|
-
### Manual Installation
|
|
144
|
+
### Installation
|
|
180
145
|
|
|
181
146
|
```bash
|
|
182
147
|
git clone https://github.com/raghulvj01/aegis-mcp.git
|
|
@@ -192,10 +157,20 @@ source .venv/bin/activate
|
|
|
192
157
|
.venv\Scripts\activate
|
|
193
158
|
|
|
194
159
|
# Install dependencies
|
|
195
|
-
pip install -r requirements.txt
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
---
|
|
199
174
|
|
|
200
175
|
## 🤖 Usage with AI Agents
|
|
201
176
|
|
|
@@ -203,16 +178,16 @@ pip install -r requirements.txt
|
|
|
203
178
|
|
|
204
179
|
Add to your MCP config (e.g., `mcp_config.json`):
|
|
205
180
|
|
|
206
|
-
```json
|
|
207
|
-
{
|
|
208
|
-
"mcpServers": {
|
|
209
|
-
"aegis": {
|
|
210
|
-
"command": "
|
|
211
|
-
"args": ["
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
```
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"mcpServers": {
|
|
184
|
+
"aegis": {
|
|
185
|
+
"command": "npx",
|
|
186
|
+
"args": ["-y", "@raghulm/aegis-mcp"]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
216
191
|
|
|
217
192
|
> ✅ **All 12 tools work**, including Semgrep SAST.
|
|
218
193
|
|
|
@@ -222,16 +197,16 @@ Add to `claude_desktop_config.json`:
|
|
|
222
197
|
- **Windows**: `%LOCALAPPDATA%\Packages\Claude_...\LocalCache\Roaming\Claude\`
|
|
223
198
|
- **Mac**: `~/Library/Application Support/Claude/`
|
|
224
199
|
|
|
225
|
-
```json
|
|
226
|
-
{
|
|
227
|
-
"mcpServers": {
|
|
228
|
-
"aegis": {
|
|
229
|
-
"command": "
|
|
230
|
-
"args": ["
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
```
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"mcpServers": {
|
|
203
|
+
"aegis": {
|
|
204
|
+
"command": "npx",
|
|
205
|
+
"args": ["-y", "@raghulm/aegis-mcp"]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
235
210
|
|
|
236
211
|
> ⚠️ **11 of 12 tools work.** Semgrep SAST does not work due to Windows pipe limitations.
|
|
237
212
|
|
|
@@ -326,17 +301,34 @@ The `@audit_tool_call` decorator emits structured JSON logs for every invocation
|
|
|
326
301
|
|
|
327
302
|
---
|
|
328
303
|
|
|
329
|
-
## 🛣️ Roadmap
|
|
330
|
-
|
|
331
|
-
- [ ] Terraform security scanner
|
|
332
|
-
- [ ] IAM policy risk detection
|
|
333
|
-
- [ ] Kubernetes misconfiguration scanner (Basic `k8s_security_audit` implemented!)
|
|
334
|
-
- [ ] GitHub Actions security audit
|
|
335
|
-
- [ ] Cloud cost analysis tools
|
|
336
|
-
|
|
337
|
-
---
|
|
338
|
-
|
|
339
|
-
##
|
|
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
|
|
340
332
|
|
|
341
333
|
1. Fork the project
|
|
342
334
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from time import perf_counter
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from server.logging import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger("mcp.aegis.audit")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def audit_tool_call(tool_name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
14
|
+
"""Decorator that emits structured audit records for every tool invocation."""
|
|
15
|
+
|
|
16
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
17
|
+
@wraps(func)
|
|
18
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
19
|
+
started = perf_counter()
|
|
20
|
+
logger.info(
|
|
21
|
+
"tool_call_started",
|
|
22
|
+
extra={
|
|
23
|
+
"extra_payload": {
|
|
24
|
+
"event": "tool_call_started",
|
|
25
|
+
"tool": tool_name,
|
|
26
|
+
"args": str(args),
|
|
27
|
+
"kwargs": kwargs,
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
try:
|
|
32
|
+
result = func(*args, **kwargs)
|
|
33
|
+
duration_ms = int((perf_counter() - started) * 1000)
|
|
34
|
+
logger.info(
|
|
35
|
+
"tool_call_succeeded",
|
|
36
|
+
extra={
|
|
37
|
+
"extra_payload": {
|
|
38
|
+
"event": "tool_call_succeeded",
|
|
39
|
+
"tool": tool_name,
|
|
40
|
+
"duration_ms": duration_ms,
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
return result
|
|
45
|
+
except Exception as exc: # noqa: BLE001
|
|
46
|
+
duration_ms = int((perf_counter() - started) * 1000)
|
|
47
|
+
logger.error(
|
|
48
|
+
"tool_call_failed",
|
|
49
|
+
extra={
|
|
50
|
+
"extra_payload": {
|
|
51
|
+
"event": "tool_call_failed",
|
|
52
|
+
"tool": tool_name,
|
|
53
|
+
"duration_ms": duration_ms,
|
|
54
|
+
"error": str(exc),
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
|
|
62
|
+
return decorator
|
package/bin/aegis-mcp.js
CHANGED
|
@@ -1,164 +1,44 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch {}
|
|
46
|
-
}
|
|
47
|
-
log("❌ Python 3.12+ not found. Please install it from https://python.org", "red");
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isInstalled() {
|
|
52
|
-
return (
|
|
53
|
-
fs.existsSync(INSTALL_DIR) &&
|
|
54
|
-
fs.existsSync(path.join(INSTALL_DIR, "run_stdio.py"))
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function install(pythonCmd) {
|
|
59
|
-
log("📦 Installing Aegis MCP...", "cyan");
|
|
60
|
-
|
|
61
|
-
if (fs.existsSync(INSTALL_DIR)) {
|
|
62
|
-
log("🔄 Updating existing installation...", "yellow");
|
|
63
|
-
execSync("git pull", { cwd: INSTALL_DIR, stdio: "inherit" });
|
|
64
|
-
} else {
|
|
65
|
-
log(`📥 Cloning from ${REPO_URL}...`, "cyan");
|
|
66
|
-
execSync(`git clone ${REPO_URL} "${INSTALL_DIR}"`, { stdio: "inherit" });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
log("📦 Creating virtual environment...", "cyan");
|
|
70
|
-
execSync(`${pythonCmd} -m venv .venv`, { cwd: INSTALL_DIR, stdio: "inherit" });
|
|
71
|
-
|
|
72
|
-
const pip = process.platform === "win32"
|
|
73
|
-
? path.join(INSTALL_DIR, ".venv", "Scripts", "pip.exe")
|
|
74
|
-
: path.join(INSTALL_DIR, ".venv", "bin", "pip");
|
|
75
|
-
|
|
76
|
-
log("📦 Installing Python dependencies...", "cyan");
|
|
77
|
-
execSync(`"${pip}" install -r requirements.txt`, { cwd: INSTALL_DIR, stdio: "inherit" });
|
|
78
|
-
|
|
79
|
-
log("✅ Aegis MCP installed successfully!", "green");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function run() {
|
|
83
|
-
const pythonExe = process.platform === "win32"
|
|
84
|
-
? path.join(INSTALL_DIR, ".venv", "Scripts", "python.exe")
|
|
85
|
-
: path.join(INSTALL_DIR, ".venv", "bin", "python");
|
|
86
|
-
|
|
87
|
-
const scriptPath = path.join(INSTALL_DIR, "run_stdio.py");
|
|
88
|
-
|
|
89
|
-
log("🚀 Starting Aegis MCP Server...\n", "green");
|
|
90
|
-
|
|
91
|
-
const child = spawn(pythonExe, [scriptPath], {
|
|
92
|
-
stdio: "inherit",
|
|
93
|
-
env: { ...process.env, MCP_AUTH_DISABLED: "true" },
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
child.on("error", (err) => {
|
|
97
|
-
log(`\n❌ Failed to start Aegis MCP: ${err.message}`, "red");
|
|
98
|
-
process.exit(1);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
child.on("exit", (code) => {
|
|
102
|
-
process.exit(code || 0);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
106
|
-
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function printMCPConfig() {
|
|
110
|
-
const pythonExe = process.platform === "win32"
|
|
111
|
-
? path.join(INSTALL_DIR, ".venv", "Scripts", "python.exe")
|
|
112
|
-
: path.join(INSTALL_DIR, ".venv", "bin", "python");
|
|
113
|
-
|
|
114
|
-
const scriptPath = path.join(INSTALL_DIR, "run_stdio.py");
|
|
115
|
-
|
|
116
|
-
log("\n📋 Add this to your MCP config (claude_desktop_config.json / mcp_config.json):\n", "cyan");
|
|
117
|
-
console.error(JSON.stringify({
|
|
118
|
-
mcpServers: {
|
|
119
|
-
aegis: {
|
|
120
|
-
command: pythonExe,
|
|
121
|
-
args: [scriptPath],
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
}, null, 2));
|
|
125
|
-
console.error();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
const args = process.argv.slice(2);
|
|
131
|
-
|
|
132
|
-
banner();
|
|
133
|
-
|
|
134
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
135
|
-
console.error(`Usage: npx aegis-mcp [options]
|
|
136
|
-
|
|
137
|
-
Options:
|
|
138
|
-
(no args) Install (if needed) and start the MCP server
|
|
139
|
-
--install Force reinstall
|
|
140
|
-
--config Print MCP config JSON and exit
|
|
141
|
-
--version, -v Show version
|
|
142
|
-
--help, -h Show this help
|
|
143
|
-
`);
|
|
144
|
-
process.exit(0);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (args.includes("--version") || args.includes("-v")) {
|
|
148
|
-
console.log(VERSION);
|
|
149
|
-
process.exit(0);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const pythonCmd = checkPython();
|
|
153
|
-
|
|
154
|
-
if (args.includes("--install") || !isInstalled()) {
|
|
155
|
-
install(pythonCmd);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (args.includes("--config")) {
|
|
159
|
-
printMCPConfig();
|
|
160
|
-
process.exit(0);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
printMCPConfig();
|
|
164
|
-
run();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
const { ensurePythonEnvironment } = require("./prepare-python-env");
|
|
7
|
+
|
|
8
|
+
function main() {
|
|
9
|
+
let runtime;
|
|
10
|
+
try {
|
|
11
|
+
runtime = ensurePythonEnvironment();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(`[aegis-mcp] ${error.message}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const runScript = path.join(runtime.projectRoot, "run_stdio.py");
|
|
18
|
+
const env = {
|
|
19
|
+
...process.env,
|
|
20
|
+
MCP_AUTH_DISABLED: process.env.MCP_AUTH_DISABLED || "true",
|
|
21
|
+
PATH: `${runtime.scriptsDir}${path.delimiter}${process.env.PATH || ""}`
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const child = spawn(runtime.pythonInVenv, [runScript, ...process.argv.slice(2)], {
|
|
25
|
+
cwd: runtime.projectRoot,
|
|
26
|
+
env,
|
|
27
|
+
stdio: "inherit"
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
child.on("error", (error) => {
|
|
31
|
+
console.error(`[aegis-mcp] Failed to start MCP server: ${error.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on("exit", (code, signal) => {
|
|
36
|
+
if (signal) {
|
|
37
|
+
process.kill(process.pid, signal);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
process.exit(code ?? 0);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main();
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
10
|
+
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");
|
|
13
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
14
|
+
|
|
15
|
+
function run(command, args, options = {}) {
|
|
16
|
+
return spawnSync(command, args, {
|
|
17
|
+
cwd: PROJECT_ROOT,
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
...options
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function candidatePythonCommands() {
|
|
24
|
+
const override = process.env.AEGIS_PYTHON;
|
|
25
|
+
if (override && override.trim()) {
|
|
26
|
+
const pieces = override.trim().split(/\s+/);
|
|
27
|
+
return [{ command: pieces[0], prefixArgs: pieces.slice(1), label: override.trim() }];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (IS_WINDOWS) {
|
|
31
|
+
return [
|
|
32
|
+
{ command: "py", prefixArgs: ["-3"], label: "py -3" },
|
|
33
|
+
{ command: "python", prefixArgs: [], label: "python" },
|
|
34
|
+
{ command: "python3", prefixArgs: [], label: "python3" }
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
{ command: "python3", prefixArgs: [], label: "python3" },
|
|
40
|
+
{ command: "python", prefixArgs: [], label: "python" }
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findWorkingPython() {
|
|
45
|
+
for (const candidate of candidatePythonCommands()) {
|
|
46
|
+
const versionCheck = run(candidate.command, [...candidate.prefixArgs, "--version"], {
|
|
47
|
+
stdio: "pipe",
|
|
48
|
+
encoding: "utf8"
|
|
49
|
+
});
|
|
50
|
+
if (versionCheck.status === 0) {
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Python 3.12+ was not found. Install Python and make sure it is on PATH, or set AEGIS_PYTHON."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function venvPythonPath() {
|
|
61
|
+
if (IS_WINDOWS) {
|
|
62
|
+
return path.join(VENV_DIR, "Scripts", "python.exe");
|
|
63
|
+
}
|
|
64
|
+
return path.join(VENV_DIR, "bin", "python");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function venvScriptsPath() {
|
|
68
|
+
if (IS_WINDOWS) {
|
|
69
|
+
return path.join(VENV_DIR, "Scripts");
|
|
70
|
+
}
|
|
71
|
+
return path.join(VENV_DIR, "bin");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requirementsHash() {
|
|
75
|
+
const content = fs.readFileSync(REQUIREMENTS_FILE);
|
|
76
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ensureVirtualEnvironment(python) {
|
|
80
|
+
const pythonInVenv = venvPythonPath();
|
|
81
|
+
if (fs.existsSync(pythonInVenv)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.error("[aegis-mcp] Creating Python virtual environment...");
|
|
86
|
+
const created = run(python.command, [...python.prefixArgs, "-m", "venv", VENV_DIR]);
|
|
87
|
+
if (created.status !== 0) {
|
|
88
|
+
throw new Error("Failed to create Python virtual environment.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
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"]);
|
|
96
|
+
if (pipUpgrade.status !== 0) {
|
|
97
|
+
throw new Error("Failed to upgrade pip in virtual environment.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pipInstall = run(pythonInVenv, ["-m", "pip", "install", "-r", REQUIREMENTS_FILE]);
|
|
101
|
+
if (pipInstall.status !== 0) {
|
|
102
|
+
throw new Error("Failed to install Python dependencies.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensurePythonEnvironment() {
|
|
107
|
+
if (!fs.existsSync(REQUIREMENTS_FILE)) {
|
|
108
|
+
throw new Error("requirements.txt was not found in package root.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const python = findWorkingPython();
|
|
112
|
+
ensureVirtualEnvironment(python);
|
|
113
|
+
|
|
114
|
+
const pythonInVenv = venvPythonPath();
|
|
115
|
+
const expectedHash = requirementsHash();
|
|
116
|
+
const currentHash = fs.existsSync(REQUIREMENTS_STAMP)
|
|
117
|
+
? fs.readFileSync(REQUIREMENTS_STAMP, "utf8").trim()
|
|
118
|
+
: "";
|
|
119
|
+
|
|
120
|
+
if (currentHash !== expectedHash) {
|
|
121
|
+
installDependencies(pythonInVenv);
|
|
122
|
+
fs.writeFileSync(REQUIREMENTS_STAMP, `${expectedHash}\n`, "utf8");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
projectRoot: PROJECT_ROOT,
|
|
127
|
+
pythonInVenv,
|
|
128
|
+
scriptsDir: venvScriptsPath()
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
ensurePythonEnvironment
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (require.main === module) {
|
|
137
|
+
try {
|
|
138
|
+
ensurePythonEnvironment();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`[aegis-mcp] ${error.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|