mcp-diagnostics 1.0.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import dns from "dns/promises";
6
+ import tls from "tls";
7
+ import https from "https";
8
+ import http from "http";
9
+ import { URL } from "url";
10
+ const L = process.env.MCP_DIAG_LICENSE_KEY || "";
11
+ const isPro = () => L.startsWith("diag_pro_") && L.length > 20;
12
+ const gate = () => isPro() ? null : "🔒 Pro feature. Get license: https://mcp-diagnostics.netlify.app";
13
+ // === DNS Lookup ===
14
+ async function dnsLookup(domain) {
15
+ const clean = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "");
16
+ const [a, aaaa, mx, txt, ns, cname, soa] = await Promise.allSettled([
17
+ dns.resolve4(clean),
18
+ dns.resolve6(clean),
19
+ dns.resolveMx(clean),
20
+ dns.resolveTxt(clean),
21
+ dns.resolveNs(clean),
22
+ dns.resolveCname(clean),
23
+ dns.resolveSoa(clean),
24
+ ]);
25
+ return {
26
+ domain: clean,
27
+ A: a.status === "fulfilled" ? a.value : [],
28
+ AAAA: aaaa.status === "fulfilled" ? aaaa.value : [],
29
+ MX: mx.status === "fulfilled" ? mx.value.sort((a, b) => a.priority - b.priority) : [],
30
+ TXT: txt.status === "fulfilled" ? txt.value.map(t => t.join("")) : [],
31
+ NS: ns.status === "fulfilled" ? ns.value : [],
32
+ CNAME: cname.status === "fulfilled" ? cname.value : [],
33
+ SOA: soa.status === "fulfilled" ? soa.value : null,
34
+ };
35
+ }
36
+ // === SSL Check ===
37
+ function sslCheck(hostname) {
38
+ const clean = hostname.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "");
39
+ return new Promise((resolve, reject) => {
40
+ const socket = tls.connect(443, clean, { servername: clean }, () => {
41
+ const cert = socket.getPeerCertificate();
42
+ const authorized = socket.authorized;
43
+ socket.end();
44
+ const validFrom = new Date(cert.valid_from);
45
+ const validTo = new Date(cert.valid_to);
46
+ const now = new Date();
47
+ const daysRemaining = Math.floor((validTo.getTime() - now.getTime()) / 86400000);
48
+ resolve({
49
+ hostname: clean,
50
+ valid: authorized,
51
+ issuer: cert.issuer,
52
+ subject: cert.subject,
53
+ validFrom: validFrom.toISOString(),
54
+ validTo: validTo.toISOString(),
55
+ daysRemaining,
56
+ serialNumber: cert.serialNumber,
57
+ fingerprint: cert.fingerprint256,
58
+ protocol: socket.getProtocol(),
59
+ warning: daysRemaining < 30 ? `⚠️ Certificate expires in ${daysRemaining} days!` : null,
60
+ });
61
+ });
62
+ socket.on("error", (e) => reject(new Error(`SSL error: ${e.message}`)));
63
+ socket.setTimeout(8000, () => { socket.destroy(); reject(new Error("SSL connection timeout")); });
64
+ });
65
+ }
66
+ // === HTTP Headers ===
67
+ async function httpHeaders(url) {
68
+ const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
69
+ return new Promise((resolve, reject) => {
70
+ const mod = parsed.protocol === "https:" ? https : http;
71
+ const req = mod.request(parsed, { method: "HEAD", timeout: 8000 }, (res) => {
72
+ resolve({
73
+ url: parsed.href,
74
+ statusCode: res.statusCode,
75
+ statusMessage: res.statusMessage,
76
+ headers: res.headers,
77
+ server: res.headers["server"] || "unknown",
78
+ poweredBy: res.headers["x-powered-by"] || null,
79
+ contentType: res.headers["content-type"] || null,
80
+ cacheControl: res.headers["cache-control"] || null,
81
+ });
82
+ });
83
+ req.on("error", (e) => reject(new Error(`HTTP error: ${e.message}`)));
84
+ req.on("timeout", () => { req.destroy(); reject(new Error("Request timeout")); });
85
+ req.end();
86
+ });
87
+ }
88
+ // === Security Headers Check ===
89
+ function analyzeSecurityHeaders(headers) {
90
+ const checks = [
91
+ { name: "Strict-Transport-Security", key: "strict-transport-security", critical: true },
92
+ { name: "Content-Security-Policy", key: "content-security-policy", critical: true },
93
+ { name: "X-Content-Type-Options", key: "x-content-type-options", critical: false },
94
+ { name: "X-Frame-Options", key: "x-frame-options", critical: false },
95
+ { name: "X-XSS-Protection", key: "x-xss-protection", critical: false },
96
+ { name: "Referrer-Policy", key: "referrer-policy", critical: false },
97
+ { name: "Permissions-Policy", key: "permissions-policy", critical: false },
98
+ { name: "Cross-Origin-Opener-Policy", key: "cross-origin-opener-policy", critical: false },
99
+ { name: "Cross-Origin-Resource-Policy", key: "cross-origin-resource-policy", critical: false },
100
+ ];
101
+ let score = 0;
102
+ const results = checks.map(check => {
103
+ const value = headers[check.key];
104
+ const present = !!value;
105
+ if (present)
106
+ score += check.critical ? 15 : 10;
107
+ return {
108
+ header: check.name,
109
+ present,
110
+ value: present ? String(value).substring(0, 100) : "❌ Missing",
111
+ critical: check.critical,
112
+ };
113
+ });
114
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
115
+ return { score, grade, maxScore: 100, results };
116
+ }
117
+ // === Ping / Uptime Check ===
118
+ async function pingCheck(url) {
119
+ const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
120
+ const start = Date.now();
121
+ return new Promise((resolve, reject) => {
122
+ const mod = parsed.protocol === "https:" ? https : http;
123
+ const req = mod.request(parsed, { method: "GET", timeout: 10000 }, (res) => {
124
+ let size = 0;
125
+ res.on("data", (chunk) => { size += chunk.length; });
126
+ res.on("end", () => {
127
+ const responseTime = Date.now() - start;
128
+ resolve({
129
+ url: parsed.href,
130
+ status: res.statusCode,
131
+ responseTime: `${responseTime}ms`,
132
+ responseTimeMs: responseTime,
133
+ contentLength: size,
134
+ rating: responseTime < 200 ? "🟢 Excellent" : responseTime < 500 ? "🟡 Good" : responseTime < 1000 ? "🟠 Slow" : "🔴 Very Slow",
135
+ });
136
+ });
137
+ });
138
+ req.on("error", (e) => resolve({ url: parsed.href, status: "error", error: e.message, rating: "🔴 Down" }));
139
+ req.on("timeout", () => { req.destroy(); resolve({ url: parsed.href, status: "timeout", rating: "🔴 Timeout" }); });
140
+ req.end();
141
+ });
142
+ }
143
+ // === WHOIS (via RDAP - free public API) ===
144
+ async function whoisLookup(domain) {
145
+ const clean = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
146
+ const res = await fetch(`https://rdap.org/domain/${clean}`, {
147
+ headers: { "Accept": "application/rdap+json" },
148
+ signal: AbortSignal.timeout(8000),
149
+ });
150
+ if (!res.ok)
151
+ throw new Error(`RDAP lookup failed: ${res.status}`);
152
+ const data = await res.json();
153
+ const events = (data.events || []).reduce((acc, e) => {
154
+ acc[e.eventAction] = e.eventDate;
155
+ return acc;
156
+ }, {});
157
+ return {
158
+ domain: data.ldhName || clean,
159
+ status: data.status || [],
160
+ registrar: data.entities?.find((e) => e.roles?.includes("registrar"))?.vcardArray?.[1]?.find((v) => v[0] === "fn")?.[3] || "unknown",
161
+ created: events.registration || null,
162
+ updated: events["last changed"] || null,
163
+ expires: events.expiration || null,
164
+ nameservers: (data.nameservers || []).map((ns) => ns.ldhName),
165
+ };
166
+ }
167
+ // === Tech Stack Detection ===
168
+ function detectTechStack(headers) {
169
+ const server = String(headers["server"] || "").toLowerCase();
170
+ const powered = String(headers["x-powered-by"] || "").toLowerCase();
171
+ const via = String(headers["via"] || "").toLowerCase();
172
+ const all = Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\n").toLowerCase();
173
+ const detected = [];
174
+ if (server.includes("nginx"))
175
+ detected.push("Nginx");
176
+ if (server.includes("apache"))
177
+ detected.push("Apache");
178
+ if (server.includes("cloudflare"))
179
+ detected.push("Cloudflare");
180
+ if (server.includes("vercel"))
181
+ detected.push("Vercel");
182
+ if (powered.includes("express"))
183
+ detected.push("Express.js");
184
+ if (powered.includes("next"))
185
+ detected.push("Next.js");
186
+ if (powered.includes("php"))
187
+ detected.push("PHP");
188
+ if (powered.includes("asp.net"))
189
+ detected.push("ASP.NET");
190
+ if (all.includes("x-amz"))
191
+ detected.push("AWS");
192
+ if (all.includes("x-goog"))
193
+ detected.push("Google Cloud");
194
+ if (all.includes("x-azure"))
195
+ detected.push("Azure");
196
+ if (all.includes("netlify"))
197
+ detected.push("Netlify");
198
+ if (all.includes("fly-request"))
199
+ detected.push("Fly.io");
200
+ if (headers["x-shopify-stage"])
201
+ detected.push("Shopify");
202
+ if (headers["x-wp-total"])
203
+ detected.push("WordPress");
204
+ return detected.length > 0 ? detected : ["Unknown"];
205
+ }
206
+ // === MCP Server ===
207
+ const server = new McpServer({ name: "mcp-diagnostics", version: "1.0.0" });
208
+ // --- FREE TOOLS ---
209
+ server.tool("dns_lookup", "Look up DNS records for a domain. Returns A, AAAA, MX, TXT, NS, CNAME, SOA records. Free tool.", { domain: z.string().describe("Domain name (e.g., google.com, example.co.jp)") }, async ({ domain }) => {
210
+ try {
211
+ return { content: [{ type: "text", text: JSON.stringify(await dnsLookup(domain), null, 2) }] };
212
+ }
213
+ catch (e) {
214
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
215
+ }
216
+ });
217
+ server.tool("ssl_check", "Check SSL/TLS certificate for a domain. Shows issuer, expiry, days remaining, protocol. Warns if expiring soon. Free tool.", { hostname: z.string().describe("Domain name to check SSL (e.g., google.com)") }, async ({ hostname }) => {
218
+ try {
219
+ return { content: [{ type: "text", text: JSON.stringify(await sslCheck(hostname), null, 2) }] };
220
+ }
221
+ catch (e) {
222
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
223
+ }
224
+ });
225
+ server.tool("http_headers", "Fetch and analyze HTTP response headers. Shows server, caching, content type. Free tool.", { url: z.string().describe("URL to check (e.g., google.com or https://example.com)") }, async ({ url }) => {
226
+ try {
227
+ const result = await httpHeaders(url);
228
+ result.techStack = detectTechStack(result.headers);
229
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
230
+ }
231
+ catch (e) {
232
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
233
+ }
234
+ });
235
+ server.tool("ping", "Check if a website is up and measure response time. Free tool.", { url: z.string().describe("URL to ping") }, async ({ url }) => {
236
+ try {
237
+ return { content: [{ type: "text", text: JSON.stringify(await pingCheck(url), null, 2) }] };
238
+ }
239
+ catch (e) {
240
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
241
+ }
242
+ });
243
+ // --- PRO TOOLS ---
244
+ server.tool("security_scan", "🔒 [PRO] Full security headers audit with scoring (A-F grade). Checks HSTS, CSP, X-Frame-Options, and 6 more headers. Requires license.", { url: z.string().describe("URL to scan") }, async ({ url }) => {
245
+ const g = gate();
246
+ if (g)
247
+ return { content: [{ type: "text", text: g }] };
248
+ try {
249
+ const h = await httpHeaders(url);
250
+ const security = analyzeSecurityHeaders(h.headers);
251
+ return { content: [{ type: "text", text: JSON.stringify({
252
+ url: h.url, statusCode: h.statusCode, ...security,
253
+ recommendation: security.grade === "A" ? "Excellent security posture!" :
254
+ security.grade === "B" ? "Good, but some headers are missing." :
255
+ `Needs improvement. Add missing critical headers first.`
256
+ }, null, 2) }] };
257
+ }
258
+ catch (e) {
259
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
260
+ }
261
+ });
262
+ server.tool("whois", "🔒 [PRO] Domain WHOIS/RDAP lookup. Registration date, expiry, registrar, nameservers. Requires license.", { domain: z.string().describe("Domain to look up") }, async ({ domain }) => {
263
+ const g = gate();
264
+ if (g)
265
+ return { content: [{ type: "text", text: g }] };
266
+ try {
267
+ return { content: [{ type: "text", text: JSON.stringify(await whoisLookup(domain), null, 2) }] };
268
+ }
269
+ catch (e) {
270
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
271
+ }
272
+ });
273
+ server.tool("full_audit", "🔒 [PRO] Complete website audit: DNS + SSL + Headers + Security + Tech Stack + Performance in one call. Requires license.", { url: z.string().describe("URL to audit") }, async ({ url }) => {
274
+ const g = gate();
275
+ if (g)
276
+ return { content: [{ type: "text", text: g }] };
277
+ try {
278
+ const domain = url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
279
+ const [dnsResult, sslResult, headersResult, pingResult] = await Promise.allSettled([
280
+ dnsLookup(domain),
281
+ sslCheck(domain),
282
+ httpHeaders(url),
283
+ pingCheck(url),
284
+ ]);
285
+ const headers = headersResult.status === "fulfilled" ? headersResult.value.headers : {};
286
+ const security = analyzeSecurityHeaders(headers);
287
+ const techStack = detectTechStack(headers);
288
+ return { content: [{ type: "text", text: JSON.stringify({
289
+ url, domain,
290
+ dns: dnsResult.status === "fulfilled" ? dnsResult.value : { error: dnsResult.reason?.message },
291
+ ssl: sslResult.status === "fulfilled" ? sslResult.value : { error: sslResult.reason?.message },
292
+ http: headersResult.status === "fulfilled" ? { statusCode: headersResult.value.statusCode, server: headersResult.value.server } : { error: headersResult.reason?.message },
293
+ performance: pingResult.status === "fulfilled" ? pingResult.value : { error: pingResult.reason?.message },
294
+ security: { grade: security.grade, score: security.score },
295
+ techStack,
296
+ summary: `Security: ${security.grade} | SSL: ${sslResult.status === "fulfilled" ? sslResult.value.daysRemaining + " days" : "error"} | Response: ${pingResult.status === "fulfilled" ? pingResult.value.responseTime : "error"}`
297
+ }, null, 2) }] };
298
+ }
299
+ catch (e) {
300
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
301
+ }
302
+ });
303
+ server.tool("compare_sites", "🔒 [PRO] Compare two websites side by side: performance, security, SSL, tech stack. Requires license.", {
304
+ url1: z.string().describe("First URL"),
305
+ url2: z.string().describe("Second URL"),
306
+ }, async ({ url1, url2 }) => {
307
+ const g = gate();
308
+ if (g)
309
+ return { content: [{ type: "text", text: g }] };
310
+ try {
311
+ const [p1, p2, h1, h2, s1, s2] = await Promise.allSettled([
312
+ pingCheck(url1), pingCheck(url2),
313
+ httpHeaders(url1), httpHeaders(url2),
314
+ sslCheck(url1.replace(/^https?:\/\//, "").replace(/\/.*$/, "")),
315
+ sslCheck(url2.replace(/^https?:\/\//, "").replace(/\/.*$/, "")),
316
+ ]);
317
+ const sec1 = h1.status === "fulfilled" ? analyzeSecurityHeaders(h1.value.headers) : null;
318
+ const sec2 = h2.status === "fulfilled" ? analyzeSecurityHeaders(h2.value.headers) : null;
319
+ return { content: [{ type: "text", text: JSON.stringify({
320
+ comparison: {
321
+ [url1]: {
322
+ responseTime: p1.status === "fulfilled" ? p1.value.responseTime : "error",
323
+ securityGrade: sec1?.grade || "?",
324
+ sslDaysRemaining: s1.status === "fulfilled" ? s1.value.daysRemaining : "error",
325
+ techStack: h1.status === "fulfilled" ? detectTechStack(h1.value.headers) : [],
326
+ },
327
+ [url2]: {
328
+ responseTime: p2.status === "fulfilled" ? p2.value.responseTime : "error",
329
+ securityGrade: sec2?.grade || "?",
330
+ sslDaysRemaining: s2.status === "fulfilled" ? s2.value.daysRemaining : "error",
331
+ techStack: h2.status === "fulfilled" ? detectTechStack(h2.value.headers) : [],
332
+ },
333
+ },
334
+ winner: {
335
+ faster: p1.status === "fulfilled" && p2.status === "fulfilled"
336
+ ? (p1.value.responseTimeMs < p2.value.responseTimeMs ? url1 : url2)
337
+ : "unknown",
338
+ moreSecure: (sec1?.score || 0) > (sec2?.score || 0) ? url1 : (sec1?.score || 0) < (sec2?.score || 0) ? url2 : "tie",
339
+ }
340
+ }, null, 2) }] };
341
+ }
342
+ catch (e) {
343
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
344
+ }
345
+ });
346
+ // === Start ===
347
+ async function main() {
348
+ const transport = new StdioServerTransport();
349
+ await server.connect(transport);
350
+ console.error("mcp-diagnostics v1.0.0 running");
351
+ }
352
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "mcp-diagnostics",
3
+ "version": "1.0.0",
4
+ "description": "Website & Server Diagnostics MCP Server. DNS, SSL, HTTP headers, security scan, performance audit.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": { "mcp-diagnostics": "dist/index.js" },
8
+ "files": ["dist", "README.md"],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": ["mcp","diagnostics","dns","ssl","security","performance","devops","seo","lighthouse","website","audit","modelcontextprotocol","claude"],
15
+ "author": "HatoTools",
16
+ "license": "MIT",
17
+ "homepage": "https://github.com/kame6493-del/mcp-diagnostics",
18
+ "repository": { "type": "git", "url": "https://github.com/kame6493-del/mcp-diagnostics.git" },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.12.1",
21
+ "zod": "^3.24.4"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.15.3",
25
+ "typescript": "^5.8.3"
26
+ }
27
+ }