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.
Files changed (2) hide show
  1. package/package.json +7 -0
  2. package/server.py +203 -0
package/package.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "agentpay-domain-intel-mcp",
3
+ "version": "1.2.0",
4
+ "description": "MCP server with rate-limited free tier. 50 free calls, then Pro subscription at $19/mo.",
5
+ "type": "module",
6
+ "license": "MIT"
7
+ }
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()