sdcvalidator 4.1.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.1.0/src/sdcvalidator.egg-info → sdcvalidator-4.2.1}/PKG-INFO +16 -2
  2. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/README.md +15 -1
  3. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/pyproject.toml +2 -1
  4. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/__init__.py +1 -1
  5. sdcvalidator-4.2.1/src/sdcvalidator/mcp_server.py +295 -0
  6. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/validator.py +4 -1
  7. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1/src/sdcvalidator.egg-info}/PKG-INFO +16 -2
  8. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/SOURCES.txt +2 -0
  9. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/entry_points.txt +1 -0
  10. sdcvalidator-4.2.1/tests/test_mcp_server.py +139 -0
  11. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_validator.py +152 -6
  12. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/LICENSE +0 -0
  13. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/setup.cfg +0 -0
  14. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/cli.py +0 -0
  15. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/constants.py +0 -0
  16. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/converters.py +0 -0
  17. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/error_classifier.py +0 -0
  18. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/schema_checker.py +0 -0
  19. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  20. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/requires.txt +0 -0
  21. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  22. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_converters.py +0 -0
  23. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_error_classifier.py +0 -0
  24. {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_schema_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.1.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
@@ -32,6 +32,7 @@ Dynamic: license-file
32
32
  - Validates XML instances against SDC4 XSD schemas
33
33
  - Classifies errors into **structural** (Tier 1: reject) vs **semantic** (Tier 2: report)
34
34
  - Checks SDC4 schema compliance (no `xsd:extension` — only `xsd:restriction`)
35
+ - **Strict validation by default** — catches invalid restriction derivations (wrong element names, type mismatches) at schema load time
35
36
  - Converts between XML and JSON using schema-aware conversion
36
37
 
37
38
  ## Install
@@ -53,7 +54,7 @@ pip install -e .
53
54
  ```python
54
55
  from sdcvalidator import SDC4Validator, ErrorTier
55
56
 
56
- # Validate an XML instance
57
+ # Validate an XML instance (strict mode by default)
57
58
  validator = SDC4Validator("my_schema.xsd")
58
59
  result = validator.validate("my_instance.xml")
59
60
 
@@ -66,6 +67,19 @@ else:
66
67
  print(f"SEMANTIC: {err.reason}")
67
68
  ```
68
69
 
70
+ ### Validation modes
71
+
72
+ The `validation` parameter controls how strictly the XSD schema itself is checked when loaded:
73
+
74
+ - **`'strict'`** (default) — Raises `XMLSchemaParseError` if the schema contains invalid restriction derivations (e.g., element names that don't match the base type). This is the recommended mode.
75
+ - **`'lax'`** — Silently collects schema derivation errors without raising. Use only for pre-existing schemas known to have issues.
76
+ - **`'skip'`** — Skips schema-level validation entirely.
77
+
78
+ ```python
79
+ # Explicit lax mode for legacy schemas
80
+ validator = SDC4Validator("legacy_schema.xsd", validation='lax')
81
+ ```
82
+
69
83
  ### Schema compliance checking
70
84
 
71
85
  ```python
@@ -10,6 +10,7 @@
10
10
  - Validates XML instances against SDC4 XSD schemas
11
11
  - Classifies errors into **structural** (Tier 1: reject) vs **semantic** (Tier 2: report)
12
12
  - Checks SDC4 schema compliance (no `xsd:extension` — only `xsd:restriction`)
13
+ - **Strict validation by default** — catches invalid restriction derivations (wrong element names, type mismatches) at schema load time
13
14
  - Converts between XML and JSON using schema-aware conversion
14
15
 
15
16
  ## Install
@@ -31,7 +32,7 @@ pip install -e .
31
32
  ```python
32
33
  from sdcvalidator import SDC4Validator, ErrorTier
33
34
 
34
- # Validate an XML instance
35
+ # Validate an XML instance (strict mode by default)
35
36
  validator = SDC4Validator("my_schema.xsd")
36
37
  result = validator.validate("my_instance.xml")
37
38
 
@@ -44,6 +45,19 @@ else:
44
45
  print(f"SEMANTIC: {err.reason}")
45
46
  ```
46
47
 
48
+ ### Validation modes
49
+
50
+ The `validation` parameter controls how strictly the XSD schema itself is checked when loaded:
51
+
52
+ - **`'strict'`** (default) — Raises `XMLSchemaParseError` if the schema contains invalid restriction derivations (e.g., element names that don't match the base type). This is the recommended mode.
53
+ - **`'lax'`** — Silently collects schema derivation errors without raising. Use only for pre-existing schemas known to have issues.
54
+ - **`'skip'`** — Skips schema-level validation entirely.
55
+
56
+ ```python
57
+ # Explicit lax mode for legacy schemas
58
+ validator = SDC4Validator("legacy_schema.xsd", validation='lax')
59
+ ```
60
+
47
61
  ### Schema compliance checking
48
62
 
49
63
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdcvalidator"
7
- version = "4.1.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()
@@ -52,7 +52,7 @@ class SDC4Validator:
52
52
  self,
53
53
  schema: Union[str, Path, XMLSchema11],
54
54
  check_sdc4_compliance: bool = True,
55
- validation: str = 'lax',
55
+ validation: str = 'strict',
56
56
  ):
57
57
  """
58
58
  Initialize the SDC4 validator.
@@ -62,6 +62,9 @@ class SDC4Validator:
62
62
  check_sdc4_compliance: If True, validate schema follows SDC4
63
63
  principles (no xsd:extension). Default: True.
64
64
  validation: Schema validation mode ('strict', 'lax', 'skip').
65
+ Default is 'strict' to catch invalid restriction derivations
66
+ (e.g. wrong element names, type mismatches). Lax mode silently
67
+ accepts these errors.
65
68
 
66
69
  Raises:
67
70
  SDC4SchemaValidationError: If compliance check fails.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.1.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
@@ -32,6 +32,7 @@ Dynamic: license-file
32
32
  - Validates XML instances against SDC4 XSD schemas
33
33
  - Classifies errors into **structural** (Tier 1: reject) vs **semantic** (Tier 2: report)
34
34
  - Checks SDC4 schema compliance (no `xsd:extension` — only `xsd:restriction`)
35
+ - **Strict validation by default** — catches invalid restriction derivations (wrong element names, type mismatches) at schema load time
35
36
  - Converts between XML and JSON using schema-aware conversion
36
37
 
37
38
  ## Install
@@ -53,7 +54,7 @@ pip install -e .
53
54
  ```python
54
55
  from sdcvalidator import SDC4Validator, ErrorTier
55
56
 
56
- # Validate an XML instance
57
+ # Validate an XML instance (strict mode by default)
57
58
  validator = SDC4Validator("my_schema.xsd")
58
59
  result = validator.validate("my_instance.xml")
59
60
 
@@ -66,6 +67,19 @@ else:
66
67
  print(f"SEMANTIC: {err.reason}")
67
68
  ```
68
69
 
70
+ ### Validation modes
71
+
72
+ The `validation` parameter controls how strictly the XSD schema itself is checked when loaded:
73
+
74
+ - **`'strict'`** (default) — Raises `XMLSchemaParseError` if the schema contains invalid restriction derivations (e.g., element names that don't match the base type). This is the recommended mode.
75
+ - **`'lax'`** — Silently collects schema derivation errors without raising. Use only for pre-existing schemas known to have issues.
76
+ - **`'skip'`** — Skips schema-level validation entirely.
77
+
78
+ ```python
79
+ # Explicit lax mode for legacy schemas
80
+ validator = SDC4Validator("legacy_schema.xsd", validation='lax')
81
+ ```
82
+
69
83
  ### Schema compliance checking
70
84
 
71
85
  ```python
@@ -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
@@ -45,7 +45,9 @@ class TestSDC4Validator(unittest.TestCase):
45
45
  xml_path = Path(f.name)
46
46
 
47
47
  try:
48
- validator = SDC4Validator(self.schema_path)
48
+ # Use lax for this pre-existing fixture (has restriction derivation
49
+ # issues under strict mode that are unrelated to this test's purpose)
50
+ validator = SDC4Validator(self.schema_path, validation='lax')
49
51
  result = validator.validate(xml_path)
50
52
 
51
53
  self.assertFalse(result.is_valid)
@@ -68,7 +70,7 @@ class TestSDC4Validator(unittest.TestCase):
68
70
  xml_path = Path(f.name)
69
71
 
70
72
  try:
71
- validator = SDC4Validator(self.schema_path)
73
+ validator = SDC4Validator(self.schema_path, validation='lax')
72
74
  result = validator.validate(xml_path)
73
75
 
74
76
  self.assertFalse(result.is_valid)
@@ -110,7 +112,7 @@ class TestSDC4Validator(unittest.TestCase):
110
112
  xml_path = Path(f.name)
111
113
 
112
114
  try:
113
- validator = SDC4Validator(self.schema_path)
115
+ validator = SDC4Validator(self.schema_path, validation='lax')
114
116
  result = validator.validate(xml_path)
115
117
 
116
118
  self.assertFalse(result.is_valid)
@@ -136,7 +138,7 @@ class TestSDC4Validator(unittest.TestCase):
136
138
  xml_path = Path(f.name)
137
139
 
138
140
  try:
139
- validator = SDC4Validator(self.schema_path)
141
+ validator = SDC4Validator(self.schema_path, validation='lax')
140
142
  structural_errors = validator.validate_structure(xml_path)
141
143
  self.assertGreater(len(structural_errors), 0)
142
144
  finally:
@@ -160,7 +162,7 @@ class TestSDC4Validator(unittest.TestCase):
160
162
  xml_path = Path(f.name)
161
163
 
162
164
  try:
163
- validator = SDC4Validator(self.schema_path)
165
+ validator = SDC4Validator(self.schema_path, validation='lax')
164
166
  structural_errors = validator.validate_structure(xml_path)
165
167
  self.assertEqual(len(structural_errors), 0)
166
168
  finally:
@@ -182,7 +184,7 @@ class TestSDC4Validator(unittest.TestCase):
182
184
  xml_path = Path(f.name)
183
185
 
184
186
  try:
185
- validator = SDC4Validator(self.schema_path)
187
+ validator = SDC4Validator(self.schema_path, validation='lax')
186
188
  report = validator.validate_and_report(xml_path)
187
189
 
188
190
  self.assertFalse(report['valid'])
@@ -207,5 +209,149 @@ class TestSDC4Validator(unittest.TestCase):
207
209
  self.assertEqual(result2.error_count, 3)
208
210
 
209
211
 
212
+ class TestStrictModeDefault(unittest.TestCase):
213
+ """Tests that strict mode is the default and catches invalid restrictions."""
214
+
215
+ def test_default_validation_is_strict(self):
216
+ """SDC4Validator defaults to validation='strict'."""
217
+ # valid_sdc4_schema.xsd is self-contained (no sdc4.xsd include)
218
+ # and has correct restriction derivation — should load fine in strict mode
219
+ schema_path = Path(__file__).parent / 'test_data' / 'valid_sdc4_schema.xsd'
220
+ validator = SDC4Validator(schema_path)
221
+ assert validator is not None
222
+ assert validator.schema is not None
223
+
224
+ def test_strict_mode_catches_invalid_restriction(self):
225
+ """Strict mode rejects schemas with invalid restriction derivations."""
226
+ # A schema where the restriction's element sequence doesn't match
227
+ # the base type — strict mode should raise during schema loading
228
+ bad_schema = '''\
229
+ <?xml version="1.0" encoding="UTF-8"?>
230
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
231
+ xmlns:sdc4="https://semanticdatacharter.com/ns/sdc4/"
232
+ targetNamespace="https://semanticdatacharter.com/ns/sdc4/"
233
+ elementFormDefault="qualified">
234
+
235
+ <xsd:complexType name="BaseType">
236
+ <xsd:sequence>
237
+ <xsd:element name="label" type="xsd:string"/>
238
+ <xsd:element name="value" type="xsd:string"/>
239
+ </xsd:sequence>
240
+ </xsd:complexType>
241
+
242
+ <xsd:complexType name="BadRestriction">
243
+ <xsd:complexContent>
244
+ <xsd:restriction base="sdc4:BaseType">
245
+ <xsd:sequence>
246
+ <xsd:element name="label" type="xsd:string"/>
247
+ <xsd:element name="wrong-name" type="xsd:string"/>
248
+ </xsd:sequence>
249
+ </xsd:restriction>
250
+ </xsd:complexContent>
251
+ </xsd:complexType>
252
+
253
+ </xsd:schema>
254
+ '''
255
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.xsd', delete=False) as f:
256
+ f.write(bad_schema)
257
+ schema_path = Path(f.name)
258
+
259
+ try:
260
+ from xmlschema.validators.exceptions import XMLSchemaParseError
261
+ with self.assertRaises(XMLSchemaParseError):
262
+ SDC4Validator(schema_path, check_sdc4_compliance=False, validation='strict')
263
+ finally:
264
+ schema_path.unlink()
265
+
266
+ def test_lax_mode_silently_accepts_invalid_restriction(self):
267
+ """Lax mode silently accepts the same invalid restriction (demonstrating the bug)."""
268
+ bad_schema = '''\
269
+ <?xml version="1.0" encoding="UTF-8"?>
270
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
271
+ xmlns:sdc4="https://semanticdatacharter.com/ns/sdc4/"
272
+ targetNamespace="https://semanticdatacharter.com/ns/sdc4/"
273
+ elementFormDefault="qualified">
274
+
275
+ <xsd:complexType name="BaseType">
276
+ <xsd:sequence>
277
+ <xsd:element name="label" type="xsd:string"/>
278
+ <xsd:element name="value" type="xsd:string"/>
279
+ </xsd:sequence>
280
+ </xsd:complexType>
281
+
282
+ <xsd:complexType name="BadRestriction">
283
+ <xsd:complexContent>
284
+ <xsd:restriction base="sdc4:BaseType">
285
+ <xsd:sequence>
286
+ <xsd:element name="label" type="xsd:string"/>
287
+ <xsd:element name="wrong-name" type="xsd:string"/>
288
+ </xsd:sequence>
289
+ </xsd:restriction>
290
+ </xsd:complexContent>
291
+ </xsd:complexType>
292
+
293
+ </xsd:schema>
294
+ '''
295
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.xsd', delete=False) as f:
296
+ f.write(bad_schema)
297
+ schema_path = Path(f.name)
298
+
299
+ try:
300
+ # Lax mode accepts this — which is the bug strict mode fixes
301
+ validator = SDC4Validator(
302
+ schema_path, check_sdc4_compliance=False, validation='lax'
303
+ )
304
+ self.assertIsNotNone(validator)
305
+ finally:
306
+ schema_path.unlink()
307
+
308
+ def test_valid_restriction_passes_strict(self):
309
+ """A correct restriction passes strict mode validation."""
310
+ good_schema = '''\
311
+ <?xml version="1.0" encoding="UTF-8"?>
312
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
313
+ xmlns:sdc4="https://semanticdatacharter.com/ns/sdc4/"
314
+ targetNamespace="https://semanticdatacharter.com/ns/sdc4/"
315
+ elementFormDefault="qualified">
316
+
317
+ <xsd:complexType name="BaseType">
318
+ <xsd:sequence>
319
+ <xsd:element name="label" type="xsd:string"/>
320
+ <xsd:element name="value" type="xsd:string"/>
321
+ </xsd:sequence>
322
+ </xsd:complexType>
323
+
324
+ <xsd:complexType name="GoodRestriction">
325
+ <xsd:complexContent>
326
+ <xsd:restriction base="sdc4:BaseType">
327
+ <xsd:sequence>
328
+ <xsd:element name="label" type="xsd:string" fixed="Test"/>
329
+ <xsd:element name="value">
330
+ <xsd:simpleType>
331
+ <xsd:restriction base="xsd:string">
332
+ <xsd:maxLength value="100"/>
333
+ </xsd:restriction>
334
+ </xsd:simpleType>
335
+ </xsd:element>
336
+ </xsd:sequence>
337
+ </xsd:restriction>
338
+ </xsd:complexContent>
339
+ </xsd:complexType>
340
+
341
+ </xsd:schema>
342
+ '''
343
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.xsd', delete=False) as f:
344
+ f.write(good_schema)
345
+ schema_path = Path(f.name)
346
+
347
+ try:
348
+ validator = SDC4Validator(
349
+ schema_path, check_sdc4_compliance=False, validation='strict'
350
+ )
351
+ self.assertIsNotNone(validator)
352
+ finally:
353
+ schema_path.unlink()
354
+
355
+
210
356
  if __name__ == '__main__':
211
357
  unittest.main()
File without changes
File without changes