agentpay-domain-intel-mcp 1.2.0
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.
- package/package.json +7 -0
- package/server.py +203 -0
package/package.json
ADDED
package/server.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Domain Intelligence MCP — WHOIS, DNS, SSL, IP info for any domain.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 server.py # Free tier (50 calls/instance)
|
|
6
|
+
python3 server.py --pro-key PROL_XXX # Pro tier (unlimited)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json, socket, ssl, datetime, sys
|
|
10
|
+
from mcp.server import Server, stdio_server
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
server = Server("domain-intel-mcp")
|
|
14
|
+
|
|
15
|
+
# ─── Rate Limiting & Pro Key ───────────────────────────────────────────
|
|
16
|
+
FREE_LIMIT = 50
|
|
17
|
+
PRO_KEYS = {"PROL_AGENTPAY_DEMO": "demo"} # Demo key for testing
|
|
18
|
+
|
|
19
|
+
# Parse --pro-key from command line
|
|
20
|
+
PRO_KEY = None
|
|
21
|
+
for i, arg in enumerate(sys.argv):
|
|
22
|
+
if arg == "--pro-key" and i + 1 < len(sys.argv):
|
|
23
|
+
PRO_KEY = sys.argv[i + 1]
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
IS_PRO = PRO_KEY in PRO_KEYS
|
|
27
|
+
call_counter = 0
|
|
28
|
+
|
|
29
|
+
STRIPE_LINK = "https://buy.stripe.com/5kQ3cxflRabW9PW1AD1oI0r" # $19/mo
|
|
30
|
+
|
|
31
|
+
def check_rate_limit():
|
|
32
|
+
"""Check if free tier has exceeded limit. Returns error dict or None."""
|
|
33
|
+
global call_counter
|
|
34
|
+
if IS_PRO:
|
|
35
|
+
return None
|
|
36
|
+
call_counter += 1
|
|
37
|
+
if call_counter > FREE_LIMIT:
|
|
38
|
+
remaining = call_counter - FREE_LIMIT
|
|
39
|
+
return {
|
|
40
|
+
"error": f"Free tier limit reached ({FREE_LIMIT} calls). Upgrade to Pro for unlimited access.",
|
|
41
|
+
"isError": True,
|
|
42
|
+
"next_steps": [
|
|
43
|
+
f"Purchase Pro at {STRIPE_LINK} ($19/mo, unlimited)",
|
|
44
|
+
"Restart the server to reset the free counter",
|
|
45
|
+
"Use --pro-key PROL_XXX to run in Pro mode"
|
|
46
|
+
],
|
|
47
|
+
"calls_used": call_counter,
|
|
48
|
+
"limit": FREE_LIMIT,
|
|
49
|
+
"over_by": remaining
|
|
50
|
+
}
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
async def _ipinfo(domain):
|
|
54
|
+
"""Get IP geolocation data via ipinfo.io (50k free/mo, no key needed for basic)."""
|
|
55
|
+
ip = socket.gethostbyname(domain)
|
|
56
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
57
|
+
resp = await client.get(f"https://ipinfo.io/{ip}/json")
|
|
58
|
+
resp.raise_for_status()
|
|
59
|
+
data = resp.json()
|
|
60
|
+
# Mask the IP for privacy if it's a residential IP
|
|
61
|
+
data["ip"] = ip
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
def _ssl_info(hostname, port=443):
|
|
65
|
+
"""Get SSL certificate info."""
|
|
66
|
+
ctx = ssl.create_default_context()
|
|
67
|
+
try:
|
|
68
|
+
with socket.create_connection((hostname, port), timeout=8) as sock:
|
|
69
|
+
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
70
|
+
cert = ssock.getpeercert()
|
|
71
|
+
if not cert:
|
|
72
|
+
return {"error": "No certificate"}
|
|
73
|
+
subject = dict(x[0] for x in cert.get("subject", []))
|
|
74
|
+
issuer = dict(x[0] for x in cert.get("issuer", []))
|
|
75
|
+
not_after = cert.get("notAfter", "")
|
|
76
|
+
expiry = datetime.datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z") if not_after else None
|
|
77
|
+
days_left = (expiry - datetime.datetime.utcnow()).days if expiry else None
|
|
78
|
+
sans = [ext[1] for ext in cert.get("subjectAltName", ()) if ext[0] == "DNS"]
|
|
79
|
+
return {
|
|
80
|
+
"issuer": issuer.get("commonName", ""),
|
|
81
|
+
"expires": not_after,
|
|
82
|
+
"days_remaining": days_left,
|
|
83
|
+
"sans": sans,
|
|
84
|
+
"san_count": len(sans),
|
|
85
|
+
}
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return {"error": str(e)}
|
|
88
|
+
|
|
89
|
+
def _dns_records(domain):
|
|
90
|
+
"""Get basic DNS records."""
|
|
91
|
+
records = {}
|
|
92
|
+
for qtype in ["A", "AAAA", "MX", "NS", "TXT"]:
|
|
93
|
+
try:
|
|
94
|
+
answers = socket.getaddrinfo(domain if qtype in ["A", "AAAA"] else f"_{qtype}.{domain}", 0)
|
|
95
|
+
if qtype in ["A", "AAAA"]:
|
|
96
|
+
records[qtype] = list(set(a[4][0] for a in answers if a[0] == (socket.AF_INET if qtype == "A" else socket.AF_INET6)))
|
|
97
|
+
else:
|
|
98
|
+
records[qtype] = []
|
|
99
|
+
except:
|
|
100
|
+
records[qtype] = []
|
|
101
|
+
# NS records via socket
|
|
102
|
+
try:
|
|
103
|
+
ns_results = socket.getaddrinfo(domain, 0, socket.AF_INET, socket.SOCK_STREAM)
|
|
104
|
+
records["ns_servers"] = list(set(a[4][0] for a in ns_results[:5]))
|
|
105
|
+
except:
|
|
106
|
+
records["ns_servers"] = []
|
|
107
|
+
return records
|
|
108
|
+
|
|
109
|
+
@server.tool(
|
|
110
|
+
name="domain_intel",
|
|
111
|
+
description="Get comprehensive intelligence for a domain: IP geolocation, SSL cert, DNS records",
|
|
112
|
+
input_schema={
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"domain": {"type": "string", "description": "Domain name (e.g. example.com)"}
|
|
116
|
+
},
|
|
117
|
+
"required": ["domain"]
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
async def domain_intel(domain: str) -> str:
|
|
121
|
+
limit_check = check_rate_limit()
|
|
122
|
+
if limit_check:
|
|
123
|
+
return json.dumps(limit_check, indent=2)
|
|
124
|
+
try:
|
|
125
|
+
ip = socket.gethostbyname(domain)
|
|
126
|
+
dns = _dns_records(domain)
|
|
127
|
+
|
|
128
|
+
result = {
|
|
129
|
+
"domain": domain,
|
|
130
|
+
"resolved_ip": ip,
|
|
131
|
+
"dns": dns,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Try IP info (may fail)
|
|
135
|
+
try:
|
|
136
|
+
ip_data = await _ipinfo(domain)
|
|
137
|
+
result["geolocation"] = {
|
|
138
|
+
"city": ip_data.get("city", ""),
|
|
139
|
+
"region": ip_data.get("region", ""),
|
|
140
|
+
"country": ip_data.get("country", ""),
|
|
141
|
+
"org": ip_data.get("org", ""),
|
|
142
|
+
"timezone": ip_data.get("timezone", ""),
|
|
143
|
+
}
|
|
144
|
+
except:
|
|
145
|
+
result["geolocation"] = {"error": "Could not resolve IP location"}
|
|
146
|
+
|
|
147
|
+
# Try SSL (may fail for non-HTTPS)
|
|
148
|
+
ssl_data = _ssl_info(domain)
|
|
149
|
+
if "error" not in ssl_data:
|
|
150
|
+
result["ssl"] = ssl_data
|
|
151
|
+
|
|
152
|
+
return json.dumps(result, indent=2)
|
|
153
|
+
except socket.gaierror:
|
|
154
|
+
return json.dumps({"domain": domain, "error": "Domain does not resolve", "isError": True}, indent=2)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
return json.dumps({"domain": domain, "error": str(e), "isError": True}, indent=2)
|
|
157
|
+
|
|
158
|
+
@server.tool(
|
|
159
|
+
name="domain_check_health",
|
|
160
|
+
description="Check if a domain's HTTP/HTTPS services are responding",
|
|
161
|
+
input_schema={
|
|
162
|
+
"type": "object",
|
|
163
|
+
"properties": {
|
|
164
|
+
"domain": {"type": "string", "description": "Domain name"},
|
|
165
|
+
"port": {"type": "integer", "description": "Port to check", "default": 443}
|
|
166
|
+
},
|
|
167
|
+
"required": ["domain"]
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
async def domain_check_health(domain: str, port: int = 443) -> str:
|
|
171
|
+
limit_check = check_rate_limit()
|
|
172
|
+
if limit_check:
|
|
173
|
+
return json.dumps(limit_check, indent=2)
|
|
174
|
+
try:
|
|
175
|
+
results = {}
|
|
176
|
+
for proto, default_port in [("http", 80), ("https", 443)]:
|
|
177
|
+
p = port if proto == ("https" if port == 443 else "http") else default_port
|
|
178
|
+
url = f"{proto}://{domain}:{p}"
|
|
179
|
+
try:
|
|
180
|
+
async with httpx.AsyncClient(timeout=8, follow_redirects=True) as client:
|
|
181
|
+
resp = await client.get(url)
|
|
182
|
+
results[proto] = {
|
|
183
|
+
"status": resp.status_code,
|
|
184
|
+
"ok": resp.is_success,
|
|
185
|
+
"server": resp.headers.get("server", ""),
|
|
186
|
+
"content_type": resp.headers.get("content-type", "")[:50],
|
|
187
|
+
}
|
|
188
|
+
except Exception as e:
|
|
189
|
+
results[proto] = {"error": str(e)[:100]}
|
|
190
|
+
|
|
191
|
+
return json.dumps({"domain": domain, "checks": results}, indent=2)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
return json.dumps({"error": str(e), "isError": True}, indent=2)
|
|
194
|
+
|
|
195
|
+
def main():
|
|
196
|
+
import anyio
|
|
197
|
+
async def run():
|
|
198
|
+
async with stdio_server() as streams:
|
|
199
|
+
await server.run(streams[0], streams[1], server.create_initialization_options())
|
|
200
|
+
anyio.run(run)
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
main()
|