Skip to content

Commit ee4c62a

Browse files
Andrew GibbsAndrew Gibbs
authored andcommitted
Add RelayShield security tools (MCP registry risk + prompt-injection breach)
1 parent 0e5d0ec commit ee4c62a

5 files changed

Lines changed: 196 additions & 0 deletions

File tree

lib/crewai-tools/src/crewai_tools/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
)
2121
from crewai_tools.tools.brave_search_tool.brave_news_tool import BraveNewsSearchTool
2222
from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool
23+
from crewai_tools.tools.relayshield_tool.relayshield_tool import (
24+
RelayShieldMCPRiskTool,
25+
RelayShieldPromptInjectionBreachTool,
26+
)
2327
from crewai_tools.tools.brave_search_tool.brave_video_tool import BraveVideoSearchTool
2428
from crewai_tools.tools.brave_search_tool.brave_web_tool import BraveWebSearchTool
2529
from crewai_tools.tools.brightdata_tool.brightdata_dataset import (
@@ -230,6 +234,8 @@
230234
"BraveLocalPOIsTool",
231235
"BraveNewsSearchTool",
232236
"BraveSearchTool",
237+
"RelayShieldMCPRiskTool",
238+
"RelayShieldPromptInjectionBreachTool",
233239
"BraveVideoSearchTool",
234240
"BraveWebSearchTool",
235241
"BrightDataDatasetTool",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# RelayShield Tools
2+
3+
Two agent-specific security checks from [RelayShield](https://api.relayshield.net/developers), a live threat-intelligence API.
4+
5+
## RelayShieldMCPRiskTool
6+
7+
Checks an MCP server URL or package name for:
8+
- Typosquat/near-miss matches against known MCP ecosystem domains
9+
- Presence in RelayShield's criminal IOC corpus
10+
- Domain-registration age (newly registered domains are higher risk)
11+
12+
```python
13+
from crewai_tools import RelayShieldMCPRiskTool
14+
15+
tool = RelayShieldMCPRiskTool()
16+
result = tool.run(server_url="https://example.com/mcp")
17+
```
18+
19+
## RelayShieldPromptInjectionBreachTool
20+
21+
Checks whether an email's credentials were exposed via a breach sourced specifically from a prompt-injection attack against an AI agent (distinct from ordinary phishing/malware-sourced breaches).
22+
23+
```python
24+
from crewai_tools import RelayShieldPromptInjectionBreachTool
25+
26+
tool = RelayShieldPromptInjectionBreachTool()
27+
result = tool.run(email="user@example.com")
28+
```
29+
30+
## Setup
31+
32+
Both tools require a RelayShield API key:
33+
34+
```bash
35+
export RELAYSHIELD_API_KEY="your-key-here"
36+
```
37+
38+
Get a key at [api.relayshield.net/developers](https://api.relayshield.net/developers) ($499/mo for 10,000 calls, or PAYG via x402 USDC with no key required — see docs).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from crewai_tools.tools.relayshield_tool.relayshield_tool import (
2+
RelayShieldMCPRiskTool,
3+
RelayShieldPromptInjectionBreachTool,
4+
)
5+
6+
__all__ = ["RelayShieldMCPRiskTool", "RelayShieldPromptInjectionBreachTool"]
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import os
2+
from typing import Any, ClassVar
3+
4+
import requests
5+
from crewai.tools import BaseTool, EnvVar
6+
from pydantic import BaseModel
7+
8+
from crewai_tools.tools.relayshield_tool.schemas import (
9+
MCPRegistryRiskParams,
10+
PromptInjectionBreachParams,
11+
)
12+
13+
API_BASE_URL: ClassVar[str] = "https://api.relayshield.net"
14+
15+
16+
def _relayshield_headers() -> dict[str, str]:
17+
api_key = os.environ.get("RELAYSHIELD_API_KEY", "")
18+
if not api_key:
19+
raise ValueError(
20+
"RELAYSHIELD_API_KEY environment variable is required. "
21+
"Get a key at https://api.relayshield.net/developers"
22+
)
23+
# Note: relayshield_agentic_api.py (which hosts these two endpoints)
24+
# expects X-RS-API-KEY specifically, not the X-API-Key convention used
25+
# by RelayShield's other metered endpoints.
26+
return {"Content-Type": "application/json", "X-RS-API-KEY": api_key}
27+
28+
29+
class RelayShieldMCPRiskTool(BaseTool):
30+
"""Typosquat / reputation / registration-age risk check for MCP servers
31+
and agent tool registries, backed by RelayShield's live threat-intel API."""
32+
33+
name: str = "RelayShield MCP Registry Risk"
34+
description: str = (
35+
"Checks an MCP server URL or package name for typosquat risk against known "
36+
"MCP ecosystem domains, presence in RelayShield's criminal IOC corpus, and "
37+
"domain-registration age. Use this before connecting an agent to an unfamiliar "
38+
"MCP server or tool registry. Returns a verdict (CRITICAL/HIGH/MEDIUM/LOW) with findings."
39+
)
40+
args_schema: type[BaseModel] = MCPRegistryRiskParams
41+
env_vars: list[EnvVar] = [
42+
EnvVar(
43+
name="RELAYSHIELD_API_KEY",
44+
description="API key for RelayShield (get one at api.relayshield.net/developers)",
45+
required=True,
46+
),
47+
]
48+
49+
def _run(self, server_url: str | None = None, package_name: str | None = None, **kwargs: Any) -> str:
50+
if not server_url and not package_name:
51+
return "Error: provide either server_url or package_name."
52+
53+
payload: dict[str, str] = {}
54+
if server_url:
55+
payload["server_url"] = server_url
56+
if package_name:
57+
payload["package_name"] = package_name
58+
59+
try:
60+
resp = requests.post(
61+
f"{API_BASE_URL}/v1/metered/mcp-registry-risk",
62+
json=payload,
63+
headers=_relayshield_headers(),
64+
timeout=15,
65+
)
66+
resp.raise_for_status()
67+
except requests.RequestException as exc:
68+
return f"RelayShield MCP registry risk check failed: {exc}"
69+
70+
# Every RelayShield response is wrapped as {"ok": bool, "data": {...}}.
71+
data = resp.json().get("data", {})
72+
verdict = data.get("verdict", "UNKNOWN")
73+
findings = data.get("findings", [])
74+
if not findings:
75+
return f"Verdict: {verdict}. No red flags found. {data.get('note', '')}"
76+
77+
lines = [f"Verdict: {verdict}", "Findings:"]
78+
for f in findings:
79+
lines.append(f"- [{f.get('severity')}] {f.get('type')}: {f.get('detail')}")
80+
return "\n".join(lines)
81+
82+
83+
class RelayShieldPromptInjectionBreachTool(BaseTool):
84+
"""Checks whether an email's credentials were exposed via a breach specifically
85+
sourced from a prompt-injection attack against an AI agent, as opposed to
86+
traditional phishing/malware-sourced breaches."""
87+
88+
name: str = "RelayShield Prompt-Injection Breach Check"
89+
description: str = (
90+
"Checks an email address for credential exposure sourced specifically from "
91+
"prompt-injection attacks against AI agents (distinct from ordinary breach/phishing "
92+
"sources). Use this to vet an agent identity or user account before granting it "
93+
"elevated trust or access."
94+
)
95+
args_schema: type[BaseModel] = PromptInjectionBreachParams
96+
env_vars: list[EnvVar] = [
97+
EnvVar(
98+
name="RELAYSHIELD_API_KEY",
99+
description="API key for RelayShield (get one at api.relayshield.net/developers)",
100+
required=True,
101+
),
102+
]
103+
104+
def _run(self, email: str, **kwargs: Any) -> str:
105+
try:
106+
resp = requests.post(
107+
f"{API_BASE_URL}/v1/metered/prompt-injection-breach",
108+
json={"email": email},
109+
headers=_relayshield_headers(),
110+
timeout=15,
111+
)
112+
resp.raise_for_status()
113+
except requests.RequestException as exc:
114+
return f"RelayShield prompt-injection breach check failed: {exc}"
115+
116+
# Every RelayShield response is wrapped as {"ok": bool, "data": {...}}.
117+
data = resp.json().get("data", {})
118+
if not data.get("found"):
119+
return f"No prompt-injection-sourced exposure found for {email}. {data.get('note', '')}"
120+
121+
count = data.get("session_count", 0)
122+
return (
123+
f"Found {count} session(s) exposed via prompt-injection-sourced breach for {email}. "
124+
"Treat this identity as compromised until credentials are rotated."
125+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class MCPRegistryRiskParams(BaseModel):
5+
server_url: str | None = Field(
6+
default=None,
7+
description="Full URL of the MCP server to check, e.g. 'https://example.com/mcp'. "
8+
"Provide this or package_name.",
9+
)
10+
package_name: str | None = Field(
11+
default=None,
12+
description="Package name of the MCP server if no server_url is available. "
13+
"Checks are more limited without a server_url.",
14+
)
15+
16+
17+
class PromptInjectionBreachParams(BaseModel):
18+
email: str = Field(
19+
description="Email address to check for credential exposure sourced from "
20+
"prompt-injection attacks against AI agents.",
21+
)

0 commit comments

Comments
 (0)