Skip to content

Commit 8a2b670

Browse files
committed
Clear Sonar + Codacy findings on PR 178
- S3516: drop invariant ``return 0`` from main(); CLI returns None. - S2068: redaction test now reads the sensitive key + placeholder from public audit constants instead of hard-coding "password" / "shhh", with a NOSONAR justification on the lone fixture value. - S1542: keep the AC_* plugin convention with a NOSONAR comment. - S1192: extract _TOOLS_CALL_METHOD and _MIME_JSON constants. - S1244: use math.isclose for the scheduler interval assertion. - S5713: drop NotImplementedError from the except tuple — RuntimeError already covers it. - Prospector pyflakes: remove the unused ``import logging``. - Semgrep dangerous-subprocess: nosemgrep justifications on the two argv-list subprocess calls (no shell, sanitised argv).
1 parent 21fe39e commit 8a2b670

7 files changed

Lines changed: 44 additions & 37 deletions

File tree

je_auto_control/utils/mcp_server/__main__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def _build_parser() -> argparse.ArgumentParser:
5050
return parser
5151

5252

53-
def main(argv: list = None) -> int:
54-
"""CLI entry point. Returns the process exit code."""
53+
def main(argv: list = None) -> None:
54+
"""CLI entry point. Performs the requested action and returns ``None``."""
5555
parser = _build_parser()
5656
args = parser.parse_args(argv)
5757
if args.fake_backend:
@@ -61,9 +61,8 @@ def main(argv: list = None) -> int:
6161
listing_modes = (args.list_tools, args.list_resources, args.list_prompts)
6262
if any(listing_modes):
6363
_print_listings(args)
64-
return 0
64+
return
6565
start_mcp_stdio_server()
66-
return 0
6766

6867

6968
def _print_listings(args: argparse.Namespace) -> None:
@@ -85,4 +84,4 @@ def _print_listings(args: argparse.Namespace) -> None:
8584

8685

8786
if __name__ == "__main__":
88-
sys.exit(main())
87+
main()

je_auto_control/utils/mcp_server/audit.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,22 @@ def record(self, *, tool: str, arguments: Dict[str, Any],
5757
handle.write(line + "\n")
5858

5959

60-
_REDACTED_KEYS = frozenset({"password", "token", "secret", "api_key",
60+
REDACTED_KEYS = frozenset({"password", "token", "secret", "api_key",
6161
"authorization"})
62+
REDACTED_PLACEHOLDER = "<redacted>"
6263

6364

6465
def _sanitise(arguments: Dict[str, Any]) -> Dict[str, Any]:
65-
"""Replace obvious secret-like values with ``"<redacted>"``."""
66+
"""Replace obvious secret-like values with ``REDACTED_PLACEHOLDER``."""
6667
if not isinstance(arguments, dict):
6768
return arguments
6869
out: Dict[str, Any] = {}
6970
for key, value in arguments.items():
70-
if key.lower() in _REDACTED_KEYS:
71-
out[key] = "<redacted>"
71+
if key.lower() in REDACTED_KEYS:
72+
out[key] = REDACTED_PLACEHOLDER
7273
else:
7374
out[key] = value
7475
return out
7576

7677

77-
__all__ = ["AuditLogger"]
78+
__all__ = ["AuditLogger", "REDACTED_KEYS", "REDACTED_PLACEHOLDER"]

je_auto_control/utils/mcp_server/resources.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from dataclasses import dataclass
1616
from typing import Any, Callable, Dict, List, Optional
1717

18+
_MIME_JSON = "application/json"
19+
1820

1921
@dataclass(frozen=True)
2022
class MCPResource:
@@ -90,7 +92,7 @@ def list(self) -> List[MCPResource]:
9092
uri=f"{self.scheme}://files/{name}",
9193
name=name,
9294
description=f"action JSON file in {self.root}",
93-
mime_type="application/json",
95+
mime_type=_MIME_JSON,
9496
))
9597
return out
9698

@@ -108,7 +110,7 @@ def read(self, uri: str) -> Optional[Dict[str, Any]]:
108110
return None
109111
with open(path, encoding="utf-8") as handle:
110112
text = handle.read()
111-
return {"uri": uri, "mimeType": "application/json", "text": text}
113+
return {"uri": uri, "mimeType": _MIME_JSON, "text": text}
112114

113115

114116
class HistoryProvider(ResourceProvider):
@@ -120,7 +122,7 @@ def list(self) -> List[MCPResource]:
120122
return [MCPResource(
121123
uri=self.URI, name="run_history",
122124
description="Recent script-run history records (last 100).",
123-
mime_type="application/json",
125+
mime_type=_MIME_JSON,
124126
)]
125127

126128
def read(self, uri: str) -> Optional[Dict[str, Any]]:
@@ -139,7 +141,7 @@ def read(self, uri: str) -> Optional[Dict[str, Any]]:
139141
"duration_seconds": row.duration_seconds,
140142
} for row in rows]
141143
return {
142-
"uri": uri, "mimeType": "application/json",
144+
"uri": uri, "mimeType": _MIME_JSON,
143145
"text": json.dumps(data, ensure_ascii=False, indent=2),
144146
}
145147

@@ -153,7 +155,7 @@ def list(self) -> List[MCPResource]:
153155
return [MCPResource(
154156
uri=self.URI, name="executor_commands",
155157
description="Every AC_* command name the executor recognises.",
156-
mime_type="application/json",
158+
mime_type=_MIME_JSON,
157159
)]
158160

159161
def read(self, uri: str) -> Optional[Dict[str, Any]]:
@@ -162,7 +164,7 @@ def read(self, uri: str) -> Optional[Dict[str, Any]]:
162164
from je_auto_control.utils.executor.action_executor import executor
163165
names = sorted(executor.known_commands())
164166
return {
165-
"uri": uri, "mimeType": "application/json",
167+
"uri": uri, "mimeType": _MIME_JSON,
166168
"text": json.dumps(names, ensure_ascii=False, indent=2),
167169
}
168170

je_auto_control/utils/mcp_server/server.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
from typing import Any, Callable, Dict, List, Optional, TextIO
1616

1717
from je_auto_control.utils.logging.logging_instance import autocontrol_logger
18-
import logging
19-
2018
from je_auto_control.utils.mcp_server.audit import AuditLogger
2119
from je_auto_control.utils.mcp_server.context import (
2220
OperationCancelledError, ToolCallContext,
@@ -41,6 +39,7 @@
4139
PROTOCOL_VERSION = "2025-06-18"
4240
SERVER_NAME = "je_auto_control"
4341
SERVER_VERSION = "0.1.0"
42+
_TOOLS_CALL_METHOD = "tools/call"
4443

4544

4645
class _MCPError(Exception):
@@ -224,7 +223,7 @@ def handle_line(self, line: str) -> Optional[str]:
224223
if msg_id is None:
225224
self._handle_notification(method, params)
226225
return None
227-
if method == "tools/call" and self._concurrent_tools:
226+
if method == _TOOLS_CALL_METHOD and self._concurrent_tools:
228227
self._dispatch_tools_call_async(msg_id, params)
229228
return None
230229
return self._build_response(msg_id, method, params)
@@ -249,7 +248,7 @@ def _dispatch_tools_call_async(self, msg_id: Any,
249248
params: Dict[str, Any]) -> None:
250249
"""Run a tools/call on a worker thread; the worker writes the reply."""
251250
def worker() -> None:
252-
payload = self._build_response(msg_id, "tools/call", params)
251+
payload = self._build_response(msg_id, _TOOLS_CALL_METHOD, params)
253252
writer = self._writer
254253
if writer is None:
255254
autocontrol_logger.warning(
@@ -374,7 +373,7 @@ def _dispatch(self, msg_id: Any, method: Optional[str],
374373
if method == "tools/list":
375374
return {"tools": [tool.to_descriptor()
376375
for tool in self._tools.values()]}
377-
if method == "tools/call":
376+
if method == _TOOLS_CALL_METHOD:
378377
return self._handle_tools_call(msg_id, params)
379378
if method == "resources/list":
380379
return {"resources": [resource.to_descriptor()
@@ -524,7 +523,8 @@ def _handle_tools_call(self, msg_id: Any,
524523
)
525524
raise
526525
except (OSError, RuntimeError, ValueError, TypeError,
527-
AttributeError, KeyError, NotImplementedError) as error:
526+
AttributeError, KeyError) as error:
527+
# NotImplementedError subclasses RuntimeError so it's already covered.
528528
autocontrol_logger.warning("MCP tool %s failed: %r", name, error)
529529
artifact = _capture_error_screenshot(name)
530530
self._audit.record(

je_auto_control/utils/mcp_server/tools/_handlers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ def launch_process(argv: List[str],
485485
cwd = os.path.realpath(os.fspath(working_directory))
486486
if not os.path.isdir(cwd):
487487
raise ValueError(f"working_directory does not exist: {cwd}")
488+
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit
488489
process = subprocess.Popen( # nosec B603 # reason: argv list, no shell expansion
489490
cleaned, cwd=cwd, stdout=subprocess.DEVNULL,
490491
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
@@ -552,6 +553,7 @@ def shell_command(command: str, timeout: float = 30.0
552553
raise ValueError("command must be a non-empty string")
553554
argv = shlex.split(command, posix=False) if os.name == "nt" \
554555
else shlex.split(command)
556+
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit
555557
proc = subprocess.run( # nosec B603 # reason: argv from shlex.split, no shell
556558
argv, capture_output=True, text=True,
557559
timeout=float(timeout), check=False,

test/unit_test/headless/test_mcp_cli.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import json
44
import sys
55

6-
import pytest
7-
86
from je_auto_control.utils.mcp_server.__main__ import main
97

108

@@ -17,7 +15,7 @@ def _capture(monkeypatch, argv):
1715

1816
def test_list_tools_emits_json_and_exits(monkeypatch):
1917
rc, output = _capture(monkeypatch, ["--list-tools"])
20-
assert rc == 0
18+
assert rc is None
2119
descriptors = json.loads(output)
2220
assert isinstance(descriptors, list)
2321
assert descriptors
@@ -34,7 +32,7 @@ def test_list_tools_with_read_only_drops_destructive(monkeypatch):
3432

3533
def test_list_resources_emits_json(monkeypatch):
3634
rc, output = _capture(monkeypatch, ["--list-resources"])
37-
assert rc == 0
35+
assert rc is None
3836
descriptors = json.loads(output)
3937
assert isinstance(descriptors, list)
4038
uris = {d["uri"] for d in descriptors}
@@ -44,7 +42,7 @@ def test_list_resources_emits_json(monkeypatch):
4442

4543
def test_list_prompts_emits_json(monkeypatch):
4644
rc, output = _capture(monkeypatch, ["--list-prompts"])
47-
assert rc == 0
45+
assert rc is None
4846
descriptors = json.loads(output)
4947
assert isinstance(descriptors, list)
5048
names = {d["name"] for d in descriptors}
@@ -58,5 +56,5 @@ def test_no_flags_starts_stdio_server(monkeypatch):
5856
monkeypatch.setattr(cli_mod, "start_mcp_stdio_server",
5957
lambda: started.append(True))
6058
rc = main([])
61-
assert rc == 0
59+
assert rc is None
6260
assert started == [True]

test/unit_test/headless/test_mcp_server.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77
import io
88
import json
9+
import math
910
import os
1011
import threading
1112
from typing import Any, Dict, List
@@ -390,7 +391,7 @@ def test_scheduler_add_job_round_trips_through_default_scheduler(tmp_path):
390391
})
391392
try:
392393
assert record["job_id"] == "test_mcp_job"
393-
assert record["interval_seconds"] == 60.0
394+
assert math.isclose(record["interval_seconds"], 60.0)
394395
listed = {job["job_id"] for job in
395396
by_name["ac_scheduler_list_jobs"].invoke({})}
396397
assert "test_mcp_job" in listed
@@ -868,7 +869,7 @@ def test_make_plugin_tool_derives_schema_from_signature():
868869
make_plugin_tool,
869870
)
870871

871-
def AC_demo(text: str, count: int = 1) -> str: # noqa: N802
872+
def AC_demo(text: str, count: int = 1) -> str: # noqa: N802 # NOSONAR S1542 reason: AutoControl plugin convention requires the AC_ prefix
872873
"""Demo plugin command."""
873874
return text * count
874875

@@ -885,7 +886,7 @@ def test_register_plugin_tools_adds_to_server_and_notifies():
885886
register_plugin_tools,
886887
)
887888

888-
def AC_one(value: str) -> str: # noqa: N802
889+
def AC_one(value: str) -> str: # noqa: N802 # NOSONAR S1542 reason: AutoControl plugin convention requires the AC_ prefix
889890
return value.upper()
890891

891892
captured = []
@@ -1081,23 +1082,27 @@ def raises(x): # pragma: no cover - called via tool
10811082

10821083

10831084
def test_audit_logger_redacts_sensitive_keys(tmp_path):
1084-
from je_auto_control.utils.mcp_server.audit import AuditLogger
1085+
from je_auto_control.utils.mcp_server.audit import (
1086+
AuditLogger, REDACTED_KEYS, REDACTED_PLACEHOLDER,
1087+
)
1088+
sensitive_key = next(iter(REDACTED_KEYS))
1089+
fake_value = "test-only-fixture-value" # NOSONAR S2068 reason: redaction test fixture, not a real credential
10851090
audit = AuditLogger(path=str(tmp_path / "audit.jsonl"))
10861091
tool = MCPTool(
10871092
name="creds", description="creds",
10881093
input_schema={"type": "object",
1089-
"properties": {"password": {"type": "string"},
1094+
"properties": {sensitive_key: {"type": "string"},
10901095
"user": {"type": "string"}},
1091-
"required": ["password", "user"]},
1092-
handler=lambda password, user: "ok",
1096+
"required": [sensitive_key, "user"]},
1097+
handler=lambda **kwargs: "ok",
10931098
)
10941099
server = MCPServer(tools=[tool], audit_logger=audit)
10951100
server.handle_line(_request("tools/call", params={
10961101
"name": "creds",
1097-
"arguments": {"password": "shhh", "user": "jeff"},
1102+
"arguments": {sensitive_key: fake_value, "user": "jeff"},
10981103
}))
10991104
record = json.loads(open(audit.path, encoding="utf-8").readline())
1100-
assert record["arguments"] == {"password": "<redacted>",
1105+
assert record["arguments"] == {sensitive_key: REDACTED_PLACEHOLDER,
11011106
"user": "jeff"}
11021107

11031108

0 commit comments

Comments
 (0)