sdcvalidator 4.5.1__tar.gz → 4.5.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.
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/PKG-INFO +1 -1
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/pyproject.toml +1 -1
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/__init__.py +1 -1
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/mcp_server.py +23 -3
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/PKG-INFO +1 -1
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_mcp_server.py +52 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/LICENSE +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/README.md +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/setup.cfg +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/cli.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/constants.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/converters.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/error_classifier.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/error_mapper.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/exceptions.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/instance_modifier.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/schema_checker.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/validator.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator/xsd11_restriction.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/SOURCES.txt +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/requires.txt +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/top_level.txt +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_converters.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_error_classifier.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_error_mapper.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_instance_modifier.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_recovery.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_schema_checker.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_validator.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_xsd11_restriction.py +0 -0
- {sdcvalidator-4.5.1 → sdcvalidator-4.5.2}/tests/test_xsi_type.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcvalidator"
|
|
7
|
-
version = "4.5.
|
|
7
|
+
version = "4.5.2"
|
|
8
8
|
description = "SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema"
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ Thin wrapper over xmlschema with two-tier error classification and the SDC4
|
|
|
14
14
|
"quarantine-and-tag" recovery pattern (ISO 21090 ExceptionalValues).
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "4.5.
|
|
17
|
+
__version__ = "4.5.2"
|
|
18
18
|
|
|
19
19
|
# etree_tostring is re-exported from xmlschema so callers can serialize the
|
|
20
20
|
# ElementTree returned by validate_with_recovery() without importing xmlschema
|
|
@@ -36,7 +36,7 @@ from sdcvalidator.schema_checker import validate_sdc4_schema_compliance
|
|
|
36
36
|
JSONRPC_VERSION = "2.0"
|
|
37
37
|
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
38
38
|
SERVER_NAME = "sdcvalidator"
|
|
39
|
-
SERVER_VERSION = "4.5.
|
|
39
|
+
SERVER_VERSION = "4.5.2"
|
|
40
40
|
|
|
41
41
|
# Validator cache: schema_path -> SDC4Validator
|
|
42
42
|
_validators: dict[str, SDC4Validator] = {}
|
|
@@ -201,10 +201,24 @@ def _jsonrpc_error(id: Any, code: int, message: str, data: Any = None) -> dict:
|
|
|
201
201
|
return {"jsonrpc": JSONRPC_VERSION, "id": id, "error": error}
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def _handle_request(request
|
|
204
|
+
def _handle_request(request) -> dict | None:
|
|
205
|
+
# ★ Attacker-reachable input over stdio with no authentication. A JSON
|
|
206
|
+
# value that is not an object, or an object whose params is not one,
|
|
207
|
+
# used to raise AttributeError and kill the server (VSL rollout R9).
|
|
208
|
+
# Every shape the protocol does not allow is answered with -32600.
|
|
209
|
+
if not isinstance(request, dict):
|
|
210
|
+
return _jsonrpc_error(None, -32600, "Invalid Request: a JSON-RPC request is an object")
|
|
211
|
+
req_id = request.get("id")
|
|
212
|
+
if not isinstance(req_id, (str, int, type(None))):
|
|
213
|
+
return _jsonrpc_error(None, -32600, "Invalid Request: id must be a string, a number or null")
|
|
205
214
|
method = request.get("method", "")
|
|
215
|
+
if not isinstance(method, str):
|
|
216
|
+
return _jsonrpc_error(req_id, -32600, "Invalid Request: method must be a string")
|
|
206
217
|
params = request.get("params", {})
|
|
207
|
-
|
|
218
|
+
if params is None:
|
|
219
|
+
params = {}
|
|
220
|
+
if not isinstance(params, dict):
|
|
221
|
+
return _jsonrpc_error(req_id, -32600, "Invalid Request: params must be an object")
|
|
208
222
|
|
|
209
223
|
if method == "initialize":
|
|
210
224
|
result = {
|
|
@@ -228,6 +242,12 @@ def _handle_request(request: dict) -> dict | None:
|
|
|
228
242
|
elif method == "tools/call":
|
|
229
243
|
tool_name = params.get("name", "")
|
|
230
244
|
tool_args = params.get("arguments", {})
|
|
245
|
+
if tool_args is None:
|
|
246
|
+
tool_args = {}
|
|
247
|
+
if not isinstance(tool_name, str) or not isinstance(tool_args, dict):
|
|
248
|
+
return _jsonrpc_error(
|
|
249
|
+
req_id, -32602, "Invalid params: name must be a string and arguments an object"
|
|
250
|
+
)
|
|
231
251
|
|
|
232
252
|
handler = TOOL_HANDLERS.get(tool_name)
|
|
233
253
|
if handler is None:
|
|
@@ -137,3 +137,55 @@ class TestConsistentSerialization:
|
|
|
137
137
|
assert content[0]["type"] == "text"
|
|
138
138
|
parsed = json.loads(content[0]["text"])
|
|
139
139
|
assert parsed is not None
|
|
140
|
+
|
|
141
|
+
class TestMalformedRequestsDoNotKillTheServer:
|
|
142
|
+
"""
|
|
143
|
+
VSL rollout inventory R9. Six inputs that are valid JSON but not a
|
|
144
|
+
JSON-RPC request raised AttributeError in _handle_request and ended the
|
|
145
|
+
process. Each is now answered with -32600 (or -32602 for bad tool params)
|
|
146
|
+
and the loop keeps reading.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
@pytest.mark.parametrize("payload", [[], ["tools/list"], "tools/list", 7, None, True])
|
|
150
|
+
def test_a_non_object_request_is_invalid_request(self, payload):
|
|
151
|
+
response = _handle_request(payload)
|
|
152
|
+
assert response["error"]["code"] == -32600
|
|
153
|
+
assert response["id"] is None
|
|
154
|
+
|
|
155
|
+
@pytest.mark.parametrize("params", [[], "x", 3, True])
|
|
156
|
+
def test_non_object_params_are_invalid_request(self, params):
|
|
157
|
+
response = _handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": params})
|
|
158
|
+
assert response["error"]["code"] == -32600
|
|
159
|
+
assert response["id"] == 1
|
|
160
|
+
|
|
161
|
+
def test_null_params_are_treated_as_empty(self):
|
|
162
|
+
response = _handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": None})
|
|
163
|
+
assert "result" in response
|
|
164
|
+
|
|
165
|
+
def test_a_non_string_method_is_invalid_request(self):
|
|
166
|
+
response = _handle_request({"jsonrpc": "2.0", "id": 1, "method": ["tools/list"]})
|
|
167
|
+
assert response["error"]["code"] == -32600
|
|
168
|
+
|
|
169
|
+
@pytest.mark.parametrize("arguments", [[], "x", 3])
|
|
170
|
+
def test_non_object_tool_arguments_are_invalid_params(self, arguments):
|
|
171
|
+
response = _handle_request({
|
|
172
|
+
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
173
|
+
"params": {"name": list(TOOL_HANDLERS)[0], "arguments": arguments},
|
|
174
|
+
})
|
|
175
|
+
assert response["error"]["code"] == -32602
|
|
176
|
+
|
|
177
|
+
def test_the_stdio_loop_survives_every_one_of_them(self, monkeypatch, capsys):
|
|
178
|
+
import io
|
|
179
|
+
import sys
|
|
180
|
+
|
|
181
|
+
from sdcvalidator.mcp_server import run_stdio
|
|
182
|
+
|
|
183
|
+
lines = ["[]", "\"tools/list\"", "7", "null",
|
|
184
|
+
json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": []}),
|
|
185
|
+
json.dumps({"jsonrpc": "2.0", "id": 2, "method": "ping"})]
|
|
186
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO("\n".join(lines) + "\n"))
|
|
187
|
+
run_stdio()
|
|
188
|
+
out = [json.loads(l) for l in capsys.readouterr().out.strip().splitlines()]
|
|
189
|
+
assert len(out) == 6
|
|
190
|
+
assert [o.get("error", {}).get("code") for o in out[:5]] == [-32600] * 5
|
|
191
|
+
assert out[5] == {"jsonrpc": "2.0", "id": 2, "result": {}}
|
|
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
|