Skill: nquangit/burp-mcp (version 0.1.0)

--- name: burp-mcp slug: burp-mcp version: 0.1.0 description: Connect to a local Burp Suite MCP Server over SSE and list or call Burp tools from the workspace. Use when Burp Suite is running with the PortSwigger MCP extension enabled on http://127.0.0.1:9876/. metadata: {"clawdbot":{"requires":{"bins":["python"]},"os":["win32","linux","darwin"],"install":[{"id":"python-mcp","kind":"pip","package":"mcp","label":"Install the official Python MCP SDK"}],"configPaths.optional":["./skills/burp-mcp/config.json"]}} --- # Burp MCP Use this skill to talk to a **local Burp Suite MCP Server** exposed by the PortSwigger extension. ## When to use - Burp Suite is already running locally - The MCP extension is loaded and enabled - You want to inspect available Burp tools - You want to call a specific Burp MCP tool from the terminal or from OpenClaw via `exec` ## Endpoint Default endpoint used by this skill: ```text http://127.0.0.1:9876/ ``` Important: on this machine, the live SSE endpoint is `/`, not `/sse`. ## Commands List Burp tools: ```bash python ./skills/burp-mcp/scripts/burp_mcp.py list-tools ``` Call a Burp tool: ```bash python ./skills/burp-mcp/scripts/burp_mcp.py call <tool_name> '<json_args>' ``` Examples: ```bash python ./skills/burp-mcp/scripts/burp_mcp.py list-tools python ./skills/burp-mcp/scripts/burp_mcp.py call get_proxy_http_history '{"offset":0,"count":5}' python ./skills/burp-mcp/scripts/burp_mcp.py call get_proxy_http_history_regex '{"offset":0,"count":10,"regex":"login|token|auth"}' python ./skills/burp-mcp/scripts/burp_mcp.py call output_project_options '{}' ``` ## How agents should use it ### 1) Always start with `list-tools` Burp MCP tool names and schemas can change with extension version. Before calling tools in a new setup, list tools first: ```bash python ./skills/burp-mcp/scripts/burp_mcp.py list-tools ``` This returns JSON entries with: - `name` - `description` - `inputSchema` ### 2) Match args to `inputSchema` When calling a tool, build the JSON object to match the tool's required schema exactly. Example: - `get_proxy_http_history` requires: - `offset` (integer) - `count` (integer) So call it like: ```bash python ./skills/burp-mcp/scripts/burp_mcp.py call get_proxy_http_history '{"offset":0,"count":20}' ``` ### 3) Prefer safe read-only tools first Safe first calls: - `get_proxy_http_history` - `get_proxy_http_history_regex` - `get_proxy_websocket_history` - `get_scanner_issues` - `output_project_options` - `output_user_options` Be careful with mutation tools such as: - `set_project_options` - `set_user_options` - `set_proxy_intercept_state` - `set_task_execution_engine_state` - `set_active_editor_contents` ## Output behavior - `list-tools` prints JSON array of tools - `call` prints JSON object: - `isError` - `content` Burp often returns tool results as `text` blocks. Some tools return newline-separated JSON strings inside `content[].text`, so callers may need a second parse step if they want structured output. ## Troubleshooting - If connection fails, verify Burp is open and the MCP extension is enabled - If `/sse` fails, use `/` instead - If tool calls return schema errors, re-run `list-tools` and check `inputSchema` - If nothing returns, test manually with: ```bash curl http://127.0.0.1:9876 -v ``` A healthy server should return `text/event-stream` ## Notes - Uses the official Python `mcp` SDK - Requires asyncio - Assumes the `mcp` package is already installed in the active Python environment

path: config.json
content: sse_url: http://127.0.0.1:9876/

path: README.md
content: # burp-mcp

Connect to a local Burp Suite MCP Server over SSE and list/call Burp tools from an OpenClaw workspace.

## What this is

This skill wraps the official Python MCP SDK and talks to the PortSwigger Burp Suite MCP extension over SSE.

Default endpoint:

```text
http://127.0.0.1:9876/
```

On this setup, the live SSE endpoint is `/`, not `/sse`.

## Files

- `SKILL.md` — skill instructions and usage guidance
- `config.json` — configurable SSE URL
- `scripts/burp_mcp.py` — CLI for listing and calling Burp MCP tools

## Requirements

- Python
- `mcp` Python package installed
- Burp Suite running locally with the PortSwigger MCP extension enabled

## Usage

From inside the repo:

List tools:

```bash
python ./scripts/burp_mcp.py list-tools
```

Call a tool:

```bash
python ./scripts/burp_mcp.py call <tool_name> '<json_args>'
```

Example:

```bash
python ./scripts/burp_mcp.py call get_proxy_http_history '{"offset":0,"count":5}'
```

From an OpenClaw workspace where this skill is installed under `skills/burp-mcp/`:

```bash
python ./skills/burp-mcp/scripts/burp_mcp.py list-tools
python ./skills/burp-mcp/scripts/burp_mcp.py call get_proxy_http_history '{"offset":0,"count":5}'
```

## Agent usage pattern

1. Run `list-tools`
2. Read `inputSchema`
3. Build matching JSON args
4. Run `call <tool_name> '<json_args>'`
5. Parse `content[].text` if the Burp tool returns embedded JSON text blocks

## Notes

- Safe read-first tools include history, scanner issue, and config export functions
- Some Burp tools mutate state; check the tool description before calling
- Output is intentionally raw MCP-shaped JSON so agents can post-process it cleanly


path: scripts/burp_mcp.py
content: import asyncio
import json
import os
import sys
from typing import Any

from mcp.client.session import ClientSession
from mcp.client.sse import sse_client

DEFAULT_SSE_URL = "http://127.0.0.1:9876/"
CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")


def get_sse_url() -> str:
    if os.path.exists(CONFIG_PATH):
        try:
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                cfg = json.load(f)
            if isinstance(cfg, dict) and isinstance(cfg.get("sse_url"), str) and cfg["sse_url"].strip():
                return cfg["sse_url"].strip()
        except Exception:
            pass
    return DEFAULT_SSE_URL


def dump_content_item(item: Any) -> Any:
    if hasattr(item, "model_dump"):
        try:
            return item.model_dump()
        except Exception:
            pass
    if isinstance(item, dict):
        return item
    out = {"type": getattr(item, "type", type(item).__name__)}
    for key in ("text", "data", "mimeType", "uri", "annotations", "meta"):
        if hasattr(item, key):
            out[key] = getattr(item, key)
    return out


async def open_session():
    sse_url = get_sse_url()
    streams_cm = sse_client(sse_url)
    streams = await streams_cm.__aenter__()
    read_stream, write_stream = streams
    session_cm = ClientSession(read_stream, write_stream)
    session = await session_cm.__aenter__()
    try:
        await session.initialize()
        return sse_url, streams_cm, session_cm, session
    except Exception:
        await session_cm.__aexit__(*sys.exc_info())
        await streams_cm.__aexit__(*sys.exc_info())
        raise


async def close_session(streams_cm, session_cm, session):
    await session_cm.__aexit__(None, None, None)
    await streams_cm.__aexit__(None, None, None)


async def list_tools() -> int:
    sse_url, streams_cm, session_cm, session = await open_session()
    try:
        resp = await session.list_tools()
        tools = getattr(resp, "tools", []) or []
        print(json.dumps({
            "sse_url": sse_url,
            "count": len(tools),
            "tools": [
                {
                    "name": t.name,
                    "description": getattr(t, "description", None),
                    "inputSchema": getattr(t, "inputSchema", None),
                }
                for t in tools
            ],
        }, indent=2, ensure_ascii=False, default=str))
        return 0
    finally:
        await close_session(streams_cm, session_cm, session)


async def call_tool(tool_name: str, args: dict[str, Any]) -> int:
    sse_url, streams_cm, session_cm, session = await open_session()
    try:
        result = await session.call_tool(tool_name, args)
        payload = {
            "sse_url": sse_url,
            "tool": tool_name,
            "args": args,
            "isError": getattr(result, "isError", False),
            "content": [dump_content_item(x) for x in (getattr(result, "content", None) or [])],
        }
        print(json.dumps(payload, indent=2, ensure_ascii=False, default=str))
        return 0 if not payload["isError"] else 2
    finally:
        await close_session(streams_cm, session_cm, session)


def usage() -> int:
    print(
        "Usage:\n"
        "  python ./skills/burp-mcp/scripts/burp_mcp.py list-tools\n"
        "  python ./skills/burp-mcp/scripts/burp_mcp.py call <tool_name> <json_args>\n\n"
        "Examples:\n"
        "  python ./skills/burp-mcp/scripts/burp_mcp.py list-tools\n"
        "  python ./skills/burp-mcp/scripts/burp_mcp.py call get_proxy_http_history \"{\\\"offset\\\":0,\\\"count\\\":5}\"\n"
    )
    return 1


async def main() -> int:
    if len(sys.argv) < 2:
        return usage()

    cmd = sys.argv[1]
    if cmd == "list-tools":
        return await list_tools()

    if cmd == "call" and len(sys.argv) >= 4:
        tool_name = sys.argv[2]
        try:
            args = json.loads(sys.argv[3])
        except json.JSONDecodeError as e:
            print(f"Invalid JSON args: {e}", file=sys.stderr)
            return 1
        if not isinstance(args, dict):
            print("JSON args must be an object", file=sys.stderr)
            return 1
        return await call_tool(tool_name, args)

    return usage()


if __name__ == "__main__":
    raise SystemExit(asyncio.run(main()))


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)