@raghulm/aegis-mcp 1.0.5 → 1.0.7

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.
@@ -1,62 +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
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/package.json CHANGED
@@ -1,54 +1,52 @@
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
- }
1
+ {
2
+ "name": "@raghulm/aegis-mcp",
3
+ "version": "1.0.7",
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
+ "registry": "https://npm.pkg.github.com"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }
@@ -1,34 +1,34 @@
1
- roles:
2
- viewer:
3
- - aws_list_ec2_instances
4
- - k8s_list_pods
5
- - git_recent_commits
6
- - security_check_ssl_certificate
7
- - security_check_http_headers
8
- - network_port_scan
9
- - security_semgrep_scan
10
- security:
11
- - k8s_security_audit
12
- - security_run_trivy_scan
13
- - git_recent_commits
14
- - security_scan_secrets
15
- - security_check_ssl_certificate
16
- - security_check_dependencies
17
- - security_check_http_headers
18
- - security_semgrep_scan
19
- - aws_check_s3_public_access
20
- - network_port_scan
21
- admin:
22
- - aws_list_ec2_instances
23
- - k8s_list_pods
24
- - k8s_security_audit
25
- - security_run_trivy_scan
26
- - git_recent_commits
27
- - cicd_pipeline_status
28
- - security_scan_secrets
29
- - security_check_ssl_certificate
30
- - security_check_dependencies
31
- - security_check_http_headers
32
- - security_semgrep_scan
33
- - aws_check_s3_public_access
34
- - network_port_scan
1
+ roles:
2
+ viewer:
3
+ - aws_list_ec2_instances
4
+ - k8s_list_pods
5
+ - git_recent_commits
6
+ - security_check_ssl_certificate
7
+ - security_check_http_headers
8
+ - network_port_scan
9
+ - security_semgrep_scan
10
+ security:
11
+ - k8s_security_audit
12
+ - security_run_trivy_scan
13
+ - git_recent_commits
14
+ - security_scan_secrets
15
+ - security_check_ssl_certificate
16
+ - security_check_dependencies
17
+ - security_check_http_headers
18
+ - security_semgrep_scan
19
+ - aws_check_s3_public_access
20
+ - network_port_scan
21
+ admin:
22
+ - aws_list_ec2_instances
23
+ - k8s_list_pods
24
+ - k8s_security_audit
25
+ - security_run_trivy_scan
26
+ - git_recent_commits
27
+ - cicd_pipeline_status
28
+ - security_scan_secrets
29
+ - security_check_ssl_certificate
30
+ - security_check_dependencies
31
+ - security_check_http_headers
32
+ - security_semgrep_scan
33
+ - aws_check_s3_public_access
34
+ - network_port_scan
@@ -1,16 +1,16 @@
1
- scopes:
2
- aegis.read:
3
- - aws_list_ec2_instances
4
- - k8s_list_pods
5
- - git_recent_commits
6
- - cicd_pipeline_status
7
- - security_check_ssl_certificate
8
- - security_check_http_headers
9
- - network_port_scan
10
- - aws_check_s3_public_access
11
- - security_semgrep_scan
12
- aegis.security:
13
- - security_run_trivy_scan
14
- - security_scan_secrets
15
- - security_check_dependencies
16
- - security_semgrep_scan
1
+ scopes:
2
+ aegis.read:
3
+ - aws_list_ec2_instances
4
+ - k8s_list_pods
5
+ - git_recent_commits
6
+ - cicd_pipeline_status
7
+ - security_check_ssl_certificate
8
+ - security_check_http_headers
9
+ - network_port_scan
10
+ - aws_check_s3_public_access
11
+ - security_semgrep_scan
12
+ aegis.security:
13
+ - security_run_trivy_scan
14
+ - security_scan_secrets
15
+ - security_check_dependencies
16
+ - security_semgrep_scan
package/requirements.txt CHANGED
@@ -1,8 +1,8 @@
1
- mcp>=1.0.0
2
- boto3>=1.34.0
3
- requests>=2.32.0
4
- fastapi>=0.111.0
5
- uvicorn>=0.30.0
6
- pyyaml>=6.0.1
7
- semgrep>=1.60.0
8
- python-jenkins>=1.8.0
1
+ mcp>=1.0.0
2
+ boto3>=1.34.0
3
+ requests>=2.32.0
4
+ fastapi>=0.111.0
5
+ uvicorn>=0.30.0
6
+ pyyaml>=6.0.1
7
+ semgrep>=1.60.0
8
+ python-jenkins>=1.8.0
package/run_stdio.py CHANGED
@@ -1,22 +1,22 @@
1
- """Launcher script for Claude Desktop — ensures the project root is on sys.path."""
2
- import sys
3
- import os
4
-
5
- # Set working directory and sys.path to the project root so that
6
- # relative policy file paths (policies/roles.yaml etc.) resolve correctly
7
- PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
8
- os.chdir(PROJECT_ROOT)
9
- sys.path.insert(0, PROJECT_ROOT)
10
-
11
- # Disable JWT auth for local stdio sessions (Claude Desktop cannot supply tokens)
12
- os.environ.setdefault("MCP_AUTH_DISABLED", "true")
13
-
14
- # Ensure the venv Scripts dir is on PATH so pysemgrep / semgrep-core are found
15
- venv_scripts = os.path.join(PROJECT_ROOT, ".venv", "Scripts")
16
- if os.path.isdir(venv_scripts) and venv_scripts not in os.environ.get("PATH", ""):
17
- os.environ["PATH"] = venv_scripts + os.pathsep + os.environ.get("PATH", "")
18
-
19
- from server.main import mcp
20
-
21
- if __name__ == "__main__":
22
- mcp.run(transport="stdio")
1
+ """Launcher script for Claude Desktop — ensures the project root is on sys.path."""
2
+ import sys
3
+ import os
4
+
5
+ # Set working directory and sys.path to the project root so that
6
+ # relative policy file paths (policies/roles.yaml etc.) resolve correctly
7
+ PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
8
+ os.chdir(PROJECT_ROOT)
9
+ sys.path.insert(0, PROJECT_ROOT)
10
+
11
+ # Disable JWT auth for local stdio sessions (Claude Desktop cannot supply tokens)
12
+ os.environ.setdefault("MCP_AUTH_DISABLED", "true")
13
+
14
+ # Ensure the venv Scripts dir is on PATH so pysemgrep / semgrep-core are found
15
+ venv_scripts = os.path.join(PROJECT_ROOT, ".venv", "Scripts")
16
+ if os.path.isdir(venv_scripts) and venv_scripts not in os.environ.get("PATH", ""):
17
+ os.environ["PATH"] = venv_scripts + os.pathsep + os.environ.get("PATH", "")
18
+
19
+ from server.main import mcp
20
+
21
+ if __name__ == "__main__":
22
+ mcp.run(transport="stdio")
package/server/auth.py CHANGED
@@ -1,69 +1,69 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import json
5
- from dataclasses import dataclass
6
-
7
- from server.config import Settings
8
-
9
-
10
- @dataclass(frozen=True)
11
- class Principal:
12
- subject: str
13
- role: str
14
- scopes: list[str]
15
-
16
-
17
- class AuthorizationError(PermissionError):
18
- pass
19
-
20
-
21
-
22
- def _decode_jwt_payload(token: str) -> dict:
23
- parts = token.split(".")
24
- if len(parts) < 2:
25
- return {}
26
- payload = parts[1]
27
- padding = "=" * ((4 - len(payload) % 4) % 4)
28
- decoded = base64.urlsafe_b64decode(payload + padding)
29
- return json.loads(decoded.decode("utf-8"))
30
-
31
-
32
-
33
- def decode_bearer_token(token: str, settings: Settings) -> Principal:
34
- """Decode claims from a bearer token payload.
35
-
36
- This implementation parses JWT payload claims and is intended as a scaffold.
37
- Replace with strict signature/JWKS validation in production.
38
- """
39
- claims = _decode_jwt_payload(token)
40
-
41
- if settings.oidc_issuer and claims.get("iss") != settings.oidc_issuer:
42
- raise AuthorizationError("token issuer mismatch")
43
- if settings.oidc_audience and settings.oidc_audience not in str(claims.get("aud", "")):
44
- raise AuthorizationError("token audience mismatch")
45
-
46
- role = str(claims.get("role", "viewer"))
47
- scope_claim = claims.get("scope", "")
48
- scopes = scope_claim.split() if isinstance(scope_claim, str) else []
49
- return Principal(subject=str(claims.get("sub", "unknown")), role=role, scopes=scopes)
50
-
51
-
52
-
53
- def authorize_tool(
54
- principal: Principal,
55
- tool_name: str,
56
- role_policies: dict[str, list[str]],
57
- scope_policies: dict[str, list[str]],
58
- ) -> None:
59
- allowed_by_role = set(role_policies.get(principal.role, []))
60
- allowed_by_scope: set[str] = set()
61
- for scope in principal.scopes:
62
- allowed_by_scope.update(scope_policies.get(scope, []))
63
-
64
- if tool_name in allowed_by_role or tool_name in allowed_by_scope:
65
- return
66
-
67
- raise AuthorizationError(
68
- f"principal '{principal.subject}' with role '{principal.role}' is not allowed to call '{tool_name}'"
69
- )
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from dataclasses import dataclass
6
+
7
+ from server.config import Settings
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Principal:
12
+ subject: str
13
+ role: str
14
+ scopes: list[str]
15
+
16
+
17
+ class AuthorizationError(PermissionError):
18
+ pass
19
+
20
+
21
+
22
+ def _decode_jwt_payload(token: str) -> dict:
23
+ parts = token.split(".")
24
+ if len(parts) < 2:
25
+ return {}
26
+ payload = parts[1]
27
+ padding = "=" * ((4 - len(payload) % 4) % 4)
28
+ decoded = base64.urlsafe_b64decode(payload + padding)
29
+ return json.loads(decoded.decode("utf-8"))
30
+
31
+
32
+
33
+ def decode_bearer_token(token: str, settings: Settings) -> Principal:
34
+ """Decode claims from a bearer token payload.
35
+
36
+ This implementation parses JWT payload claims and is intended as a scaffold.
37
+ Replace with strict signature/JWKS validation in production.
38
+ """
39
+ claims = _decode_jwt_payload(token)
40
+
41
+ if settings.oidc_issuer and claims.get("iss") != settings.oidc_issuer:
42
+ raise AuthorizationError("token issuer mismatch")
43
+ if settings.oidc_audience and settings.oidc_audience not in str(claims.get("aud", "")):
44
+ raise AuthorizationError("token audience mismatch")
45
+
46
+ role = str(claims.get("role", "viewer"))
47
+ scope_claim = claims.get("scope", "")
48
+ scopes = scope_claim.split() if isinstance(scope_claim, str) else []
49
+ return Principal(subject=str(claims.get("sub", "unknown")), role=role, scopes=scopes)
50
+
51
+
52
+
53
+ def authorize_tool(
54
+ principal: Principal,
55
+ tool_name: str,
56
+ role_policies: dict[str, list[str]],
57
+ scope_policies: dict[str, list[str]],
58
+ ) -> None:
59
+ allowed_by_role = set(role_policies.get(principal.role, []))
60
+ allowed_by_scope: set[str] = set()
61
+ for scope in principal.scopes:
62
+ allowed_by_scope.update(scope_policies.get(scope, []))
63
+
64
+ if tool_name in allowed_by_role or tool_name in allowed_by_scope:
65
+ return
66
+
67
+ raise AuthorizationError(
68
+ f"principal '{principal.subject}' with role '{principal.role}' is not allowed to call '{tool_name}'"
69
+ )