"""Python 3.10+. The caller owns OAuth, protected token storage, and DPoP signing.""" import json import re from typing import Any, Callable from urllib.error import HTTPError from urllib.parse import urlsplit from urllib.request import HTTPRedirectHandler, Request, build_opener OPERATIONS = frozenset(( "get_connection_status", "get_profile_context", "get_profile_template", "get_taste_summary", "search_taste", "get_connections", "get_boundaries", "get_context_for_task", "evaluate_candidates", "get_recent_changes", "search_catalog", "get_fingerprint_update_status", )) REQUEST_OPERATIONS = OPERATIONS | frozenset(( "propose_fingerprint_update", "propose_profile_update", "suggest_map_update", "save_fingerprint_update", )) class FingerprintError(Exception): def __init__(self, status: int, message: str, www_authenticate: str | None = None): super().__init__(message) self.status = status self.www_authenticate = www_authenticate class _RejectRedirects(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None class FingerprintClient: def __init__(self, base_url: str, get_credentials: Callable[[], dict[str, str]], create_proof: Callable[[dict[str, str]], str] | None = None): base = urlsplit(base_url) if (not base.hostname or base.username is not None or base.password is not None or base.path not in ("", "/") or base.query or base.fragment or (base.scheme != "https" and not (base.scheme == "http" and base.hostname in ("localhost", "127.0.0.1", "::1")))): raise ValueError("Use an HTTPS origin, or an HTTP localhost origin for development.") host = base.hostname.encode("idna").decode("ascii") authority = f"[{host}]" if ":" in host else host if base.port is not None and base.port != (443 if base.scheme == "https" else 80): authority += f":{base.port}" self._origin = f"{base.scheme}://{authority}" self._get_credentials = get_credentials self._create_proof = create_proof self._opener = build_opener(_RejectRedirects()) def read(self, operation: str, input: dict[str, Any] | None = None) -> dict[str, Any]: if operation not in OPERATIONS: raise ValueError("Unknown read operation.") return self.request(operation, input) def request(self, operation: str, input: dict[str, Any] | None = None) -> dict[str, Any]: if operation not in REQUEST_OPERATIONS: raise ValueError("Unknown operation.") if input is not None and not isinstance(input, dict): raise ValueError("Input must be an object.") url = f"{self._origin}/mcp/v1/{operation}" credentials = self._get_credentials() token = credentials.get("accessToken", "") token_type = credentials.get("tokenType") if (token_type not in ("Bearer", "DPoP") or not isinstance(token, str) or not re.fullmatch(r"[A-Za-z0-9_-]{40,256}", token)): raise ValueError("Invalid credentials.") headers = {"Accept": "application/json", "Content-Type": "application/json", "Authorization": f"{token_type} {token}"} if token_type == "DPoP": if self._create_proof is None: raise ValueError("A DPoP proof callback is required for bound credentials.") headers["DPoP"] = self._create_proof({"method": "POST", "url": url, "accessToken": token}) if not headers["DPoP"]: raise ValueError("The DPoP proof callback returned no proof.") request = Request(url, data=json.dumps({} if input is None else input, allow_nan=False).encode("utf-8"), headers=headers, method="POST") try: response = self._opener.open(request, timeout=30) except HTTPError as error: response = error with response: www_authenticate = response.headers.get("WWW-Authenticate") if 300 <= response.status < 400: raise FingerprintError(response.status, "Provider redirects are not accepted.", www_authenticate) try: data = json.load(response) except (ValueError, UnicodeDecodeError): raise FingerprintError(response.status, "The provider did not return JSON.", www_authenticate) from None if not isinstance(data, dict): raise FingerprintError(response.status, "The provider did not return a JSON object.", www_authenticate) if not 200 <= response.status < 300: message = data.get("error") raise FingerprintError(response.status, message if isinstance(message, str) else "The provider rejected the request.", www_authenticate) return data