sdcvalidator 4.2.0__tar.gz → 4.2.1__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 (24) hide show
  1. {sdcvalidator-4.2.0/src/sdcvalidator.egg-info → sdcvalidator-4.2.1}/PKG-INFO +1 -1
  2. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/pyproject.toml +2 -1
  3. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/__init__.py +1 -1
  4. sdcvalidator-4.2.1/src/sdcvalidator/mcp_server.py +295 -0
  5. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1/src/sdcvalidator.egg-info}/PKG-INFO +1 -1
  6. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/SOURCES.txt +2 -0
  7. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/entry_points.txt +1 -0
  8. sdcvalidator-4.2.1/tests/test_mcp_server.py +139 -0
  9. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/LICENSE +0 -0
  10. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/README.md +0 -0
  11. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/setup.cfg +0 -0
  12. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/cli.py +0 -0
  13. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/constants.py +0 -0
  14. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/converters.py +0 -0
  15. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/error_classifier.py +0 -0
  16. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/schema_checker.py +0 -0
  17. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator/validator.py +0 -0
  18. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  19. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/requires.txt +0 -0
  20. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  21. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/tests/test_converters.py +0 -0
  22. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/tests/test_error_classifier.py +0 -0
  23. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/tests/test_schema_checker.py +0 -0
  24. {sdcvalidator-4.2.0 → sdcvalidator-4.2.1}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.2.0
3
+ Version: 4.2.1
4
4
  Summary: SDC4 structural validator — thin wrapper over xmlschema with error classification
5
5
  Author: Semantic Data Charter Foundation
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdcvalidator"
7
- version = "4.2.0"
7
+ version = "4.2.1"
8
8
  description = "SDC4 structural validator — thin wrapper over xmlschema with error classification"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.10"
@@ -30,6 +30,7 @@ dev = ["pytest>=7.0", "pytest-cov"]
30
30
  sdcvalidate = "sdcvalidator.cli:validate_main"
31
31
  sdcvalidator-xml2json = "sdcvalidator.cli:xml2json_main"
32
32
  sdcvalidator-json2xml = "sdcvalidator.cli:json2xml_main"
33
+ sdcvalidator-mcp = "sdcvalidator.mcp_server:main"
33
34
 
34
35
  [tool.setuptools.packages.find]
35
36
  where = ["src"]
@@ -13,7 +13,7 @@ sdcvalidator — SDC4 structural validator.
13
13
  Thin wrapper over xmlschema with two-tier error classification.
14
14
  """
15
15
 
16
- __version__ = "4.1.0"
16
+ __version__ = "4.2.1"
17
17
 
18
18
  from .validator import SDC4Validator, ValidationResult
19
19
  from .error_classifier import ErrorClassifier
@@ -0,0 +1,295 @@
1
+ #
2
+ # Copyright 2026 Semantic Data Charter Foundation
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ """
11
+ MCP server exposing sdcvalidator tools to any agent framework.
12
+
13
+ Implements the MCP (Model Context Protocol) as a raw JSON-RPC 2.0
14
+ stdio server. No external MCP SDK dependency.
15
+
16
+ Start the server::
17
+
18
+ sdcvalidator serve --mcp
19
+
20
+ MCP Tools exposed:
21
+ - validate_instance: validate an XML instance against its XSD schema
22
+ - validate_and_report: detailed validation report with error classification
23
+ - check_schema_compliance: verify schema follows SDC4 principles
24
+
25
+ Protocol: JSON-RPC 2.0 over stdio (one JSON object per line).
26
+ """
27
+
28
+ import json
29
+ import sys
30
+ from typing import Any
31
+
32
+ from sdcvalidator.validator import SDC4Validator
33
+ from sdcvalidator.schema_checker import validate_sdc4_schema_compliance
34
+
35
+ # Protocol constants
36
+ JSONRPC_VERSION = "2.0"
37
+ MCP_PROTOCOL_VERSION = "2024-11-05"
38
+ SERVER_NAME = "sdcvalidator"
39
+ SERVER_VERSION = "4.2.1"
40
+
41
+ # Validator cache: schema_path -> SDC4Validator
42
+ _validators: dict[str, SDC4Validator] = {}
43
+
44
+
45
+ def _get_validator(schema_path: str, check_compliance: bool = True) -> SDC4Validator:
46
+ """Get or create a validator for the given schema."""
47
+ key = f"{schema_path}:{check_compliance}"
48
+ if key not in _validators:
49
+ _validators[key] = SDC4Validator(
50
+ schema_path,
51
+ check_sdc4_compliance=check_compliance,
52
+ )
53
+ return _validators[key]
54
+
55
+
56
+ # --- Tool definitions ---
57
+
58
+ TOOLS = [
59
+ {
60
+ "name": "validate_instance",
61
+ "description": (
62
+ "Validate an XML instance against its SDC4 XSD schema. "
63
+ "Returns pass/fail with error count and classified errors."
64
+ ),
65
+ "inputSchema": {
66
+ "type": "object",
67
+ "properties": {
68
+ "schema_path": {
69
+ "type": "string",
70
+ "description": "Path to the SDC4 data model XSD file.",
71
+ },
72
+ "instance_path": {
73
+ "type": "string",
74
+ "description": "Path to the XML instance file to validate.",
75
+ },
76
+ "check_compliance": {
77
+ "type": "boolean",
78
+ "description": "Check schema follows SDC4 principles (no xsd:extension). Default true.",
79
+ "default": True,
80
+ },
81
+ },
82
+ "required": ["schema_path", "instance_path"],
83
+ },
84
+ },
85
+ {
86
+ "name": "validate_and_report",
87
+ "description": (
88
+ "Validate an XML instance and return a detailed report with "
89
+ "two-tier error classification (structural vs semantic)."
90
+ ),
91
+ "inputSchema": {
92
+ "type": "object",
93
+ "properties": {
94
+ "schema_path": {
95
+ "type": "string",
96
+ "description": "Path to the SDC4 data model XSD file.",
97
+ },
98
+ "instance_path": {
99
+ "type": "string",
100
+ "description": "Path to the XML instance file to validate.",
101
+ },
102
+ "check_compliance": {
103
+ "type": "boolean",
104
+ "description": "Check schema follows SDC4 principles. Default true.",
105
+ "default": True,
106
+ },
107
+ },
108
+ "required": ["schema_path", "instance_path"],
109
+ },
110
+ },
111
+ {
112
+ "name": "check_schema_compliance",
113
+ "description": (
114
+ "Check if an XSD schema follows SDC4 principles "
115
+ "(no xsd:extension, restriction only). Does not validate instances."
116
+ ),
117
+ "inputSchema": {
118
+ "type": "object",
119
+ "properties": {
120
+ "schema_path": {
121
+ "type": "string",
122
+ "description": "Path to the XSD schema file to check.",
123
+ },
124
+ },
125
+ "required": ["schema_path"],
126
+ },
127
+ },
128
+ ]
129
+
130
+
131
+ # --- Tool handlers ---
132
+
133
+ def _handle_validate_instance(args: dict[str, Any]) -> Any:
134
+ schema_path = args["schema_path"]
135
+ instance_path = args["instance_path"]
136
+ check_compliance = args.get("check_compliance", True)
137
+
138
+ try:
139
+ validator = _get_validator(schema_path, check_compliance)
140
+ except Exception as exc:
141
+ return {
142
+ "valid": False,
143
+ "error": f"Schema error: {exc}",
144
+ "error_count": 0,
145
+ }
146
+
147
+ result = validator.validate(instance_path)
148
+ return {
149
+ "valid": result.is_valid,
150
+ "error_count": result.error_count,
151
+ "structural_error_count": len(result.structural_errors),
152
+ "semantic_error_count": len(result.semantic_errors),
153
+ }
154
+
155
+
156
+ def _handle_validate_and_report(args: dict[str, Any]) -> Any:
157
+ schema_path = args["schema_path"]
158
+ instance_path = args["instance_path"]
159
+ check_compliance = args.get("check_compliance", True)
160
+
161
+ try:
162
+ validator = _get_validator(schema_path, check_compliance)
163
+ except Exception as exc:
164
+ return {
165
+ "valid": False,
166
+ "error": f"Schema error: {exc}",
167
+ "error_count": 0,
168
+ "structural_errors": [],
169
+ "semantic_errors": [],
170
+ }
171
+
172
+ return validator.validate_and_report(instance_path)
173
+
174
+
175
+ def _handle_check_schema_compliance(args: dict[str, Any]) -> Any:
176
+ schema_path = args["schema_path"]
177
+ is_valid, errors = validate_sdc4_schema_compliance(schema_path)
178
+ return {
179
+ "compliant": is_valid,
180
+ "errors": errors,
181
+ }
182
+
183
+
184
+ TOOL_HANDLERS = {
185
+ "validate_instance": _handle_validate_instance,
186
+ "validate_and_report": _handle_validate_and_report,
187
+ "check_schema_compliance": _handle_check_schema_compliance,
188
+ }
189
+
190
+
191
+ # --- JSON-RPC 2.0 stdio server ---
192
+
193
+ def _jsonrpc_response(id: Any, result: Any) -> dict:
194
+ return {"jsonrpc": JSONRPC_VERSION, "id": id, "result": result}
195
+
196
+
197
+ def _jsonrpc_error(id: Any, code: int, message: str, data: Any = None) -> dict:
198
+ error = {"code": code, "message": message}
199
+ if data is not None:
200
+ error["data"] = data
201
+ return {"jsonrpc": JSONRPC_VERSION, "id": id, "error": error}
202
+
203
+
204
+ def _handle_request(request: dict) -> dict | None:
205
+ method = request.get("method", "")
206
+ params = request.get("params", {})
207
+ req_id = request.get("id")
208
+
209
+ if method == "initialize":
210
+ result = {
211
+ "protocolVersion": MCP_PROTOCOL_VERSION,
212
+ "capabilities": {"tools": {"listChanged": False}},
213
+ "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
214
+ "instructions": (
215
+ "SDC4 structural validator. Validates XML instances against "
216
+ "XSD schemas with two-tier error classification (structural vs semantic). "
217
+ "Independent from sdcgovernance - agents call each library separately."
218
+ ),
219
+ }
220
+ return _jsonrpc_response(req_id, result)
221
+
222
+ elif method == "notifications/initialized":
223
+ return None
224
+
225
+ elif method == "tools/list":
226
+ return _jsonrpc_response(req_id, {"tools": TOOLS})
227
+
228
+ elif method == "tools/call":
229
+ tool_name = params.get("name", "")
230
+ tool_args = params.get("arguments", {})
231
+
232
+ handler = TOOL_HANDLERS.get(tool_name)
233
+ if handler is None:
234
+ return _jsonrpc_error(req_id, -32601, f"Unknown tool: {tool_name}")
235
+
236
+ try:
237
+ result = handler(tool_args)
238
+ return _jsonrpc_response(req_id, {
239
+ "content": [{"type": "text", "text": json.dumps(result, default=str)}],
240
+ })
241
+ except Exception as exc:
242
+ return _jsonrpc_error(req_id, -32000, f"Tool execution error: {exc}")
243
+
244
+ elif method == "ping":
245
+ return _jsonrpc_response(req_id, {})
246
+
247
+ else:
248
+ if req_id is not None:
249
+ return _jsonrpc_error(req_id, -32601, f"Method not found: {method}")
250
+ return None
251
+
252
+
253
+ def run_stdio() -> None:
254
+ """Run the MCP server on stdio."""
255
+ for line in sys.stdin:
256
+ line = line.strip()
257
+ if not line:
258
+ continue
259
+ try:
260
+ request = json.loads(line)
261
+ except json.JSONDecodeError as exc:
262
+ response = _jsonrpc_error(None, -32700, f"Parse error: {exc}")
263
+ sys.stdout.write(json.dumps(response) + "\n")
264
+ sys.stdout.flush()
265
+ continue
266
+
267
+ response = _handle_request(request)
268
+ if response is not None:
269
+ sys.stdout.write(json.dumps(response) + "\n")
270
+ sys.stdout.flush()
271
+
272
+
273
+ def main() -> None:
274
+ """Entry point for the sdcvalidator MCP server."""
275
+ import argparse
276
+
277
+ parser = argparse.ArgumentParser(
278
+ prog="sdcvalidator-mcp",
279
+ description="SDC4 structural validator - MCP server",
280
+ )
281
+ subparsers = parser.add_subparsers(dest="command")
282
+
283
+ serve_parser = subparsers.add_parser("serve", help="Start the validation server")
284
+ serve_parser.add_argument("--mcp", action="store_true", help="Run as MCP stdio server")
285
+
286
+ args = parser.parse_args()
287
+
288
+ if args.command == "serve" and args.mcp:
289
+ run_stdio()
290
+ else:
291
+ parser.print_help()
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.2.0
3
+ Version: 4.2.1
4
4
  Summary: SDC4 structural validator — thin wrapper over xmlschema with error classification
5
5
  Author: Semantic Data Charter Foundation
6
6
  License-Expression: Apache-2.0
@@ -6,6 +6,7 @@ src/sdcvalidator/cli.py
6
6
  src/sdcvalidator/constants.py
7
7
  src/sdcvalidator/converters.py
8
8
  src/sdcvalidator/error_classifier.py
9
+ src/sdcvalidator/mcp_server.py
9
10
  src/sdcvalidator/schema_checker.py
10
11
  src/sdcvalidator/validator.py
11
12
  src/sdcvalidator.egg-info/PKG-INFO
@@ -16,5 +17,6 @@ src/sdcvalidator.egg-info/requires.txt
16
17
  src/sdcvalidator.egg-info/top_level.txt
17
18
  tests/test_converters.py
18
19
  tests/test_error_classifier.py
20
+ tests/test_mcp_server.py
19
21
  tests/test_schema_checker.py
20
22
  tests/test_validator.py
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
2
  sdcvalidate = sdcvalidator.cli:validate_main
3
3
  sdcvalidator-json2xml = sdcvalidator.cli:json2xml_main
4
+ sdcvalidator-mcp = sdcvalidator.mcp_server:main
4
5
  sdcvalidator-xml2json = sdcvalidator.cli:xml2json_main
@@ -0,0 +1,139 @@
1
+ #
2
+ # Copyright 2026 Semantic Data Charter Foundation
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ """
6
+ Tests for MCP server - JSON-RPC 2.0 protocol and tool definitions.
7
+ """
8
+
9
+ import json
10
+ import pytest
11
+ from pathlib import Path
12
+ from sdcvalidator.mcp_server import _handle_request, TOOLS, TOOL_HANDLERS
13
+
14
+ TEST_DATA_DIR = Path(__file__).parent / "test_data"
15
+
16
+
17
+ def call_tool(name: str, arguments: dict) -> dict:
18
+ """Helper: call an MCP tool via JSON-RPC and return parsed result."""
19
+ request = {
20
+ "jsonrpc": "2.0",
21
+ "id": 1,
22
+ "method": "tools/call",
23
+ "params": {"name": name, "arguments": arguments},
24
+ }
25
+ response = _handle_request(request)
26
+ assert "result" in response, f"Expected result, got: {response}"
27
+ content = response["result"]["content"]
28
+ assert len(content) == 1
29
+ assert content[0]["type"] == "text"
30
+ return json.loads(content[0]["text"])
31
+
32
+
33
+ class TestMcpProtocol:
34
+ """JSON-RPC 2.0 MCP protocol handling."""
35
+
36
+ def test_initialize(self):
37
+ request = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
38
+ response = _handle_request(request)
39
+ assert response["id"] == 1
40
+ result = response["result"]
41
+ assert result["serverInfo"]["name"] == "sdcvalidator"
42
+ assert "tools" in result["capabilities"]
43
+
44
+ def test_initialized_notification(self):
45
+ request = {"jsonrpc": "2.0", "method": "notifications/initialized"}
46
+ response = _handle_request(request)
47
+ assert response is None
48
+
49
+ def test_tools_list(self):
50
+ request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
51
+ response = _handle_request(request)
52
+ tools = response["result"]["tools"]
53
+ assert len(tools) == 3
54
+ names = {t["name"] for t in tools}
55
+ assert names == {"validate_instance", "validate_and_report", "check_schema_compliance"}
56
+
57
+ def test_tools_have_input_schemas(self):
58
+ request = {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}
59
+ response = _handle_request(request)
60
+ for tool in response["result"]["tools"]:
61
+ assert "inputSchema" in tool
62
+ assert tool["inputSchema"]["type"] == "object"
63
+
64
+ def test_ping(self):
65
+ request = {"jsonrpc": "2.0", "id": 4, "method": "ping", "params": {}}
66
+ response = _handle_request(request)
67
+ assert response["id"] == 4
68
+ assert response["result"] == {}
69
+
70
+ def test_unknown_method(self):
71
+ request = {"jsonrpc": "2.0", "id": 5, "method": "nonexistent", "params": {}}
72
+ response = _handle_request(request)
73
+ assert "error" in response
74
+ assert response["error"]["code"] == -32601
75
+
76
+ def test_unknown_tool(self):
77
+ request = {
78
+ "jsonrpc": "2.0", "id": 6,
79
+ "method": "tools/call",
80
+ "params": {"name": "nonexistent_tool", "arguments": {}},
81
+ }
82
+ response = _handle_request(request)
83
+ assert "error" in response
84
+ assert "Unknown tool" in response["error"]["message"]
85
+
86
+
87
+ class TestCheckSchemaCompliance:
88
+ """check_schema_compliance MCP tool."""
89
+
90
+ def test_valid_schema(self):
91
+ data = call_tool("check_schema_compliance", {
92
+ "schema_path": str(TEST_DATA_DIR / "valid_sdc4_schema.xsd"),
93
+ })
94
+ assert data["compliant"] is True
95
+ assert data["errors"] == []
96
+
97
+ def test_invalid_schema_with_extension(self):
98
+ data = call_tool("check_schema_compliance", {
99
+ "schema_path": str(TEST_DATA_DIR / "invalid_sdc4_schema_with_extension.xsd"),
100
+ })
101
+ assert data["compliant"] is False
102
+ assert len(data["errors"]) > 0
103
+ assert any("extension" in e.lower() for e in data["errors"])
104
+
105
+ def test_non_sdc4_schema_passes(self):
106
+ data = call_tool("check_schema_compliance", {
107
+ "schema_path": str(TEST_DATA_DIR / "non_sdc4_schema_with_extension.xsd"),
108
+ })
109
+ assert data["compliant"] is True
110
+
111
+ def test_nonexistent_file(self):
112
+ data = call_tool("check_schema_compliance", {
113
+ "schema_path": "/nonexistent/path/schema.xsd",
114
+ })
115
+ assert data["compliant"] is False
116
+ assert any("not found" in e.lower() for e in data["errors"])
117
+
118
+
119
+ class TestConsistentSerialization:
120
+ """Verify all tools return consistent JSON-RPC format."""
121
+
122
+ def test_all_tools_return_single_text_content(self):
123
+ """Every tool returns exactly one content block with type=text."""
124
+ test_calls = [
125
+ ("check_schema_compliance", {"schema_path": str(TEST_DATA_DIR / "valid_sdc4_schema.xsd")}),
126
+ ]
127
+ for tool_name, args in test_calls:
128
+ request = {
129
+ "jsonrpc": "2.0", "id": 1,
130
+ "method": "tools/call",
131
+ "params": {"name": tool_name, "arguments": args},
132
+ }
133
+ response = _handle_request(request)
134
+ assert "result" in response, f"{tool_name} returned error: {response}"
135
+ content = response["result"]["content"]
136
+ assert len(content) == 1, f"{tool_name} returned {len(content)} content blocks"
137
+ assert content[0]["type"] == "text"
138
+ parsed = json.loads(content[0]["text"])
139
+ assert parsed is not None
File without changes
File without changes
File without changes