#!/usr/bin/env node /** * BurpSuite MCP Stdio Bridge * * Bridges the custom HTTP API (port 9876) to standard MCP JSON-RPC 2.0 stdio protocol. * This allows any MCP client (Claude Code, Kiro, Cursor, Cline, etc.) to use all 78 Burp tools. * * Cross-platform: Works on Windows, Linux (Kali), macOS. * * Usage in MCP config (all platforms): * { "command": "node", "args": ["/mcp-bridge.js"] } * * Environment variables: * BURP_MCP_HOST - Burp HTTP API host (default: 127.0.0.1) * BURP_MCP_PORT - Burp HTTP API port (default: 9876) */ const http = require('http'); const readline = require('readline'); const fs = require('fs'); const os = require('os'); const path = require('path'); const BURP_HOST = process.env.BURP_MCP_HOST || '127.0.0.1'; const BURP_PORT = parseInt(process.env.BURP_MCP_PORT || '9876', 10); function resolveToken() { if (process.env.BURP_MCP_TOKEN) return process.env.BURP_MCP_TOKEN; try { const tokenFile = path.join(os.homedir(), '.burp-mcp-token'); if (fs.existsSync(tokenFile)) return fs.readFileSync(tokenFile, 'utf8').trim(); } catch (e) { /* ignore */ } return null; } const BURP_TOKEN = resolveToken(); const AUTH_HEADERS = BURP_TOKEN ? { 'Authorization': `Bearer ${BURP_TOKEN}` } : {}; let TOOLS = null; let toolsRequest = null; async function fetchTools() { return new Promise((resolve, reject) => { const req = http.request({ hostname: BURP_HOST, port: BURP_PORT, path: '/tools', method: 'GET', headers: AUTH_HEADERS }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode !== 200) { reject(new Error(`Cannot fetch tools: HTTP ${res.statusCode} ${data.slice(0, 200)}`)); return; } try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on('error', reject); req.end(); }); } function disconnectedResponse(id) { return { jsonrpc: '2.0', id, error: { code: -32000, message: `Burp MCP not connected at ${BURP_HOST}:${BURP_PORT}. Start Burp with the "MCP Full Control" extension loaded, then retry.` } }; } async function ensureTools() { if (TOOLS) return TOOLS; if (!toolsRequest) { toolsRequest = fetchTools() .then(tools => { TOOLS = tools; process.stderr.write(`[burp-mcp-bridge] Connected to Burp. ${TOOLS.length} tools available.\n`); return TOOLS; }) .finally(() => { toolsRequest = null; }); } return toolsRequest; } async function callTool(toolName, params) { return new Promise((resolve, reject) => { const body = JSON.stringify({ tool: toolName, params: params || {} }); const req = http.request({ hostname: BURP_HOST, port: BURP_PORT, path: '/', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), ...AUTH_HEADERS } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode === 403) { reject(new Error('Burp MCP rejected the request: missing or invalid BURP_MCP_TOKEN. Set BURP_MCP_TOKEN to match the token in ~/.burp-mcp-token.')); return; } try { resolve(JSON.parse(data)); } catch (e) { resolve({ error: data }); } }); }); req.on('error', (err) => { reject(new Error( `Cannot reach Burp MCP at ${BURP_HOST}:${BURP_PORT} (${err.code || err.message}). ` + `Ensure Burp Suite is running with the "MCP Full Control" extension loaded. ` + `If the port differs, set BURP_MCP_PORT and -Dburp.mcp.port= on Burp.` )); }); req.write(body); req.end(); }); } function buildToolDefinitions(toolNames) { return toolNames.map(name => ({ name: `burp_${name}`, description: getToolDescription(name), inputSchema: { type: 'object', properties: getToolParams(name), } })); } function getToolDescription(name) { const descriptions = { proxy_history: 'Get Burp proxy history with optional filtering by URL, method, status code', proxy_detail: 'Get full request/response details for a specific proxy history item by index', proxy_websocket: 'Get WebSocket message history', proxy_clear: 'Clear proxy history', proxy_history_filtered: 'Filter proxy history by annotation color or notes', send_request: 'Send an HTTP request through Burp and get the response', send_to_repeater: 'Send a raw request to Burp Repeater tab', repeater_send: 'Send a request and get response (like Repeater)', repeater_modify_send: 'Modify headers/body of a request then send it', send_to_intruder: 'Send a request to Burp Intruder', intruder_attack: 'Run a numeric range brute force attack (synchronous)', intruder_attack_async: 'Run a multi-threaded numeric range brute force attack', intruder_attack_wordlist: 'Run a wordlist-based attack', intruder_pitchfork: 'Run a Pitchfork attack (parallel multi-param)', intruder_cluster_bomb: 'Run a Cluster Bomb attack (cartesian product multi-param)', intruder_battering_ram: 'Run a Battering Ram attack (same payload all positions)', intruder_with_options: 'Run attack with advanced options (throttle, encoding, grep, timing)', sitemap: 'Get site map entries with optional URL prefix filter', target_info: 'Get target information (hosts, technologies detected)', intercept_toggle: 'Enable or disable proxy intercept', encode: 'Encode a string (base64, url, hex)', decode: 'Decode a string (base64, url)', convert_request: 'Convert HTTP request method (e.g. GET to POST)', export_request: 'Export a request as curl command', generate_csrf_poc: 'Generate a CSRF proof-of-concept HTML page', extract_from_response: 'Extract data from a response using regex', payload_process: 'Process a payload (hash, encode, reverse, etc.)', scan: 'Start a vulnerability scan', scan_active: 'Start active scan on a specific request', scan_results: 'Get scan results (discovered vulnerabilities)', scan_issue_detail: 'Get detailed information about a specific scan issue', crawl: 'Start crawling a URL (adds to scope)', get_scope: 'Check if a URL is in Burp scope', add_to_scope: 'Add a URL to Burp scope', remove_from_scope: 'Remove a URL from Burp scope', collaborator_generate: 'Generate Burp Collaborator payloads for OOB testing', collaborator_poll: 'Poll for Collaborator interactions (DNS/HTTP callbacks)', search_history: 'Search proxy history with regex (in URL, request, or response)', highlight: 'Highlight a proxy history item with a color', annotate: 'Add a note/annotation to a proxy history item', compare: 'Compare two proxy history responses (diff)', export_config: 'Export Burp project configuration as JSON', import_config: 'Import Burp project configuration from JSON', set_upstream_proxy: 'Set upstream proxy (SOCKS/HTTP) for all Burp traffic', set_dns_override: 'Override DNS resolution for a hostname', set_http2: 'Enable or disable HTTP/2', cookie_jar: 'View cookies in Burp cookie jar (with optional domain filter)', token_analysis: 'Analyze token entropy and randomness', sequencer: 'Analyze a batch of tokens for randomness quality', save_project: 'Save the current Burp project', burp_version: 'Get Burp Suite version information', add_issue: 'Manually add a vulnerability issue to the site map', register_http_handler: 'Register an auto-modify rule for HTTP requests (add header or replace text)', remove_http_handler: 'Remove/clear HTTP handler rules', register_proxy_rule: 'Register a proxy intercept rule (intercept URLs containing a string)', remove_proxy_rule: 'Remove/clear proxy intercept rules', extensions_list: 'Get information about loaded extensions', log: 'Write a message to Burp extension output log', audit_log: 'View audit log entries', privacy_mode: 'Set privacy mode (strict/off)', scope_gate: 'Enable/disable scope gate', inline_fuzzer: 'FUZZ marker fuzzing', race_condition: 'Race condition testing', access_control_sweep: 'Test different auth levels', injection_probe: 'SQLi/SSTI/LFI probe', jwt_attack: 'JWT attacks (alg:none)', jwt_decode: 'Decode JWT tokens', session_remove_rule: 'Remove session rule', session_list_rules: 'List session rules', session_create_rule: 'Create session handling rule', passive_intel: 'Extract secrets from proxy history', websocket_list: 'List active WebSockets', websocket_close: 'Close WebSocket', websocket_send_binary: 'Send binary on WebSocket', websocket_send_text: 'Send text on WebSocket', websocket_create: 'Create WebSocket connection', send_request_parallel: 'Send parallel HTTP requests', cookie_jar_set: 'Set cookie in Burp cookie jar', }; return descriptions[name] || `Burp Suite tool: ${name}`; } function getToolParams(name) { // Common parameter schemas const schemas = { proxy_history: { limit: {type:'number',description:'Max items (default 100)'}, offset: {type:'number'}, url_filter: {type:'string'}, method_filter: {type:'string'}, status_filter: {type:'number'} }, proxy_detail: { index: {type:'number',description:'History item index'} }, proxy_websocket: { limit: {type:'number'} }, proxy_history_filtered: { has_notes: {type:'string'}, color: {type:'string'}, limit: {type:'number'} }, send_request: { method: {type:'string'}, url: {type:'string',description:'Full URL'}, body: {type:'string'}, headers: {type:'object'} }, repeater_send: { request: {type:'string',description:'Raw HTTP request'}, host: {type:'string'}, port: {type:'number'}, https: {type:'boolean'} }, repeater_modify_send: { request: {type:'string'}, host: {type:'string'}, port: {type:'number'}, https: {type:'boolean'}, replace_header: {type:'object'}, add_header: {type:'object'}, replace_body: {type:'string'} }, send_to_repeater: { request: {type:'string'}, tab_name: {type:'string'} }, send_to_intruder: { request: {type:'string'} }, intruder_attack: { url_template: {type:'string',description:'URL with @@ placeholder'}, from: {type:'number'}, to: {type:'number'}, pad_digits: {type:'number'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, success_contains: {type:'string'} }, intruder_attack_async: { url_template: {type:'string'}, from: {type:'number'}, to: {type:'number'}, pad_digits: {type:'number'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, threads: {type:'number'} }, intruder_attack_wordlist: { url_template: {type:'string'}, wordlist: {type:'array',items:{type:'string'}}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, body_template: {type:'string'} }, intruder_pitchfork: { url_template: {type:'string'}, placeholders: {type:'object'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'} }, intruder_cluster_bomb: { url_template: {type:'string'}, placeholders: {type:'object'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, max_requests: {type:'number'} }, intruder_battering_ram: { url_template: {type:'string'}, wordlist: {type:'array',items:{type:'string'}}, placeholder: {type:'string'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'} }, intruder_with_options: { url_template: {type:'string'}, from: {type:'number'}, to: {type:'number'}, pad_digits: {type:'number'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, throttle_ms: {type:'number'}, payload_prefix: {type:'string'}, payload_suffix: {type:'string'}, payload_encoding: {type:'string'}, grep_extract: {type:'string'}, record_time: {type:'boolean'} }, sitemap: { url_prefix: {type:'string'}, limit: {type:'number'} }, target_info: { url: {type:'string'} }, intercept_toggle: { enable: {type:'boolean'} }, encode: { input: {type:'string'}, type: {type:'string',description:'base64, url, or hex'} }, decode: { input: {type:'string'}, type: {type:'string',description:'base64 or url'} }, convert_request: { request: {type:'string'}, convert_to: {type:'string'} }, export_request: { request: {type:'string'}, host: {type:'string'}, format: {type:'string',description:'curl or python'}, https: {type:'boolean'} }, generate_csrf_poc: { request: {type:'string'}, host: {type:'string'}, https: {type:'boolean'} }, extract_from_response: { index: {type:'number'}, regex: {type:'string'} }, payload_process: { input: {type:'string'}, operation: {type:'string',description:'base64_encode/decode, url_encode/decode, md5, sha1, sha256, hex_encode, lowercase, uppercase, reverse, length'} }, scan_active: { request: {type:'string'}, host: {type:'string'}, port: {type:'number'}, https: {type:'boolean'} }, scan_results: { limit: {type:'number'} }, scan_issue_detail: { index: {type:'number'} }, crawl: { url: {type:'string'} }, get_scope: { url: {type:'string'} }, add_to_scope: { url: {type:'string'} }, remove_from_scope: { url: {type:'string'} }, collaborator_generate: { count: {type:'number'} }, search_history: { regex: {type:'string'}, search_in: {type:'string',description:'url, request, or response'}, limit: {type:'number'} }, highlight: { index: {type:'number'}, color: {type:'string'} }, annotate: { index: {type:'number'}, note: {type:'string'} }, compare: { index1: {type:'number'}, index2: {type:'number'} }, import_config: { config: {type:'string'} }, set_upstream_proxy: { proxy_host: {type:'string'}, proxy_port: {type:'number'}, type: {type:'string'} }, set_dns_override: { hostname: {type:'string'}, ip: {type:'string'} }, set_http2: { enable: {type:'boolean'} }, cookie_jar: { limit: {type:'number'}, domain: {type:'string'} }, token_analysis: { tokens: {type:'array',items:{type:'string'}} }, sequencer: { tokens: {type:'array',items:{type:'string'}} }, add_issue: { name: {type:'string'}, url: {type:'string'}, detail: {type:'string'}, severity: {type:'string'}, confidence: {type:'string'} }, register_http_handler: { header_name: {type:'string'}, header_value: {type:'string'}, match: {type:'string'}, replace: {type:'string'} }, register_proxy_rule: { url_contains: {type:'string'} }, log: { message: {type:'string'}, level: {type:'string'} }, audit_log: { limit: {type:'number'} }, privacy_mode: { mode: {type:'string'} }, scope_gate: { action: {type:'string'} }, inline_fuzzer: { template: {type:'string'}, host: {type:'string'}, wordlist: {type:'array'} }, race_condition: { request: {type:'string'}, host: {type:'string'}, count: {type:'number'} }, access_control_sweep: { request: {type:'string'}, host: {type:'string'}, auth_headers: {type:'string'} }, injection_probe: { url: {type:'string'}, param: {type:'string'}, type: {type:'string'} }, jwt_attack: { token: {type:'string'}, attack: {type:'string'} }, jwt_decode: { token: {type:'string'} }, session_remove_rule: {}, session_list_rules: {}, session_create_rule: { find: {type:'string'}, replace: {type:'string'} }, passive_intel: { limit: {type:'number'} }, websocket_list: {}, websocket_close: { id: {type:'string'} }, websocket_send_binary: { id: {type:'string'}, data: {type:'string'} }, websocket_send_text: { id: {type:'string'}, text: {type:'string'} }, websocket_create: { host: {type:'string'}, port: {type:'number'} }, send_request_parallel: { requests: {type:'array'} }, cookie_jar_set: { url: {type:'string'}, name: {type:'string'}, value: {type:'string'} }, }; return schemas[name] || {}; } async function handleRequest(msg) { const { method, id, params } = msg; switch (method) { case 'initialize': return { jsonrpc: '2.0', id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'burpsuite-mcp', version: '2.0.0' } }}; case 'notifications/initialized': return null; // No response needed case 'tools/list': try { const tools = await ensureTools(); return { jsonrpc: '2.0', id, result: { tools: buildToolDefinitions(tools) } }; } catch (err) { return disconnectedResponse(id); } case 'tools/call': { try { await ensureTools(); } catch (err) { return disconnectedResponse(id); } if (!params || typeof params.name !== 'string') { return { jsonrpc: '2.0', id, error: { code: -32602, message: 'tools/call requires params.name' } }; } const toolName = params.name.replace(/^burp_/, ''); const toolArgs = params.arguments || {}; try { const result = await callTool(toolName, toolArgs); return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }; } catch (err) { TOOLS = null; return { jsonrpc: '2.0', id, error: { code: -1, message: err.message || 'Tool call failed' } }; } } default: return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }; } } // Main stdio loop async function main() { try { await ensureTools(); } catch (e) { process.stderr.write(`[burp-mcp-bridge] WARNING: Cannot connect to Burp at ${BURP_HOST}:${BURP_PORT}. Start Burp first.\n`); } const rl = readline.createInterface({ input: process.stdin, terminal: false }); let pending = 0; let stdinClosed = false; rl.on('line', (line) => { if (!line.trim()) return; let msg; try { msg = JSON.parse(line); } catch (e) { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }) + '\n'); return; } pending++; handleRequest(msg) .then(response => { if (response) process.stdout.write(JSON.stringify(response) + '\n'); }) .catch(err => { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id ?? null, error: { code: -1, message: err.message || 'Handler error' } }) + '\n'); }) .finally(() => { pending--; if (stdinClosed && pending === 0) process.exit(0); }); }); rl.on('close', () => { stdinClosed = true; if (pending === 0) process.exit(0); }); } main();