sec-report-kit 0.7.0__tar.gz → 0.7.2__tar.gz
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.
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/CHANGELOG.md +14 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/PKG-INFO +1 -1
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/pyproject.toml +1 -1
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/requirements.txt +5 -6
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/__init__.py +1 -1
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/protocol.py +91 -3
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/server.py +35 -3
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_mcp_security.py +91 -1
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.gitattributes +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/pip-audit.yml +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/publish-pypi.yml +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/publish-testpypi.yml +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/trivy-fs-audit.yml +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/unit-test-coverage-gate.yml +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.gitignore +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/README.md +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/check_assets.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/download_safety_db.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/download_safety_db.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/download_safety_db.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/install_external_clis.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/install_tools.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/install_tools.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/mcp_add_workspace_filesystem.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/mcp_add_workspace_filesystem.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/mcp_add_workspace_filesystem_help.md +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_bandit_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_bandit_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_checkov_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_checkov_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_codeql_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_codeql_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_consolidated_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_consolidated_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_gitleaks_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_gitleaks_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_osv_scanner_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_osv_scanner_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_pip_audit_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_pip_audit_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_safety_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_safety_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_semgrep_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_semgrep_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_tfsec_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_tfsec_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trivy_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trivy_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trufflehog_html.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trufflehog_html.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_all_security_scans.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_all_security_scans.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_bandit.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_bandit.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_checkov.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_checkov.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_codeql.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_codeql.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_gitleaks.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_gitleaks.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_osv_scanner.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_osv_scanner.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_pip_audit.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_pip_audit.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_safety.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_safety.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_semgrep.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_semgrep.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_tfsec.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_tfsec.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trivy.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trivy.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trufflehog.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trufflehog.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_unit_tests_with_coverage.bat +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_unit_tests_with_coverage.sh +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/__main__.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/cli.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/generate_consolidated_security_report.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/__init__.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/catalog.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/paths.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/prompts.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/resources.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/models.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/__init__.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/bandit.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/checkov.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/codeql.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/cyclonedx.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/gitleaks.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/pip_audit.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/safety.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/semgrep.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/tfsec.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/trivy.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/trufflehog.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/__init__.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/charts.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/html_renderer.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/sbom_renderer.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/services/__init__.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/services/normalize.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/services/summarize.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_charts.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_cli.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_cyclonedx.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_html_renderer.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_mcp_prompts_resources.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_mcp_server.py +0 -0
- {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_parsers.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.7.2 - 2026-11-04
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- MCP tool calls with an explicit `null` argument now get a short validation error (`argument 'source_type' must be a string, got null`) instead of raw pydantic output. `null` is accepted only where the tool's schema allows it.
|
|
9
|
+
- Invalid input on the MCP server's stdio stream now gets a JSON-RPC 2.0 error instead of being silently dropped: `-32700` for unparseable JSON, and `-32600` for invalid requests and batch arrays, echoing the request id when it can be read and `null` otherwise. Blank lines are ignored. The server now uses its own stdio reader in place of the SDK's.
|
|
10
|
+
|
|
11
|
+
## 0.7.1 - 2026-11-04
|
|
12
|
+
|
|
13
|
+
### Security
|
|
14
|
+
- Bumped pinned dependencies in `requirements.txt` to clear 13 fixable CVEs (1 critical, 2 high, 10 medium) reported by the Trivy filesystem audit, plus one pip-audit advisory: certifi 2026.10.18 (compromised emSign intermediate CA keys), filelock 4.2.1, msgpack 1.2.3, mcp 1.30.0, python-multipart 0.0.31.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Removed a stale `-e git+...#egg=sec_report_kit` entry from `requirements.txt` that made `pip install -r requirements.txt` replace a local checkout with an old commit of the package.
|
|
18
|
+
|
|
5
19
|
## 0.7.0 - 2026-11-04
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sec-report-kit"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Generate HTML security reports from Trivy, pip-audit, Safety, Bandit, and Gitleaks JSON"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -6,7 +6,7 @@ bandit==1.9.4
|
|
|
6
6
|
boolean.py==5.0
|
|
7
7
|
build==1.5.0
|
|
8
8
|
CacheControl==0.14.4
|
|
9
|
-
certifi==2026.
|
|
9
|
+
certifi==2026.10.18
|
|
10
10
|
cffi==2.0.0
|
|
11
11
|
charset-normalizer==3.4.7
|
|
12
12
|
click==8.3.3
|
|
@@ -16,7 +16,7 @@ cryptography==50.0.0
|
|
|
16
16
|
cyclonedx-python-lib==11.7.0
|
|
17
17
|
defusedxml==0.7.1
|
|
18
18
|
docutils==0.22.4
|
|
19
|
-
filelock==
|
|
19
|
+
filelock==4.2.1
|
|
20
20
|
h11==0.16.0
|
|
21
21
|
httpcore==1.0.9
|
|
22
22
|
httpx==0.28.1
|
|
@@ -32,10 +32,10 @@ jsonschema-specifications==2025.9.1
|
|
|
32
32
|
keyring==25.7.0
|
|
33
33
|
license-expression==30.4.4
|
|
34
34
|
markdown-it-py==4.0.0
|
|
35
|
-
mcp==1.
|
|
35
|
+
mcp==1.30.0
|
|
36
36
|
mdurl==0.1.2
|
|
37
37
|
more-itertools==11.0.2
|
|
38
|
-
msgpack==1.2.
|
|
38
|
+
msgpack==1.2.3
|
|
39
39
|
nh3==0.3.5
|
|
40
40
|
packageurl-python==0.17.6
|
|
41
41
|
packaging==26.2
|
|
@@ -57,7 +57,7 @@ pyproject_hooks==1.2.0
|
|
|
57
57
|
pytest==9.0.3
|
|
58
58
|
pytest-cov==7.1.0
|
|
59
59
|
python-dotenv==1.2.2
|
|
60
|
-
python-multipart==0.0.
|
|
60
|
+
python-multipart==0.0.31
|
|
61
61
|
pywin32==311
|
|
62
62
|
pywin32-ctypes==0.2.3
|
|
63
63
|
PyYAML==6.0.3
|
|
@@ -68,7 +68,6 @@ requests-toolbelt==1.0.0
|
|
|
68
68
|
rfc3986==2.0.0
|
|
69
69
|
rich==15.0.0
|
|
70
70
|
rpds-py==0.30.0
|
|
71
|
-
-e git+https://github.com/ShanKonduru/sec-report-kit.git@b6d196bac687e07189e884e2fe25af5c8842da1d#egg=sec_report_kit
|
|
72
71
|
shellingham==1.5.4
|
|
73
72
|
sortedcontainers==2.4.0
|
|
74
73
|
sse-starlette==3.4.1
|
|
@@ -15,6 +15,7 @@ The SDK leaves a few rough edges that strict clients notice:
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import json
|
|
18
19
|
import typing
|
|
19
20
|
from typing import Any, Callable, Iterable
|
|
20
21
|
|
|
@@ -34,6 +35,19 @@ def _type_ok(value: Any, json_type: str) -> bool:
|
|
|
34
35
|
return isinstance(value, _JSON_TYPES.get(json_type, (object,)))
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def _types_of(prop: dict) -> list[str]:
|
|
39
|
+
declared = prop.get("type")
|
|
40
|
+
if declared is None:
|
|
41
|
+
return []
|
|
42
|
+
return list(declared) if isinstance(declared, list) else [declared]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _allows_null(prop: dict) -> bool:
|
|
46
|
+
if "null" in _types_of(prop):
|
|
47
|
+
return True
|
|
48
|
+
return any(_allows_null(sub) for key in ("anyOf", "oneOf") for sub in prop.get(key, []))
|
|
49
|
+
|
|
50
|
+
|
|
37
51
|
def validate_arguments(tool_name: str, schema: dict, arguments: Any) -> list[str]:
|
|
38
52
|
"""Return human-readable problems with ``arguments``; empty when valid."""
|
|
39
53
|
if arguments is None:
|
|
@@ -51,9 +65,16 @@ def validate_arguments(tool_name: str, schema: dict, arguments: Any) -> list[str
|
|
|
51
65
|
f"unexpected argument '{name}' (allowed: {', '.join(sorted(properties))})"
|
|
52
66
|
)
|
|
53
67
|
continue
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
prop = properties[name]
|
|
69
|
+
types = [t for t in _types_of(prop) if t != "null"]
|
|
70
|
+
expected = " or ".join(types) if types else "value"
|
|
71
|
+
if value is None:
|
|
72
|
+
# An explicit null must be checked too: pydantic rejects it for a
|
|
73
|
+
# plain `str` parameter and would otherwise leak its raw message.
|
|
74
|
+
if not _allows_null(prop):
|
|
75
|
+
problems.append(f"argument '{name}' must be a {expected}, got null")
|
|
76
|
+
elif types and not any(_type_ok(value, t) for t in types):
|
|
77
|
+
problems.append(f"argument '{name}' must be a {expected}, got {type(value).__name__}")
|
|
57
78
|
return problems
|
|
58
79
|
|
|
59
80
|
|
|
@@ -106,3 +127,70 @@ async def filter_requests(
|
|
|
106
127
|
await reply(root, INVALID_PARAMS, f"Unknown tool: {name}")
|
|
107
128
|
continue
|
|
108
129
|
await forward_stream.send(item)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# stdio transport
|
|
134
|
+
#
|
|
135
|
+
# The SDK's stdio reader turns any line that is not a valid JSON-RPC message
|
|
136
|
+
# into an exception the server only logs, so the sender gets no response (and
|
|
137
|
+
# a request with an id is never answered). JSON-RPC 2.0 requires -32700 for
|
|
138
|
+
# unparseable JSON and -32600 for an invalid request, with "id": null when the
|
|
139
|
+
# id cannot be read. The SDK's JSONRPCError cannot carry a null id, so these
|
|
140
|
+
# replies are written as raw JSON by this transport.
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
PARSE_ERROR = -32700
|
|
144
|
+
INVALID_REQUEST = -32600
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _error_line(request_id: Any, code: int, message: str) -> str:
|
|
148
|
+
return json.dumps({"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def parse_line(line: str) -> Any:
|
|
152
|
+
"""Classify one line of stdin.
|
|
153
|
+
|
|
154
|
+
Returns a ``SessionMessage`` for a valid JSON-RPC message, a ``str`` (the
|
|
155
|
+
JSON-RPC error response to write back) for invalid input, or ``None`` for
|
|
156
|
+
a blank line.
|
|
157
|
+
"""
|
|
158
|
+
from mcp.shared.message import SessionMessage
|
|
159
|
+
from mcp.types import JSONRPCMessage
|
|
160
|
+
|
|
161
|
+
if not line.strip():
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
payload = json.loads(line)
|
|
165
|
+
except ValueError:
|
|
166
|
+
return _error_line(None, PARSE_ERROR, "Parse error: invalid JSON")
|
|
167
|
+
if isinstance(payload, list):
|
|
168
|
+
return _error_line(None, INVALID_REQUEST, "Invalid Request: batch requests are not supported")
|
|
169
|
+
try:
|
|
170
|
+
return SessionMessage(message=JSONRPCMessage.model_validate(payload))
|
|
171
|
+
except Exception:
|
|
172
|
+
request_id = payload.get("id") if isinstance(payload, dict) else None
|
|
173
|
+
if isinstance(request_id, bool) or not isinstance(request_id, (str, int)):
|
|
174
|
+
request_id = None
|
|
175
|
+
return _error_line(
|
|
176
|
+
request_id, INVALID_REQUEST, "Invalid Request: not a valid JSON-RPC 2.0 message"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def pump_input(lines: Any, read_stream_writer: Any, write_line: Callable[[str], Any]) -> None:
|
|
181
|
+
"""Feed valid messages to the server; answer invalid lines directly."""
|
|
182
|
+
async with read_stream_writer:
|
|
183
|
+
async for line in lines:
|
|
184
|
+
parsed = parse_line(line)
|
|
185
|
+
if parsed is None:
|
|
186
|
+
continue
|
|
187
|
+
if isinstance(parsed, str):
|
|
188
|
+
await write_line(parsed)
|
|
189
|
+
else:
|
|
190
|
+
await read_stream_writer.send(parsed)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def pump_output(write_stream_reader: Any, write_line: Callable[[str], Any]) -> None:
|
|
194
|
+
async with write_stream_reader:
|
|
195
|
+
async for session_message in write_stream_reader:
|
|
196
|
+
await write_line(session_message.message.model_dump_json(by_alias=True, exclude_none=True))
|
|
@@ -268,10 +268,42 @@ def run_server(transport: str = "stdio", roots: Iterable[str | Path] | None = No
|
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
async def _serve_stdio(mcp: Any) -> None: # pragma: no cover - binds the real process stdin/stdout
|
|
271
|
-
|
|
271
|
+
import sys
|
|
272
|
+
from io import TextIOWrapper
|
|
272
273
|
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
import anyio
|
|
275
|
+
|
|
276
|
+
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8"))
|
|
277
|
+
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
|
|
278
|
+
|
|
279
|
+
async def write_line(text: str) -> None:
|
|
280
|
+
await stdout.write(text + "\n")
|
|
281
|
+
await stdout.flush()
|
|
282
|
+
|
|
283
|
+
await serve_lines(mcp, stdin, write_line)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def serve_lines(mcp: Any, lines: Any, write_line: Any) -> None:
|
|
287
|
+
"""Serve newline-delimited JSON-RPC from ``lines``, writing each response
|
|
288
|
+
line with ``write_line``. Replaces the SDK's stdio_server so that invalid
|
|
289
|
+
input gets a JSON-RPC error instead of being silently dropped."""
|
|
290
|
+
import anyio
|
|
291
|
+
|
|
292
|
+
from sec_report_kit.mcp.protocol import pump_input, pump_output
|
|
293
|
+
|
|
294
|
+
lock = anyio.Lock()
|
|
295
|
+
|
|
296
|
+
async def locked_write(text: str) -> None:
|
|
297
|
+
async with lock:
|
|
298
|
+
await write_line(text)
|
|
299
|
+
|
|
300
|
+
read_send, read_recv = anyio.create_memory_object_stream(0)
|
|
301
|
+
write_send, write_recv = anyio.create_memory_object_stream(0)
|
|
302
|
+
async with anyio.create_task_group() as tg:
|
|
303
|
+
tg.start_soon(pump_input, lines, read_send, locked_write)
|
|
304
|
+
tg.start_soon(pump_output, write_recv, locked_write)
|
|
305
|
+
async with write_send:
|
|
306
|
+
await serve_streams(mcp, read_recv, write_send)
|
|
275
307
|
|
|
276
308
|
|
|
277
309
|
async def serve_streams(mcp: Any, read_stream: Any, write_stream: Any) -> None:
|
|
@@ -188,13 +188,37 @@ def test_validate_arguments_messages():
|
|
|
188
188
|
assert "argument 'n' must be a integer, got bool" in problems
|
|
189
189
|
assert "unexpected argument 'q' (allowed: a, n, x, z)" in problems
|
|
190
190
|
assert len(problems) == 3
|
|
191
|
-
assert protocol.validate_arguments("t", schema, {"a": "ok", "n":
|
|
191
|
+
assert protocol.validate_arguments("t", schema, {"a": "ok", "n": 3}) == []
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_validate_arguments_rejects_null_unless_schema_allows_it():
|
|
195
|
+
schema = {
|
|
196
|
+
"properties": {
|
|
197
|
+
"s": {"type": "string"},
|
|
198
|
+
"typed_null": {"type": ["string", "null"]},
|
|
199
|
+
"any_of_null": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
200
|
+
"untyped": {},
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
problems = protocol.validate_arguments(
|
|
204
|
+
"t", schema, {"s": None, "typed_null": None, "any_of_null": None, "untyped": None}
|
|
205
|
+
)
|
|
206
|
+
assert problems == [
|
|
207
|
+
"argument 's' must be a string, got null",
|
|
208
|
+
"argument 'untyped' must be a value, got null",
|
|
209
|
+
]
|
|
210
|
+
assert protocol.validate_arguments("t", schema, {"typed_null": 5}) == [
|
|
211
|
+
"argument 'typed_null' must be a string, got int"
|
|
212
|
+
]
|
|
192
213
|
|
|
193
214
|
|
|
194
215
|
def test_server_rejects_extra_and_bad_args_cleanly(root):
|
|
195
216
|
server = srv.build_server(roots=[root])
|
|
196
217
|
is_error, text = _call(server, "summarize_json", {"source_type": "auto", "input_path": "scan.json", "bogus": 1})
|
|
197
218
|
assert is_error and "unexpected argument 'bogus'" in text
|
|
219
|
+
is_error, text = _call(server, "summarize_json", {"source_type": None, "input_path": "scan.json"})
|
|
220
|
+
assert is_error and "argument 'source_type' must be a string, got null" in text
|
|
221
|
+
assert "pydantic" not in text and "input_value" not in text
|
|
198
222
|
is_error, text = _call(server, "summarize_json", {"source_type": 5})
|
|
199
223
|
assert is_error
|
|
200
224
|
assert "missing required argument 'input_path'" in text
|
|
@@ -255,3 +279,69 @@ def test_serve_streams_answers_unknown_method_and_tool(root):
|
|
|
255
279
|
result = responses[4]["result"]
|
|
256
280
|
assert result.get("isError") is not True
|
|
257
281
|
assert json.loads(result["content"][0]["text"])["total"] == 1
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------- SR-006: invalid stdio input gets a JSON-RPC error ----------
|
|
285
|
+
|
|
286
|
+
@pytest.mark.parametrize("line, expected_id, code", [
|
|
287
|
+
("{not json", None, -32700),
|
|
288
|
+
('{"jsonrpc": "2.0", "id": 3}', 3, -32600),
|
|
289
|
+
('{"id": "abc", "method": "tools/list"}', "abc", -32600),
|
|
290
|
+
('{"jsonrpc": "2.0", "id": true}', None, -32600),
|
|
291
|
+
('{"jsonrpc": "2.0", "id": [1]}', None, -32600),
|
|
292
|
+
('"just a string"', None, -32600),
|
|
293
|
+
("[1, 2]", None, -32600),
|
|
294
|
+
])
|
|
295
|
+
def test_parse_line_invalid_input(line, expected_id, code):
|
|
296
|
+
reply = protocol.parse_line(line)
|
|
297
|
+
assert isinstance(reply, str)
|
|
298
|
+
data = json.loads(reply)
|
|
299
|
+
assert data["jsonrpc"] == "2.0" and data["id"] == expected_id and data["error"]["code"] == code
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_parse_line_valid_and_blank():
|
|
303
|
+
from mcp.shared.message import SessionMessage
|
|
304
|
+
|
|
305
|
+
assert protocol.parse_line(" \n") is None
|
|
306
|
+
msg = protocol.parse_line('{"jsonrpc": "2.0", "id": 1, "method": "ping"}')
|
|
307
|
+
assert isinstance(msg, SessionMessage)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_serve_lines_end_to_end(root):
|
|
311
|
+
server = srv.build_server(roots=[root])
|
|
312
|
+
lines = [
|
|
313
|
+
json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
|
|
314
|
+
"protocolVersion": "2025-06-18", "capabilities": {},
|
|
315
|
+
"clientInfo": {"name": "t", "version": "0"}}}),
|
|
316
|
+
json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}),
|
|
317
|
+
"{broken",
|
|
318
|
+
"",
|
|
319
|
+
json.dumps({"jsonrpc": "2.0", "id": 7}),
|
|
320
|
+
json.dumps({"jsonrpc": "2.0", "id": 8, "method": "no/such"}),
|
|
321
|
+
json.dumps({"jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": {
|
|
322
|
+
"name": "summarize_json", "arguments": {"source_type": None, "input_path": "scan.json"}}}),
|
|
323
|
+
]
|
|
324
|
+
written: list[dict] = []
|
|
325
|
+
|
|
326
|
+
async def source():
|
|
327
|
+
for line in lines:
|
|
328
|
+
yield line + "\n"
|
|
329
|
+
await anyio.sleep(0.05)
|
|
330
|
+
|
|
331
|
+
async def write_line(text):
|
|
332
|
+
written.append(json.loads(text))
|
|
333
|
+
|
|
334
|
+
async def main():
|
|
335
|
+
with anyio.fail_after(30):
|
|
336
|
+
await srv.serve_lines(server, source(), write_line)
|
|
337
|
+
|
|
338
|
+
anyio.run(main)
|
|
339
|
+
|
|
340
|
+
by_id = {m.get("id", "none"): m for m in written}
|
|
341
|
+
assert by_id[1]["result"]["serverInfo"]["name"] == "sec-report-kit"
|
|
342
|
+
assert by_id[None]["error"]["code"] == -32700
|
|
343
|
+
assert by_id[7]["error"]["code"] == -32600
|
|
344
|
+
assert by_id[8]["error"]["code"] == -32601
|
|
345
|
+
text = by_id[9]["result"]["content"][0]["text"]
|
|
346
|
+
assert by_id[9]["result"]["isError"] and "got null" in text and "pydantic" not in text
|
|
347
|
+
assert len(written) == 5 # blank line produced nothing
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|