From 6ee0388835c86a7652a444106510e12bb96a2c00 Mon Sep 17 00:00:00 2001 From: FineComputer14451 Date: Mon, 7 Sep 2026 00:29:51 +0000 Subject: [PATCH] feat(grok-build-connect): HTTP MCP bridge for local grok CLI Add grokhunter-grok-build-connect for Termux and desktop: Bearer-auth HTTP MCP on :8766 wrapping grok version/plugins/mcp/ask plus jailed files, install/bootstrap/tunnel scripts, marketplace registration. --- .grok-plugin/marketplace.json | 33 ++- .grok-plugin/plugin-index.json | 24 ++ docs/MARKETPLACE.md | 2 + .../.cursor-plugin/plugin.json | 22 ++ .../.grok-plugin/plugin.json | 18 ++ plugins/grokhunter-grok-build-connect/LICENSE | 21 ++ .../grokhunter-grok-build-connect/README.md | 28 ++ .../assets/logo.png | Bin 0 -> 11968 bytes .../commands/grok-build-connect-install.md | 11 + .../commands/grok-build-connect-tunnel.md | 8 + .../skills/grok-build-connect/SKILL.md | 41 +++ .../grok-build-connect/scripts/bootstrap.sh | 143 ++++++++++ .../scripts/ensure-cloudflared.sh | 70 +++++ .../grok-build-connect/scripts/install.sh | 63 +++++ .../scripts/requirements.txt | 2 + .../grok-build-connect/scripts/server.py | 256 ++++++++++++++++++ .../scripts/tunnel-cloudflare.sh | 40 +++ .../scripts/tunnel-ngrok.sh | 10 + skills/grok-build-connect/SKILL.md | 41 +++ .../grok-build-connect/scripts/bootstrap.sh | 143 ++++++++++ .../scripts/ensure-cloudflared.sh | 70 +++++ skills/grok-build-connect/scripts/install.sh | 63 +++++ .../scripts/requirements.txt | 2 + skills/grok-build-connect/scripts/server.py | 256 ++++++++++++++++++ .../scripts/tunnel-cloudflare.sh | 40 +++ .../scripts/tunnel-ngrok.sh | 10 + 26 files changed, 1415 insertions(+), 2 deletions(-) create mode 100644 plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json create mode 100644 plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json create mode 100644 plugins/grokhunter-grok-build-connect/LICENSE create mode 100644 plugins/grokhunter-grok-build-connect/README.md create mode 100644 plugins/grokhunter-grok-build-connect/assets/logo.png create mode 100644 plugins/grokhunter-grok-build-connect/commands/grok-build-connect-install.md create mode 100644 plugins/grokhunter-grok-build-connect/commands/grok-build-connect-tunnel.md create mode 100644 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/SKILL.md create mode 100755 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh create mode 100755 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh create mode 100755 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh create mode 100644 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt create mode 100755 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py create mode 100755 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh create mode 100755 plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh create mode 100644 skills/grok-build-connect/SKILL.md create mode 100755 skills/grok-build-connect/scripts/bootstrap.sh create mode 100755 skills/grok-build-connect/scripts/ensure-cloudflared.sh create mode 100755 skills/grok-build-connect/scripts/install.sh create mode 100644 skills/grok-build-connect/scripts/requirements.txt create mode 100755 skills/grok-build-connect/scripts/server.py create mode 100755 skills/grok-build-connect/scripts/tunnel-cloudflare.sh create mode 100755 skills/grok-build-connect/scripts/tunnel-ngrok.sh diff --git a/.grok-plugin/marketplace.json b/.grok-plugin/marketplace.json index e8340b7..481e75b 100644 --- a/.grok-plugin/marketplace.json +++ b/.grok-plugin/marketplace.json @@ -463,8 +463,37 @@ "path": "./plugins/grokhunter-termux-connect" }, "homepage": "https://finecomputer14451.github.io/GrokHunter/", - "keywords": ["grokhunter termux connect", "grokhunter termux mcp", "termux connect grokhunter"], - "domains": ["finecomputer14451.github.io"] + "keywords": [ + "grokhunter termux connect", + "grokhunter termux mcp", + "termux connect grokhunter" + ], + "domains": [ + "finecomputer14451.github.io" + ] + }, + { + "name": "grokhunter-grok-build-connect", + "description": "Bridge Grok Bot to local Grok Build (grok CLI) via HTTP MCP + tunnel. Termux and desktop.", + "category": "development", + "version": "1.0.0", + "author": { + "name": "FineComputer14451", + "url": "https://github.com/FineComputer14451/GrokHunter" + }, + "source": { + "type": "local", + "path": "./plugins/grokhunter-grok-build-connect" + }, + "homepage": "https://finecomputer14451.github.io/GrokHunter/", + "keywords": [ + "grokhunter grok build connect", + "grok build mcp", + "grok connect" + ], + "domains": [ + "finecomputer14451.github.io" + ] } ] } diff --git a/.grok-plugin/plugin-index.json b/.grok-plugin/plugin-index.json index 76f1e64..f7adad5 100644 --- a/.grok-plugin/plugin-index.json +++ b/.grok-plugin/plugin-index.json @@ -748,6 +748,30 @@ } ], "version": "1.0.0" + }, + { + "agents": [], + "commands": [ + { + "description": "Install Grok Build Connect MCP on Termux or desktop", + "name": "grok-build-connect-install" + }, + { + "description": "Tunnel Grok Build Connect MCP with Cloudflare", + "name": "grok-build-connect-tunnel" + } + ], + "hooks": [], + "lspServers": [], + "mcpServers": [], + "name": "grokhunter-grok-build-connect", + "skills": [ + { + "description": "Bridge Grok Bot to the local Grok Build (grok) CLI via authenticated HTTP MCP + Cloudflare/ngrok tunnel on Termux or desktop.", + "name": "grok-build-connect" + } + ], + "version": "1.0.0" } ] } diff --git a/docs/MARKETPLACE.md b/docs/MARKETPLACE.md index 9d12a42..55dfef2 100644 --- a/docs/MARKETPLACE.md +++ b/docs/MARKETPLACE.md @@ -56,6 +56,7 @@ grok plugin install grokhunter-ci --trust | `grokhunter-desktop` | GrokHunter X11 desktop — grokhunter binds, nh-x11, bwrap black-screen triage (rootless). | | `grokhunter-overlay` | GrokHunter overlay cache — install.sh --overlay-only, MODULES_VERSION, PATH wrappers. | | `grokhunter-tookie` | Authorized public username OSINT with Tookie-OSINT (Sherlock-class). Scoped GrokHunter mod… | +| `grokhunter-grok-build-connect` | Bridge Grok Bot ↔ local `grok` CLI via HTTP MCP + tunnel (Termux + desktop). | | `grokhunter-termux-connect` | Bridge Grok Bot ↔ Termux via HTTP MCP + tunnel (not native Local Computer). | | `grokhunter-full` | Complete GrokHunter Grok Build pack — core, Coding Team, all lab specialists, desktop, ove… | @@ -88,6 +89,7 @@ Native Grok Bot Local Computer is Mac/Windows only. To drive GrokHunter on Termu ```bash grok plugin install grokhunter-termux-connect --trust +grok plugin install grokhunter-grok-build-connect --trust bash ~/.… # see plugin README / skill termux-connect ``` diff --git a/plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json b/plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json new file mode 100644 index 0000000..0e35266 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "grokhunter-grok-build-connect", + "version": "1.0.0", + "description": "Bridge Grok Bot to local Grok Build (grok CLI) via authenticated HTTP MCP + tunnel. Termux and desktop. Not affiliated with xAI / OffSec / Termux / jorexdeveloper.", + "author": { + "name": "FineComputer14451", + "url": "https://github.com/FineComputer14451/GrokHunter" + }, + "homepage": "https://finecomputer14451.github.io/GrokHunter/", + "repository": "https://github.com/FineComputer14451/GrokHunter", + "license": "MIT", + "keywords": [ + "grokhunter grok build connect", + "grok build mcp", + "grok connect" + ], + "logo": "assets/logo.png", + "displayName": "GrokHunter Grok Build Connect", + "category": "developer-tools", + "skills": "./skills/", + "commands": "./commands/" +} diff --git a/plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json b/plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json new file mode 100644 index 0000000..d2487ed --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json @@ -0,0 +1,18 @@ +{ + "name": "grokhunter-grok-build-connect", + "version": "1.0.0", + "description": "Bridge Grok Bot to local Grok Build (grok CLI) via authenticated HTTP MCP + tunnel. Termux and desktop. Not affiliated with xAI / OffSec / Termux / jorexdeveloper.", + "author": { + "name": "FineComputer14451", + "url": "https://github.com/FineComputer14451/GrokHunter" + }, + "homepage": "https://finecomputer14451.github.io/GrokHunter/", + "repository": "https://github.com/FineComputer14451/GrokHunter", + "license": "MIT", + "keywords": [ + "grokhunter grok build connect", + "grok build mcp", + "grok connect" + ], + "logo": "assets/logo.png" +} diff --git a/plugins/grokhunter-grok-build-connect/LICENSE b/plugins/grokhunter-grok-build-connect/LICENSE new file mode 100644 index 0000000..3a6be55 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GrokHunter contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/grokhunter-grok-build-connect/README.md b/plugins/grokhunter-grok-build-connect/README.md new file mode 100644 index 0000000..4626999 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/README.md @@ -0,0 +1,28 @@ +# grokhunter-grok-build-connect + +Bridge **Grok Bot → local Grok Build (`grok` CLI)** via authenticated HTTP MCP + tunnel. +Works on **Termux and desktop**. Not affiliated with xAI / OffSec / Termux / jorexdeveloper. + +## Why + +Grok Bot’s native Local Computer is Mac/Windows. This plugin exposes your installed +`grok` CLI (phone or desktop) over HTTPS MCP so the Bot can call version/plugins/ask tools. + +## Quick start + +```bash +curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +``` + +Default port **8766** (keeps Termux Connect on 8765 free). + +## Security + +- Listens on `127.0.0.1` only +- Bearer token required (min 24 chars) +- Path jail: `$HOME` (+ `$PREFIX` on Termux) +- Never commit `~/.grokhunter-grok-build-connect/token` + +## Credits + +See [CREDITS.md](https://github.com/FineComputer14451/GrokHunter/blob/main/CREDITS.md). diff --git a/plugins/grokhunter-grok-build-connect/assets/logo.png b/plugins/grokhunter-grok-build-connect/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..da352aa8a936635dbe9fc6841e6e1914c0cb893b GIT binary patch literal 11968 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 pT9kXl&^$6Rk- + Use when bridging Grok Bot to the local Grok Build (`grok`) CLI via HTTP MCP + + Cloudflare/ngrok tunnel on Termux or desktop. +--- +# Grok Build Connect (MVP) + +Expose the **local `grok` CLI** to Grok Bot through an authenticated **HTTP MCP** +(and a tunnel). Works on **Termux and desktop**. Port default **8766** (Termux +Connect uses 8765). + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +``` + +Or from a checkout: + +```bash +bash skills/grok-build-connect/scripts/install.sh +~/.grokhunter-grok-build-connect/start.sh +bash skills/grok-build-connect/scripts/tunnel-cloudflare.sh +``` + +## Grok Bot connector + +1. Add custom connector (chat / desktop / grok.com/connectors) +2. URL: `https:///mcp` +3. Auth: Bearer from `~/.grokhunter-grok-build-connect/token` — never paste in chat + +## Tools + +`grok_version`, `grok_plugin_list`, `grok_mcp_list`, `grok_mcp_doctor`, `grok_ask`, `list_dir`, `read_file` + +## Hard rules + +- Never print the bearer token or secrets.env +- Not Magisk/root; not native Android Local Computer +- Coding lab only diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh new file mode 100755 index 0000000..0b1d319 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Grok Build Connect — one-shot bootstrap (Termux or desktop). +# curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +set -euo pipefail + +REPO_URL="${GROKHUNTER_REPO_URL:-https://github.com/FineComputer14451/GrokHunter.git}" +BRANCH="${GROKHUNTER_BRANCH:-main}" +TARGET_DIR="${GROKHUNTER_DIR:-$HOME/GrokHunter}" +START_MCP=1 +START_TUNNEL=0 +START_FOREGROUND=0 +REGISTER_MARKETPLACE=1 +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +usage() { + cat <<'U' +GrokHunter Grok Build Connect bootstrap (Termux + desktop) + +Options: + --dir PATH Clone/update location (default: ~/GrokHunter) + --tunnel Also start Cloudflare quick tunnel (tmux session: gbc-tun) + --no-start Do not auto-start MCP in tmux + --start Start MCP in the foreground (blocks; skips tmux) + --no-marketplace Skip grok plugin marketplace add + -h, --help Show help + +Env: + GROKHUNTER_DIR, GROKHUNTER_REPO_URL, GROKHUNTER_BRANCH, GROK_BUILD_CONNECT_PORT +U +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) TARGET_DIR="$2"; shift 2 ;; + --tunnel) START_TUNNEL=1; shift ;; + --no-start) START_MCP=0; shift ;; + --start) START_FOREGROUND=1; START_MCP=0; shift ;; + --no-marketplace) REGISTER_MARKETPLACE=0; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;; + esac +done + +need() { + command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; } +} +need git +need python3 +need bash + +SCRIPTS="" +resolve_scripts() { + if [[ -f "$TARGET_DIR/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/skills/grok-build-connect/scripts" + elif [[ -f "$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts" + else + return 1 + fi +} + +echo "==> Ensuring GrokHunter at $TARGET_DIR ($BRANCH)" +if [[ -d "$TARGET_DIR/.git" ]]; then + git -C "$TARGET_DIR" fetch --depth 1 origin "$BRANCH" + git -C "$TARGET_DIR" checkout -B "$BRANCH" "origin/$BRANCH" 2>/dev/null \ + || { git -C "$TARGET_DIR" checkout "$BRANCH" 2>/dev/null || true; git -C "$TARGET_DIR" pull --ff-only origin "$BRANCH" || true; } +else + mkdir -p "$(dirname "$TARGET_DIR")" + if [[ -e "$TARGET_DIR" ]] && [[ ! -d "$TARGET_DIR/.git" ]]; then + echo "Path exists but is not a git repo: $TARGET_DIR" >&2 + exit 1 + fi + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$TARGET_DIR" +fi + +if ! resolve_scripts; then + echo "grok-build-connect scripts not found in $TARGET_DIR" >&2 + exit 1 +fi + +echo "==> Installing Grok Build Connect MCP" +bash "$SCRIPTS/install.sh" + +echo "==> Ensuring cloudflared" +bash "$SCRIPTS/ensure-cloudflared.sh" || echo "WARN: cloudflared ensure failed" >&2 + +if [[ "$REGISTER_MARKETPLACE" -eq 1 ]] && command -v grok >/dev/null 2>&1; then + grok plugin marketplace add FineComputer14451/GrokHunter 2>/dev/null || true + grok plugin install grokhunter-grok-build-connect --trust 2>/dev/null \ + || grok plugin install "$TARGET_DIR/plugins/grokhunter-grok-build-connect" --trust 2>/dev/null \ + || echo "WARN: plugin install skipped" >&2 +fi + +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" + +if [[ "$START_FOREGROUND" -eq 1 ]]; then + exec "$PREFIX_DIR/start.sh" +fi + +if [[ "$START_MCP" -eq 1 ]]; then + if ! command -v tmux >/dev/null 2>&1; then + echo "tmux missing — start manually: $PREFIX_DIR/start.sh" >&2 + else + tmux has-session -t gbc 2>/dev/null && tmux kill-session -t gbc + echo "==> Starting MCP in tmux session 'gbc'" + tmux new-session -d -s gbc "$PREFIX_DIR/start.sh" + for i in $(seq 1 20); do + curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1 && break + sleep 0.5 + done + if curl -fsS "http://127.0.0.1:${PORT}/healthz"; then + echo + echo "MCP healthy on :$PORT" + else + echo "WARN: MCP healthz not ready — tmux attach -t gbc" >&2 + fi + fi +fi + +if [[ "$START_TUNNEL" -eq 1 ]]; then + if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "Cannot start tunnel — MCP unhealthy" >&2 + exit 1 + fi + if command -v tmux >/dev/null 2>&1; then + tmux has-session -t gbc-tun 2>/dev/null && tmux kill-session -t gbc-tun + echo "==> Starting Cloudflare tunnel in tmux session 'gbc-tun'" + tmux new-session -d -s gbc-tun "bash '$SCRIPTS/tunnel-cloudflare.sh'" + echo "Attach: tmux attach -t gbc-tun" + else + exec bash "$SCRIPTS/tunnel-cloudflare.sh" + fi +fi + +cat </mcp + Token: $PREFIX_DIR/token (do not paste into chat) +EOF diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh new file mode 100755 index 0000000..30fda9a --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Ensure cloudflared is on PATH (Termux pkg or GitHub release binary). +set -euo pipefail + +if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 +fi + +BIN_DIR="${TERMUX_CONNECT_BIN:-$HOME/.grokhunter-termux-connect/bin}" +mkdir -p "$BIN_DIR" +TARGET="$BIN_DIR/cloudflared" + +if command -v pkg >/dev/null 2>&1; then + echo "==> Trying: pkg install cloudflared" + if pkg install -y cloudflared 2>/dev/null; then + if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 + fi + fi +fi + +arch="$(uname -m)" +case "$arch" in + aarch64|arm64) asset="cloudflared-linux-arm64" ;; + armv7l|armv8l|arm) asset="cloudflared-linux-arm" ;; + x86_64|amd64) asset="cloudflared-linux-amd64" ;; + i686|i386) asset="cloudflared-linux-386" ;; + *) + echo "Unsupported arch for cloudflared binary: $arch" >&2 + echo "Install manually: https://github.com/cloudflare/cloudflared/releases" >&2 + exit 1 + ;; +esac + +url="https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}" +echo "==> Downloading $url" +if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$TARGET" "$url" +elif command -v wget >/dev/null 2>&1; then + wget -q -O "$TARGET" "$url" +else + echo "Need curl or wget to fetch cloudflared" >&2 + exit 1 +fi +chmod +x "$TARGET" + +# prepend bin dir for this shell / recommend PATH +case ":$PATH:" in + *":$BIN_DIR:"*) ;; + *) export PATH="$BIN_DIR:$PATH" ;; +esac + +# persist hint +profile_snip="$HOME/.grokhunter-termux-connect/path.sh" +cat >"$profile_snip" <

/dev/null; then + echo "[ -f \"$profile_snip\" ] && . \"$profile_snip\"" >>"$HOME/.bashrc" +fi + +if ! command -v cloudflared >/dev/null 2>&1; then + echo "Installed to $TARGET — add to PATH: export PATH=\"$BIN_DIR:\$PATH\"" >&2 + # still usable via absolute path; create shim if ~/bin exists +fi +echo "cloudflared: $TARGET" +echo "CF_BIN=$TARGET" diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh new file mode 100755 index 0000000..ca14406 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Install Grok Build Connect MCP (Termux or desktop). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" +VENV="$PREFIX_DIR/venv" +TOKEN_FILE="$PREFIX_DIR/token" +ENV_FILE="$PREFIX_DIR/env" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +mkdir -p "$PREFIX_DIR" +python3 -m venv "$VENV" +# shellcheck disable=SC1091 +source "$VENV/bin/activate" +pip install -U pip +pip install -r "$ROOT/requirements.txt" +cp -f "$ROOT/server.py" "$PREFIX_DIR/server.py" +chmod +x "$PREFIX_DIR/server.py" + +if [[ ! -f "$TOKEN_FILE" ]] || [[ "$(wc -c <"$TOKEN_FILE" | tr -d ' ')" -lt 24 ]]; then + python3 -c 'import secrets; print(secrets.token_urlsafe(32))' >"$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" +fi +TOKEN="$(tr -d '[:space:]' <"$TOKEN_FILE")" + +cat >"$ENV_FILE" <"$PREFIX_DIR/start.sh" <<'EOS' +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/venv/bin/activate" +# shellcheck disable=SC1091 +source "$DIR/env" +if ! command -v grok >/dev/null 2>&1; then + echo "WARN: grok not on PATH — install Grok Build before using ask/plugin tools" >&2 +fi +exec python3 "$DIR/server.py" +EOS +chmod +x "$PREFIX_DIR/start.sh" + +cat >"$PREFIX_DIR/print-connector.txt" </mcp + Auth: Bearer (from $TOKEN_FILE — never chat it) + Port: $PORT (default; Termux Connect uses 8765) + +Start: $PREFIX_DIR/start.sh +Tunnel: bash $ROOT/tunnel-cloudflare.sh +Health: curl -s http://127.0.0.1:${PORT}/healthz +EOF + +echo "Installed → $PREFIX_DIR" +echo "Token file (mode 600): $TOKEN_FILE" +echo "Start: $PREFIX_DIR/start.sh" +echo "Works on Termux and desktop. Expose with tunnel-*.sh, register HTTPS URL in Grok Bot." diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt new file mode 100644 index 0000000..75f9516 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt @@ -0,0 +1,2 @@ +mcp>=1.9.0,<2 +uvicorn>=0.30.0 diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py new file mode 100755 index 0000000..97233df --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""GrokHunter Grok Build Connect — HTTP MCP bridging Grok Bot to the grok CLI. + +Works on Termux and desktop (Linux/macOS). Binds 127.0.0.1 by default; +put Cloudflare/ngrok in front. Requires Authorization: Bearer . +""" +from __future__ import annotations + +import os +import re +import secrets +import shutil +import subprocess +import sys +from pathlib import Path + +HOST = os.environ.get("GROK_BUILD_CONNECT_HOST", "127.0.0.1") +PORT = int(os.environ.get("GROK_BUILD_CONNECT_PORT", "8766")) +TOKEN = os.environ.get("GROK_BUILD_CONNECT_TOKEN", "").strip() +HOME = Path(os.environ.get("HOME", str(Path.home()))).resolve() +EXTRA_ROOTS = [ + Path(p).resolve() + for p in os.environ.get("GROK_BUILD_CONNECT_ROOTS", "").split(":") + if p.strip() +] +MAX_BYTES = int(os.environ.get("GROK_BUILD_CONNECT_MAX_BYTES", str(256 * 1024))) +ASK_TIMEOUT = int(os.environ.get("GROK_BUILD_CONNECT_ASK_TIMEOUT", "180")) +ASK_MAX_CHARS = int(os.environ.get("GROK_BUILD_CONNECT_ASK_MAX_CHARS", "8000")) +CWD_DEFAULT = Path(os.environ.get("GROK_BUILD_CONNECT_CWD", str(HOME))).expanduser().resolve() + +# Optional override for grok binary +GROK_BIN = os.environ.get("GROK_BUILD_CONNECT_GROK", "").strip() + + +def _grok() -> str: + if GROK_BIN: + return GROK_BIN + found = shutil.which("grok") + if not found: + raise FileNotFoundError( + "grok CLI not found on PATH. Install Grok Build, then re-run start.sh." + ) + return found + + +def _roots() -> list[Path]: + roots = [HOME, CWD_DEFAULT, *EXTRA_ROOTS] + prefix = os.environ.get("PREFIX") + if prefix: + roots.append(Path(prefix).resolve()) + out: list[Path] = [] + seen = set() + for r in roots: + s = str(r) + if s not in seen: + seen.add(s) + out.append(r) + return out + + +def _jail(path: str) -> Path: + raw = Path(path).expanduser() + if not raw.is_absolute(): + raw = CWD_DEFAULT / raw + resolved = raw.resolve() + for root in _roots(): + try: + resolved.relative_to(root) + return resolved + except ValueError: + continue + raise PermissionError(f"path outside jail: {path}") + + +def _check_token() -> None: + if not TOKEN or len(TOKEN) < 24: + print( + "GROK_BUILD_CONNECT_TOKEN missing or too short (min 24 chars).\n" + 'Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"', + file=sys.stderr, + ) + sys.exit(2) + + +def _run(argv: list[str], timeout: int | None = None, cwd: Path | None = None) -> str: + try: + proc = subprocess.run( + argv, + capture_output=True, + text=True, + timeout=timeout or 90, + cwd=str(cwd or CWD_DEFAULT), + env={**os.environ, "TERM": os.environ.get("TERM", "xterm-256color"), "CI": "1"}, + ) + except FileNotFoundError as e: + return f"not found: {e}" + except subprocess.TimeoutExpired: + return f"timeout after {timeout or 90}s" + out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "") + raw = out.encode("utf-8", errors="replace") + if len(raw) > MAX_BYTES: + out = raw[:MAX_BYTES].decode("utf-8", errors="replace") + "\n…[truncated]" + return f"exit {proc.returncode}\n{out}".rstrip() + + +def main() -> None: + _check_token() + try: + from mcp.server.fastmcp import FastMCP + from mcp.server.transport_security import TransportSecuritySettings + from starlette.middleware.base import BaseHTTPMiddleware + from starlette.requests import Request + from starlette.responses import JSONResponse + from starlette.routing import Route + import uvicorn + except ImportError as e: + print("Missing deps. Run: pip install -r requirements.txt\n" + str(e), file=sys.stderr) + sys.exit(1) + + # Cloudflare quick tunnels send Host: *.trycloudflare.com (see termux-connect fix). + transport_security = TransportSecuritySettings(enable_dns_rebinding_protection=False) + extra_hosts = [ + h.strip() + for h in os.environ.get("GROK_BUILD_CONNECT_ALLOWED_HOSTS", "").split(",") + if h.strip() + ] + if extra_hosts: + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=[ + f"{HOST}:{PORT}", + HOST, + "localhost", + f"localhost:{PORT}", + "127.0.0.1:*", + "localhost:*", + *extra_hosts, + ], + ) + + mcp = FastMCP( + "grokhunter-grok-build-connect", + instructions=( + "Scoped tools for the local Grok Build (`grok`) CLI on Termux or desktop. " + "Prefer grok_version / grok_plugin_list / grok_ask. Paths are jailed to HOME." + ), + host=HOST, + port=PORT, + streamable_http_path="/mcp", + stateless_http=True, + transport_security=transport_security, + ) + + @mcp.tool() + def grok_version() -> str: + """Return `grok --version` and resolved binary path.""" + g = _grok() + return f"binary={g}\n" + _run([g, "--version"], timeout=30) + + @mcp.tool() + def grok_plugin_list() -> str: + """List installed Grok Build plugins (`grok plugin list`).""" + return _run([_grok(), "plugin", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_list() -> str: + """List MCP servers configured in Grok Build (`grok mcp list`).""" + return _run([_grok(), "mcp", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_doctor() -> str: + """Diagnose Grok Build MCP config (`grok mcp doctor`).""" + return _run([_grok(), "mcp", "doctor"], timeout=90) + + @mcp.tool() + def grok_ask(prompt: str, model: str = "", cwd: str = "") -> str: + """Single-turn headless `grok -p` (prints response and exits). Caps prompt length.""" + p = (prompt or "").strip() + if not p: + return "empty prompt" + if len(p) > ASK_MAX_CHARS: + return f"prompt too long (max {ASK_MAX_CHARS} chars)" + work = _jail(cwd) if cwd.strip() else CWD_DEFAULT + if not work.is_dir(): + return f"cwd not a directory: {work}" + argv = [_grok(), "-p", p, "--output-format", "text"] + if model.strip(): + argv.extend(["-m", model.strip()]) + return _run(argv, timeout=ASK_TIMEOUT, cwd=work) + + @mcp.tool() + def list_dir(path: str = ".") -> str: + """List a directory under the jail (HOME / PREFIX / configured cwd).""" + target = _jail(path) + if not target.is_dir(): + return f"not a directory: {target}" + lines = [] + for child in sorted(target.iterdir(), key=lambda p: p.name.lower())[:500]: + kind = "d" if child.is_dir() else "f" + lines.append(f"{kind}\t{child.name}") + return "\n".join(lines) or "(empty)" + + @mcp.tool() + def read_file(path: str, max_bytes: int = 65536) -> str: + """Read a text file under the jail (capped).""" + target = _jail(path) + if not target.is_file(): + return f"not a file: {target}" + n = min(max(1, max_bytes), MAX_BYTES) + data = target.read_bytes()[: n + 1] + truncated = len(data) > n + text = data[:n].decode("utf-8", errors="replace") + if truncated: + text += "\n…[truncated]" + return text + + class BearerAuth(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.url.path in {"/healthz", "/"}: + return await call_next(request) + auth = request.headers.get("authorization", "") + expected = f"Bearer {TOKEN}" + if not secrets.compare_digest(auth, expected): + return JSONResponse({"error": "unauthorized"}, status_code=401) + return await call_next(request) + + async def healthz(request: Request): + grok_ok = False + try: + _grok() + grok_ok = True + except Exception: + grok_ok = False + return JSONResponse( + { + "ok": True, + "service": "grokhunter-grok-build-connect", + "grok_on_path": grok_ok, + "port": PORT, + } + ) + + app = mcp.streamable_http_app() + app.add_middleware(BearerAuth) + app.routes.insert(0, Route("/healthz", healthz, methods=["GET"])) + + print( + f"grok-build-connect listening on http://{HOST}:{PORT}/mcp " + f"(cwd={CWD_DEFAULT}, jail={_roots()})", + flush=True, + ) + uvicorn.run(app, host=HOST, port=PORT, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh new file mode 100755 index 0000000..5c1a602 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Cloudflare quick tunnel in front of Grok Build Connect MCP. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +ensure_log="$(mktemp)" +bash "$ROOT/ensure-cloudflared.sh" | tee "$ensure_log" +CF="" +if command -v cloudflared >/dev/null 2>&1; then + CF="$(command -v cloudflared)" +elif [[ -x "$HOME/.grokhunter-grok-build-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-grok-build-connect/bin/cloudflared" +elif [[ -x "$HOME/.grokhunter-termux-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-termux-connect/bin/cloudflared" +else + CF="$(grep '^CF_BIN=' "$ensure_log" | tail -n1 | cut -d= -f2- || true)" +fi +rm -f "$ensure_log" + +if [[ -z "${CF:-}" || ! -x "$CF" ]]; then + echo "cloudflared still missing" >&2 + exit 1 +fi + +for i in $(seq 1 15); do + if curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + break + fi + sleep 1 +done +if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "MCP not healthy on :${PORT} — start it first (tmux attach -t gbc)" >&2 + exit 1 +fi + +echo "Starting quick tunnel → http://127.0.0.1:${PORT}" +echo "Copy the https://….trycloudflare.com URL, append /mcp" +echo "Bearer token file: ~/.grokhunter-grok-build-connect/token (do not paste into chat)" +exec "$CF" tunnel --url "http://127.0.0.1:${PORT}" diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh new file mode 100755 index 0000000..9e88937 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" +if ! command -v ngrok >/dev/null 2>&1; then + echo "Install ngrok or use tunnel-cloudflare.sh" + exit 1 +fi +echo "Starting ngrok → http://127.0.0.1:${PORT}" +echo "Copy the https URL, append /mcp, Bearer from ~/.grokhunter-grok-build-connect/token" +exec ngrok http "$PORT" diff --git a/skills/grok-build-connect/SKILL.md b/skills/grok-build-connect/SKILL.md new file mode 100644 index 0000000..0fb9572 --- /dev/null +++ b/skills/grok-build-connect/SKILL.md @@ -0,0 +1,41 @@ +--- +name: grok-build-connect +description: >- + Use when bridging Grok Bot to the local Grok Build (`grok`) CLI via HTTP MCP + + Cloudflare/ngrok tunnel on Termux or desktop. +--- +# Grok Build Connect (MVP) + +Expose the **local `grok` CLI** to Grok Bot through an authenticated **HTTP MCP** +(and a tunnel). Works on **Termux and desktop**. Port default **8766** (Termux +Connect uses 8765). + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +``` + +Or from a checkout: + +```bash +bash skills/grok-build-connect/scripts/install.sh +~/.grokhunter-grok-build-connect/start.sh +bash skills/grok-build-connect/scripts/tunnel-cloudflare.sh +``` + +## Grok Bot connector + +1. Add custom connector (chat / desktop / grok.com/connectors) +2. URL: `https:///mcp` +3. Auth: Bearer from `~/.grokhunter-grok-build-connect/token` — never paste in chat + +## Tools + +`grok_version`, `grok_plugin_list`, `grok_mcp_list`, `grok_mcp_doctor`, `grok_ask`, `list_dir`, `read_file` + +## Hard rules + +- Never print the bearer token or secrets.env +- Not Magisk/root; not native Android Local Computer +- Coding lab only diff --git a/skills/grok-build-connect/scripts/bootstrap.sh b/skills/grok-build-connect/scripts/bootstrap.sh new file mode 100755 index 0000000..0b1d319 --- /dev/null +++ b/skills/grok-build-connect/scripts/bootstrap.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Grok Build Connect — one-shot bootstrap (Termux or desktop). +# curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +set -euo pipefail + +REPO_URL="${GROKHUNTER_REPO_URL:-https://github.com/FineComputer14451/GrokHunter.git}" +BRANCH="${GROKHUNTER_BRANCH:-main}" +TARGET_DIR="${GROKHUNTER_DIR:-$HOME/GrokHunter}" +START_MCP=1 +START_TUNNEL=0 +START_FOREGROUND=0 +REGISTER_MARKETPLACE=1 +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +usage() { + cat <<'U' +GrokHunter Grok Build Connect bootstrap (Termux + desktop) + +Options: + --dir PATH Clone/update location (default: ~/GrokHunter) + --tunnel Also start Cloudflare quick tunnel (tmux session: gbc-tun) + --no-start Do not auto-start MCP in tmux + --start Start MCP in the foreground (blocks; skips tmux) + --no-marketplace Skip grok plugin marketplace add + -h, --help Show help + +Env: + GROKHUNTER_DIR, GROKHUNTER_REPO_URL, GROKHUNTER_BRANCH, GROK_BUILD_CONNECT_PORT +U +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) TARGET_DIR="$2"; shift 2 ;; + --tunnel) START_TUNNEL=1; shift ;; + --no-start) START_MCP=0; shift ;; + --start) START_FOREGROUND=1; START_MCP=0; shift ;; + --no-marketplace) REGISTER_MARKETPLACE=0; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;; + esac +done + +need() { + command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; } +} +need git +need python3 +need bash + +SCRIPTS="" +resolve_scripts() { + if [[ -f "$TARGET_DIR/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/skills/grok-build-connect/scripts" + elif [[ -f "$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts" + else + return 1 + fi +} + +echo "==> Ensuring GrokHunter at $TARGET_DIR ($BRANCH)" +if [[ -d "$TARGET_DIR/.git" ]]; then + git -C "$TARGET_DIR" fetch --depth 1 origin "$BRANCH" + git -C "$TARGET_DIR" checkout -B "$BRANCH" "origin/$BRANCH" 2>/dev/null \ + || { git -C "$TARGET_DIR" checkout "$BRANCH" 2>/dev/null || true; git -C "$TARGET_DIR" pull --ff-only origin "$BRANCH" || true; } +else + mkdir -p "$(dirname "$TARGET_DIR")" + if [[ -e "$TARGET_DIR" ]] && [[ ! -d "$TARGET_DIR/.git" ]]; then + echo "Path exists but is not a git repo: $TARGET_DIR" >&2 + exit 1 + fi + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$TARGET_DIR" +fi + +if ! resolve_scripts; then + echo "grok-build-connect scripts not found in $TARGET_DIR" >&2 + exit 1 +fi + +echo "==> Installing Grok Build Connect MCP" +bash "$SCRIPTS/install.sh" + +echo "==> Ensuring cloudflared" +bash "$SCRIPTS/ensure-cloudflared.sh" || echo "WARN: cloudflared ensure failed" >&2 + +if [[ "$REGISTER_MARKETPLACE" -eq 1 ]] && command -v grok >/dev/null 2>&1; then + grok plugin marketplace add FineComputer14451/GrokHunter 2>/dev/null || true + grok plugin install grokhunter-grok-build-connect --trust 2>/dev/null \ + || grok plugin install "$TARGET_DIR/plugins/grokhunter-grok-build-connect" --trust 2>/dev/null \ + || echo "WARN: plugin install skipped" >&2 +fi + +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" + +if [[ "$START_FOREGROUND" -eq 1 ]]; then + exec "$PREFIX_DIR/start.sh" +fi + +if [[ "$START_MCP" -eq 1 ]]; then + if ! command -v tmux >/dev/null 2>&1; then + echo "tmux missing — start manually: $PREFIX_DIR/start.sh" >&2 + else + tmux has-session -t gbc 2>/dev/null && tmux kill-session -t gbc + echo "==> Starting MCP in tmux session 'gbc'" + tmux new-session -d -s gbc "$PREFIX_DIR/start.sh" + for i in $(seq 1 20); do + curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1 && break + sleep 0.5 + done + if curl -fsS "http://127.0.0.1:${PORT}/healthz"; then + echo + echo "MCP healthy on :$PORT" + else + echo "WARN: MCP healthz not ready — tmux attach -t gbc" >&2 + fi + fi +fi + +if [[ "$START_TUNNEL" -eq 1 ]]; then + if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "Cannot start tunnel — MCP unhealthy" >&2 + exit 1 + fi + if command -v tmux >/dev/null 2>&1; then + tmux has-session -t gbc-tun 2>/dev/null && tmux kill-session -t gbc-tun + echo "==> Starting Cloudflare tunnel in tmux session 'gbc-tun'" + tmux new-session -d -s gbc-tun "bash '$SCRIPTS/tunnel-cloudflare.sh'" + echo "Attach: tmux attach -t gbc-tun" + else + exec bash "$SCRIPTS/tunnel-cloudflare.sh" + fi +fi + +cat </mcp + Token: $PREFIX_DIR/token (do not paste into chat) +EOF diff --git a/skills/grok-build-connect/scripts/ensure-cloudflared.sh b/skills/grok-build-connect/scripts/ensure-cloudflared.sh new file mode 100755 index 0000000..30fda9a --- /dev/null +++ b/skills/grok-build-connect/scripts/ensure-cloudflared.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Ensure cloudflared is on PATH (Termux pkg or GitHub release binary). +set -euo pipefail + +if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 +fi + +BIN_DIR="${TERMUX_CONNECT_BIN:-$HOME/.grokhunter-termux-connect/bin}" +mkdir -p "$BIN_DIR" +TARGET="$BIN_DIR/cloudflared" + +if command -v pkg >/dev/null 2>&1; then + echo "==> Trying: pkg install cloudflared" + if pkg install -y cloudflared 2>/dev/null; then + if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 + fi + fi +fi + +arch="$(uname -m)" +case "$arch" in + aarch64|arm64) asset="cloudflared-linux-arm64" ;; + armv7l|armv8l|arm) asset="cloudflared-linux-arm" ;; + x86_64|amd64) asset="cloudflared-linux-amd64" ;; + i686|i386) asset="cloudflared-linux-386" ;; + *) + echo "Unsupported arch for cloudflared binary: $arch" >&2 + echo "Install manually: https://github.com/cloudflare/cloudflared/releases" >&2 + exit 1 + ;; +esac + +url="https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}" +echo "==> Downloading $url" +if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$TARGET" "$url" +elif command -v wget >/dev/null 2>&1; then + wget -q -O "$TARGET" "$url" +else + echo "Need curl or wget to fetch cloudflared" >&2 + exit 1 +fi +chmod +x "$TARGET" + +# prepend bin dir for this shell / recommend PATH +case ":$PATH:" in + *":$BIN_DIR:"*) ;; + *) export PATH="$BIN_DIR:$PATH" ;; +esac + +# persist hint +profile_snip="$HOME/.grokhunter-termux-connect/path.sh" +cat >"$profile_snip" <

/dev/null; then + echo "[ -f \"$profile_snip\" ] && . \"$profile_snip\"" >>"$HOME/.bashrc" +fi + +if ! command -v cloudflared >/dev/null 2>&1; then + echo "Installed to $TARGET — add to PATH: export PATH=\"$BIN_DIR:\$PATH\"" >&2 + # still usable via absolute path; create shim if ~/bin exists +fi +echo "cloudflared: $TARGET" +echo "CF_BIN=$TARGET" diff --git a/skills/grok-build-connect/scripts/install.sh b/skills/grok-build-connect/scripts/install.sh new file mode 100755 index 0000000..ca14406 --- /dev/null +++ b/skills/grok-build-connect/scripts/install.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Install Grok Build Connect MCP (Termux or desktop). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" +VENV="$PREFIX_DIR/venv" +TOKEN_FILE="$PREFIX_DIR/token" +ENV_FILE="$PREFIX_DIR/env" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +mkdir -p "$PREFIX_DIR" +python3 -m venv "$VENV" +# shellcheck disable=SC1091 +source "$VENV/bin/activate" +pip install -U pip +pip install -r "$ROOT/requirements.txt" +cp -f "$ROOT/server.py" "$PREFIX_DIR/server.py" +chmod +x "$PREFIX_DIR/server.py" + +if [[ ! -f "$TOKEN_FILE" ]] || [[ "$(wc -c <"$TOKEN_FILE" | tr -d ' ')" -lt 24 ]]; then + python3 -c 'import secrets; print(secrets.token_urlsafe(32))' >"$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" +fi +TOKEN="$(tr -d '[:space:]' <"$TOKEN_FILE")" + +cat >"$ENV_FILE" <"$PREFIX_DIR/start.sh" <<'EOS' +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/venv/bin/activate" +# shellcheck disable=SC1091 +source "$DIR/env" +if ! command -v grok >/dev/null 2>&1; then + echo "WARN: grok not on PATH — install Grok Build before using ask/plugin tools" >&2 +fi +exec python3 "$DIR/server.py" +EOS +chmod +x "$PREFIX_DIR/start.sh" + +cat >"$PREFIX_DIR/print-connector.txt" </mcp + Auth: Bearer (from $TOKEN_FILE — never chat it) + Port: $PORT (default; Termux Connect uses 8765) + +Start: $PREFIX_DIR/start.sh +Tunnel: bash $ROOT/tunnel-cloudflare.sh +Health: curl -s http://127.0.0.1:${PORT}/healthz +EOF + +echo "Installed → $PREFIX_DIR" +echo "Token file (mode 600): $TOKEN_FILE" +echo "Start: $PREFIX_DIR/start.sh" +echo "Works on Termux and desktop. Expose with tunnel-*.sh, register HTTPS URL in Grok Bot." diff --git a/skills/grok-build-connect/scripts/requirements.txt b/skills/grok-build-connect/scripts/requirements.txt new file mode 100644 index 0000000..75f9516 --- /dev/null +++ b/skills/grok-build-connect/scripts/requirements.txt @@ -0,0 +1,2 @@ +mcp>=1.9.0,<2 +uvicorn>=0.30.0 diff --git a/skills/grok-build-connect/scripts/server.py b/skills/grok-build-connect/scripts/server.py new file mode 100755 index 0000000..97233df --- /dev/null +++ b/skills/grok-build-connect/scripts/server.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""GrokHunter Grok Build Connect — HTTP MCP bridging Grok Bot to the grok CLI. + +Works on Termux and desktop (Linux/macOS). Binds 127.0.0.1 by default; +put Cloudflare/ngrok in front. Requires Authorization: Bearer . +""" +from __future__ import annotations + +import os +import re +import secrets +import shutil +import subprocess +import sys +from pathlib import Path + +HOST = os.environ.get("GROK_BUILD_CONNECT_HOST", "127.0.0.1") +PORT = int(os.environ.get("GROK_BUILD_CONNECT_PORT", "8766")) +TOKEN = os.environ.get("GROK_BUILD_CONNECT_TOKEN", "").strip() +HOME = Path(os.environ.get("HOME", str(Path.home()))).resolve() +EXTRA_ROOTS = [ + Path(p).resolve() + for p in os.environ.get("GROK_BUILD_CONNECT_ROOTS", "").split(":") + if p.strip() +] +MAX_BYTES = int(os.environ.get("GROK_BUILD_CONNECT_MAX_BYTES", str(256 * 1024))) +ASK_TIMEOUT = int(os.environ.get("GROK_BUILD_CONNECT_ASK_TIMEOUT", "180")) +ASK_MAX_CHARS = int(os.environ.get("GROK_BUILD_CONNECT_ASK_MAX_CHARS", "8000")) +CWD_DEFAULT = Path(os.environ.get("GROK_BUILD_CONNECT_CWD", str(HOME))).expanduser().resolve() + +# Optional override for grok binary +GROK_BIN = os.environ.get("GROK_BUILD_CONNECT_GROK", "").strip() + + +def _grok() -> str: + if GROK_BIN: + return GROK_BIN + found = shutil.which("grok") + if not found: + raise FileNotFoundError( + "grok CLI not found on PATH. Install Grok Build, then re-run start.sh." + ) + return found + + +def _roots() -> list[Path]: + roots = [HOME, CWD_DEFAULT, *EXTRA_ROOTS] + prefix = os.environ.get("PREFIX") + if prefix: + roots.append(Path(prefix).resolve()) + out: list[Path] = [] + seen = set() + for r in roots: + s = str(r) + if s not in seen: + seen.add(s) + out.append(r) + return out + + +def _jail(path: str) -> Path: + raw = Path(path).expanduser() + if not raw.is_absolute(): + raw = CWD_DEFAULT / raw + resolved = raw.resolve() + for root in _roots(): + try: + resolved.relative_to(root) + return resolved + except ValueError: + continue + raise PermissionError(f"path outside jail: {path}") + + +def _check_token() -> None: + if not TOKEN or len(TOKEN) < 24: + print( + "GROK_BUILD_CONNECT_TOKEN missing or too short (min 24 chars).\n" + 'Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"', + file=sys.stderr, + ) + sys.exit(2) + + +def _run(argv: list[str], timeout: int | None = None, cwd: Path | None = None) -> str: + try: + proc = subprocess.run( + argv, + capture_output=True, + text=True, + timeout=timeout or 90, + cwd=str(cwd or CWD_DEFAULT), + env={**os.environ, "TERM": os.environ.get("TERM", "xterm-256color"), "CI": "1"}, + ) + except FileNotFoundError as e: + return f"not found: {e}" + except subprocess.TimeoutExpired: + return f"timeout after {timeout or 90}s" + out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "") + raw = out.encode("utf-8", errors="replace") + if len(raw) > MAX_BYTES: + out = raw[:MAX_BYTES].decode("utf-8", errors="replace") + "\n…[truncated]" + return f"exit {proc.returncode}\n{out}".rstrip() + + +def main() -> None: + _check_token() + try: + from mcp.server.fastmcp import FastMCP + from mcp.server.transport_security import TransportSecuritySettings + from starlette.middleware.base import BaseHTTPMiddleware + from starlette.requests import Request + from starlette.responses import JSONResponse + from starlette.routing import Route + import uvicorn + except ImportError as e: + print("Missing deps. Run: pip install -r requirements.txt\n" + str(e), file=sys.stderr) + sys.exit(1) + + # Cloudflare quick tunnels send Host: *.trycloudflare.com (see termux-connect fix). + transport_security = TransportSecuritySettings(enable_dns_rebinding_protection=False) + extra_hosts = [ + h.strip() + for h in os.environ.get("GROK_BUILD_CONNECT_ALLOWED_HOSTS", "").split(",") + if h.strip() + ] + if extra_hosts: + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=[ + f"{HOST}:{PORT}", + HOST, + "localhost", + f"localhost:{PORT}", + "127.0.0.1:*", + "localhost:*", + *extra_hosts, + ], + ) + + mcp = FastMCP( + "grokhunter-grok-build-connect", + instructions=( + "Scoped tools for the local Grok Build (`grok`) CLI on Termux or desktop. " + "Prefer grok_version / grok_plugin_list / grok_ask. Paths are jailed to HOME." + ), + host=HOST, + port=PORT, + streamable_http_path="/mcp", + stateless_http=True, + transport_security=transport_security, + ) + + @mcp.tool() + def grok_version() -> str: + """Return `grok --version` and resolved binary path.""" + g = _grok() + return f"binary={g}\n" + _run([g, "--version"], timeout=30) + + @mcp.tool() + def grok_plugin_list() -> str: + """List installed Grok Build plugins (`grok plugin list`).""" + return _run([_grok(), "plugin", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_list() -> str: + """List MCP servers configured in Grok Build (`grok mcp list`).""" + return _run([_grok(), "mcp", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_doctor() -> str: + """Diagnose Grok Build MCP config (`grok mcp doctor`).""" + return _run([_grok(), "mcp", "doctor"], timeout=90) + + @mcp.tool() + def grok_ask(prompt: str, model: str = "", cwd: str = "") -> str: + """Single-turn headless `grok -p` (prints response and exits). Caps prompt length.""" + p = (prompt or "").strip() + if not p: + return "empty prompt" + if len(p) > ASK_MAX_CHARS: + return f"prompt too long (max {ASK_MAX_CHARS} chars)" + work = _jail(cwd) if cwd.strip() else CWD_DEFAULT + if not work.is_dir(): + return f"cwd not a directory: {work}" + argv = [_grok(), "-p", p, "--output-format", "text"] + if model.strip(): + argv.extend(["-m", model.strip()]) + return _run(argv, timeout=ASK_TIMEOUT, cwd=work) + + @mcp.tool() + def list_dir(path: str = ".") -> str: + """List a directory under the jail (HOME / PREFIX / configured cwd).""" + target = _jail(path) + if not target.is_dir(): + return f"not a directory: {target}" + lines = [] + for child in sorted(target.iterdir(), key=lambda p: p.name.lower())[:500]: + kind = "d" if child.is_dir() else "f" + lines.append(f"{kind}\t{child.name}") + return "\n".join(lines) or "(empty)" + + @mcp.tool() + def read_file(path: str, max_bytes: int = 65536) -> str: + """Read a text file under the jail (capped).""" + target = _jail(path) + if not target.is_file(): + return f"not a file: {target}" + n = min(max(1, max_bytes), MAX_BYTES) + data = target.read_bytes()[: n + 1] + truncated = len(data) > n + text = data[:n].decode("utf-8", errors="replace") + if truncated: + text += "\n…[truncated]" + return text + + class BearerAuth(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.url.path in {"/healthz", "/"}: + return await call_next(request) + auth = request.headers.get("authorization", "") + expected = f"Bearer {TOKEN}" + if not secrets.compare_digest(auth, expected): + return JSONResponse({"error": "unauthorized"}, status_code=401) + return await call_next(request) + + async def healthz(request: Request): + grok_ok = False + try: + _grok() + grok_ok = True + except Exception: + grok_ok = False + return JSONResponse( + { + "ok": True, + "service": "grokhunter-grok-build-connect", + "grok_on_path": grok_ok, + "port": PORT, + } + ) + + app = mcp.streamable_http_app() + app.add_middleware(BearerAuth) + app.routes.insert(0, Route("/healthz", healthz, methods=["GET"])) + + print( + f"grok-build-connect listening on http://{HOST}:{PORT}/mcp " + f"(cwd={CWD_DEFAULT}, jail={_roots()})", + flush=True, + ) + uvicorn.run(app, host=HOST, port=PORT, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/skills/grok-build-connect/scripts/tunnel-cloudflare.sh b/skills/grok-build-connect/scripts/tunnel-cloudflare.sh new file mode 100755 index 0000000..5c1a602 --- /dev/null +++ b/skills/grok-build-connect/scripts/tunnel-cloudflare.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Cloudflare quick tunnel in front of Grok Build Connect MCP. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +ensure_log="$(mktemp)" +bash "$ROOT/ensure-cloudflared.sh" | tee "$ensure_log" +CF="" +if command -v cloudflared >/dev/null 2>&1; then + CF="$(command -v cloudflared)" +elif [[ -x "$HOME/.grokhunter-grok-build-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-grok-build-connect/bin/cloudflared" +elif [[ -x "$HOME/.grokhunter-termux-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-termux-connect/bin/cloudflared" +else + CF="$(grep '^CF_BIN=' "$ensure_log" | tail -n1 | cut -d= -f2- || true)" +fi +rm -f "$ensure_log" + +if [[ -z "${CF:-}" || ! -x "$CF" ]]; then + echo "cloudflared still missing" >&2 + exit 1 +fi + +for i in $(seq 1 15); do + if curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + break + fi + sleep 1 +done +if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "MCP not healthy on :${PORT} — start it first (tmux attach -t gbc)" >&2 + exit 1 +fi + +echo "Starting quick tunnel → http://127.0.0.1:${PORT}" +echo "Copy the https://….trycloudflare.com URL, append /mcp" +echo "Bearer token file: ~/.grokhunter-grok-build-connect/token (do not paste into chat)" +exec "$CF" tunnel --url "http://127.0.0.1:${PORT}" diff --git a/skills/grok-build-connect/scripts/tunnel-ngrok.sh b/skills/grok-build-connect/scripts/tunnel-ngrok.sh new file mode 100755 index 0000000..9e88937 --- /dev/null +++ b/skills/grok-build-connect/scripts/tunnel-ngrok.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" +if ! command -v ngrok >/dev/null 2>&1; then + echo "Install ngrok or use tunnel-cloudflare.sh" + exit 1 +fi +echo "Starting ngrok → http://127.0.0.1:${PORT}" +echo "Copy the https URL, append /mcp, Bearer from ~/.grokhunter-grok-build-connect/token" +exec ngrok http "$PORT"