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
JavaScript
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);
});