From eccc8471e87539fbeacebba729818569a3cb411d Mon Sep 17 00:00:00 2001 From: giveen Date: Wed, 21 Jan 2026 12:08:11 -0700 Subject: [PATCH] chore(mcp): remove duplicate adapters, scripts, archives, example config, PR body and log --- PR_BODY.txt | 33 ----- dupe-workspace.tar.gz | Bin 444 -> 0 bytes expimp-workspace.tar.gz | Bin 502 -> 0 bytes mcp_servers.json.example | 10 -- scripts/add_metasploit_subtree.sh | 84 ----------- scripts/install_metasploit_deps.sh | 40 ----- .../hexstrike/hexstrike_stdio_adapter.py | 136 ----------------- third_party/mcp/stdio_adapter.py | 137 ------------------ 8 files changed, 440 deletions(-) delete mode 100644 PR_BODY.txt delete mode 100644 dupe-workspace.tar.gz delete mode 100644 expimp-workspace.tar.gz delete mode 100644 mcp_servers.json.example delete mode 100644 scripts/add_metasploit_subtree.sh delete mode 100644 scripts/install_metasploit_deps.sh delete mode 100644 third_party/hexstrike/hexstrike_stdio_adapter.py delete mode 100644 third_party/mcp/stdio_adapter.py diff --git a/PR_BODY.txt b/PR_BODY.txt deleted file mode 100644 index b51cc69..0000000 --- a/PR_BODY.txt +++ /dev/null @@ -1,33 +0,0 @@ -Summary: -Fixes runtime and UX bugs that prevented tool execution and caused inconsistent target selection in the TUI. Improves robustness across Textual versions and makes the target visible and authoritative to the LLM. - -What was broken: -- TypeError when scheduling Textual workers: asyncio.create_task was given a Textual Worker (not a coroutine). -- LLM-generated flags-only terminal commands (e.g. -p 1-1024 ...) were passed to /bin/sh and caused '/bin/sh: Illegal option -'. -- Active workspace scope checks blocked scans when the target was not in the workspace, while stale/manual targets could persist in conversation and be used by the LLM. -- UI errors on some Textual versions from calling unsupported APIs (e.g. ScrollableContainer.mount_before), and duplicated in-chat target messages cluttered the chat. - -What I changed (key files): -- pentestagent/interface/tui.py - - Stop wrapping @work-decorated methods with asyncio.create_task; use the returned Worker correctly. - - Ensure workspace activation/deactivation behavior: clear TUI/agent target on /workspace clear; restore last_target on activation. - - When operator sets a manual target (/target), append a short system AgentMessage so the LLM sees the change; track manual target and remove/supersede it when a workspace restores its saved target. - - Add a persistent header widget to display runtime/mode/target and remove duplicate in-chat target lines. - - Guard mount_before calls with a try/fallback to mount to support Textual versions without mount_before. -- pentestagent/agents/base_agent.py - - If a requested tool name is not found, fall back to the terminal tool and construct a best-effort command string from function-call arguments so semantic tool names (e.g., nmap) execute. - - Preserve workspace-scope validation but return explicit errors that instruct the operator how to proceed. -- pentestagent/tools/terminal/__init__.py - - Detect flags-only command strings and prefix them with a likely binary (nmap, gobuster, rustscan, masscan, curl, wget, etc.) using runtime-detected tools, preventing shell option errors. - - Make terminal execution tolerant to malformed inputs and avoid uncaught exceptions. - -Rationale: -Textual workers are Worker objects; scheduling them as coroutines caused runtime errors. Handling Worker objects correctly preserves Textual semantics. LLMs sometimes emit partial or semantic commands; best-effort normalization reduces shell failures and improves task success. Explicit system messages and deterministic workspace restores ensure the LLM uses the intended target. A persistent header provides immediate operator context and avoids losing the active target when the chat scrolls. - -Testing performed: -- Reproduced and fixed the Worker scheduling TypeError. -- Verified flags-only commands are now prefixed (nmap test) and no longer produce '/bin/sh: Illegal option -'. -- Walked through workspace/target flows to confirm authoritative target behavior. -- Confirmed mount_before fallback avoids AttributeError on older Textual versions. - -Branch: changes pushed to giveen/bug-fix. diff --git a/dupe-workspace.tar.gz b/dupe-workspace.tar.gz deleted file mode 100644 index 7bb4cfc3b49a29989df537c9382f29914798f1c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 444 zcmV;t0Ym;DiwFo-fNp65|73M=Wi5Aaa%*#NVPj=3bYXG;?b=UE!eAW0@tu7YyLD=N zo@dV=9RwXacIZ?@JhcZYor`UfzWXLYMIm9$MfUx8m~6s8`F)>FP0}PiJ@>1#;EPa4 zdm&zI+X|8Cx96MvQYfjScohv`*|fgyP9ObGY8;pCeHl)qv*WRzbdN+^VT4no2nX+} zs%OokzY9%TEFVKUTU1Z;P)tMG@BBMwHe2b8c)xv}d1+*pneKqH-TWJ4WfYqs=l>zp zpf^PygvZxc;aA^q>CIf9#|G^QvmoJg;crFxzU$LqwcMmhPKJ$t841O`p^2$`j3NJ m{})x;%WpnEyZ_TJtCFmeAa!o z3wN_@`JKe&v2JP2#b&)*1Yhru@7~axo7b`RQO*1&O*a9Z_n*}!%{cPpTby~h+w)~y zeoS{a<}Wevw0pPvyX4az)<9vN1G~QM%Gz(Y!*55|n)ry~!&Bz?T*|CIx>BHNl>x*5 z`Ms;Bef2u;V|0Kw7lRMHF+NMe};RNC|6u>;fAjy|TC>W18qZBFt^TwBoF9Bn^bbE+&SLui?CtAz zbw0fQ->d9r`~QdaKjit&{E;ZGY@%_8j5E7d@2xG|nqK%YZC8WY p1)!Z@>~~!Fr$6`o!!yq({/dev/null)" ]; then - echo "Detected empty directory at ${PREFIX}; adding subtree into it..." - mkdir -p "$(dirname "${PREFIX}")" - if git subtree add --prefix="${PREFIX}" "${REPO_URL}" "${BRANCH}" --squash; then - echo "MetasploitMCP subtree added under ${PREFIX}." - else - echo "Failed to add subtree from ${REPO_URL}." >&2 - echo "Check that the URL is correct or override with METASPLOIT_SUBTREE_REPO." >&2 - exit 1 - fi - exit 0 - fi - # Directory exists; check whether the path is tracked in git. - if git ls-files --error-unmatch "${PREFIX}" >/dev/null 2>&1; then - echo "Detected existing subtree at ${PREFIX}." - if [ "${FORCE_SUBTREE_PULL:-false}" = "true" ]; then - echo "FORCE_SUBTREE_PULL=true: pulling latest changes into existing subtree..." - git subtree pull --prefix="${PREFIX}" "${REPO_URL}" "${BRANCH}" --squash || { - echo "git subtree pull failed; attempting without --squash..." - git subtree pull --prefix="${PREFIX}" "${REPO_URL}" "${BRANCH}" || exit 1 - } - echo "Subtree at ${PREFIX} updated." - else - echo "To update the existing subtree run:" - echo " FORCE_SUBTREE_PULL=true bash scripts/add_metasploit_subtree.sh" - echo "Or run manually: git subtree pull --prefix=\"${PREFIX}\" ${REPO_URL} ${BRANCH} --squash" - fi - else - # Directory exists but not tracked by git. - echo "Directory ${PREFIX} exists but is not tracked in git." - if [ "${FORCE_SUBTREE_PULL:-false}" = "true" ]; then - echo "FORCE_SUBTREE_PULL=true: backing up existing directory and attempting to add subtree..." - BACKUP="${PREFIX}.backup.$(date +%s)" - mv "${PREFIX}" "${BACKUP}" || { echo "Failed to move ${PREFIX} to ${BACKUP}" >&2; exit 1; } - # Ensure parent exists after move - mkdir -p "$(dirname "${PREFIX}")" - if git subtree add --prefix="${PREFIX}" "${REPO_URL}" "${BRANCH}" --squash; then - echo "MetasploitMCP subtree added under ${PREFIX}." - echo "Removing backup ${BACKUP}." - rm -rf "${BACKUP}" - else - echo "Failed to add subtree from ${REPO_URL}. Restoring backup." >&2 - rm -rf "${PREFIX}" || true - mv "${BACKUP}" "${PREFIX}" || { echo "Failed to restore ${BACKUP} to ${PREFIX}" >&2; exit 1; } - exit 1 - fi - else - echo "To add the subtree into the existing directory, either remove/rename ${PREFIX} and retry," - echo "or run with FORCE_SUBTREE_PULL=true to back up and add:" - echo " FORCE_SUBTREE_PULL=true bash scripts/add_metasploit_subtree.sh" - echo "Or override the repo with METASPLOIT_SUBTREE_REPO to use a different source." - exit 1 - fi - fi -else - echo "Adding subtree for the first time..." - # Ensure parent dir exists for clearer errors - mkdir -p "$(dirname "${PREFIX}")" - - if git subtree add --prefix="${PREFIX}" "${REPO_URL}" "${BRANCH}" --squash; then - echo "MetasploitMCP subtree added under ${PREFIX}." - else - echo "Failed to add subtree from ${REPO_URL}." >&2 - echo "Check that the URL is correct or override with METASPLOIT_SUBTREE_REPO." >&2 - exit 1 - fi -fi diff --git a/scripts/install_metasploit_deps.sh b/scripts/install_metasploit_deps.sh deleted file mode 100644 index d8d9145..0000000 --- a/scripts/install_metasploit_deps.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Install vendored MetasploitMCP Python dependencies. -# This script will source a local .env if present so any environment -# variables (proxies/indices/LLM keys) are respected during installation. - -HERE=$(dirname "${BASH_SOURCE[0]}") -ROOT=$(cd "$HERE/.." && pwd) - -cd "$ROOT" - -if [ -f ".env" ]; then - echo "Sourcing .env" - set -a - # shellcheck disable=SC1091 - source .env - set +a -fi - -REQ=third_party/MetasploitMCP/requirements.txt - -if [ ! -f "$REQ" ]; then - echo "Cannot find $REQ. Is the MetasploitMCP subtree present?" - exit 1 -fi - -echo "Installing MetasploitMCP requirements from $REQ" - -PY=$(which python || true) -if [ -n "${VIRTUAL_ENV:-}" ]; then - PY="$VIRTUAL_ENV/bin/python" -fi - -"$PY" -m pip install --upgrade pip -"$PY" -m pip install -r "$REQ" - -echo "MetasploitMCP dependencies installed. Note: external components may still be required." - -exit 0 diff --git a/third_party/hexstrike/hexstrike_stdio_adapter.py b/third_party/hexstrike/hexstrike_stdio_adapter.py deleted file mode 100644 index e467ef3..0000000 --- a/third_party/hexstrike/hexstrike_stdio_adapter.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -"""Small stdio JSON-RPC bridge for HexStrike HTTP API. - -This adapter implements the minimal MCP stdio JSON-RPC surface -expected by `pentestagent`'s `StdioTransport`: - -- Responds to `initialize` with a simple result -- Accepts `notifications/initialized` (notification) -- Implements `tools/list` returning one proxy tool: `http_api` -- Implements `tools/call` for the `http_api` tool and forwards - requests to the HexStrike HTTP API (configured via env var - `HEXSTRIKE_SERVER`, default `http://127.0.0.1:8888`). - -Usage (mcp_servers.json): - { - "mcpServers": { - "hexstrike-local": { - "command": "python3", - "args": ["-u", "third_party/hexstrike/hexstrike_stdio_adapter.py"], - "description": "StdIO adapter bridge to HexStrike HTTP API" - } - } - } -""" -from __future__ import annotations - -import json -import os -import sys -from typing import Any, Dict - -try: - import requests -except Exception: - requests = None - - -BASE = os.environ.get("HEXSTRIKE_SERVER", "http://127.0.0.1:8888").rstrip("/") - - -def send_response(req_id: Any, result: Any = None, error: Any = None) -> None: - resp: Dict[str, Any] = {"jsonrpc": "2.0", "id": req_id} - if error is not None: - resp["error"] = {"code": -32000, "message": str(error)} - else: - resp["result"] = result if result is not None else {} - print(json.dumps(resp, separators=(",", ":")), flush=True) - - -def handle_tools_list(req_id: Any) -> None: - tools = [{"name": "http_api", "description": "Proxy to HexStrike HTTP API"}] - send_response(req_id, {"tools": tools}) - - -def forward_http(path: str, method: str = "POST", params: Dict[str, Any] | None = None, body: Any | None = None) -> Any: - if requests is None: - raise RuntimeError("requests library is not available in adapter process") - url = path if path.startswith("http") else BASE + (path if path.startswith("/") else "/" + path) - method = (method or "POST").upper() - if method == "GET": - r = requests.get(url, params=params or {}, timeout=60) - else: - r = requests.post(url, json=body or {}, params=params or {}, timeout=300) - try: - return r.json() - except Exception: - return r.text - - -def handle_tools_call(req: Dict[str, Any]) -> None: - req_id = req.get("id") - params = req.get("params", {}) or {} - name = params.get("name") - arguments = params.get("arguments") or {} - - if name != "http_api": - send_response(req_id, error=f"unknown tool '{name}'") - return - - path = arguments.get("path") - if not path: - send_response(req_id, error="missing 'path' in arguments") - return - - method = arguments.get("method", "POST") - body = arguments.get("body") - qparams = arguments.get("params") - - try: - content = forward_http(path, method=method, params=qparams, body=body) - # Manager expects: result -> {"content": ...} - send_response(req_id, {"content": content}) - except Exception as e: - send_response(req_id, error=str(e)) - - -def main() -> None: - # Read newline-delimited JSON-RPC messages from stdin. - # Keep process alive until stdin is closed. - while True: - line = sys.stdin.readline() - if not line: - break - line = line.strip() - if not line: - continue - try: - req = json.loads(line) - except Exception: - # ignore invalid json - continue - - method = req.get("method") - # Notifications have no id - req_id = req.get("id") - - if method == "initialize": - send_response(req_id, {"capabilities": {}}) - elif method == "notifications/initialized": - # notification — no response - continue - elif method == "tools/list": - handle_tools_list(req_id) - elif method == "tools/call": - handle_tools_call(req) - else: - # unknown method - if req_id is not None: - send_response(req_id, error=f"unsupported method '{method}'") - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - pass diff --git a/third_party/mcp/stdio_adapter.py b/third_party/mcp/stdio_adapter.py deleted file mode 100644 index ae804b2..0000000 --- a/third_party/mcp/stdio_adapter.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -"""Generic stdio JSON-RPC adapter bridge to an HTTP API. - -Configure via environment variables: -- `STDIO_TARGET` (default: "http://127.0.0.1:8888") -- `STDIO_TOOLS` (JSON list of tool descriptors, default: `[{"name":"http_api","description":"Generic HTTP proxy"}]`) - -The adapter implements the minimal MCP/stdio surface required by -`pentestagent`'s `StdioTransport`: -- handle `initialize` and `notifications/initialized` -- respond to `tools/list` -- handle `tools/call` and forward to HTTP endpoints - -`tools/call` arguments format (generic): - {"path": "/api/foo", "method": "POST", "params": {...}, "body": {...} } - -This file is intentionally small and dependency-light; it uses `requests` -when available and returns response JSON or text. -""" -from __future__ import annotations - -import json -import os -import sys -from typing import Any, Dict, List - -try: - import requests -except Exception: - requests = None - - -TARGET = os.environ.get("STDIO_TARGET", "http://127.0.0.1:8888").rstrip("/") -_tools_env = os.environ.get("STDIO_TOOLS") -if _tools_env: - try: - TOOLS: List[Dict[str, str]] = json.loads(_tools_env) - except Exception: - TOOLS = [{"name": "http_api", "description": "Generic HTTP proxy"}] -else: - TOOLS = [{"name": "http_api", "description": "Generic HTTP proxy"}] - - -def _send(resp: Dict[str, Any]) -> None: - print(json.dumps(resp, separators=(",", ":")), flush=True) - - -def send_response(req_id: Any, result: Any = None, error: Any = None) -> None: - resp: Dict[str, Any] = {"jsonrpc": "2.0", "id": req_id} - if error is not None: - resp["error"] = {"code": -32000, "message": str(error)} - else: - resp["result"] = result if result is not None else {} - _send(resp) - - -def handle_tools_list(req_id: Any) -> None: - send_response(req_id, {"tools": TOOLS}) - - -def _http_forward(path: str, method: str = "POST", params: Dict[str, Any] | None = None, body: Any | None = None) -> Any: - if requests is None: - raise RuntimeError("`requests` not installed in adapter process") - url = path if path.startswith("http") else TARGET + (path if path.startswith("/") else "/" + path) - method = (method or "POST").upper() - if method == "GET": - r = requests.get(url, params=params or {}, timeout=60) - else: - r = requests.request(method, url, json=body or {}, params=params or {}, timeout=300) - try: - return r.json() - except Exception: - return r.text - - -def handle_tools_call(req: Dict[str, Any]) -> None: - req_id = req.get("id") - params = req.get("params", {}) or {} - name = params.get("name") - arguments = params.get("arguments") or {} - - # Validate tool - if not any(t.get("name") == name for t in TOOLS): - send_response(req_id, error=f"unknown tool '{name}'") - return - - path = arguments.get("path") - if not path: - send_response(req_id, error="missing 'path' in arguments") - return - - method = arguments.get("method", "POST") - body = arguments.get("body") - qparams = arguments.get("params") - - try: - content = _http_forward(path, method=method, params=qparams, body=body) - send_response(req_id, {"content": content}) - except Exception as e: - send_response(req_id, error=str(e)) - - -def main() -> None: - while True: - line = sys.stdin.readline() - if not line: - break - line = line.strip() - if not line: - continue - try: - req = json.loads(line) - except Exception: - continue - - method = req.get("method") - req_id = req.get("id") - - if method == "initialize": - send_response(req_id, {"capabilities": {}}) - elif method == "notifications/initialized": - # ignore notification - continue - elif method == "tools/list": - handle_tools_list(req_id) - elif method == "tools/call": - handle_tools_call(req) - else: - if req_id is not None: - send_response(req_id, error=f"unsupported method '{method}'") - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - pass