mcp4api 0.1.0__py3-none-any.whl

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.
mcp4api/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
mcp4api/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from mcp4api.cli import main
4
+
5
+ sys.exit(main())
mcp4api/cassettes.py ADDED
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from mcp4api.model import JsonObject, JsonValue, dump_json
8
+
9
+ _SECRET_NAME = re.compile(r"(KEY|TOKEN|SECRET|PASSWORD|AUTH)", re.IGNORECASE)
10
+ _BEARER = re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]{8,}")
11
+
12
+
13
+ def write_cassettes(out: Path, results: JsonObject, secrets: list[str]) -> None:
14
+ """Write one scrubbed cassette per tool.
15
+
16
+ Args:
17
+ out: Artifact root. Cassettes are written under ``tests/cassettes``.
18
+ results: Verification result object with a ``calls`` array.
19
+ secrets: Secret values to replace before the cassette is stored.
20
+
21
+ Returns:
22
+ None.
23
+ """
24
+ grouped: dict[str, list[JsonValue]] = {}
25
+ for call in _result_calls(results):
26
+ if call.get("kind") == "negative":
27
+ continue
28
+ tool = call.get("tool")
29
+ exchanges = call.get("exchanges")
30
+ if not isinstance(tool, str) or not isinstance(exchanges, list):
31
+ continue
32
+ grouped.setdefault(tool, []).extend(item for item in exchanges if isinstance(item, dict))
33
+ directory = out / "tests" / "cassettes"
34
+ directory.mkdir(parents=True, exist_ok=True)
35
+ for tool, rows in grouped.items():
36
+ cleaned = scrub(rows, secrets)
37
+ if not isinstance(cleaned, list):
38
+ continue
39
+ (directory / f"{tool}.json").write_text(
40
+ dump_json({"exchanges": cleaned}),
41
+ encoding="utf-8",
42
+ newline="\n",
43
+ )
44
+
45
+
46
+ def secret_values(names: tuple[str, ...]) -> list[str]:
47
+ """Collect secret environment values that cassettes must not keep.
48
+
49
+ Args:
50
+ names: Environment variable names known to hold credentials.
51
+
52
+ Returns:
53
+ Secret values, longest first.
54
+ """
55
+ wanted = set(names)
56
+ found: list[str] = []
57
+ for name, value in os.environ.items():
58
+ if (name in wanted or _SECRET_NAME.search(name)) and len(value) >= 4:
59
+ found.append(value)
60
+ return sorted(set(found), key=len, reverse=True)
61
+
62
+
63
+ def scrub(value: JsonValue, secrets: list[str]) -> JsonValue:
64
+ """Replace secrets and bearer tokens in a JSON value.
65
+
66
+ Args:
67
+ value: JSON value to scrub.
68
+ secrets: Secret strings to replace with ``***``.
69
+
70
+ Returns:
71
+ A copy of the value with those secrets removed.
72
+ """
73
+ if isinstance(value, str):
74
+ return _scrub_text(value, secrets)
75
+ if isinstance(value, list):
76
+ return [scrub(item, secrets) for item in value]
77
+ if isinstance(value, dict):
78
+ result: JsonObject = {}
79
+ for key, item in value.items():
80
+ result[_scrub_text(key, secrets)] = scrub(item, secrets)
81
+ return result
82
+ return value
83
+
84
+
85
+ def _result_calls(results: JsonObject) -> list[JsonObject]:
86
+ raw = results.get("calls")
87
+ if not isinstance(raw, list):
88
+ return []
89
+ return [item for item in raw if isinstance(item, dict)]
90
+
91
+
92
+ def _scrub_text(value: str, secrets: list[str]) -> str:
93
+ text = value
94
+ for secret in secrets:
95
+ text = text.replace(secret, "***")
96
+ return _BEARER.sub("Bearer ***", text)