sdcvalidator 4.5.1__tar.gz → 4.5.3__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 (34) hide show
  1. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/PKG-INFO +6 -2
  2. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/README.md +5 -1
  3. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/pyproject.toml +1 -1
  4. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/__init__.py +1 -1
  5. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/mcp_server.py +44 -9
  6. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator.egg-info/PKG-INFO +6 -2
  7. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator.egg-info/SOURCES.txt +1 -0
  8. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_mcp_server.py +52 -0
  9. sdcvalidator-4.5.3/tests/test_registry_launch.py +79 -0
  10. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/LICENSE +0 -0
  11. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/setup.cfg +0 -0
  12. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/cli.py +0 -0
  13. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/constants.py +0 -0
  14. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/converters.py +0 -0
  15. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/error_classifier.py +0 -0
  16. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/error_mapper.py +0 -0
  17. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/exceptions.py +0 -0
  18. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/instance_modifier.py +0 -0
  19. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/schema_checker.py +0 -0
  20. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/validator.py +0 -0
  21. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator/xsd11_restriction.py +0 -0
  22. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  23. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
  24. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator.egg-info/requires.txt +0 -0
  25. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  26. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_converters.py +0 -0
  27. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_error_classifier.py +0 -0
  28. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_error_mapper.py +0 -0
  29. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_instance_modifier.py +0 -0
  30. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_recovery.py +0 -0
  31. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_schema_checker.py +0 -0
  32. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_validator.py +0 -0
  33. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_xsd11_restriction.py +0 -0
  34. {sdcvalidator-4.5.1 → sdcvalidator-4.5.3}/tests/test_xsi_type.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.5.1
3
+ Version: 4.5.3
4
4
  Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
5
5
  Author: Axius SDC, Inc.
6
6
  License-Expression: Apache-2.0
@@ -151,9 +151,13 @@ sdcvalidator-json2xml data.json schema.xsd -o output.xml
151
151
  `sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
152
152
 
153
153
  ```bash
154
- sdcvalidator-mcp serve --mcp
154
+ sdcvalidator # what an MCP registry client runs (uvx sdcvalidator)
155
+ sdcvalidator mcp # the same, spelled like the other Semantic Data Charter servers
156
+ sdcvalidator-mcp serve --mcp # still works for existing client configurations
155
157
  ```
156
158
 
159
+ A client configuration needs only `"command": "uvx", "args": ["sdcvalidator"]`.
160
+
157
161
  ### Tools
158
162
 
159
163
  | Tool | Purpose |
@@ -129,9 +129,13 @@ sdcvalidator-json2xml data.json schema.xsd -o output.xml
129
129
  `sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
130
130
 
131
131
  ```bash
132
- sdcvalidator-mcp serve --mcp
132
+ sdcvalidator # what an MCP registry client runs (uvx sdcvalidator)
133
+ sdcvalidator mcp # the same, spelled like the other Semantic Data Charter servers
134
+ sdcvalidator-mcp serve --mcp # still works for existing client configurations
133
135
  ```
134
136
 
137
+ A client configuration needs only `"command": "uvx", "args": ["sdcvalidator"]`.
138
+
135
139
  ### Tools
136
140
 
137
141
  | Tool | Purpose |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdcvalidator"
7
- version = "4.5.1"
7
+ version = "4.5.3"
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.1"
17
+ __version__ = "4.5.3"
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.1"
39
+ SERVER_VERSION = "4.5.3"
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: dict) -> dict | None:
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
- req_id = request.get("id")
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:
@@ -270,22 +290,37 @@ def run_stdio() -> None:
270
290
  sys.stdout.flush()
271
291
 
272
292
 
273
- def main() -> None:
274
- """Entry point for the sdcvalidator MCP server."""
293
+ def main(argv: list[str] | None = None) -> None:
294
+ """Entry point of the ``sdcvalidator`` and ``sdcvalidator-mcp`` console scripts.
295
+
296
+ Three spellings start the stdio server: no arguments at all (what an MCP
297
+ registry client runs: ``uvx sdcvalidator``), ``mcp`` (the spelling the other
298
+ Semantic Data Charter servers use) and ``serve --mcp`` (kept for existing
299
+ client configurations). Until 4.5.3 the bare command printed this help and
300
+ exited, so the registry entry never started a server (issue #15).
301
+ """
275
302
  import argparse
276
303
 
277
304
  parser = argparse.ArgumentParser(
278
- prog="sdcvalidator-mcp",
279
- description="SDC4 structural validator - MCP server",
305
+ prog="sdcvalidator",
306
+ description="SDC4 structural validator - MCP stdio server",
307
+ epilog="With no command the MCP server starts, reading JSON-RPC on stdin.",
280
308
  )
281
309
  subparsers = parser.add_subparsers(dest="command")
282
310
 
311
+ subparsers.add_parser("mcp", help="Run the MCP stdio server")
283
312
  serve_parser = subparsers.add_parser("serve", help="Start the validation server")
284
313
  serve_parser.add_argument("--mcp", action="store_true", help="Run as MCP stdio server")
285
314
 
286
- args = parser.parse_args()
315
+ args = parser.parse_args(argv)
287
316
 
288
- if args.command == "serve" and args.mcp:
317
+ if args.command is None or args.command == "mcp" or (args.command == "serve" and args.mcp):
318
+ if args.command is None and sys.stdin.isatty():
319
+ sys.stderr.write(
320
+ "sdcvalidator: MCP server reading JSON-RPC 2.0 on stdin "
321
+ "(sdcvalidator --help for options, Ctrl-D to stop)\n"
322
+ )
323
+ sys.stderr.flush()
289
324
  run_stdio()
290
325
  else:
291
326
  parser.print_help()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.5.1
3
+ Version: 4.5.3
4
4
  Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
5
5
  Author: Axius SDC, Inc.
6
6
  License-Expression: Apache-2.0
@@ -151,9 +151,13 @@ sdcvalidator-json2xml data.json schema.xsd -o output.xml
151
151
  `sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
152
152
 
153
153
  ```bash
154
- sdcvalidator-mcp serve --mcp
154
+ sdcvalidator # what an MCP registry client runs (uvx sdcvalidator)
155
+ sdcvalidator mcp # the same, spelled like the other Semantic Data Charter servers
156
+ sdcvalidator-mcp serve --mcp # still works for existing client configurations
155
157
  ```
156
158
 
159
+ A client configuration needs only `"command": "uvx", "args": ["sdcvalidator"]`.
160
+
157
161
  ### Tools
158
162
 
159
163
  | Tool | Purpose |
@@ -25,6 +25,7 @@ tests/test_error_mapper.py
25
25
  tests/test_instance_modifier.py
26
26
  tests/test_mcp_server.py
27
27
  tests/test_recovery.py
28
+ tests/test_registry_launch.py
28
29
  tests/test_schema_checker.py
29
30
  tests/test_validator.py
30
31
  tests/test_xsd11_restriction.py
@@ -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": {}}
@@ -0,0 +1,79 @@
1
+ """
2
+ The MCP registry entry starts the server (issue #15).
3
+
4
+ ``server.json`` declared no ``packageArguments``, so ``uvx sdcvalidator`` ran
5
+ ``main()`` with no arguments, which printed the CLI help and exited. These
6
+ tests start the console script's entry point the way a registry client, a
7
+ hand-written configuration and the pre-4.5.3 configuration do, and check that
8
+ ``server.json`` names the released version.
9
+ """
10
+ import json
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ import sdcvalidator
19
+ from sdcvalidator.mcp_server import SERVER_VERSION
20
+
21
+ ROOT = Path(__file__).resolve().parents[1]
22
+ SERVER_JSON = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
23
+
24
+ INITIALIZE = {"jsonrpc": "2.0", "id": 1, "method": "initialize",
25
+ "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "t", "version": "0"}}}
26
+
27
+
28
+ def _launch(args):
29
+ proc = subprocess.run(
30
+ [sys.executable, "-m", "sdcvalidator.mcp_server", *args],
31
+ input=json.dumps(INITIALIZE) + "\n",
32
+ capture_output=True, text=True, timeout=60,
33
+ )
34
+ return proc
35
+
36
+
37
+ def _registry_args():
38
+ package = SERVER_JSON["packages"][0]
39
+ return [a["value"] for a in package.get("packageArguments", []) if a["type"] == "positional"]
40
+
41
+
42
+ @pytest.mark.parametrize("args", [
43
+ pytest.param([], id="bare, what a registry client without packageArguments runs"),
44
+ pytest.param(_registry_args(), id="server.json packageArguments"),
45
+ pytest.param(["mcp"], id="mcp"),
46
+ pytest.param(["serve", "--mcp"], id="serve --mcp (pre-4.5.3 configurations)"),
47
+ ])
48
+ def test_each_launch_spelling_answers_initialize(args):
49
+ proc = _launch(args)
50
+ assert proc.returncode == 0, proc.stderr
51
+ lines = [line for line in proc.stdout.splitlines() if line.strip()]
52
+ assert lines, f"no JSON-RPC response; stdout was help text?\n{proc.stdout}"
53
+ response = json.loads(lines[0])
54
+ assert response["id"] == 1
55
+ assert response["result"]["serverInfo"]["name"] == "sdcvalidator"
56
+ assert response["result"]["serverInfo"]["version"] == SERVER_VERSION
57
+ assert "usage:" not in proc.stdout
58
+
59
+
60
+ def test_serve_without_mcp_still_prints_help():
61
+ proc = _launch(["serve"])
62
+ assert proc.returncode == 0
63
+ assert "usage:" in proc.stdout
64
+
65
+
66
+ def test_server_json_declares_the_mcp_launch():
67
+ package = SERVER_JSON["packages"][0]
68
+ assert package["identifier"] == "sdcvalidator"
69
+ assert package["runtimeHint"] == "uvx"
70
+ assert _registry_args() == ["mcp"]
71
+
72
+
73
+ def test_server_json_names_the_released_version():
74
+ # A regex, not tomllib: the test matrix includes Python 3.10.
75
+ pyproject = re.search(r'^version = "([^"]+)"', (ROOT / "pyproject.toml").read_text(encoding="utf-8"), re.M).group(1)
76
+ assert SERVER_JSON["version"] == pyproject
77
+ assert SERVER_JSON["packages"][0]["version"] == pyproject
78
+ assert sdcvalidator.__version__ == pyproject
79
+ assert SERVER_VERSION == pyproject
File without changes
File without changes