Outbound MCP server that drives Grok Bot cloud agents via grokbot-client
  • Python 99.6%
  • Dockerfile 0.4%
Find a file
Kenzi Marcel cdc6db4c7e
All checks were successful
OWASP / dependency-check (push) Successful in 43s
ci / test (push) Successful in 57s
Container Build / build (push) Successful in 1m31s
SonarQube / sonarqube (push) Successful in 8m40s
feat: add Cursor browser PKCE login and systemd unit
2026-09-27 14:50:02 +00:00
.forgejo/workflows feat: add grokbot MCP server 2026-09-27 14:10:15 +00:00
deploy feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
grokbot_mcp feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
tests feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
.dockerignore feat: add grokbot MCP server 2026-09-27 14:10:15 +00:00
.env.example feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
.gitignore feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
docker-compose.yml feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
Dockerfile feat: add grokbot MCP server 2026-09-27 14:10:15 +00:00
LICENSE feat: add grokbot MCP server 2026-09-27 14:10:15 +00:00
pyproject.toml feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
README.md feat: add Cursor browser PKCE login and systemd unit 2026-09-27 14:50:02 +00:00
sonar-project.properties feat: add grokbot MCP server 2026-09-27 14:10:15 +00:00

grokbot-mcp

Outbound MCP server: Cursor, Claude, and VS Code call tools that drive your Grok Bot cloud agents. It wraps grokbot-client (import grokbot) against https://api2.cursor.sh.

This is not the official Grok MCP and not Forge’s inbound grokbot_mcp_gateway.py. Those let a Bot call your tools. This server is the opposite direction (IDE → Bot). Never install this MCP on a Grok Bot.

Not affiliated with Cursor or xAI. The backend is unofficial and can change when the desktop app updates. Bundled client schemas match grok-bot 0.51.0 (SCHEMA_VERSION). MIT licensed.

Package: grokbot_mcp. CLI: grokbot-mcp / python -m grokbot_mcp. Default transport is stdio. --http is Streamable HTTP (stateless=True, json_response=True). Logs go to stderr only (WARNING; httpx/httpcore silenced). PYTHONUNBUFFERED=1.

Server instructions (what the MCP tells the host): grokbot_send is fire-and-forget — it returns message_id and delivery immediately. Retry a send with the same message_id. Use grokbot_wait or grokbot_history for the reply. If the transcript is quiet, check grokbot_pending for widgets or handoff. Only create/delete agents, put_secret, or desktop wake=true when the user asked.

Auth

Sign in from the CLI (no Cursor app required). This is Cursor’s PKCE device login: the CLI prints a loginDeepControl URL, you approve it in a browser, and the process polls https://api2.cursor.sh/auth/poll until it gets an access JWT.

grokbot-mcp login              # prints a URL, opens the browser, waits up to 5 min
grokbot-mcp login --no-open    # URL only (SSH / Docker / headless)
grokbot-mcp login --print-token  # also write the JWT to stdout (avoid on shared terminals)
grokbot-mcp logout             # delete the saved file

Credentials are stored mode 0600 at ~/.config/grokbot-mcp/cursor-auth.json (or $GROKBOT_AUTH_FILE). The MCP process prefers that file, then Cursor host files, then $GROKBOT_TOKEN:

  1. grokbot-mcp login file
  2. ~/.config/cursor/auth.json, ~/.cursor/auth.json, ~/.cursor/cli-config.json
  3. else $GROKBOT_TOKEN (raw JWT)

The login JWT is a session access token, not a Cursor dashboard API key. Do not commit it. Compose mounts ~/.config/grokbot-mcp so a host login works inside the HTTP container.

HTTP also requires $GROKBOT_MCP_TOKEN (a shared secret you mint). That token is not the Cursor JWT.

Local venv

Python 3.10+. grokbot-client is not on PyPI.

python3 -m venv .venv
source .venv/bin/activate
pip install "git+https://github.com/Kenzim/grokbot-client.git"
pip install -e ".[dev]"
grokbot-mcp login              # browser PKCE; saves ~/.config/grokbot-mcp/cursor-auth.json
grokbot-mcp                    # stdio
grokbot-mcp --http             # needs GROKBOT_MCP_TOKEN

LAN: pip install "git+https://git.stackken.com/kenzim/grokbot-client.git" or pip install -e /path/to/grokbot-client.

Copy .env.example to .env and fill values. Compose loads .env; stdio does not require it if host files or GROKBOT_TOKEN are present.

cp .env.example .env

Flags: --http, --host (default 127.0.0.1), --port (default 8080), --path (default /mcp). Env wins if a flag is omitted.

IDE config — three transports

Use one of: venv stdio, docker run -i (no -t), or HTTP URL + Bearer. Replace paths and tokens.

Cursor (~/.cursor/mcp.json)

Stdio (venv):

{
  "mcpServers": {
    "grokbot": {
      "command": "/ABS/PATH/grokbot-mcp/.venv/bin/grokbot-mcp"
    }
  }
}

Stdio (Docker). Do not pass -t — TTY breaks MCP framing. -i is required.

{
  "mcpServers": {
    "grokbot": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "GROKBOT_TOKEN",
        "-v", "${HOME}/.config/cursor:/root/.config/cursor:ro",
        "-v", "${HOME}/.cursor:/root/.cursor:ro",
        "git.stackken.com/kenzim/grokbot-mcp:latest"
      ]
    }
  }
}

HTTP (after compose is up):

{
  "mcpServers": {
    "grokbot": {
      "url": "http://127.0.0.1:8080/mcp",
      "headers": { "Authorization": "Bearer YOUR_GROKBOT_MCP_TOKEN" }
    }
  }
}

Claude Desktop

Same mcpServers object in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Windows: %APPDATA%\Claude\; Linux: ~/.config/Claude/). Stdio command/args as above. HTTP uses url + headers when the host supports Streamable HTTP.

VS Code (.vscode/mcp.json)

{
  "servers": {
    "grokbot": {
      "type": "stdio",
      "command": "/ABS/PATH/grokbot-mcp/.venv/bin/grokbot-mcp"
    }
  }
}

Docker stdio: "command": "docker", "args": ["run", "-i", "--rm", ...] (no -t). HTTP:

{
  "servers": {
    "grokbot": {
      "type": "http",
      "url": "http://127.0.0.1:8080/mcp",
      "headers": { "Authorization": "Bearer YOUR_GROKBOT_MCP_TOKEN" }
    }
  }
}

Claude Code

claude mcp add grokbot -- /ABS/PATH/.venv/bin/grokbot-mcp (stdio), or claude mcp add --transport http grokbot http://127.0.0.1:8080/mcp plus an Authorization header / env for Bearer. Project file .mcp.json uses the same three shapes as Cursor.

Docker

Image: python:3.11-slim-bookworm. Build arg GROKBOT_CLIENT_GIT defaults to https://git.stackken.com/kenzim/grokbot-client.git. Public: --build-arg GROKBOT_CLIENT_GIT=https://github.com/Kenzim/grokbot-client.git. ENTRYPOINT is python -m grokbot_mcp; default CMD is empty (stdio).

Stdio container (host MCP spawns it). Interactive stdin, no TTY:

docker build -t grokbot-mcp .
docker run -i --rm \
  -e GROKBOT_TOKEN \
  -v "$HOME/.config/cursor:/root/.config/cursor:ro" \
  grokbot-mcp

Compose HTTP

cp .env.example .env   # set GROKBOT_MCP_TOKEN (required) and GROKBOT_TOKEN if no host files
docker compose up --build

Service grokbot-mcp runs --http --host 0.0.0.0, publishes 127.0.0.1:8080:8080, volume /data (cursors), optional ~/.config/cursor:ro, env_file: .env. HEALTHCHECK curls /health.

  • GET /health — unauthenticated {"ok": true}
  • /mcp — Authorization: Bearer <GROKBOT_MCP_TOKEN>; missing/wrong token → 401
  • HTTP refuses to start without GROKBOT_MCP_TOKEN
  • Rate limit: 60 requests/min per token (override GROKBOT_MCP_RATE_PER_MIN)

The HTTP process starts the in-process transcript watcher immediately. Stdio starts it lazily on first wait / pending / resource read.

Tools

Every result is one MCP text blob: JSON {"ok": true, ...} or {"ok": false, "error": "...", "code": "..."}. Codes map UnauthorizedError / AuthError / RefusalError / NotFoundError / GrokBotError. Protobuf .raw is never serialized. Default session_id is "" (MAIN). Optional session_id on chat/live tools.

Reads have readOnlyHint. grokbot_delete_*, grokbot_put_secret, and grokbot_desktop with wake have destructiveHint. grokbot_send is mutating and idempotent if message_id is reused.

Roster

  • grokbot_whoami — email, user_id, SCHEMA_VERSION
  • grokbot_list_agents — id, agent_id, name, description, harness
  • grokbot_get_agent — list row plus todos, sessions, capabilities (agent_id required)
  • grokbot_create_agent — name required; harness box|temporal (default box); optional description, title. Only if the user asked.
  • grokbot_update_agent — agent_id; optional name, description, title, avatar_shape, avatar_color
  • grokbot_delete_agent — preview unless confirm=true (does not call delete on preview)

Chat — send vs wait

  • grokbot_send — fire-and-forget. Args: agent_id, text; optional session_id, message_id, rich_text, reply_to_id, attachments, fork. Returns message_id + delivery immediately (ACCEPTED_BOX, ACCEPTED_TEMPORAL, DUPLICATE, …). It does not wait for the assistant. Retry by sending the same message_id.
  • grokbot_wait — block until the next assistant-ish transcript entry after send, the agent stops (is_running false), or timeout (default 45s from GROKBOT_WAIT_TIMEOUT_SEC, clamp 1–120). Returns {timed_out, entries, is_running}. Honours MCP cancellation. Does not use MCP Tasks. Polls history if the watcher is down.
  • grokbot_status — GetGrokBotSendStatus for message_id
  • grokbot_history — default limit 40, max 100; optional before_seq. Entries use Forge entry_public shape: seq, entry_id, entry_kind, role, text, ts_ms, widget
  • grokbot_interrupt — returns had_active_run
  • grokbot_draft / grokbot_discard_draft / grokbot_react — wrap ChatSession. Draft needs exactly one of email or slack object.

Typical loop: send → wait (or history if you already know after_seq) → if quiet, pending.

Secrets

  • grokbot_list_secrets — names + descriptions only
  • grokbot_put_secret — agent_id, name, value; optional description. Result {ok, name} only. Value is never echoed in logs or the result. Only if the user asked.
  • grokbot_delete_secret — confirm=true required or preview-only

Widgets, handoff, desktop

  • grokbot_pending — widgets + HandoffRequested. Drops stale items older than 6 hours. Optional agent_id / session_id.
  • grokbot_resolve_widget — action: respond | dismiss | form | secret | approve. Requires agent_id + entry_id. Payload: value (respond/secret), values object (form), approved (approve).
  • grokbot_desktop — account sandbox (not per-agent; temporal has none). wake default false. wake=true requires confirm=true (may boot a hibernated box). Returns run_state, connect_url, and websocket header names (not token values). No in-process RFB/noVNC.
  • grokbot_end_handoff — client.end_handoff(agent_id, request_id); optional trigger (default DISMISSED)

Resources (JSON): grokbot://me, grokbot://agents, grokbot://agents/{id}/transcript, grokbot://agents/{id}/pending. Watcher persists Forge-shaped cursors in $GROKBOT_DATA_DIR/grokbot_cursors.json (default ./data, Docker /data). Stdio process death is fine.

Environment

Copy .env.example → .env. Empty values mean “unset / default”.

  • GROKBOT_TOKEN — Cursor JWT if no auth.json
  • GROKBOT_MCP_TOKEN — required for HTTP; Bearer shared secret
  • GROKBOT_MCP_HOST / GROKBOT_MCP_PORT / GROKBOT_MCP_PATH — HTTP bind (defaults 127.0.0.1, 8080, /mcp)
  • GROKBOT_MCP_RATE_PER_MIN — default 60
  • GROKBOT_DATA_DIR — default ./data (Docker /data)
  • GROKBOT_AUTH_FILE — override PKCE credential path (default ~/.config/grokbot-mcp/cursor-auth.json)
  • GROKBOT_WAIT_TIMEOUT_SEC — default 45, clamp 1..120

Security

  • HTTP will not listen without GROKBOT_MCP_TOKEN. Compare Bearer with hmac.compare_digest. /health is the only unauthenticated route.
  • Bind compose to localhost (127.0.0.1:8080). Treat the MCP token like a password.
  • Do not install this server on Grok Bots (inbound vs outbound).
  • Do not log JWT, MCP token, secret value, or desktop header values. Desktop returns header names only; put_secret / widget secret never echo the secret. login does not print the JWT unless --print-token.
  • delete_* without confirm=true is preview-only. Desktop wake=true needs confirm=true. Create/delete/put_secret/wake only when the user asked.

Tests

Offline (FakeClient, no network). Pytest asyncio_mode=auto, --cov=grokbot_mcp --cov-fail-under=70.

ruff check grokbot_mcp tests
pytest -q

Markers: live / live_mutate skip unless env is set. GROKBOT_LIVE_MUTATE=1 creates and sends on the production backend — throwaway agent only. GROKBOT_LIVE=1 is read-mostly (GetMe, list, watch, desktop wake=false).

CI (Forgejo)

Workflows under .forgejo/workflows/ (ci, sonarqube, owasp, container). No .github/workflows (GitHub is a push mirror). Jobs runs-on: docker with LAN checkout of this repo and grokbot-client. sonar.projectKey=kenzim_grokbot-mcp. OWASP DC JSON is not imported into Sonar.

Repository secrets:

  • SONAR_HOST_URL — SonarQube base URL
  • SONAR_TOKEN — analysis token
  • REGISTRY_USER plus REGISTRY_TOKEN or PACKAGE_WRITABLE_TOKEN — push git.stackken.com/kenzim/grokbot-mcp:{sha,latest,tag}

Out of scope: Forge inbound gateway, Electron keychain, usage/billing RPCs, MCP Tasks, in-process VNC, PyPI publish.