File size: 55,130 Bytes
06d418f 53fcd8e 99b813e 06d418f 815e73e 53fcd8e 815e73e 99b813e d36d3be 53fcd8e 99b813e 06d418f 99b813e 7cbab00 99b813e 06d418f 7cbab00 06d418f 99b813e 815e73e 6eea131 06d418f 99b813e 06d418f 12286c3 06d418f 99b813e 06d418f 99b813e 06d418f 7cbab00 06d418f 99b813e 06d418f 99b813e 06d418f 12286c3 06d418f 99b813e 06d418f 99b813e 06d418f 7cbab00 99b813e 06d418f 815e73e 12286c3 815e73e 99b813e 815e73e 99b813e 815e73e 7cbab00 815e73e 99b813e 815e73e 99b813e 815e73e 99b813e 815e73e 12286c3 815e73e 99b813e 815e73e 99b813e 815e73e 7cbab00 815e73e 99b813e 815e73e 99b813e 7cbab00 99b813e 7cbab00 99b813e 7cbab00 99b813e 7cbab00 99b813e 815e73e 99b813e d36d3be 99b813e 7cbab00 99b813e d36d3be 99b813e 815e73e 53fcd8e 7cbab00 53fcd8e 7cbab00 53fcd8e 06d418f 7cbab00 06d418f 7cbab00 06d418f 815e73e 7cbab00 815e73e 7cbab00 815e73e 99b813e 7cbab00 99b813e 7cbab00 99b813e 7cbab00 99b813e 7cbab00 99b813e 7cbab00 99b813e d36d3be 99b813e 53fcd8e 7cbab00 53fcd8e 7cbab00 53fcd8e 06d418f 99b813e 53fcd8e 99b813e 53fcd8e 99b813e 06d418f f1dd567 7cbab00 f1dd567 d36d3be f1dd567 99b813e f1dd567 99b813e d36d3be 99b813e 53fcd8e f1dd567 7cbab00 cb5f656 06d418f 0234581 cb5f656 0234581 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 | """RelayShield Agentic Attack Surface MCP Server β Hugging Face Space.
Exposes 13 of RelayShield's agent-security checks as MCP tools via Gradio's
mcp_server=True mode:
- check_mcp_server_risk: typosquat/reputation/registration-age risk for
an MCP server URL.
- check_prompt_injection_breach: credential breach exposure sourced
specifically from prompt-injection attacks against AI agents.
- check_tech_stack_cve: CISA KEV / high-EPSS CVEs targeting a declared
AI agent framework / tech stack, now with a bounded public-PoC/exploit-
availability signal on top-ranked non-KEV matches.
- check_bulk_identity_risk: hierarchical org + AI-agent-identity risk
scoring for a domain and its agent/service-account identities.
- check_oauth_watchlist: OAuth-app breach + stolen-token exposure.
- check_supply_chain: up to 10 vendor domains checked for breach/
infostealer exposure.
- check_session_risk: active/reusable stolen session (cookie/token)
exposure.
- check_nhi_exposure: non-human-identity credential exposure (API keys,
service tokens, PATs).
- check_secret_scan: secrets exposed in public GitHub repos.
- check_llm_credential_exposure: exposed LLM/AI provider API keys
(LLMjacking) β free to try, no api_key required (shared demo quota).
- check_agent_risk_summary: composite check β breach + LLM credential
exposure + tech-stack CVE in one call instead of four.
- get_stix_indicators: RelayShield's IOC corpus as STIX 2.1 objects via
the TAXII 2.1 feed (requires a TI subscription key).
- check_server_status: introspection β which tools/sources are live, no
api_key required.
v3 (2026-07-26) β LLMjacking + MCP enhancement sprint: added the last 4
tools above (check_llm_credential_exposure, check_agent_risk_summary,
get_stix_indicators, check_server_status), a zero-key free tier on
check_llm_credential_exposure specifically, and a bounded PoC/exploit-
availability signal on check_tech_stack_cve's top non-KEV matches.
v2 (2026-07-19) β response to community feedback: added 5 tools, and every
tool now returns a typed structured result instead of a human-formatted
string:
{
"outcome": "finding" | "no_known_finding" | "error",
"recommended_action": "allow" | "review" | "deny" | "defer",
"reason_codes": [...],
"evidence": [...],
"coverage": {"complete": bool, "scope": "what was actually checked"},
"freshness": {"observed_at": "...", "expires_at": null},
"error": {"kind": "...", "message": "...", "retryable": bool} | null
}
"no_known_finding" deliberately does not mean "safe" β it means nothing was
found in the sources and scope actually queried.
Each caller supplies their own RelayShield API key as a tool argument β
this Space is a single shared server handling many remote MCP callers at
once, so there is no per-caller "environment" to read a key from. Get a
key at https://api.relayshield.net/developers.
"""
import json
import os
import re
import gradio as gr
import requests
# ---------------------------------------------------------------------------
# AWS Marketplace mode β added 2026-07-27. This same file is deployed as two
# separate HF Spaces: the original public Space (this env var unset) keeps
# directing callers to the public self-serve signup page exactly as before;
# a second Space (AWS_MARKETPLACE_MODE=true, registered as Bundle D's MCP
# endpoint URL on AWS Marketplace) scrubs every reference to that page. AWS's
# Tier-1 audit treats any reachable link to an external payment-collecting
# page as a violation, even one buried in a tool parameter description
# rather than the AWS listing text itself β this is what caused Bundle D's
# visibility request to fail a second time after the listing text itself was
# already fixed. Keep both Spaces' code identical except for this one env
# var; redeploy both whenever this file changes.
# ---------------------------------------------------------------------------
AWS_MARKETPLACE_MODE = os.environ.get("AWS_MARKETPLACE_MODE", "").lower() == "true"
_KEY_URL_RE = re.compile(
r"(?:Get (?:a(?:\s+key)?|one|your own(?:\s+key)?)|get your own key)\s+at\s+"
r"(?:https?://)?api\.relayshield\.net/developers(?:#[\w-]+)?\.?",
)
_AWS_KEY_TEXT = "Included with your AWS Marketplace subscription, issued automatically by email"
def _scrub(text: str) -> str:
"""No-op unless AWS_MARKETPLACE_MODE is set β see comment above."""
if not AWS_MARKETPLACE_MODE or not text:
return text
return _KEY_URL_RE.sub(_AWS_KEY_TEXT, text)
__doc__ = _scrub(__doc__)
API_BASE_URL = "https://api.relayshield.net"
def _headers(api_key: str) -> dict[str, str]:
# All endpoints (hosted across relayshield_agentic_api.py and the main
# relayshield_api.py) accept X-RS-API-KEY as the primary header β
# confirmed by reading both dispatchers directly, not assumed.
# X-RS-Source lets the backend attribute billed usage to this Space
# specifically in the weekly metrics report (both dispatchers log a
# tagged row to relayshield_payg_settlements when this header is present).
return {
"Content-Type": "application/json",
"X-RS-API-KEY": api_key,
"X-RS-Source": "hf-mcp-space",
}
def _outcome_for_severity(highest_severity: str | None, found: bool = True) -> tuple[str, str]:
"""Maps a RelayShield severity string to (outcome, recommended_action).
CRITICAL escalates to a deny recommendation; HIGH/MEDIUM/LOW are surfaced as
findings for human/agent review rather than an automatic block, since these
are enrichment signals, not a certified detection.
"""
if not found or highest_severity in (None, "NONE", "CLEAN"):
return "no_known_finding", "allow"
if highest_severity == "CRITICAL":
return "finding", "deny"
return "finding", "review"
def _structured_result(
outcome: str,
recommended_action: str,
*,
reason_codes: list[str] | None = None,
evidence: list | None = None,
scope: str,
coverage_complete: bool = True,
observed_at: str | None = None,
) -> str:
return json.dumps({
"outcome": outcome,
"recommended_action": recommended_action,
"reason_codes": reason_codes or [],
"evidence": evidence or [],
"coverage": {"complete": coverage_complete, "scope": scope},
"freshness": {"observed_at": observed_at, "expires_at": None},
"error": None,
}, indent=2)
def _error_result(kind: str, message: str, retryable: bool = False) -> str:
return json.dumps({
"outcome": "error",
"recommended_action": "defer",
"reason_codes": [],
"evidence": [],
"coverage": {"complete": False, "scope": ""},
"freshness": {"observed_at": None, "expires_at": None},
"error": {"kind": kind, "message": message, "retryable": retryable},
}, indent=2)
def _call(path: str, payload: dict, api_key: str) -> tuple[dict | None, str | None]:
"""POSTs to a RelayShield metered endpoint. Returns (data, error_json_string).
error_json_string is None on success, or a ready-to-return _error_result() on
failure."""
try:
resp = requests.post(
f"{API_BASE_URL}{path}",
json=payload,
headers=_headers(api_key),
timeout=15,
)
except requests.Timeout:
return None, _error_result("timeout", f"RelayShield API call to {path} timed out after 15s.", retryable=True)
except requests.RequestException as exc:
return None, _error_result("upstream", f"RelayShield API call to {path} failed: {exc}", retryable=True)
if resp.status_code in (401, 403):
return None, _error_result("auth", f"RelayShield API call to {path} returned {resp.status_code} β check api_key.", retryable=False)
if resp.status_code == 429:
return None, _error_result("rate_limited", f"RelayShield API call to {path} was rate-limited.", retryable=True)
if resp.status_code >= 500:
return None, _error_result("upstream", f"RelayShield API call to {path} returned {resp.status_code}.", retryable=True)
if resp.status_code >= 400:
return None, _error_result("other", f"RelayShield API call to {path} returned {resp.status_code}: {resp.text[:200]}", retryable=False)
try:
response_json = resp.json()
except ValueError:
return None, _error_result("malformed_response", f"RelayShield API call to {path} returned non-JSON content.", retryable=False)
if not isinstance(response_json, dict):
return None, _error_result("malformed_response", f"RelayShield API call to {path} returned an unexpected response shape.", retryable=False)
return response_json.get("data", {}), None
def check_mcp_server_risk(server_url: str, api_key: str) -> str:
"""Check an MCP server URL for typosquat risk, presence in RelayShield's
criminal IOC corpus, and domain-registration age.
Args:
server_url: Full URL of the MCP server to check, e.g. https://example.com/mcp
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-mcp-registry-risk
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
if not server_url:
return _error_result("other", "server_url is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-mcp-registry-risk"))
data, error = _call("/v1/metered/mcp-registry-risk", {"server_url": server_url}, api_key)
if error:
return error
findings = data.get("findings", [])
outcome, action = _outcome_for_severity(data.get("verdict"), found=bool(findings))
return _structured_result(
outcome, action,
reason_codes=[f.get("type", "unknown") for f in findings],
evidence=findings,
scope=f"Checked {data.get('queried', server_url)!r} against known-MCP-domain typosquat distance, "
"RelayShield's criminal IOC corpus, and RDAP registration age.",
)
def check_prompt_injection_breach(email: str, api_key: str) -> str:
"""Check an email address for credential exposure sourced specifically
from prompt-injection attacks against AI agents, distinct from ordinary
phishing/malware-sourced breaches.
Args:
email: Email address to check, e.g. agent@example.com
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-prompt-injection-breach
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
if not email or "@" not in email:
return _error_result("other", "a valid email address is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-prompt-injection-breach"))
data, error = _call("/v1/metered/prompt-injection-breach", {"email": email}, api_key)
if error:
return error
sessions = data.get("sessions", [])
severities = [s.get("severity") for s in sessions if s.get("severity")]
highest = max(severities, key=lambda s: {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1}.get(s, 0), default=None)
outcome, action = _outcome_for_severity(highest, found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=["prompt_injection_sourced_breach"] if data.get("found") else [],
evidence=sessions,
scope=f"Checked {email} against stolen-session records flagged as prompt-injection-sourced "
"(heuristic keyword classifier over dump-announcement text, not confirmed attribution).",
)
def check_tech_stack_cve(tech_stack: str, api_key: str) -> str:
"""Check a declared AI agent framework / tech stack for CISA KEV or
high-EPSS CVEs actively being exploited. Covers AI agent orchestration
frameworks (LangChain, CrewAI, AutoGPT, Flowise, n8n self-hosted) and
common companion infrastructure (Nacos, MinIO).
Args:
tech_stack: Comma-separated list of products, e.g. "langchain, nacos, minio"
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-tech-stack-cve
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
stack_list = [t.strip() for t in tech_stack.split(",") if t.strip()]
if not stack_list:
return _error_result("other", "tech_stack is required (comma-separated list of products).")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-tech-stack-cve"))
data, error = _call("/v1/metered/tech-stack-cve", {"tech_stack": stack_list}, api_key)
if error:
return error
matched = data.get("all_matches", [])
critical = data.get("critical_cves", [])
highest = "CRITICAL" if critical else ("HIGH" if matched else None)
outcome, action = _outcome_for_severity(highest, found=bool(matched))
return _structured_result(
outcome, action,
reason_codes=[c.get("cve_id", "unknown") for c in critical] if critical else [],
evidence=matched,
scope=f"Checked {data.get('tech_stack_queried', stack_list)} against CISA KEV and high-EPSS-score CVEs.",
)
def check_bulk_identity_risk(domain: str, agent_emails: str, api_key: str) -> str:
"""Hierarchical org + AI-agent-identity risk scoring for a domain and up
to 5 agent/service-account identities. A critically-exposed agent
automatically elevates the organizational risk rating.
Args:
domain: Organization domain to score, e.g. example.com
agent_emails: Comma-separated agent/service-account emails, up to 5, e.g. "ai-agent@example.com, svc@example.com"
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-bulk-identity-risk
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
if not domain:
return _error_result("other", "domain is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-bulk-identity-risk"))
agents = [e.strip() for e in agent_emails.split(",") if e.strip()][:5]
data, error = _call("/v1/metered/bulk-identity-risk", {"targets": [{"domain": domain, "agents": agents}]}, api_key)
if error:
return error
results = data.get("results", [])
critical = data.get("critical_count", 0)
high = data.get("high_count", 0)
highest = "CRITICAL" if critical else ("HIGH" if high else None)
outcome, action = _outcome_for_severity(highest, found=bool(critical or high))
return _structured_result(
outcome, action,
reason_codes=[r.get("domain", "unknown") for r in results
if r.get("domain_risk") in ("CRITICAL", "HIGH") or r.get("highest_agent_risk") in ("CRITICAL", "HIGH")],
evidence=results,
scope=f"Scored {data.get('queried', len(results))} domain(s) for combined breach/infostealer/session/CVE risk.",
)
def check_oauth_watchlist(email: str, api_key: str) -> str:
"""Check an email for OAuth-connected-app exposure: apps matched against
breach history, plus stolen OAuth/session tokens found in criminal
stealer logs.
Args:
email: Email address to check for OAuth app exposure, e.g. agent@example.com
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-oauth-watchlist
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
if not email or "@" not in email:
return _error_result("other", "a valid email address is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-oauth-watchlist"))
data, error = _call("/v1/metered/oauth-watchlist", {"email": email}, api_key)
if error:
return error
matched_apps = data.get("matched_apps", [])
stolen_tokens = data.get("stolen_tokens", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=bool(matched_apps or stolen_tokens))
return _structured_result(
outcome, action,
reason_codes=(["oauth_app_breach_match"] if matched_apps else []) + (["stolen_oauth_token"] if stolen_tokens else []),
evidence=matched_apps + stolen_tokens,
scope=f"Checked {email} against HIBP breach history for OAuth-app matches and RelayShield's "
"stealer-log corpus for stolen session/OAuth tokens.",
observed_at=data.get("checked_at"),
)
def check_supply_chain(vendor_domains: str, api_key: str) -> str:
"""Check up to 10 vendor domains for breach and infostealer exposure β
third-party and supply-chain risk inherited through vendor relationships.
Args:
vendor_domains: Comma-separated vendor domains, up to 10, e.g. "vendor1.com, vendor2.com"
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-supply-chain
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
domains = [d.strip() for d in vendor_domains.split(",") if d.strip()][:10]
if not domains:
return _error_result("other", "vendor_domains is required (comma-separated list, up to 10).")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-supply-chain"))
data, error = _call("/v1/metered/supply-chain", {"vendor_domains": domains}, api_key)
if error:
return error
results = data.get("results", [])
highest = data.get("highest_risk")
outcome, action = _outcome_for_severity(highest if highest != "CLEAN" else None, found=highest not in (None, "CLEAN"))
return _structured_result(
outcome, action,
reason_codes=data.get("critical_vendors", []) + data.get("high_risk_vendors", []),
evidence=results,
scope=f"Checked {data.get('domains_checked', 0)} vendor domain(s) for breach and infostealer exposure.",
observed_at=data.get("checked_at"),
)
def check_session_risk(email: str, api_key: str) -> str:
"""Check an email for active or reusable stolen session material
(cookies, tokens) found in criminal stealer logs β exposure that can
bypass MFA/authentication entirely, not just a password.
Args:
email: Email address to check for active session/AiTM exposure, e.g. agent@example.com
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-session-risk
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
if not email or "@" not in email:
return _error_result("other", "a valid email address is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-session-risk"))
data, error = _call("/v1/metered/session-risk", {"email": email}, api_key)
if error:
return error
sessions = data.get("sessions", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=[s.get("service_category", "unknown") for s in sessions],
evidence=sessions,
scope=f"Checked {email} against RelayShield's stolen-session corpus for reusable session/cookie material.",
)
def check_nhi_exposure(domain: str, vendor_domains: str, api_key: str) -> str:
"""Check a domain, or up to 10 vendor domains, for exposed non-human-
identity (NHI) credentials β API keys, service-account tokens, personal
access tokens, and other machine identities found in criminal stealer logs.
Args:
domain: Your own domain to check. Leave blank if only using vendor_domains.
vendor_domains: Comma-separated vendor/supply-chain domains, up to 10. Leave blank if only using domain.
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-nhi-exposure
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
domain = (domain or "").strip()
domains = [d.strip() for d in (vendor_domains or "").split(",") if d.strip()][:10]
if not domain and not domains:
return _error_result("other", "domain or vendor_domains is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-nhi-exposure"))
payload: dict = {}
if domain:
payload["domain"] = domain
if domains:
payload["vendor_domains"] = domains
data, error = _call("/v1/metered/nhi-exposure", payload, api_key)
if error:
return error
findings = data.get("findings", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=[f.get("type", "unknown") for f in findings],
evidence=findings,
scope=f"Checked {data.get('domains_checked', 0)} domain(s) against RelayShield's stealer-log corpus "
"for exposed non-human-identity credentials.",
observed_at=data.get("checked_at"),
)
def check_secret_scan(domain: str, vendor_domains: str, api_key: str) -> str:
"""Check a domain, or up to 5 vendor domains, for secrets and
credentials exposed in public GitHub repositories.
Args:
domain: Your own domain to check. Leave blank if only using vendor_domains.
vendor_domains: Comma-separated vendor domains, up to 5. Leave blank if only using domain.
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-secret-scan
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
domain = (domain or "").strip()
domains = [d.strip() for d in (vendor_domains or "").split(",") if d.strip()][:5]
if not domain and not domains:
return _error_result("other", "domain or vendor_domains is required.")
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-secret-scan"))
payload: dict = {}
if domain:
payload["domain"] = domain
if domains:
payload["vendor_domains"] = domains
data, error = _call("/v1/metered/secret-scan", payload, api_key)
if error:
return error
findings = data.get("findings", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=[f.get("severity", "unknown") for f in findings],
evidence=findings,
scope=f"Checked {data.get('domains_checked', 0)} domain(s) against public GitHub repositories "
"for exposed secrets.",
observed_at=data.get("checked_at"),
)
# Shared demo key for the zero-key free tier below (mcp_space_demo source,
# 20 calls/day shared across ALL callers, enforced server-side by the
# RelayShield API itself -- see DEMO_QUOTA_SOURCES in relayshield_api.py).
# Same established pattern as the hf_smolagents_demo key already embedded
# in relayshield_smolagents_tool.py; the cap bounds worst-case abuse to a
# small fixed daily amount regardless of who has this string.
_DEMO_API_KEY = "rs_live_9797cff2e80b407eae16a12c8faac71e"
def check_llm_credential_exposure(domain: str, api_key: str = "") -> str:
"""Check a domain for exposed LLM/AI provider API keys (OpenAI, Anthropic,
Google, Groq, xAI, Replicate) in criminal stealer logs -- LLMjacking, a
fast-growing threat where a leaked key becomes a live, uncapped billing
liability rather than just a data exposure. Real incidents have run from
tens of thousands of dollars per day to a $500K single-month bill from
one leaked, unthrottled key.
Args:
domain: Domain to check, e.g. example.com
api_key: Your RelayShield API key. Leave blank to try this specific
tool for free (shared demo quota, 20 calls/day across all users --
get your own key at https://api.relayshield.net/developers#ep-llm-credential-exposure
for unlimited use and access to the other 12 tools on this server).
Returns:
A structured result: outcome/recommended_action plus evidence and reason codes.
"""
if not domain:
return _error_result("other", "domain is required.")
used_key = (api_key or "").strip() or _DEMO_API_KEY
data, error = _call("/v1/metered/llm-credential-exposure", {"domain": domain}, used_key)
if error:
return error
findings = data.get("findings", [])
providers = data.get("providers_affected", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=bool(findings))
return _structured_result(
outcome, action,
reason_codes=providers,
evidence=findings,
scope=f"Checked {domain} against RelayShield's stealer-log corpus for exposed LLM/AI provider "
f"API keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).{' Used the shared free demo key -- get your own for unlimited use.' if used_key == _DEMO_API_KEY else ''}",
observed_at=data.get("checked_at"),
)
def check_agent_risk_summary(domain: str, email: str, tech_stack: str, api_key: str) -> str:
"""Composite risk check -- one call instead of four. Runs breach exposure,
LLM credential exposure, and tech-stack CVE checks together and returns a
single fused risk verdict, so a calling agent doesn't have to make
multiple tool calls and reason about how to combine their results itself.
Args:
domain: Domain to check (used for LLM credential exposure and tech-stack CVE lookup). Leave blank to skip these.
email: Email address to check for breach exposure. Leave blank to skip this check.
tech_stack: Comma-separated tech stack for CVE matching, e.g. "langchain, nacos". Leave blank to skip.
api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-agent-risk-summary
Returns:
A structured result combining all sub-checks that were run, with the
highest severity found across them driving the overall outcome.
"""
if not api_key:
return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-agent-risk-summary"))
if not domain and not email and not tech_stack:
return _error_result("other", "at least one of domain, email, or tech_stack is required.")
severity_rank = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1}
sub_results: dict = {}
highest_seen = None
all_evidence: list = []
reason_codes: list = []
any_error = None
def _note(check_name: str, data: dict | None, err: str | None, sev_key: str, evidence_key: str):
nonlocal highest_seen, any_error
if err:
sub_results[check_name] = {"error": True}
any_error = any_error or err
return
sub_results[check_name] = data
sev = data.get(sev_key)
if sev and severity_rank.get(sev, 0) > severity_rank.get(highest_seen, 0):
highest_seen = sev
ev = data.get(evidence_key) or []
if ev:
all_evidence.extend(ev if isinstance(ev, list) else [ev])
if email and "@" in email:
data, err = _call("/v1/metered/breach", {"email": email}, api_key)
_note("breach", data, err, "highest_severity", "breaches")
if not err and data.get("breach_count"):
reason_codes.append("email_breach_exposure")
if domain:
data, err = _call("/v1/metered/llm-credential-exposure", {"domain": domain}, api_key)
_note("llm_credential_exposure", data, err, "highest_severity", "findings")
if not err and data.get("found"):
reason_codes.append("llm_credential_exposure")
if tech_stack.strip():
stack_list = [t.strip() for t in tech_stack.split(",") if t.strip()]
data, err = _call("/v1/metered/tech-stack-cve", {"tech_stack": stack_list}, api_key)
if not err:
highest = "CRITICAL" if data.get("critical_count") else ("HIGH" if data.get("total_matches") else None)
sub_results["tech_stack_cve"] = data
if highest and severity_rank.get(highest, 0) > severity_rank.get(highest_seen, 0):
highest_seen = highest
if data.get("critical_count"):
reason_codes.append("tech_stack_cve_critical")
all_evidence.extend(data.get("critical_cves", []))
else:
sub_results["tech_stack_cve"] = {"error": True}
any_error = any_error or err
outcome, action = _outcome_for_severity(highest_seen, found=bool(reason_codes))
result = _structured_result(
outcome, action,
reason_codes=reason_codes,
evidence=all_evidence[:20],
scope=f"Composite check across: {', '.join(k for k in sub_results if sub_results[k] and not sub_results[k].get('error'))}.",
)
if any_error and not reason_codes and outcome == "no_known_finding":
# At least one sub-check failed and nothing was found elsewhere -- don't
# silently report a clean bill of health when part of the check didn't run.
parsed = json.loads(result)
parsed["coverage"]["complete"] = False
parsed["coverage"]["scope"] += " One or more sub-checks failed to complete -- see logs."
return json.dumps(parsed, indent=2)
return result
def get_stix_indicators(limit: int, api_key: str) -> str:
"""Fetch RelayShield's IOC corpus as STIX 2.1 Indicator objects via the
TAXII 2.1 feed -- for agents/tools that consume threat intel in STIX
format directly (SIEM/TIP ingestion, correlation engines) rather than
RelayShield's own JSON shape. Requires a TI (Threat Intelligence)
subscription API key, not just any metered key -- a standard metered
key will get an auth error here, that's expected.
Args:
limit: Number of STIX objects to return, up to 2000. Defaults to 50 if 0 or not set.
api_key: Your RelayShield TI subscription API key. Get one at https://api.relayshield.net/developers#ti-subscription
Returns:
A structured result with STIX 2.1 Indicator objects as evidence.
"""
if not api_key:
return _error_result("auth", _scrub("api_key is required (must be a TI subscription key). Get one at https://api.relayshield.net/developers#ti-subscription"))
page_size = limit if limit and 0 < limit <= 2000 else 50
try:
resp = requests.get(
f"{API_BASE_URL}/v1/intel/taxii/collections/iocs/objects/",
params={"limit": page_size},
headers=_headers(api_key),
timeout=20,
)
except requests.RequestException as exc:
return _error_result("upstream", f"TAXII request failed: {exc}", retryable=True)
if resp.status_code in (401, 403):
return _error_result("auth", f"TAXII endpoint returned {resp.status_code} -- this endpoint requires a TI subscription key, not a standard metered key.", retryable=False)
if resp.status_code >= 400:
return _error_result("other", f"TAXII endpoint returned {resp.status_code}: {resp.text[:200]}", retryable=resp.status_code >= 500)
try:
body = resp.json()
except ValueError:
return _error_result("malformed_response", "TAXII endpoint returned non-JSON content.", retryable=False)
objects = body.get("objects", [])
return _structured_result(
"finding" if objects else "no_known_finding",
"review" if objects else "allow",
reason_codes=["stix_indicator_bundle"] if objects else [],
evidence=objects,
scope=f"Fetched {len(objects)} STIX 2.1 Indicator object(s) from RelayShield's TAXII 2.1 feed "
"(/v1/intel/taxii/collections/iocs/objects/).",
)
def check_server_status() -> str:
"""Check which RelayShield tools and data sources are currently live on
this MCP server -- a lightweight introspection call for an agent (or a
human debugging) to confirm connectivity and see what's available before
making a real check. Requires no API key.
Returns:
A structured result listing available tools and a live upstream connectivity check.
"""
tool_names = [t["name"] for t in _SERVER_CARD["tools"]]
try:
resp = requests.get(f"{API_BASE_URL}/developers", timeout=8)
upstream_reachable = resp.status_code < 500
except requests.RequestException:
upstream_reachable = False
return _structured_result(
"no_known_finding", "allow",
evidence=[{"tools_available": tool_names, "tool_count": len(tool_names), "upstream_reachable": upstream_reachable}],
scope="Introspection check -- lists available tools and confirms the RelayShield API is reachable. "
"Not a security finding; 'outcome' is always no_known_finding for this tool.",
)
mcp_risk_tab = gr.Interface(
fn=check_mcp_server_risk,
inputs=[
gr.Textbox(label="MCP Server URL", placeholder="https://example.com/mcp"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-mcp-registry-risk")),
],
outputs=gr.Textbox(label="Result"),
title="MCP Server Risk Check",
description="Typosquat, IOC-corpus, and registration-age risk check for an MCP server URL.",
)
breach_tab = gr.Interface(
fn=check_prompt_injection_breach,
inputs=[
gr.Textbox(label="Email Address", placeholder="agent@example.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-prompt-injection-breach")),
],
outputs=gr.Textbox(label="Result"),
title="Prompt-Injection Breach Check",
description="Checks for credential breaches sourced from prompt-injection attacks against AI agents.",
)
tech_stack_tab = gr.Interface(
fn=check_tech_stack_cve,
inputs=[
gr.Textbox(label="Tech Stack (comma-separated)", placeholder="langchain, nacos, minio"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-tech-stack-cve")),
],
outputs=gr.Textbox(label="Result"),
title="Tech Stack CVE Check",
description="CISA KEV / high-EPSS CVEs targeting a declared AI agent framework or tech stack.",
)
bulk_identity_tab = gr.Interface(
fn=check_bulk_identity_risk,
inputs=[
gr.Textbox(label="Domain", placeholder="example.com"),
gr.Textbox(label="Agent Emails (comma-separated, up to 5)", placeholder="ai-agent@example.com, svc@example.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-bulk-identity-risk")),
],
outputs=gr.Textbox(label="Result"),
title="Bulk Identity Risk Check",
description="Hierarchical org + AI-agent-identity risk scoring for a domain and its agent/service-account identities.",
)
oauth_watchlist_tab = gr.Interface(
fn=check_oauth_watchlist,
inputs=[
gr.Textbox(label="Email Address", placeholder="agent@example.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-oauth-watchlist")),
],
outputs=gr.Textbox(label="Result"),
title="OAuth Watchlist Check",
description="OAuth-connected-app breach exposure plus stolen OAuth/session tokens found in criminal stealer logs.",
)
supply_chain_tab = gr.Interface(
fn=check_supply_chain,
inputs=[
gr.Textbox(label="Vendor Domains (comma-separated, up to 10)", placeholder="vendor1.com, vendor2.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-supply-chain")),
],
outputs=gr.Textbox(label="Result"),
title="Supply Chain Risk Check",
description="Breach and infostealer exposure check for up to 10 vendor domains.",
)
session_risk_tab = gr.Interface(
fn=check_session_risk,
inputs=[
gr.Textbox(label="Email Address", placeholder="agent@example.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-session-risk")),
],
outputs=gr.Textbox(label="Result"),
title="Session Risk Check",
description="Active or reusable stolen session (cookie/token) exposure that can bypass MFA entirely.",
)
nhi_exposure_tab = gr.Interface(
fn=check_nhi_exposure,
inputs=[
gr.Textbox(label="Domain (optional)", placeholder="example.com"),
gr.Textbox(label="Vendor Domains (comma-separated, up to 10, optional)", placeholder="vendor1.com, vendor2.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-nhi-exposure")),
],
outputs=gr.Textbox(label="Result"),
title="Non-Human Identity Exposure Check",
description="API keys, service-account tokens, PATs, and other machine credentials found in criminal stealer logs.",
)
secret_scan_tab = gr.Interface(
fn=check_secret_scan,
inputs=[
gr.Textbox(label="Domain (optional)", placeholder="example.com"),
gr.Textbox(label="Vendor Domains (comma-separated, up to 5, optional)", placeholder="vendor1.com, vendor2.com"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-secret-scan")),
],
outputs=gr.Textbox(label="Result"),
title="Secret Scan Check",
description="Secrets and credentials exposed in public GitHub repositories.",
)
llm_credential_tab = gr.Interface(
fn=check_llm_credential_exposure,
inputs=[
gr.Textbox(label="Domain", placeholder="example.com"),
gr.Textbox(label="RelayShield API Key (optional β try free, 20 calls/day shared demo)", type="password", placeholder=_scrub("Leave blank to try for free, or get your own key at api.relayshield.net/developers#ep-llm-credential-exposure")),
],
outputs=gr.Textbox(label="Result"),
title="LLM Credential Exposure (LLMjacking)",
description="Exposed OpenAI/Anthropic/Google/Groq/xAI/Replicate API keys in criminal stealer logs β a live, uncapped billing liability. Free to try, no key required.",
)
agent_risk_summary_tab = gr.Interface(
fn=check_agent_risk_summary,
inputs=[
gr.Textbox(label="Domain (optional)", placeholder="example.com"),
gr.Textbox(label="Email Address (optional)", placeholder="agent@example.com"),
gr.Textbox(label="Tech Stack (comma-separated, optional)", placeholder="langchain, nacos"),
gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-agent-risk-summary")),
],
outputs=gr.Textbox(label="Result"),
title="Agent Risk Summary (Composite)",
description="One call instead of four β combines breach, LLM credential exposure, and tech-stack CVE checks into a single fused verdict.",
)
stix_tab = gr.Interface(
fn=get_stix_indicators,
inputs=[
gr.Number(label="Limit (up to 2000, default 50)", value=50),
gr.Textbox(label="RelayShield API Key (TI subscription required)", type="password", placeholder="Requires a TI subscription key, not a standard metered key"),
],
outputs=gr.Textbox(label="Result"),
title="STIX 2.1 Indicators (TAXII)",
description="Fetch RelayShield's IOC corpus as STIX 2.1 Indicator objects for SIEM/TIP ingestion. Requires a TI subscription key.",
)
status_tab = gr.Interface(
fn=check_server_status,
inputs=[],
outputs=gr.Textbox(label="Result"),
title="Server Status",
description="Lists available tools and confirms upstream connectivity. No API key required.",
)
demo = gr.TabbedInterface(
[
mcp_risk_tab, breach_tab, tech_stack_tab, bulk_identity_tab,
oauth_watchlist_tab, supply_chain_tab, session_risk_tab, nhi_exposure_tab, secret_scan_tab,
llm_credential_tab, agent_risk_summary_tab, stix_tab, status_tab,
],
[
"MCP Server Risk", "Prompt-Injection Breach", "Tech Stack CVE", "Bulk Identity Risk",
"OAuth Watchlist", "Supply Chain", "Session Risk", "NHI Exposure", "Secret Scan",
"LLM Credential Exposure", "Agent Risk Summary", "STIX Indicators", "Server Status",
],
title="RelayShield Agentic Attack Surface",
)
# Serve a static server-card.json so directories like Smithery can read our
# tool schema directly instead of relying on a live MCP handshake scan
# (their scanner returned a 404 during initialization against the plain
# Gradio MCP endpoint β this is Smithery's own documented workaround:
# https://smithery.ai/docs/build/publish#troubleshooting).
_SERVER_CARD_RAW = {
"serverInfo": {
"name": "RelayShield Agentic Attack Surface",
"version": "3.0.1",
},
"authentication": {
"required": False,
},
"tools": [
{
"name": "check_mcp_server_risk",
"description": "Check an MCP server URL for typosquat risk, presence in RelayShield's criminal IOC corpus, and domain-registration age. Returns a structured outcome/recommended_action result with evidence.",
"inputSchema": {
"type": "object",
"properties": {
"server_url": {"type": "string", "description": "Full URL of the MCP server to check, e.g. https://example.com/mcp"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-mcp-registry-risk"},
},
"required": ["server_url", "api_key"],
},
},
{
"name": "check_prompt_injection_breach",
"description": "Check an email address for credential exposure sourced specifically from prompt-injection attacks against AI agents, distinct from ordinary phishing/malware-sourced breaches.",
"inputSchema": {
"type": "object",
"properties": {
"email": {"type": "string", "description": "Email address to check, e.g. agent@example.com"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-prompt-injection-breach"},
},
"required": ["email", "api_key"],
},
},
{
"name": "check_tech_stack_cve",
"description": "Check a declared AI agent framework / tech stack for CISA KEV or high-EPSS CVEs actively being exploited. Covers AI agent orchestration frameworks and common companion infrastructure.",
"inputSchema": {
"type": "object",
"properties": {
"tech_stack": {"type": "string", "description": "Comma-separated list of products, e.g. \"langchain, nacos, minio\""},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-tech-stack-cve"},
},
"required": ["tech_stack", "api_key"],
},
},
{
"name": "check_bulk_identity_risk",
"description": "Hierarchical org + AI-agent-identity risk scoring for a domain and up to 5 agent/service-account identities. A critically-exposed agent automatically elevates the organizational risk rating.",
"inputSchema": {
"type": "object",
"properties": {
"domain": {"type": "string", "description": "Organization domain to score, e.g. example.com"},
"agent_emails": {"type": "string", "description": "Comma-separated agent/service-account emails, up to 5"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-bulk-identity-risk"},
},
"required": ["domain", "api_key"],
},
},
{
"name": "check_oauth_watchlist",
"description": "Check an email for OAuth-connected-app exposure: apps matched against breach history, plus stolen OAuth/session tokens found in criminal stealer logs. Part of the oauth_watchlist/nhi_exposure/session_risk agent-authority family.",
"inputSchema": {
"type": "object",
"properties": {
"email": {"type": "string", "description": "Email address to check, e.g. agent@example.com"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-oauth-watchlist"},
},
"required": ["email", "api_key"],
},
},
{
"name": "check_supply_chain",
"description": "Check up to 10 vendor domains for breach and infostealer exposure β third-party and supply-chain risk inherited through vendor relationships.",
"inputSchema": {
"type": "object",
"properties": {
"vendor_domains": {"type": "string", "description": "Comma-separated vendor domains, up to 10"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-supply-chain"},
},
"required": ["vendor_domains", "api_key"],
},
},
{
"name": "check_session_risk",
"description": "Check an email for active or reusable stolen session material (cookies, tokens) found in criminal stealer logs β exposure that can bypass MFA entirely. Part of the oauth_watchlist/nhi_exposure/session_risk agent-authority family.",
"inputSchema": {
"type": "object",
"properties": {
"email": {"type": "string", "description": "Email address to check, e.g. agent@example.com"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-session-risk"},
},
"required": ["email", "api_key"],
},
},
{
"name": "check_nhi_exposure",
"description": "Check a domain, or up to 10 vendor domains, for exposed non-human-identity credentials β API keys, service-account tokens, PATs, and other machine identities. Part of the oauth_watchlist/nhi_exposure/session_risk agent-authority family.",
"inputSchema": {
"type": "object",
"properties": {
"domain": {"type": "string", "description": "Your own domain to check (optional if vendor_domains supplied)"},
"vendor_domains": {"type": "string", "description": "Comma-separated vendor domains, up to 10 (optional if domain supplied)"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-nhi-exposure"},
},
"required": ["api_key"],
},
},
{
"name": "check_secret_scan",
"description": "Check a domain, or up to 5 vendor domains, for secrets and credentials exposed in public GitHub repositories.",
"inputSchema": {
"type": "object",
"properties": {
"domain": {"type": "string", "description": "Your own domain to check (optional if vendor_domains supplied)"},
"vendor_domains": {"type": "string", "description": "Comma-separated vendor domains, up to 5 (optional if domain supplied)"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-secret-scan"},
},
"required": ["api_key"],
},
},
{
"name": "check_llm_credential_exposure",
"description": "Check a domain for exposed LLM/AI provider API keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate) in criminal stealer logs -- LLMjacking, a live billing liability, not just a data exposure. Free to try (no api_key required, shared demo quota); pass your own key for unlimited use.",
"inputSchema": {
"type": "object",
"properties": {
"domain": {"type": "string", "description": "Domain to check, e.g. example.com"},
"api_key": {"type": "string", "description": "Your RelayShield API key. Optional -- leave blank to try for free (shared 20/day demo quota). Get your own at https://api.relayshield.net/developers#ep-llm-credential-exposure"},
},
"required": ["domain"],
},
},
{
"name": "check_agent_risk_summary",
"description": "Composite check -- one call instead of four. Runs breach exposure, LLM credential exposure, and tech-stack CVE checks together and returns a single fused risk verdict.",
"inputSchema": {
"type": "object",
"properties": {
"domain": {"type": "string", "description": "Domain to check (LLM credential exposure + tech-stack CVE). Optional."},
"email": {"type": "string", "description": "Email to check for breach exposure. Optional."},
"tech_stack": {"type": "string", "description": "Comma-separated tech stack for CVE matching. Optional."},
"api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-agent-risk-summary"},
},
"required": ["api_key"],
},
},
{
"name": "get_stix_indicators",
"description": "Fetch RelayShield's IOC corpus as STIX 2.1 Indicator objects via the TAXII 2.1 feed, for SIEM/TIP ingestion. Requires a TI subscription API key, not a standard metered key.",
"inputSchema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "Number of STIX objects to return, up to 2000. Defaults to 50."},
"api_key": {"type": "string", "description": "Your RelayShield TI subscription API key. Get one at https://api.relayshield.net/developers#ti-subscription"},
},
"required": ["api_key"],
},
},
{
"name": "check_server_status",
"description": "Lists available tools on this MCP server and confirms upstream RelayShield API connectivity. No API key required.",
"inputSchema": {
"type": "object",
"properties": {},
"required": [],
},
},
],
"resources": [],
"prompts": [],
}
# Scrub the developers-page reference from the manifest too (no-op in public
# mode) β this is served directly at /.well-known/mcp/server-card.json for
# directories like Smithery, a separate surface from the live MCP handshake.
_SERVER_CARD = json.loads(_scrub(json.dumps(_SERVER_CARD_RAW)))
# Scrub every tool function's docstring (no-op in public mode) β must happen
# before demo.launch() below, since that's the point Gradio reads __doc__ to
# build the live MCP tool schema an MCP client actually sees.
for _fn in (
check_mcp_server_risk, check_prompt_injection_breach, check_tech_stack_cve,
check_bulk_identity_risk, check_oauth_watchlist, check_supply_chain,
check_session_risk, check_nhi_exposure, check_secret_scan,
check_llm_credential_exposure, check_agent_risk_summary,
get_stix_indicators, check_server_status,
):
if _fn.__doc__:
_fn.__doc__ = _scrub(_fn.__doc__)
# NOTE 2026-07-27: root-caused via gradio's own source (routes.py,
# node_server.py), not guessed. gr.mount_gradio_app()/Blocks.launch() both
# conditionally call start_node_server() when ssr_mode resolves True β this
# spawns an actual Node.js child process for server-side rendering, and its
# default port fallback is ALSO 7860 (node_server.py: INITIAL_PORT_VALUE =
# int(os.getenv("GRADIO_SERVER_PORT", "7860"))). ssr_mode resolves from the
# GRADIO_SSR_MODE env var when not passed explicitly, and HF's Docker Space
# runtime apparently sets it β reproduced both bugs locally by simply
# exporting GRADIO_SSR_MODE=true: (1) the original custom-FastAPI +
# mount_gradio_app + manual uvicorn.run() pattern crashed with "[Errno 98]
# address already in use" because the Node process grabs 7860 first, then
# uvicorn.run() tries to grab the same port; (2) even after switching to
# demo.launch() to dodge that crash, the custom /.well-known/mcp/server-
# card.json route (Smithery's discovery workaround) came back as Gradio's
# generic SPA shell instead of real JSON β the Node server becomes the
# actual thing listening on the port and proxies through to Python, and
# (matching the original 2026-07 comment about "a Node.js frontend proxy
# ... only forwards a fixed whitelist of paths", now in git history) a
# custom Python-side route isn't in that whitelist. Fix: ssr_mode=False
# below, explicit rather than relying on the (HF-overridden) default β
# confirmed locally that no Node process spawns and the custom route
# resolves to real JSON with this set, even with GRADIO_SSR_MODE=true
# forced in the environment.
if __name__ == "__main__":
import threading
from fastapi.responses import JSONResponse
demo.launch(
server_name="0.0.0.0", server_port=7860, mcp_server=True,
ssr_mode=False, prevent_thread_lock=True,
)
@demo.app.get("/.well-known/mcp/server-card.json")
async def server_card():
return JSONResponse(_SERVER_CARD)
threading.Event().wait()
|