UNPKG

gc-katana-mcp

Version:

Model Context Protocol (MCP) server for Katana - Fast web crawler with JavaScript parsing capabilities

104 lines (103 loc) 4.27 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { spawn } from 'node:child_process'; // Get katana path from environment variable const katanaPath = process.env.KATANA_PATH; if (!katanaPath) { console.error("KATANA_PATH environment variable not set"); process.exit(1); } // Create server instance const server = new McpServer({ name: "katana", version: "1.0.0", }); function removeAnsiCodes(str) { // eslint-disable-next-line no-control-regex return str.replace(/\x1b\[[0-9;]*m/g, ''); } server.tool("do-katana", "Performs fast and configurable web crawling on the given target URLs, identifying endpoints, parameters, and JS-based links.", { target: z.array(z.string()).describe("List of target URLs (e.g., https://example.com) to scan for endpoints and JavaScript-based links."), exclude: z.array(z.string()).optional().describe("List of URLs or regex patterns to exclude from crawling."), depth: z.number().optional().describe("Maximum crawl depth (e.g., 3 for three levels deep)."), js_crawl: z.boolean().optional().describe("Enable crawling and endpoint extraction from JavaScript files."), jsluice: z.boolean().optional().describe("Enable JSluice parsing for deeper JavaScript-based link analysis (memory intensive)."), headers: z.array(z.string()).optional().describe("List of custom headers or cookies to include in requests (format: Header:Value)."), strategy: z.enum(["depth-first", "breadth-first"]).optional().describe("Crawling strategy to use: 'depth-first' or 'breadth-first' (default is depth-first)."), headless: z.boolean().optional().describe("Enable headless browser-based hybrid crawling (experimental)."), system_chrome: z.boolean().optional().describe("Use the locally installed Chrome browser instead of the built-in one."), show_brwoser: z.boolean().optional().describe("Show the browser window even in headless mode (for debugging/visual inspection)."), }, async ({ target, exclude, depth, js_crawl, jsluice, headers, strategy, headless, system_chrome, show_brwoser }) => { const katanaArgs = ["-u", target.join(","), "-silent"]; if (exclude && exclude.length > 0) { katanaArgs.push("-exclude", exclude.join(",")); } if (depth !== undefined) { katanaArgs.push("-d", depth.toString()); } if (js_crawl) { katanaArgs.push("-jc"); } if (jsluice) { katanaArgs.push("-jsl"); } if (headers && headers.length > 0) { for (const header of headers) { katanaArgs.push("-H", header); } } if (strategy) { katanaArgs.push("-strategy", strategy); } if (headless) { katanaArgs.push("-headless"); } if (system_chrome) { katanaArgs.push("-system-chrome"); } if (show_brwoser) { katanaArgs.push("-show-browser"); } let output = ""; const katana = spawn(katanaPath, katanaArgs); // Handle stdout katana.stdout.on('data', (data) => { output += data.toString(); }); // Handle stderr katana.stderr.on('data', (data) => { output += data.toString(); }); // Handle process completion return new Promise((resolve, reject) => { katana.on('close', (code) => { if (code === 0 || typeof code === "undefined") { output = removeAnsiCodes(output); resolve({ content: [{ type: "text", text: output }] }); } else { reject(new Error(`katana exited with code ${code}`)); } }); katana.on('error', (error) => { reject(new Error(`Error to start katana: ${error.message}`)); }); }); }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Katana MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });