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.
Files changed (113) hide show
  1. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/CHANGELOG.md +14 -0
  2. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/PKG-INFO +1 -1
  3. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/pyproject.toml +1 -1
  4. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/requirements.txt +5 -6
  5. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/__init__.py +1 -1
  6. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/protocol.py +91 -3
  7. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/server.py +35 -3
  8. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_mcp_security.py +91 -1
  9. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.gitattributes +0 -0
  10. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/pip-audit.yml +0 -0
  11. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/publish-pypi.yml +0 -0
  12. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/publish-testpypi.yml +0 -0
  13. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/trivy-fs-audit.yml +0 -0
  14. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.github/workflows/unit-test-coverage-gate.yml +0 -0
  15. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/.gitignore +0 -0
  16. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/README.md +0 -0
  17. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
  18. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/check_assets.py +0 -0
  19. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/download_safety_db.bat +0 -0
  20. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/download_safety_db.py +0 -0
  21. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/download_safety_db.sh +0 -0
  22. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/install_external_clis.py +0 -0
  23. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/install_tools.bat +0 -0
  24. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/install_tools.sh +0 -0
  25. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/mcp_add_workspace_filesystem.bat +0 -0
  26. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/mcp_add_workspace_filesystem.sh +0 -0
  27. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/mcp_add_workspace_filesystem_help.md +0 -0
  28. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_bandit_html.bat +0 -0
  29. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_bandit_html.sh +0 -0
  30. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_checkov_html.bat +0 -0
  31. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_checkov_html.sh +0 -0
  32. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_codeql_html.bat +0 -0
  33. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_codeql_html.sh +0 -0
  34. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_consolidated_html.bat +0 -0
  35. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_consolidated_html.sh +0 -0
  36. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_gitleaks_html.bat +0 -0
  37. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_gitleaks_html.sh +0 -0
  38. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_osv_scanner_html.bat +0 -0
  39. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_osv_scanner_html.sh +0 -0
  40. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_pip_audit_html.bat +0 -0
  41. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_pip_audit_html.sh +0 -0
  42. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_safety_html.bat +0 -0
  43. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_safety_html.sh +0 -0
  44. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_semgrep_html.bat +0 -0
  45. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_semgrep_html.sh +0 -0
  46. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_tfsec_html.bat +0 -0
  47. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_tfsec_html.sh +0 -0
  48. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trivy_html.bat +0 -0
  49. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trivy_html.sh +0 -0
  50. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trufflehog_html.bat +0 -0
  51. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/render_trufflehog_html.sh +0 -0
  52. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_all_security_scans.bat +0 -0
  53. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_all_security_scans.sh +0 -0
  54. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_bandit.bat +0 -0
  55. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_bandit.sh +0 -0
  56. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_checkov.bat +0 -0
  57. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_checkov.sh +0 -0
  58. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_codeql.bat +0 -0
  59. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_codeql.sh +0 -0
  60. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_gitleaks.bat +0 -0
  61. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_gitleaks.sh +0 -0
  62. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_osv_scanner.bat +0 -0
  63. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_osv_scanner.sh +0 -0
  64. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_pip_audit.bat +0 -0
  65. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_pip_audit.sh +0 -0
  66. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_safety.bat +0 -0
  67. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_safety.sh +0 -0
  68. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_semgrep.bat +0 -0
  69. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_semgrep.sh +0 -0
  70. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_tfsec.bat +0 -0
  71. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_tfsec.sh +0 -0
  72. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trivy.bat +0 -0
  73. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trivy.sh +0 -0
  74. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trufflehog.bat +0 -0
  75. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_trufflehog.sh +0 -0
  76. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_unit_tests_with_coverage.bat +0 -0
  77. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/scripts/run_unit_tests_with_coverage.sh +0 -0
  78. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/__main__.py +0 -0
  79. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/cli.py +0 -0
  80. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/generate_consolidated_security_report.py +0 -0
  81. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/__init__.py +0 -0
  82. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/catalog.py +0 -0
  83. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/paths.py +0 -0
  84. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/prompts.py +0 -0
  85. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/mcp/resources.py +0 -0
  86. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/models.py +0 -0
  87. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/__init__.py +0 -0
  88. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/bandit.py +0 -0
  89. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/checkov.py +0 -0
  90. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/codeql.py +0 -0
  91. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/cyclonedx.py +0 -0
  92. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/gitleaks.py +0 -0
  93. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
  94. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/pip_audit.py +0 -0
  95. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/safety.py +0 -0
  96. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/semgrep.py +0 -0
  97. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/tfsec.py +0 -0
  98. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/trivy.py +0 -0
  99. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/parsers/trufflehog.py +0 -0
  100. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/__init__.py +0 -0
  101. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/charts.py +0 -0
  102. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/html_renderer.py +0 -0
  103. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/report/sbom_renderer.py +0 -0
  104. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/services/__init__.py +0 -0
  105. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/services/normalize.py +0 -0
  106. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/src/sec_report_kit/services/summarize.py +0 -0
  107. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_charts.py +0 -0
  108. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_cli.py +0 -0
  109. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_cyclonedx.py +0 -0
  110. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_html_renderer.py +0 -0
  111. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_mcp_prompts_resources.py +0 -0
  112. {sec_report_kit-0.7.0 → sec_report_kit-0.7.2}/tests/test_mcp_server.py +0 -0
  113. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sec-report-kit
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Generate HTML security reports from Trivy, pip-audit, Safety, Bandit, and Gitleaks JSON
5
5
  Author: Ravi Konduru
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sec-report-kit"
7
- version = "0.7.0"
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.4.22
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==3.29.0
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.28.1
35
+ mcp==1.30.0
36
36
  mdurl==0.1.2
37
37
  more-itertools==11.0.2
38
- msgpack==1.2.1
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.30
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
@@ -1,4 +1,4 @@
1
1
  """sec_report_kit package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.7.0"
4
+ __version__ = "0.7.2"
@@ -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
- json_type = properties[name].get("type")
55
- if json_type and value is not None and not _type_ok(value, json_type):
56
- problems.append(f"argument '{name}' must be a {json_type}, got {type(value).__name__}")
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
- from mcp.server.stdio import stdio_server
271
+ import sys
272
+ from io import TextIOWrapper
272
273
 
273
- async with stdio_server() as (read_stream, write_stream):
274
- await serve_streams(mcp, read_stream, write_stream)
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": None}) == []
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