MCP server and CLI for connecting AI coding agents to OpenClaw instances. Submit tasks, poll for progress, and continue conversations — all through the MCP protocol.
Use at your own risk. This software is provided as-is under the MIT License. ClawConnect connects to your OpenClaw instance using credentials you provide — you are responsible for securing your
OPENCLAW_PASSWORDandOPENCLAW_URL. Treat these like any other secret: never commit them to version control, restrict network access to your OpenClaw instance, and rotate credentials regularly. The authors are not liable for any damages, data loss, or security incidents arising from the use of this software.
| Package | Description |
|---|---|
packages/core |
Shared gateway, session management, and tool handlers |
packages/mcp |
MCP server (stdio transport) |
packages/cli |
CLI (clawconnect) |
apps/chatgpt |
ChatGPT MCP app (HTTP transport + widget) |
git clone git@github.com:lambda-curry/clawconnect.git
cd clawconnect
pnpm install
pnpm run readyThe core, unversioned task-contract surface — same names and behavior across every client (Claude Code, Cursor, Codex, ChatGPT, the CLI); see docs/decisions/2026-07-27-task-contract.md and docs/architecture/2026-07-27-multi-client-compatibility.md for the accepted contract and the implementation boundaries behind it:
run_task— Submit a task to your OpenClaw agent. ReturnsjobId/taskId,sessionKey, status, and a structurednextAction({tool: "check_task", args: {...}}) telling the caller exactly what to call next.check_task— The only tool that waits. Blocks server-side for up towaitMs(default 45000ms, override per call, clamped to[1000, 120000]— out-of-range values clamp rather than error) and returns early on a terminal status (completed/completed_no_summary/error). A timeout return is not an error and not terminal:continuePollingistrue,nextActionsays to callcheck_taskagain with the samejobId, andretryAfterMssuggests a delay before that next call (0normally — a wait-mode call already blocked for its full window, so calling again immediately is fine;10000during late-recovery, since the transcript is only re-read on that cadence server-side) — never submit a newrun_taskbecause a poll timed out (the session-busy guard would refuse it as a duplicate anyway).mode: "poll"(also bounded bywaitMs) returns as soon as any new log activity appears, for live-progress use cases distinct from "give me the final result".get_task— An immediate, non-waiting snapshot for diagnostics, manual reads, or UI refresh — includingstatus: "running"if that's the current truth. Never blocks, unlikecheck_task.detailcontrols which fields come back (core/summary/updates/artifacts/diagnostics/full/fullWithDiagnostics), plusdetail: "prompt"to retrieve the original submitted{task, context, senderName}— not included at any other detail level, so it never appears in a normal response by accident.list_tasks— Aggregate, immediate view of tasks across agents;view: "active"filters to non-terminal ones.get_session— Debug-level inspection of one session:snapshot(default),events(bounded slice), ortail(cursor-based pagination viaafter/nextAfter).list_sessions/list_agents/search_memory/get_memory/list_collections— session/agent listing and QMD memory search, independent of the task lifecycle above.
MCP tool annotations (readOnlyHint, idempotentHint, etc.) classify intent per the MCP spec — they are hints for client UX, not behavioral guarantees.
| Mode | Behavior | Best for |
|---|---|---|
check_task mode: "wait" (default) |
Blocks up to waitMs (default 45s) and returns early only on a terminal status |
AI agents (Claude Code, Codex) — minimizes round-trips, continuePolling/nextAction make the loop machine-readable |
check_task mode: "poll" |
Returns as soon as any new log activity occurs (still bounded by waitMs) |
Live-progress consumers that want intermediate updates, not just the final result |
get_task |
Never waits — immediate snapshot of whatever state exists right now | Diagnostics, manual reads, UI refresh — read-only, no polling loop implied |
A typical 3-minute task with check_task mode: "wait" at the 45s default needs a handful of calls (1 run_task + a few check_task calls).
The MCP server reads three environment variables:
| Variable | Required | Description |
|---|---|---|
OPENCLAW_URL |
Yes | WebSocket URL for your OpenClaw instance (e.g., ws://127.0.0.1:18789) |
OPENCLAW_PASSWORD |
Yes | OpenClaw gateway password |
OPENCLAW_AGENT_ID |
No | Agent name to connect to (default: main) |
Add to ~/.claude/settings.json:
{
"mcpServers": {
"clawconnect": {
"command": "node",
"args": ["/path/to/clawconnect/packages/mcp/dist/bin.mjs"],
"env": {
"OPENCLAW_URL": "ws://YOUR_OPENCLAW_HOST:18789",
"OPENCLAW_PASSWORD": "your-openclaw-password",
"OPENCLAW_AGENT_ID": "your-agent-name"
}
}
}
}Restart Claude Code to pick up the new MCP server.
Add to .cursor/mcp.json in your project root (or ~/.cursor/mcp.json for global):
{
"mcpServers": {
"clawconnect": {
"command": "node",
"args": ["/path/to/clawconnect/packages/mcp/dist/bin.mjs"],
"env": {
"OPENCLAW_URL": "ws://YOUR_OPENCLAW_HOST:18789",
"OPENCLAW_PASSWORD": "your-openclaw-password",
"OPENCLAW_AGENT_ID": "your-agent-name"
}
}
}
}Restart Cursor after adding the config.
Add to ~/.codex/config.toml:
[mcp_servers.clawconnect]
command = "node"
args = ["/path/to/clawconnect/packages/mcp/dist/bin.mjs"]
env = { OPENCLAW_URL = "ws://YOUR_OPENCLAW_HOST:18789", OPENCLAW_PASSWORD = "your-openclaw-password", OPENCLAW_AGENT_ID = "your-agent-name" }
tool_timeout_sec = 60.0Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"clawconnect": {
"command": "node",
"args": ["/path/to/clawconnect/packages/mcp/dist/bin.mjs"],
"env": {
"OPENCLAW_URL": "ws://YOUR_OPENCLAW_HOST:18789",
"OPENCLAW_PASSWORD": "your-openclaw-password",
"OPENCLAW_AGENT_ID": "your-agent-name"
}
}
}
}The ChatGPT integration runs as an HTTP MCP server with an optional live progress widget. It requires ChatGPT's Developer Mode and a publicly reachable URL.
- ChatGPT Plus/Pro/Team account with Developer Mode enabled
- A way to expose the server publicly (e.g., Tailscale Funnel, ngrok, Cloudflare Tunnel, or a VPS)
- Open ChatGPT → Settings → Developer (or Beta features)
- Toggle Developer Mode on
- You should now see an MCP Servers section under Settings → Developer
cd apps/chatgpt
cp .env.example .envEdit .env with your values:
PORT=7331
OPENCLAW_URL=ws://YOUR_OPENCLAW_HOST:18789
OPENCLAW_PASSWORD=your-openclaw-password
OPENCLAW_AGENT_ID=main
ENABLE_CHATGPT_UI_WIDGET=true # optional: enables a live progress widget in ChatGPTThen start the server:
pnpm run dev # development with hot reload
# or
pnpm run build && pnpm run start # productionChatGPT needs a publicly reachable HTTPS URL. Example with Tailscale Funnel:
tailscale funnel 7331This gives you a URL like https://your-machine.tail1234.ts.net:443.
- Go to Settings → Developer → MCP Servers
- Click Add MCP Server
- Enter your public URL with the
/mcppath:https://your-machine.tail1234.ts.net/mcp - Save and start a new chat
In a new ChatGPT conversation, ask:
"Use the run_task tool to ask Clawdy to say hello"
ChatGPT will call run_task, then poll with check_task until the task completes. If the widget is enabled, you'll see live progress inline.
check_taskis annotated as read-only/idempotent, which may reduce approval prompts during polling- The widget is a read-only "task center": session-first rows (one per
sessionKey), collapsing to a focused single-task view when only one session has active work, expandable into per-session task history. It pollsget_task/list_tasks/get_sessiondirectly (nevercheck_task) on an attention-shaped cadence that stops entirely once everything in scope is terminal — the assistant's owncheck_task(mode:"wait")loop is unaffected by and independent of whether the widget is mounted, enabled, or even loads successfully. Seedocs/architecture/2026-07-27-chatgpt-ui-reconciliation.mdfor the full design. - Task titles are generated from non-sensitive fields (summary/artifacts/status) by default; the original submitted prompt is never shown unless you explicitly use the "Show original request" action (
get_task detail:"prompt", same authorization boundary as the API) - If the widget causes issues, set
ENABLE_CHATGPT_UI_WIDGET=falseand restart —run_task/check_taskcorrectness never depends on it - In-flight jobs survive a connector restart: each agent's currently-
runningjobs (jobId/sessionKey/prompt only — no logs, nothing terminal) are written toapps/chatgpt/.job-store/<agentId>.jsonand reloaded on boot, then reattached via the same transcript-recovery path an empty livechat.finalalready uses. A job that was already terminal before the restart is never written, so the file can't grow into a log; delete the directory any time to reset to pre-persistence behavior (a restart during that job just re-derives from scratch instead of reattaching) - Not verified against live ChatGPT rendering — this is a local, test-ready prototype (unit-tested state logic + HTTP-integration-tested server wiring), not a deployed/live-tested one. See the Local Testing coverage table below
The HTTP server (apps/chatgpt) gates /mcp with tokens supplied via ?pass=<token> on the connector URL or Authorization: Bearer <token>. Two env vars control it:
# Personal tokens — token authenticates AND identifies the caller.
MCP_USER_TOKENS=Jake:a1b2c3...,Mohsen:d4e5f6...
# Optional runtime-editable JSON token file. Changes are picked up without restart.
MCP_USER_TOKENS_FILE=/data/clawconnect/user-tokens.json
# Legacy shared pass — still accepted, but resolves to an anonymous caller.
PUBLIC_MCP_PASS=shared-secretWhen a request authenticates with a personal token:
- every
run_taskis stamped with that person's name ([Message from: Jake]in the task the agent receives) — the model-suppliedsenderNameargument is ignored, so identity derives from the credential rather than from anything spoofable serverInfo.namebecomesClawConnect (Jake)so multiple people's connectors are distinguishable
MCP_USER_TOKENS_FILE is additive with MCP_USER_TOKENS and supports these JSON shapes:
{ "Faraz": "cc-faraz-...", "Junaid": "cc-junaid-..." }{ "tokens": { "Faraz": "cc-faraz-...", "Junaid": "cc-junaid-..." } }[{ "name": "Faraz", "token": "cc-faraz-..." }]When a request authenticates with the legacy PUBLIC_MCP_PASS, tasks arrive unattributed (unless the model passes senderName) and tool responses nudge the caller to get a personal token. Revoking one person = removing their entry from MCP_USER_TOKENS or the runtime token file; the shared pass never needs rotating for that.
If neither env var is set, /mcp is open (no gate) — only do that on a private network.
Once configured, your AI agent has access to the MCP tools. Example flow:
- Ask your agent to delegate work: "Ask clawdy to fix the login bug"
- The agent calls
run_taskwith the task description - It polls
check_task(mode: "wait")until the task completes - It presents the results: summary, files changed, PRs created, etc.
- Follow up: "Tell clawdy to add tests for that fix" — continues the same session
Every task returns a sessionKey. Passing it back to run_task continues the same conversation thread in OpenClaw, preserving context from previous tasks.
For a streamlined experience, copy the slash command:
mkdir -p ~/.claude/commands
cp .claude/commands/claw.md ~/.claude/commands/claw.mdThen use /claw fix the auth bug from any project.
The CLI is also available for shell-based workflows:
# Install globally
pnpm -w run ready
npm install -g ./packages/cli/clawconnect-cli-0.0.0.tgz
# Submit and wait
clawconnect run "fix the login bug" --wait --json
# Submit and poll separately
clawconnect run "fix the login bug" --json
clawconnect status <job-id> --json
# Continue a session
clawconnect run "add tests" --session <session-key> --wait --jsonEvery run_task/check_task/get_task/list_tasks call emits one structured JSON line to stderr (never stdout, which would corrupt the stdio JSON-RPC stream): tool name, job/task id, session key, agent, poll count, requested/effective wait duration, request duration, returned status, duplicate-job detection, and whether the retrieval was terminal.
What is never logged, structurally (not just by convention): the submitted task text, context, sender name, log/summary content, or anything else from a run. TelemetryEvent (packages/core/src/telemetry.ts) has no field that could hold that content — there's nothing to accidentally forget to redact. The original prompt is stored on the job record for retrieval (get_task detail: "prompt", gated by the same per-agent scope that hides out-of-scope jobs) but is never included in telemetry or in any other get_task/check_task response.
To capture telemetry events in your own tooling instead of stderr, use setTelemetrySink from @clawconnect/core.
pnpm install
./node_modules/.bin/vp test # full workspace test suite (vitest via vite-plus)
./node_modules/.bin/vp run -r build --force # full workspace build + typecheck, all 4 packagesAs of this contract implementation: 9 test files, 105 tests, all passing. Coverage by client surface:
| Surface | What's verified | Where |
|---|---|---|
| Generic MCP (Claude Code, Cursor, Codex, any bare MCP client) | tools/list/tools/call over the real stdio McpServer via the SDK's InMemoryTransport — unversioned tool names, no ChatGPT-only _meta leakage, check_task has waitMs, get_task has neither waitMs nor mode (it never waits) |
packages/mcp/src/server.test.ts |
| Claude / Claude Code | Unrecognized extra properties in tool arguments and a request-level _meta block (progress tokens, io.modelcontextprotocol/related-task) don't reject the call |
packages/mcp/src/server.test.ts |
| ChatGPT — protocol/meta layer | MCP Apps constants/builders (protocolVersion negotiation, extensions capability, _meta.ui/legacy-alias shapes) verified against the installed @modelcontextprotocol/ext-apps@1.5.0 package, not assumed; every builder returns nothing at all when the widget is disabled |
apps/chatgpt/src/ui-meta.test.ts |
| ChatGPT — HTTP transport | Real ephemeral http.Server + real fetch() against createApp()'s request listener (no live OpenClaw gateway — agents point at an unroutable loopback port). Unmounted core independence (run_task→get_task succeeds identically whether the widget is disabled, enabled, or enabled-with-a-broken-resource-file); missing-metadata fallback (disabled ⇒ empty resources/list, no extensions capability, no ui/openai/outputTemplate/openai/widgetAccessible on any tool); enabled-with-a-real-resource (resources/read serves it, only run_task carries a resourceUri, get_task/list_tasks/get_session are app-callable without one, check_task carries no _meta at all) |
apps/chatgpt/src/app.test.ts |
| ChatGPT — widget decision logic | The full UX model as pure functions: status grouping (exact status preserved), generated titles (never from the stored prompt), aggregate-command-center-collapsing-to-focused-task, session-first rows with expandable task history, Active/Recent views, context-aware detail sections, attention-shaped poll cadence, dedup ("never a second read in flight") and bound ("stop once everything is terminal") guards, out-of-order-read safety, and the documented conversation-scope fallback | apps/chatgpt/src/widget/state.test.ts |
| ChatGPT — widget rendering seam | shell.html is a template whose single <script> gets state.js/protocol.js textually inlined at build time, and nothing else in the toolchain link-checks across that seam (tsc doesn't read .html; the unit tests import state.js directly). Asserts every bare function call in shell.html resolves to something defined in the shell, exported by an inlined module, or a real platform global — plus a negative control proving the check can fail. This exists because commit 30a5669 shipped five call sites with no definitions, which made render() throw on its first call and the widget paint an empty div while never issuing a tool call |
apps/chatgpt/src/widget/references.test.ts |
| ChatGPT — widget markup safety | Agent-authored summaries/prompts render as minimal markdown into innerHTML, so: the whole source is HTML-escaped before any transform, only the tag set shell.html styles is emitted, and links are restricted to http/https/mailto. Covered per block path (fenced code, list, blockquote, heading, table cell) plus an <img onerror> and a javascript: href, both of which must come out as inert text |
apps/chatgpt/src/widget/state.test.ts |
| Wait semantics | Fake-clock coverage: 45s default, explicit waitMs override, invalid values (negative/NaN/huge) clamp instead of erroring, terminal status returns immediately regardless of waitMs, a timeout is non-terminal with no duplicate job created on re-poll, pollCount increments per call, retryAfterMs is 0 on an ordinary timeout and 10000 during late-recovery |
packages/core/src/session-wait.test.ts |
| Concurrency, sessions, prompt authorization | Concurrent taskId/sessionKey pairs across agents never cross-resolve; a second run_task on a still-running session is refused, not silently overwritten; get_session snapshot/events/tail/tasks modes; get_task detail: "prompt" round-trip and its absence from every other detail level |
packages/core/src/tools.test.ts |
| Job-store persistence (restart recovery) | JsonFileJobStore load/save round-trip, missing file, corrupt JSON, non-array JSON, and atomic write-then-rename (no .tmp left behind) all covered without a real SessionManager |
packages/core/src/job-store.test.ts |
Restart recovery (SessionManager) |
A running job persists immediately on submission and drops out of the next save the moment it goes terminal (completed or error); a reloaded job is resolvable/pollable before its recovery attempt even resolves, reattaches via the same transcript-recovery path an empty live chat.final uses, and is visible through getLatestJobForSession/getJobHistory exactly like a freshly-submitted job; with no store configured, behavior is unchanged (no crash, nothing written) |
packages/core/src/session-persistence.test.ts |
None of the above touch a live OpenClaw gateway or a live ChatGPT connection — GatewayPool connects lazily, so tests that never resolve an agent (unknown job/session ids) do no network I/O at all, tests that do submit a task either mock OpenClawGateway at the constructor level or point at an unroutable loopback port, and nothing here was verified by actually rendering inside ChatGPT (a hard constraint on this build — see docs/architecture/2026-07-27-chatgpt-ui-reconciliation.md §6).
The widget is, however, smoke-tested by rendering the built dist/widget.html in a real browser against a stubbed window.openai (fixtures shaped exactly like TaskSummary/JobSnapshot, including deliberately long and terminal-state data). That is what catches the class of bug the unit tests structurally cannot: a shell↔module reference that doesn't resolve, a CSS rule that loses the cascade, raw markdown syntax leaking into a title. It is not a substitute for live ChatGPT, which remains out of scope — a browser harness stubs the host, so anything host-specific (whether ChatGPT proxies app-initiated tool calls at all, whether ui.visibility is honored) is still unverified. See §6 of the design note for the full unknowns list.
Known build-tool quirks (vite-plus, not this project's code):
— fixed.vp packrewrites thebinfield inpackages/cli/package.jsonandpackages/mcp/package.jsonvp pack's bin auto-detection derives the command name from the package name without its scope (@clawconnect/mcp→mcp) and rewrotepackage.jsonon every build, not just--force. Both packages now name the binary explicitly viaexports.binin theirvite.config.ts, soclawconnect/clawconnect-mcpsurvive avp cache clean && vp run -r build --force. If you add another package with a CLI entry, setexports.binthere too rather than relying on auto-detect.- After deleting a package's
dist/directory,vp run -r build --forcecan replay a cached "success" for a step without actually regenerating output, so a later step in the same package (e.g.apps/chatgpt's widget-copy/build step) fails against a directory that was never recreated../node_modules/.bin/vp cache cleanresolves it.
AI Agent (Claude Code / Cursor / Codex)
|
|-- MCP (stdio) --> packages/mcp --> packages/core --> OpenClaw Gateway (WebSocket)
| |
| OpenClaw Agent
| (clawdy, molty, etc.)
|
ChatGPT
|
|-- MCP (HTTP) --> apps/chatgpt --> packages/core --> OpenClaw Gateway (WebSocket)
packages/core handles all communication with OpenClaw — the MCP server and ChatGPT app are thin layers that adapt the transport and response format. See docs/architecture/2026-07-27-multi-client-compatibility.md for the layer boundaries between client-neutral core/structuredContent and ChatGPT-only _meta/resource metadata.