- Python 99.6%
- Dockerfile 0.4%
| .forgejo/workflows | ||
| deploy | ||
| grokbot_mcp | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| sonar-project.properties | ||
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:
- grokbot-mcp login file
~/.config/cursor/auth.json,~/.cursor/auth.json,~/.cursor/cli-config.json- 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_idrequired) - grokbot_create_agent —
namerequired;harnessbox|temporal(defaultbox); optionaldescription,title. Only if the user asked. - grokbot_update_agent —
agent_id; optionalname,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; optionalsession_id,message_id,rich_text,reply_to_id,attachments,fork. Returnsmessage_id+deliveryimmediately (ACCEPTED_BOX,ACCEPTED_TEMPORAL,DUPLICATE, …). It does not wait for the assistant. Retry by sending the samemessage_id. - grokbot_wait — block until the next assistant-ish transcript entry after send, the agent stops (
is_runningfalse), or timeout (default 45s fromGROKBOT_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 —
GetGrokBotSendStatusformessage_id - grokbot_history — default limit 40, max 100; optional
before_seq. Entries use Forgeentry_publicshape: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 ofemailorslackobject.
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; optionaldescription. Result{ok, name}only. Value is never echoed in logs or the result. Only if the user asked. - grokbot_delete_secret —
confirm=truerequired or preview-only
Widgets, handoff, desktop
- grokbot_pending — widgets +
HandoffRequested. Drops stale items older than 6 hours. Optionalagent_id/session_id. - grokbot_resolve_widget —
action:respond|dismiss|form|secret|approve. Requiresagent_id+entry_id. Payload:value(respond/secret),valuesobject (form),approved(approve). - grokbot_desktop — account sandbox (not per-agent;
temporalhas none).wakedefault false.wake=truerequiresconfirm=true(may boot a hibernated box). Returnsrun_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); optionaltrigger(defaultDISMISSED)
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 noauth.jsonGROKBOT_MCP_TOKEN— required for HTTP; Bearer shared secretGROKBOT_MCP_HOST/GROKBOT_MCP_PORT/GROKBOT_MCP_PATH— HTTP bind (defaults127.0.0.1,8080,/mcp)GROKBOT_MCP_RATE_PER_MIN— default 60GROKBOT_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 withhmac.compare_digest./healthis 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/ widgetsecretnever echo the secret.logindoes not print the JWT unless--print-token. delete_*withoutconfirm=trueis preview-only. Desktopwake=trueneedsconfirm=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 URLSONAR_TOKEN— analysis tokenREGISTRY_USERplusREGISTRY_TOKENorPACKAGE_WRITABLE_TOKEN— pushgit.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.