Files
ITWorx_MCP_Hub-Public/scripts/verify_demo_token_flow.py
T

115 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Prove one Fleet Ops demo-token MCP call, then prove its revocation."""
from __future__ import annotations
import argparse
import json
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from scripts.revoke_demo_token import revoke
from services.runtime.demo_token import load_demo_token_signing_key, mint_demo_token
@dataclass(frozen=True)
class HttpResult:
status: int
content_type: str
body: bytes
def post_tool_call(endpoint: str, token: str, *, timeout_seconds: float) -> HttpResult:
request = urllib.request.Request( # noqa: S310 - operator supplies an explicit HTTP(S) MCP endpoint
endpoint,
data=json.dumps(
{
"jsonrpc": "2.0",
"id": "demo-token-flow-verification",
"method": "tools/call",
"params": {"name": "mobilityops.operations.summary", "arguments": {}},
},
separators=(",", ":"),
).encode("utf-8"),
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"MCP-Protocol-Version": "2025-11-25",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=timeout_seconds) as response: # noqa: S310
return HttpResult(response.status, response.headers.get_content_type(), response.read())
except urllib.error.HTTPError as error:
try:
body = error.read()
except (OSError, ConnectionError):
# The status/header proof remains authoritative when the peer closes
# the optional error-body stream immediately after sending its 401.
body = b""
return HttpResult(error.code, error.headers.get_content_type(), body)
def verify_flow(
endpoint: str,
key_file: Path,
revocations_file: Path,
*,
timeout_seconds: float = 15.0,
) -> tuple[HttpResult, HttpResult]:
"""Run a bounded live proof and revoke its generated bearer in all cases."""
key = load_demo_token_signing_key(key_file)
token, jti = mint_demo_token(key, ttl_hours=1)
try:
valid = post_tool_call(endpoint, token, timeout_seconds=timeout_seconds)
if valid.status != 200:
raise RuntimeError(f"valid demo-token call returned HTTP {valid.status}")
if valid.content_type != "application/json":
raise RuntimeError("valid demo-token call did not return application/json")
try:
document: Any = json.loads(valid.body)
except (UnicodeDecodeError, ValueError) as exc:
raise RuntimeError("valid demo-token call returned invalid JSON") from exc
if not isinstance(document, dict):
raise RuntimeError("valid demo-token call returned a non-object JSON payload")
finally:
revoke(revocations_file, jti)
revoked = post_tool_call(endpoint, token, timeout_seconds=timeout_seconds)
if revoked.status != 401:
raise RuntimeError(f"revoked demo-token call returned HTTP {revoked.status}, expected 401")
return valid, revoked
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--endpoint", required=True)
parser.add_argument("--key-file", type=Path, required=True)
parser.add_argument("--revocations-file", type=Path, required=True)
parser.add_argument("--timeout-seconds", type=float, default=15.0)
args = parser.parse_args()
if not 1 <= args.timeout_seconds <= 30:
raise ValueError("timeout-seconds must be between 1 and 30")
valid, revoked = verify_flow(
args.endpoint,
args.key_file,
args.revocations_file,
timeout_seconds=args.timeout_seconds,
)
print(f"valid_call_http={valid.status}")
print(f"valid_call_content_type={valid.content_type}")
print(f"valid_call_bytes={len(valid.body)}")
print(f"revoked_call_http={revoked.status}")
return 0
if __name__ == "__main__":
sys.exit(main())