grokbot-mcp
# grokbot-mcp
Outbound MCP server: Cursor, Claude, and VS Code call tools that drive **your** Grok Bot cloud agents. It wraps [`grokbot-client`](https://github.com/Kenzim/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.
```bash
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.
```bash
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.
```bash
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):
```json
{
"mcpServers": {
"grokbot": {
"command": "/ABS/PATH/grokbot-mcp/.venv/bin/grokbot-mcp"
}
}
}
```
Stdio (Docker). **Do not pass `-t`** — TTY breaks MCP framing. `-i` is required.
```json
{
"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):
```json
{
"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`)
```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:
```json
{
"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**:
```bash
docker build -t grokbot-mcp .
docker run -i --rm \
-e GROKBOT_TOKEN \
-v "$HOME/.config/cursor:/root/.config/cursor:ro" \
grokbot-mcp
```
## Compose HTTP
```bash
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`.
```bash
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.
TDQS
Scored across 21 tools
Most tools target distinct resources or actions, and the agent and secret CRUD groups are clearly separated. Some overlap exists among send/status/wait/draft and between pending/history, but descriptions provide enough context to choose correctly.
All tool names share the grokbot_ prefix and snake_case, which is consistent. However, the set mixes verb_noun patterns (list_agents, create_agent, resolve_widget) with bare nouns/verbs (status, history, wait, desktop, whoami), making the convention less predictable.
21 tools is on the heavy side for a single server, even though the capabilities span agents, messaging, widgets, secrets, desktop, and handoff. Each area is distinct, but some niche operations could likely be consolidated.
Core CRUD for agents and secrets, messaging, widget handling, desktop access, and handoff are all covered. Minor gaps remain, such as todo/session management, draft listing/approval, and transcript search, but the main workflows are supported.