web-intel-mcp 0.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 (3) hide show
  1. package/README.md +74 -0
  2. package/index.js +294 -0
  3. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # web-intel-mcp
2
+
3
+ MCP server that gives AI agents web intelligence capabilities. DNS lookups, SSL checks, tech stack detection, page metadata, link safety analysis — all in one package.
4
+
5
+ ## Why
6
+
7
+ AI agents can't do DNS lookups, read SSL certificates, or detect web technologies natively. This MCP server adds those capabilities with zero configuration.
8
+
9
+ ## Tools
10
+
11
+ | Tool | What it does |
12
+ |---|---|
13
+ | `web_investigate` | Complete website investigation — DNS, SSL, tech stack, metadata, link safety in one call |
14
+ | `web_dns` | DNS records (A, AAAA, MX, NS, TXT, CNAME) + mail provider detection |
15
+ | `web_ssl` | SSL certificate details — issuer, expiry, validity, SANs |
16
+ | `web_tech_stack` | Detect frameworks, CMS, analytics, CDN, hosting, security headers |
17
+ | `web_meta` | Page title, description, Open Graph, Twitter cards, canonical URL |
18
+ | `web_link_safety` | URL safety verdict — safe/suspicious/dangerous with risk score |
19
+
20
+ ## Install
21
+
22
+ ### Claude Code
23
+
24
+ ```bash
25
+ claude mcp add web-intel node /path/to/web-intel-mcp/index.js
26
+ ```
27
+
28
+ Or add to `.mcp.json`:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "web-intel": {
34
+ "command": "node",
35
+ "args": ["/path/to/web-intel-mcp/index.js"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### npx (no install)
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "web-intel": {
47
+ "command": "npx",
48
+ "args": ["web-intel-mcp"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Examples
55
+
56
+ **"Check if this website is legit"**
57
+ → Agent calls `web_investigate` → gets trust score, SSL status, tech stack, safety signals
58
+
59
+ **"What DNS records does example.com have?"**
60
+ → Agent calls `web_dns` → gets A, MX, NS records + mail provider
61
+
62
+ **"Is this link safe to click?"**
63
+ → Agent calls `web_link_safety` → gets safe/suspicious/dangerous verdict
64
+
65
+ **"What framework is this site built with?"**
66
+ → Agent calls `web_tech_stack` → gets Next.js, React, Cloudflare, etc.
67
+
68
+ ## No API keys needed
69
+
70
+ Everything runs locally using Node.js built-in `dns` and `https` modules. No external API dependencies. No rate limits. No costs.
71
+
72
+ ## License
73
+
74
+ MIT
package/index.js ADDED
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // x402 Web Intel — MCP Server
5
+ // ---------------------------------------------------------------------------
6
+ // Standalone MCP server that gives any AI agent web intelligence capabilities.
7
+ // No API keys. No platform dependency. Just add to your MCP config and go.
8
+ //
9
+ // Tools:
10
+ // web_investigate — Complete website investigation (DNS, SSL, tech, meta, safety, carbon)
11
+ // web_dns — DNS records + mail provider detection
12
+ // web_ssl — SSL certificate details
13
+ // web_tech_stack — Detect web technologies and security headers
14
+ // web_meta — Page metadata, OG tags, Twitter cards
15
+ // web_link_safety — URL safety verdict (safe/suspicious/dangerous)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { z } from "zod";
21
+ import dns from "node:dns/promises";
22
+ import https from "node:https";
23
+
24
+ const server = new McpServer({
25
+ name: "web-intel",
26
+ version: "0.2.0",
27
+ });
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Core functions (self-contained, no external API dependency)
31
+ // ---------------------------------------------------------------------------
32
+
33
+ async function dnsLookup(domain) {
34
+ const [a, aaaa, mx, ns, txt, cname] = await Promise.allSettled([
35
+ dns.resolve(domain, "A"), dns.resolve(domain, "AAAA"),
36
+ dns.resolve(domain, "MX"), dns.resolve(domain, "NS"),
37
+ dns.resolve(domain, "TXT"), dns.resolve(domain, "CNAME"),
38
+ ]);
39
+
40
+ const mxRecords = mx.status === "fulfilled" ? mx.value : [];
41
+ const mxStr = mxRecords.map((r) => r.exchange).join(" ").toLowerCase();
42
+ let mailProvider = null;
43
+ if (mxStr.includes("google") || mxStr.includes("gmail")) mailProvider = "Google Workspace";
44
+ else if (mxStr.includes("outlook") || mxStr.includes("microsoft")) mailProvider = "Microsoft 365";
45
+ else if (mxStr.includes("protonmail")) mailProvider = "ProtonMail";
46
+ else if (mxStr.includes("zoho")) mailProvider = "Zoho Mail";
47
+
48
+ return {
49
+ A: a.status === "fulfilled" ? a.value : [],
50
+ AAAA: aaaa.status === "fulfilled" ? aaaa.value : [],
51
+ MX: mxRecords.sort((a, b) => a.priority - b.priority),
52
+ NS: ns.status === "fulfilled" ? ns.value : [],
53
+ TXT: txt.status === "fulfilled" ? txt.value.map((t) => t.join("")) : [],
54
+ CNAME: cname.status === "fulfilled" ? cname.value : [],
55
+ mail_provider: mailProvider,
56
+ has_ipv6: aaaa.status === "fulfilled" && aaaa.value.length > 0,
57
+ };
58
+ }
59
+
60
+ async function sslCheck(domain) {
61
+ return new Promise((resolve, reject) => {
62
+ const req = https.request({ hostname: domain, port: 443, method: "HEAD", timeout: 10000 }, (res) => {
63
+ const cert = res.socket.getPeerCertificate();
64
+ const now = new Date();
65
+ const validFrom = new Date(cert.valid_from);
66
+ const validTo = new Date(cert.valid_to);
67
+ const daysRemaining = Math.floor((validTo - now) / 86400000);
68
+
69
+ resolve({
70
+ issuer: cert.issuer?.O || cert.issuer?.CN || "Unknown",
71
+ subject: cert.subject?.CN || domain,
72
+ valid_from: validFrom.toISOString(),
73
+ valid_to: validTo.toISOString(),
74
+ days_remaining: daysRemaining,
75
+ is_valid: daysRemaining > 0,
76
+ is_expiring_soon: daysRemaining > 0 && daysRemaining < 30,
77
+ san: cert.subjectaltname ? cert.subjectaltname.split(", ").map((s) => s.replace("DNS:", "")) : [],
78
+ });
79
+ res.destroy();
80
+ });
81
+ req.on("error", reject);
82
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
83
+ req.end();
84
+ });
85
+ }
86
+
87
+ async function techStack(url) {
88
+ const response = await fetch(url, {
89
+ signal: AbortSignal.timeout(10_000),
90
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; web-intel-mcp/0.2)" },
91
+ redirect: "follow",
92
+ });
93
+
94
+ const headers = Object.fromEntries(response.headers.entries());
95
+ const html = await response.text();
96
+ const head = html.slice(0, 15000).toLowerCase();
97
+ const techs = [];
98
+
99
+ if (headers.server) techs.push({ name: headers.server, category: "server" });
100
+ if (headers["x-powered-by"]) techs.push({ name: headers["x-powered-by"], category: "framework" });
101
+ if (head.includes("__next")) techs.push({ name: "Next.js", category: "framework" });
102
+ if (head.includes("__nuxt")) techs.push({ name: "Nuxt.js", category: "framework" });
103
+ if (head.includes("wp-content") || head.includes("wordpress")) techs.push({ name: "WordPress", category: "cms" });
104
+ if (head.includes("shopify")) techs.push({ name: "Shopify", category: "ecommerce" });
105
+ if (head.includes("wix.com")) techs.push({ name: "Wix", category: "builder" });
106
+ if (head.includes("google-analytics") || head.includes("gtag")) techs.push({ name: "Google Analytics", category: "analytics" });
107
+ if (head.includes("gtm.js") || head.includes("googletagmanager")) techs.push({ name: "GTM", category: "analytics" });
108
+ if (headers["cf-ray"] || headers.server?.includes("cloudflare")) techs.push({ name: "Cloudflare", category: "cdn" });
109
+ if (headers["x-vercel-id"]) techs.push({ name: "Vercel", category: "hosting" });
110
+ if (headers["fly-request-id"]) techs.push({ name: "Fly.io", category: "hosting" });
111
+ if (head.includes("tailwind")) techs.push({ name: "Tailwind", category: "css" });
112
+ if (head.includes("bootstrap")) techs.push({ name: "Bootstrap", category: "css" });
113
+ if (head.includes("react")) techs.push({ name: "React", category: "js" });
114
+ if (head.includes("vue")) techs.push({ name: "Vue.js", category: "js" });
115
+ if (head.includes("jquery")) techs.push({ name: "jQuery", category: "js" });
116
+
117
+ const security = {};
118
+ if (headers["strict-transport-security"]) security.hsts = true;
119
+ if (headers["content-security-policy"]) security.csp = true;
120
+ if (headers["x-frame-options"]) security.xframe = headers["x-frame-options"];
121
+
122
+ return { technologies: techs, security_headers: security, html_size: html.length };
123
+ }
124
+
125
+ async function pageMeta(url) {
126
+ const response = await fetch(url, {
127
+ signal: AbortSignal.timeout(10_000),
128
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; web-intel-mcp/0.2)" },
129
+ redirect: "follow",
130
+ });
131
+ const html = await response.text();
132
+ const head = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] || html.slice(0, 15000);
133
+
134
+ const getMeta = (name) => {
135
+ const m = head.match(new RegExp(`<meta[^>]*(?:name|property)=["']${name}["'][^>]*content=["']([^"']+)["']`, "i"))
136
+ || head.match(new RegExp(`<meta[^>]*content=["']([^"']+)["'][^>]*(?:name|property)=["']${name}["']`, "i"));
137
+ return m?.[1] || null;
138
+ };
139
+
140
+ return {
141
+ title: head.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim() || null,
142
+ description: getMeta("description"),
143
+ og_title: getMeta("og:title"),
144
+ og_description: getMeta("og:description"),
145
+ og_image: getMeta("og:image"),
146
+ og_type: getMeta("og:type"),
147
+ twitter_card: getMeta("twitter:card"),
148
+ canonical: head.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']+)["']/i)?.[1] || null,
149
+ language: html.match(/<html[^>]*lang=["']([^"']+)["']/i)?.[1] || null,
150
+ };
151
+ }
152
+
153
+ function linkSafety(url) {
154
+ const signals = [];
155
+ let riskScore = 0;
156
+ const parsed = new URL(url);
157
+
158
+ if (parsed.protocol !== "https:") { signals.push("no-https"); riskScore += 30; }
159
+ const badTlds = [".tk", ".ml", ".ga", ".cf", ".gq", ".xyz", ".top", ".click", ".loan"];
160
+ if (badTlds.some((t) => parsed.hostname.endsWith(t))) { signals.push("suspicious-tld"); riskScore += 20; }
161
+ if (parsed.hostname.match(/^\d+\.\d+\.\d+\.\d+$/)) { signals.push("ip-address"); riskScore += 15; }
162
+ if (url.length > 200) { signals.push("long-url"); riskScore += 10; }
163
+ if (parsed.hostname.split(".").length > 5) { signals.push("deep-subdomains"); riskScore += 15; }
164
+
165
+ const verdict = riskScore >= 50 ? "dangerous" : riskScore >= 25 ? "suspicious" : "safe";
166
+ return { verdict, risk_score: Math.max(0, Math.min(100, riskScore)), signals };
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // MCP Tools
171
+ // ---------------------------------------------------------------------------
172
+
173
+ server.tool(
174
+ "web_investigate",
175
+ "Complete website investigation — DNS, SSL, tech stack, metadata, link safety. One call gives you everything about a website. Use this when you need to understand, verify, or analyze any website.",
176
+ { url: z.string().describe("Full URL to investigate (e.g. 'https://example.com')") },
177
+ async ({ url }) => {
178
+ const domain = new URL(url).hostname;
179
+
180
+ const [dnsResult, sslResult, techResult, metaResult] = await Promise.allSettled([
181
+ dnsLookup(domain),
182
+ sslCheck(domain),
183
+ techStack(url),
184
+ pageMeta(url),
185
+ ]);
186
+
187
+ const dnsData = dnsResult.status === "fulfilled" ? dnsResult.value : null;
188
+ const sslData = sslResult.status === "fulfilled" ? sslResult.value : null;
189
+ const techData = techResult.status === "fulfilled" ? techResult.value : null;
190
+ const metaData = metaResult.status === "fulfilled" ? metaResult.value : null;
191
+ const safety = linkSafety(url);
192
+
193
+ // Trust scoring
194
+ let trust = 50;
195
+ const flags = [];
196
+ if (safety.verdict === "dangerous") { trust -= 30; flags.push("Dangerous URL signals"); }
197
+ else if (safety.verdict === "safe") trust += 10;
198
+ if (sslData?.is_valid) trust += 10; else if (sslData && !sslData.is_valid) { trust -= 20; flags.push("Invalid SSL"); }
199
+ if (sslData?.is_expiring_soon) flags.push(`SSL expires in ${sslData.days_remaining}d`);
200
+ if (dnsData?.A?.length === 0) { trust -= 15; flags.push("No DNS A records"); }
201
+ if (dnsData?.mail_provider) trust += 5;
202
+ if (metaData?.title) trust += 5; else flags.push("No page title");
203
+ trust = Math.max(0, Math.min(100, trust));
204
+
205
+ const verdict = trust >= 70 ? "trustworthy" : trust >= 40 ? "questionable" : "risky";
206
+
207
+ const report = `## ${domain} — ${verdict} (${trust}/100)
208
+ ${flags.length > 0 ? `**Flags:** ${flags.join(", ")}\n` : ""}
209
+ ### DNS
210
+ - IP: ${dnsData?.A?.[0] || "unknown"}
211
+ - Nameservers: ${dnsData?.NS?.join(", ") || "unknown"}
212
+ - Mail: ${dnsData?.mail_provider || "unknown"}
213
+ - IPv6: ${dnsData?.has_ipv6 ? "yes" : "no"}
214
+
215
+ ### SSL
216
+ - Issuer: ${sslData?.issuer || "unknown"}
217
+ - Expires: ${sslData?.days_remaining ?? "?"}d (${sslData?.is_valid ? "valid" : "INVALID"})
218
+ - SANs: ${sslData?.san?.slice(0, 5).join(", ") || "unknown"}
219
+
220
+ ### Technology
221
+ ${techData?.technologies?.map((t) => `- ${t.name} (${t.category})`).join("\n") || "- Unknown"}
222
+
223
+ ### Page
224
+ - Title: ${metaData?.title || "none"}
225
+ - Description: ${metaData?.description?.slice(0, 100) || "none"}
226
+ - Language: ${metaData?.language || "unknown"}
227
+
228
+ ### Safety
229
+ - Verdict: ${safety.verdict} (risk ${safety.risk_score}/100)
230
+ ${safety.signals.length > 0 ? safety.signals.map((s) => `- ${s}`).join("\n") : "- No issues detected"}`;
231
+
232
+ return { content: [{ type: "text", text: report }] };
233
+ },
234
+ );
235
+
236
+ server.tool(
237
+ "web_dns",
238
+ "Get DNS records for any domain — A, AAAA, MX, NS, TXT, CNAME. Also detects mail provider (Google Workspace, Microsoft 365, etc). Use when you need to check domain configuration or troubleshoot DNS issues.",
239
+ { domain: z.string().describe("Domain name (e.g. 'example.com')") },
240
+ async ({ domain }) => {
241
+ const result = await dnsLookup(domain);
242
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
243
+ },
244
+ );
245
+
246
+ server.tool(
247
+ "web_ssl",
248
+ "Check SSL certificate for any domain — issuer, expiry, validity, SANs. Use when you need to verify HTTPS configuration or check if a certificate is expiring.",
249
+ { domain: z.string().describe("Domain name (e.g. 'example.com')") },
250
+ async ({ domain }) => {
251
+ const result = await sslCheck(domain);
252
+ const text = `${domain}: ${result.issuer}, ${result.days_remaining}d remaining, ${result.is_valid ? "valid" : "INVALID"}${result.is_expiring_soon ? " ⚠ EXPIRING SOON" : ""}\nSANs: ${result.san.join(", ")}`;
253
+ return { content: [{ type: "text", text }] };
254
+ },
255
+ );
256
+
257
+ server.tool(
258
+ "web_tech_stack",
259
+ "Detect technologies used by any website — frameworks, CMS, analytics, CDN, hosting, CSS, JS libraries, security headers. Use when you need to understand what a website is built with.",
260
+ { url: z.string().describe("Full URL (e.g. 'https://example.com')") },
261
+ async ({ url }) => {
262
+ const result = await techStack(url);
263
+ const lines = result.technologies.map((t) => `- ${t.name} (${t.category})`);
264
+ if (Object.keys(result.security_headers).length > 0) {
265
+ lines.push("", "Security headers:", ...Object.entries(result.security_headers).map(([k, v]) => `- ${k}: ${v}`));
266
+ }
267
+ return { content: [{ type: "text", text: lines.join("\n") }] };
268
+ },
269
+ );
270
+
271
+ server.tool(
272
+ "web_meta",
273
+ "Extract page metadata — title, description, Open Graph tags, Twitter cards, canonical URL, language. Use for SEO analysis or understanding page content without rendering.",
274
+ { url: z.string().describe("Full URL (e.g. 'https://example.com')") },
275
+ async ({ url }) => {
276
+ const result = await pageMeta(url);
277
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
278
+ },
279
+ );
280
+
281
+ server.tool(
282
+ "web_link_safety",
283
+ "Check if a URL is safe — detects suspicious TLDs, missing HTTPS, IP addresses, unusual patterns. Returns safe/suspicious/dangerous verdict. Use before recommending links to users.",
284
+ { url: z.string().describe("URL to check (e.g. 'https://example.com')") },
285
+ async ({ url }) => {
286
+ const result = linkSafety(url);
287
+ const text = `${url}: ${result.verdict} (risk ${result.risk_score}/100)${result.signals.length > 0 ? "\nSignals: " + result.signals.join(", ") : ""}`;
288
+ return { content: [{ type: "text", text }] };
289
+ },
290
+ );
291
+
292
+ // Start
293
+ const transport = new StdioServerTransport();
294
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "web-intel-mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for web intelligence — DNS, SSL, tech stack, metadata, link safety. One tool to investigate any website. No API keys needed.",
5
+ "type": "module",
6
+ "bin": {
7
+ "web-intel-mcp": "./index.js"
8
+ },
9
+ "files": ["index.js", "README.md"],
10
+ "keywords": ["mcp", "mcp-server", "web-intel", "dns", "ssl", "tech-stack", "security", "claude-code", "model-context-protocol"],
11
+ "author": "Tedysek01",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/Tedysek01/web-intel-mcp"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.12.1"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ }
23
+ }