UNPKG

domainlooker

Version:

A fast CLI and MCP server for inspecting domains: WHOIS/RDAP, DNS, SSL, ports, subdomains, with CSV/JSON export.

128 lines 6.39 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { DomainCollector } from '../core/collector.js'; import { assertPublicDomain } from '../core/target-guard.js'; import { JsonExportService } from '../services/json-export.js'; import { VERSION } from '../version.js'; const CACHE_TTL_MS = 5 * 60 * 1000; // domain data changes slowly; 5 minutes keeps repeat calls instant const CACHE_MAX_ENTRIES = 500; /** Transform a raw DomainInfo into the structured export schema (same shape as --export-json). */ function toAnalysis(info, options) { const exporter = new JsonExportService({ quick: options.quick, subdomains: options.subdomains }); exporter.addDomain(info); return exporter.getStructuredData().data[0]; } function textResult(value) { return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] }; } /** Render a granular lookup result, making "no data" explicit rather than a bare null. */ function lookupResult(domain, value) { return textResult(value ?? { domain, found: false }); } /** An MCP error result carrying a caller-safe message (e.g. a blocked target). */ function guardError(error) { return { content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true, }; } export function createServer(deps = {}) { const collector = deps.collector ?? new DomainCollector(); const assertTarget = deps.assertTarget ?? assertPublicDomain; const cache = new Map(); const cacheWrite = (key, value) => { const now = Date.now(); // Drop expired entries, then bound total size (oldest-first) so a long-lived // server queried across many domains cannot grow the cache without limit. for (const [k, entry] of cache) { if (entry.expires <= now) cache.delete(k); } while (cache.size >= CACHE_MAX_ENTRIES) { const oldest = cache.keys().next().value; if (oldest === undefined) break; cache.delete(oldest); } cache.set(key, { expires: now + CACHE_TTL_MS, value }); }; const server = new McpServer({ name: 'domainlooker', version: VERSION }); const domainArg = z.string().min(1).describe('The domain to inspect, e.g. "example.com"'); // Primary tool: everything about a domain in one structured payload. server.registerTool('inspect_domain', { title: 'Inspect domain', description: 'Fetch full intelligence for a domain (WHOIS/RDAP registration, DNS records, ' + 'SSL certificate, open ports, and rule-based advisories) as one structured JSON object. ' + 'Results are cached for a few minutes, so repeated calls are instant.', inputSchema: { domain: domainArg, includePorts: z.boolean().optional().describe('Scan common ports (default true).'), includeSubdomains: z.boolean().optional().describe('Discover subdomains — slower (default false).'), }, }, async ({ domain, includePorts, includeSubdomains }) => { try { await assertTarget(domain); } catch (error) { return guardError(error); } const options = { quick: includePorts === false, subdomains: includeSubdomains === true, }; const key = `${domain}|ports=${options.quick ? 0 : 1}|subs=${options.subdomains ? 1 : 0}`; const cached = cache.get(key); if (cached && cached.expires > Date.now()) { return textResult(cached.value); } const info = await collector.collect(domain, options); const analysis = toAnalysis(info, options); cacheWrite(key, analysis); return textResult(analysis); }); // Granular tools mirror the CLI subcommands, for agents that need just one aspect fast. // Every tool runs the target through `assertPublicDomain` first, so an agent // cannot turn the server into a scanner of internal/loopback/metadata hosts. const granularTool = (name, title, description, run) => { server.registerTool(name, { title, description, inputSchema: { domain: domainArg } }, async ({ domain }) => { try { await assertTarget(domain); } catch (error) { return guardError(error); } return lookupResult(domain, await run(domain)); }); }; granularTool('whois_lookup', 'WHOIS lookup', 'Registration data (registrar, dates, name servers, status) via RDAP with WHOIS fallback.', d => collector.whois(d)); granularTool('dns_records', 'DNS records', 'A, AAAA, MX, NS, TXT, and SOA records for a domain.', d => collector.dns(d)); granularTool('ssl_certificate', 'SSL certificate', 'TLS certificate details and days until expiry.', d => collector.ssl(d)); granularTool('scan_ports', 'Scan ports', 'Scan common TCP ports and identify the services behind the open ones.', d => collector.ports(d)); granularTool('find_subdomains', 'Find subdomains', 'Discover subdomains via certificate transparency and common-name checks.', d => collector.subdomains(d)); server.registerTool('dns_propagation', { title: 'DNS propagation', description: 'Check whether a DNS record has propagated across major public resolvers worldwide. ' + 'Returns per-resolver answers, the majority (expected) value, and an in-sync percentage.', inputSchema: { domain: domainArg, recordType: z.enum(['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT']).optional().describe('Record type (default A).'), }, }, async ({ domain, recordType }) => { try { await assertTarget(domain); } catch (error) { return guardError(error); } return lookupResult(domain, await collector.propagation(domain, recordType ?? 'A')); }); return server; } /** Start the MCP server over stdio. Only MCP protocol messages go to stdout. */ export async function startStdioServer() { const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); } //# sourceMappingURL=server.js.map