Files
ITWorx_MCP_Hub-Public/scripts/audit_projectbrain_contracts.py
T

66 lines
2.8 KiB
Python

"""Compare a captured ProjectBrain tools/list with the reviewed Hub catalog.
Read-only: this never imports tools or edits catalog policy. Capture discovery
from the owned connector and pass the JSON document using --discovery-file.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
def audit(discovery: dict[str, Any], catalog_root: Path) -> dict[str, Any]:
tools = discovery.get("result", discovery).get("tools")
if not isinstance(tools, list) or not tools:
raise ValueError("non-empty tools/list result required")
catalog = {}
for path in sorted((catalog_root / "tools").glob("projectbrain-*.yaml")):
spec = json.loads(path.read_text(encoding="utf-8"))["spec"]
catalog[spec["canonicalName"]] = spec["inputSchema"]
issues = []
seen = set()
for tool in tools:
name = tool["name"]
if name in seen:
issues.append({"tool": name, "code": "duplicate_tool"})
seen.add(name)
if name not in catalog:
issues.append({"tool": name, "code": "unreviewed_tool"})
continue
upstream = tool["inputSchema"]
local = catalog[name]
for field in sorted(set(upstream.get("properties", {})) - set(local.get("properties", {}))):
issues.append({"tool": name, "code": "missing_field", "field": field})
if set(upstream.get("required", [])) != set(local.get("required", [])):
issues.append({"tool": name, "code": "required_fields_differ"})
for field, value in upstream.get("properties", {}).items():
target = local.get("properties", {}).get(field, {})
if "type" in value and "type" in target and value["type"] != target["type"]:
issues.append({"tool": name, "code": "field_type_differs", "field": field})
if "enum" in value and "enum" in target and value["enum"] != target["enum"]:
issues.append({"tool": name, "code": "field_enum_differs", "field": field})
for name in sorted(set(catalog) - seen):
issues.append({"tool": name, "code": "upstream_tool_missing"})
return {"passed": not issues, "toolCount": len(tools), "issues": issues}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--discovery-file", type=Path, required=True)
parser.add_argument("--catalog-root", type=Path, default=ROOT / "catalog")
args = parser.parse_args()
if args.discovery_file.stat().st_size > 2 * 1024 * 1024:
parser.error("discovery exceeds 2 MiB")
result = audit(json.loads(args.discovery_file.read_text(encoding="utf-8")), args.catalog_root)
print(json.dumps(result, sort_keys=True))
return 0 if result["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())