reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
# BurpSuite MCP Full Control Extension
通过 MCP 协议完整控制 BurpSuite 的所有核心功能。跨平台支持 Windows / Linux (Kali) / macOS。
## 快速开始
### 1. 编译扩展
**Windows**:
```cmd
cd burp-mcp-full
build.bat
```
**Linux / Kali / macOS**:
```bash
cd burp-mcp-full
chmod +x build.sh
./build.sh
```
构建脚本会自动:检测 JDK 21+、下载依赖(montoya-api 2025.5 / gson / nanohttpd)、编译、把扩展描述符(`META-INF/extensions/burp-extension.properties`)打入 jar、打包 fat jar。无需 Gradle。
输出:`build/libs/burp-mcp-full.jar`。
### 2. 加载到 Burp
```
Burp Suite → Extensions → Add → Java → 选择 build/libs/burp-mcp-full.jar
```
加载后在 Output 看到:
```
[MCP] Server started on http://127.0.0.1:9876
```
### 3. 鉴权(v2 起默认启用)
扩展启动时自动生成随机 token 并写入 `~/.burp-mcp-token`。`mcp-bridge.js` 会自动读取该文件并在每个请求携带 `Authorization: Bearer <token>` 头,无需手动配置。
需要固定 token 时(例如多个客户端共享),可用:
- JVM 参数:`-Dburp.mcp.token=<token>`
- 环境变量:`BURP_MCP_TOKEN=<token>`(同时用于 bridge 侧)
所有 `/health`、`/tools`、`/`(POST)请求均要求携带该头,否则返回 403。CORS 已收敛为仅允许 `http://127.0.0.1` 来源。
### 4. 配置 MCP 客户端
在任何 MCP 客户端(Claude Code / Kiro / Cursor / Cline / Windsurf)中添加(stdio 模式):
```json
{
"mcpServers": {
"burpsuite": {
"command": "node",
"args": ["<本目录路径>/mcp-bridge.js"]
}
}
}
```
### 5. 开始使用
对 AI 说:"分析 Burp 代理历史中的请求,找出安全漏洞"
## 功能列表
扩展暴露 78 个工具。常用分类如下(完整列表见 `src/main/java/com/burpmcp/McpHttpServer.java` 的 `getToolList()`,或访问 `GET http://127.0.0.1:9876/tools`,需携带 Authorization 头):
| 分类 | 工具 |
|------|------|
| Proxy 历史 | `proxy_history`, `proxy_detail`, `proxy_history_filtered`, `proxy_websocket`, `proxy_clear`, `search_history`, `highlight`, `annotate`, `compare` |
| 发送请求 | `send_request`, `send_to_repeater`, `repeater_send`, `repeater_modify_send`, `send_to_intruder` |
| Intruder 攻击 | `intruder_attack`, `intruder_attack_async`, `intruder_attack_wordlist`, `intruder_pitchfork`, `intruder_cluster_bomb`, `intruder_battering_ram`, `intruder_with_options`, `payload_process` |
| 扫描 / 爬取 | `scan`(主动/被动), `scan_active`, `scan_results`, `scan_issue_detail`, `crawl`, `sequencer` |
| Scope / Sitemap | `sitemap`, `target_info`, `get_scope`, `add_to_scope`, `remove_from_scope`, `add_issue` |
| 拦截 / 规则 | `intercept_toggle`, `register_http_handler`, `remove_http_handler`, `register_proxy_rule`, `remove_proxy_rule` |
| 编解码 | `encode`, `decode`, `convert_request`, `export_request`, `generate_csrf_poc`, `extract_from_response`, `token_analysis` |
| Collaborator | `collaborator_generate`, `collaborator_poll` |
| 配置 | `export_config`, `import_config`, `set_upstream_proxy`, `set_dns_override`, `set_http2`, `cookie_jar`, `save_project`, `burp_version`, `extensions_list`, `log` |
> 扫描/爬取(`scan`、`scan_active`、`crawl`)需要 **Burp Professional**。Community 版会返回明确的许可证错误。手动添加的 issue(`add_issue`)会写入 Site map。
## 关键工具参数
### `intruder_attack` — 自动化枚举攻击
| 参数 | 说明 |
|------|------|
| `url_template` | URL 模板,占位符默认 `@@` |
| `placeholder` | 占位符字符串(默认 `@@`) |
| `from` / `to` | 枚举起止值 |
| `pad_digits` | 补零位数(0 不补) |
| `method` | HTTP 方法(默认 GET) |
| `body_template` | 请求体模板(含占位符) |
| `headers` | 请求头对象 |
| `success_length_not` | 命中条件:响应长度 ≠ 此值 |
| `success_contains` | 命中条件:响应体包含此字符串 |
### `scan` — 启动审计
| 参数 | 说明 |
|------|------|
| `url` | 目标 URL(必填,自动加入 scope) |
| `mode` | `active`(默认)或 `passive` |
启动后用 `scan_results` 轮询 issues 与活动审计状态(请求数、错误数、插入点数)。
### `register_proxy_rule` — 代理请求拦截规则
| 参数 | 说明 |
|------|------|
| `url_contains` | 命中条件:URL 包含此串 |
| `intercept` | `true` 拦截 / `false` 放行不拦截(默认 true) |
通过 `remove_proxy_rule` 注销规则(基于 `Registration.deregister()`,真正从 Burp 卸载)。
## 调用示例
### 查看代理历史
```json
POST http://127.0.0.1:9876
{"tool": "proxy_history", "params": {"limit": 10, "url_filter": "personalblog"}}
```
### 发送请求
```json
POST http://127.0.0.1:9876
{"tool": "send_request", "params": {"method": "GET", "url": "https://example.com/api/test"}}
```
### 自动化枚举攻击(核心功能)
```json
POST http://127.0.0.1:9876
{
"tool": "intruder_attack",
"params": {
"url_template": "https://target.com/api/verify?code=@@",
"method": "POST",
"from": 0,
"to": 999999,
"pad_digits": 6,
"success_length_not": 176,
"headers": {"User-Agent": "Mozilla/5.0"}
}
}
```
### 开关拦截
```json
POST http://127.0.0.1:9876
{"tool": "intercept_toggle", "params": {"enable": false}}
```
## 端口配置
默认监听 `127.0.0.1:9876`。如需更改(例如与 PortSwigger 官方 MCP 扩展同端口冲突):
1. **Burp 侧**:启动 Burp 时传 JVM 参数 `-Dburp.mcp.port=9877`,或设环境变量 `BURP_MCP_PORT=9877`。
2. **桥接侧**:MCP 客户端配置里设环境变量 `BURP_MCP_PORT=9877` 与 `BURP_MCP_HOST=127.0.0.1`。
两侧端口必须一致。若 Burp 未运行或端口不通,桥接会在 `tools/list` 与 `tools/call` 返回明确的连接错误指引。
## 故障排查
| 现象 | 排查 |
|------|------|
| Burp Output 无 "[MCP] Server started" | 端口被占用或扩展加载失败,查 Burp Errors 面板 |
| MCP 客户端报 "Burp MCP not connected" | 确认 Burp 已运行且扩展已加载;确认两侧端口一致 |
| 扫描返回 "requires Burp Professional" | 正常,Community 版不支持 Scanner API |
| `remove_http_handler` / `remove_proxy_rule` 无效 | 确认之前 `register_*` 返回 success=true |
## 源码构建(Gradle 可选)
```bash
cd burp-mcp-full
gradle jar # 需本机已装 Gradle 8.7+
# 输出: build/libs/burp-mcp-full.jar
```
> 推荐使用 `build.bat` / `build.sh`(零依赖,自动下载 jar)。Gradle 路径仅作备选。

View File

@@ -0,0 +1,94 @@
@echo off
setlocal enabledelayedexpansion
:: BurpSuite MCP Full Control - Windows Build Script
:: Produces: build\libs\burp-mcp-full.jar (fat jar, Burp-loadable)
:: ===== Detect JDK (JAVA_HOME -> PATH -> common install dirs) =====
set "JAVAC="
set "JAR_CMD="
if defined JAVA_HOME (
if exist "%JAVA_HOME%\bin\javac.exe" (
set "JAVAC=%JAVA_HOME%\bin\javac.exe"
set "JAR_CMD=%JAVA_HOME%\bin\jar.exe"
)
)
if not defined JAVAC (
where javac >nul 2>nul
if not errorlevel 1 (
for /f "delims=" %%i in ('where javac') do (
set "JAVAC=%%i"
goto :javac_found
)
)
)
:javac_found
if not defined JAVAC (
for %%P in (
"C:\Program Files\Java"
"C:\Program Files\Eclipse Adoptium"
"C:\Program Files\Microsoft\jdk-21.0.5.11-hotspot"
) do (
if exist %%P (
for /d %%D in (%%P\jdk-2*) do (
if exist "%%D\bin\javac.exe" (
set "JAVAC=%%D\bin\javac.exe"
set "JAR_CMD=%%D\bin\jar.exe"
)
)
)
)
)
if not defined JAVAC (
echo ERROR: javac not found in JAVA_HOME, PATH, or common install dirs.
echo Set JAVA_HOME to your JDK 21+ install directory, e.g.:
echo set JAVA_HOME=C:\Program Files\Java\jdk-21
exit /b 1
)
set "SRC=src\main\java\com\burpmcp"
set "RES=src\main\resources"
set "LIB=lib"
set "OUT=build\classes"
set "DIST=build\libs"
echo [1/4] Downloading dependencies...
if not exist %LIB% mkdir %LIB%
if not exist %LIB%\montoya-api.jar curl -sL -o %LIB%\montoya-api.jar "https://repo1.maven.org/maven2/net/portswigger/burp/extensions/montoya-api/2025.5/montoya-api-2025.5.jar"
if not exist %LIB%\gson.jar curl -sL -o %LIB%\gson.jar "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar"
if not exist %LIB%\nanohttpd.jar curl -sL -o %LIB%\nanohttpd.jar "https://repo1.maven.org/maven2/org/nanohttpd/nanohttpd/2.3.1/nanohttpd-2.3.1.jar"
echo [2/4] Compiling...
if not exist %OUT% mkdir %OUT%
"%JAVAC%" --release 21 -encoding UTF-8 -cp "%LIB%\montoya-api.jar;%LIB%\gson.jar;%LIB%\nanohttpd.jar" -d %OUT% %SRC%\*.java
if errorlevel 1 (
echo COMPILE FAILED
exit /b 1
)
:: Copy resources (extension descriptor) so the jar is loadable by Burp
xcopy /e /i /y "%RES%\META-INF" "%OUT%\META-INF" >nul
echo [3/4] Packaging fat jar...
if not exist %DIST% mkdir %DIST%
:: Extract dependencies into classes dir
cd %OUT%
"%JAR_CMD%" xf ..\..\%LIB%\gson.jar com
"%JAR_CMD%" xf ..\..\%LIB%\nanohttpd.jar fi
cd ..\..
:: Create jar
"%JAR_CMD%" cf %DIST%\burp-mcp-full.jar -C %OUT% .
echo [4/4] Done!
echo Output: %DIST%\burp-mcp-full.jar
echo.
echo Install: Burp Suite -^> Extensions -^> Add -^> Java -^> Select %DIST%\burp-mcp-full.jar
echo.
echo MCP config (any client):
echo { "command": "node", "args": ["%~dp0mcp-bridge.js"] }

View File

@@ -0,0 +1,35 @@
plugins {
id 'java'
}
group = 'com.burpmcp'
version = '1.0.0'
sourceCompatibility = '21'
targetCompatibility = '21'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'net.portswigger.burp.extensions:montoya-api:2025.5'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'org.nanohttpd:nanohttpd:2.3.1'
}
jar {
manifest {
attributes 'Implementation-Title': 'BurpMCP Full Control'
attributes 'Implementation-Version': version
}
// Fat jar - include all dependencies
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveFileName = 'burp-mcp-full.jar'
}

View File

@@ -0,0 +1,63 @@
#!/bin/bash
# BurpSuite MCP Full Control - Linux / macOS Build Script
# Requires: JDK 21+, curl
# Produces: build/libs/burp-mcp-full.jar (fat jar, Burp-loadable)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# ===== Auto-detect JAVA_HOME =====
if [ -z "$JAVA_HOME" ]; then
if command -v javac >/dev/null 2>&1; then
JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
else
echo "ERROR: javac not found. Install JDK 21+ or set JAVA_HOME."
exit 1
fi
fi
JAVAC="$JAVA_HOME/bin/javac"
JAR="$JAVA_HOME/bin/jar"
if ! "$JAVAC" -version 2>/dev/null | grep -qE '(1\.)?(2[1-9]|[3-9][0-9])'; then
echo "WARNING: JDK 21+ recommended. Detected: $("$JAVAC" -version 2>&1)"
fi
SRC="src/main/java/com/burpmcp"
RES="src/main/resources"
LIB="lib"
OUT="build/classes"
DIST="build/libs"
MONTOYA="https://repo1.maven.org/maven2/net/portswigger/burp/extensions/montoya-api/2025.5/montoya-api-2025.5.jar"
GSON="https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar"
NANO="https://repo1.maven.org/maven2/org/nanohttpd/nanohttpd/2.3.1/nanohttpd-2.3.1.jar"
echo "[1/4] Downloading dependencies..."
mkdir -p "$LIB"
[ -f "$LIB/montoya-api.jar" ] || curl -sL -o "$LIB/montoya-api.jar" "$MONTOYA"
[ -f "$LIB/gson.jar" ] || curl -sL -o "$LIB/gson.jar" "$GSON"
[ -f "$LIB/nanohttpd.jar" ] || curl -sL -o "$LIB/nanohttpd.jar" "$NANO"
echo "[2/4] Compiling..."
mkdir -p "$OUT"
"$JAVAC" --release 21 -encoding UTF-8 -cp "$LIB/montoya-api.jar:$LIB/gson.jar:$LIB/nanohttpd.jar" -d "$OUT" "$SRC"/*.java
# Copy resources (extension descriptor) so the jar is loadable by Burp
cp -r "$RES/META-INF" "$OUT/"
echo "[3/4] Packaging fat jar..."
mkdir -p "$DIST"
cd "$OUT"
"$JAR" xf "../../$LIB/gson.jar" com
"$JAR" xf "../../$LIB/nanohttpd.jar" fi
cd ../..
"$JAR" cf "$DIST/burp-mcp-full.jar" -C "$OUT" .
echo "[4/4] Done!"
echo "Output: $DIST/burp-mcp-full.jar"
echo ""
echo "Install: Burp Suite -> Extensions -> Add -> Java -> Select $DIST/burp-mcp-full.jar"
echo ""
echo "MCP config (any client):"
echo " { \"command\": \"node\", \"args\": [\"$SCRIPT_DIR/mcp-bridge.js\"] }"

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
reverse-skill-repo/burp-mcp-full/gradlew vendored Normal file
View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,379 @@
#!/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": ["<path-to-this-file>/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=<same> 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();

View File

@@ -0,0 +1 @@
rootProject.name = 'burp-mcp-full'

View File

@@ -0,0 +1,71 @@
package com.burpmcp;
import burp.api.montoya.BurpExtension;
import burp.api.montoya.MontoyaApi;
import burp.api.montoya.logging.Logging;
/**
* BurpSuite MCP Full Control Extension
* Exposes ALL Burp functionality via MCP protocol on HTTP port 9876
*
* Features:
* - Proxy history access & filtering
* - Intruder attack control (create, configure payloads, start, get results)
* - Repeater tab management
* - Scanner control
* - Sitemap access
* - Send HTTP requests through Burp
* - Intercept control
* - Encoding/decoding utilities
*/
public class BurpMcpExtension implements BurpExtension {
private MontoyaApi api;
private Logging logging;
private McpHttpServer server;
@Override
public void initialize(MontoyaApi api) {
this.api = api;
this.logging = api.logging();
api.extension().setName("MCP Full Control");
int port = resolvePort(logging);
try {
server = new McpHttpServer(api, port);
server.start();
logging.logToOutput("[MCP] Server started on http://127.0.0.1:" + port);
logging.logToOutput("[MCP] Configure port via -Dburp.mcp.port=<n> or BURP_MCP_PORT env var (default 9876)");
logging.logToOutput("[MCP] Auth token: ~/.burp-mcp-token (override with -Dburp.mcp.token or BURP_MCP_TOKEN)");
logging.logToOutput("[MCP] Tools: proxy_history, send_request, intruder_attack, repeater, scanner, sitemap, intercept, encode/decode");
} catch (Exception e) {
logging.logToError("[MCP] Failed to start server on port " + port + ": " + e.getMessage());
logging.logToError("[MCP] If port is in use, set -Dburp.mcp.port=<other> and restart Burp.");
}
api.extension().registerUnloadingHandler(() -> {
if (server != null) {
server.stop();
logging.logToOutput("[MCP] Server stopped");
}
});
}
private int resolvePort(Logging logging) {
String prop = System.getProperty("burp.mcp.port");
String env = System.getenv("BURP_MCP_PORT");
String raw = prop != null ? prop : env;
if (raw == null || raw.isBlank()) return 9876;
try {
int p = Integer.parseInt(raw.trim());
if (p < 1 || p > 65535) throw new IllegalArgumentException("out of range");
if (prop != null) logging.logToOutput("[MCP] Port from -Dburp.mcp.port: " + p);
else logging.logToOutput("[MCP] Port from BURP_MCP_PORT: " + p);
return p;
} catch (Exception e) {
logging.logToError("[MCP] Invalid port '" + raw + "', falling back to 9876. " + e.getMessage());
return 9876;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
extension.name=MCP Full Control
extension.class=com.burpmcp.BurpMcpExtension

View File

@@ -0,0 +1,141 @@
'use strict';
const assert = require('node:assert/strict');
const { once } = require('node:events');
const http = require('node:http');
const net = require('node:net');
const { spawn } = require('node:child_process');
const { test } = require('node:test');
const path = require('node:path');
class LineReader {
constructor(stream) {
this.lines = [];
this.waiters = [];
this.buffer = '';
stream.setEncoding('utf8');
stream.on('data', chunk => {
this.buffer += chunk;
const parts = this.buffer.split('\n');
this.buffer = parts.pop();
for (const line of parts) {
this.push(line.replace(/\r$/, ''));
}
});
}
push(line) {
const waiter = this.waiters.shift();
if (waiter) {
waiter.resolve(line);
} else {
this.lines.push(line);
}
}
next(timeoutMs = 5000) {
if (this.lines.length > 0) return Promise.resolve(this.lines.shift());
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
const index = this.waiters.indexOf(waiter);
if (index >= 0) this.waiters.splice(index, 1);
reject(new Error('Timed out waiting for a line'));
}, timeoutMs);
const waiter = {
resolve: line => {
clearTimeout(timeout);
resolve(line);
}
};
this.waiters.push(waiter);
});
}
}
function getFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
}
function close(server) {
if (!server) return Promise.resolve();
return new Promise(resolve => server.close(() => resolve()));
}
test('reconnects after Burp starts and handles a later tool call', async () => {
const port = await getFreePort();
const bridgePath = path.join(__dirname, '..', 'mcp-bridge.js');
const bridge = spawn(process.execPath, [bridgePath], {
env: {
...process.env,
BURP_MCP_HOST: '127.0.0.1',
BURP_MCP_PORT: String(port),
BURP_MCP_TOKEN: 'test-token'
},
stdio: ['pipe', 'pipe', 'pipe']
});
const stdout = new LineReader(bridge.stdout);
const stderr = new LineReader(bridge.stderr);
let server;
const requests = [];
try {
const warning = await stderr.next();
assert.match(warning, /Cannot connect to Burp/);
server = http.createServer((request, response) => {
if (request.method === 'GET' && request.url === '/tools') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(['proxy_history']));
return;
}
let body = '';
request.on('data', chunk => { body += chunk; });
request.on('end', () => {
requests.push(JSON.parse(body));
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
});
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', resolve);
});
bridge.stdin.write(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {}
}) + '\n');
const listResponse = JSON.parse(await stdout.next());
assert.equal(listResponse.id, 1);
assert.equal(listResponse.result.tools[0].name, 'burp_proxy_history');
bridge.stdin.write(JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'burp_proxy_history', arguments: { limit: 1 } }
}) + '\n');
const callResponse = JSON.parse(await stdout.next());
assert.equal(callResponse.id, 2);
assert.deepEqual(requests[0], { tool: 'proxy_history', params: { limit: 1 } });
assert.match(callResponse.result.content[0].text, /"ok": true/);
} finally {
const childClosed = bridge.exitCode === null && bridge.signalCode === null
? once(bridge, 'close')
: Promise.resolve();
bridge.stdin.end();
bridge.kill();
await close(server);
await childClosed.catch(() => {});
}
});