diff --git a/docs/plugins/cursor-mcp.md b/docs/plugins/cursor-mcp.md new file mode 100644 index 000000000..7cfefdd3f --- /dev/null +++ b/docs/plugins/cursor-mcp.md @@ -0,0 +1,215 @@ +--- +title: Cursor IDE Integration +description: Use OpenClaw as an AI backend in Cursor IDE via MCP +--- + +# Cursor IDE Integration + +OpenClaw provides a Model Context Protocol (MCP) server that integrates with [Cursor IDE](https://cursor.com), enabling you to use OpenClaw's AI capabilities directly in Cursor's Composer Agent. + +## Overview + +The Cursor MCP integration allows you to: + +- **Chat with OpenClaw**: Use OpenClaw's AI agent directly in Cursor +- **Manage Sessions**: Create, list, and manage conversation sessions +- **Send Messages**: Route messages through WhatsApp, Telegram, Discord, and more +- **Access Models**: Use any AI model configured in OpenClaw +- **Code Assistance**: Built-in prompts for code review, debugging, and testing + +## Quick Setup + +### Prerequisites + +1. [Install OpenClaw](/install) +2. Start the OpenClaw gateway: + ```bash + openclaw gateway run + ``` +3. Install [Cursor IDE](https://cursor.com) + +### Configure Cursor + +#### Option 1: Cursor Settings UI + +1. Open **Cursor Settings** → **Features** → **MCP** +2. Click **"+ Add New MCP Server"** +3. Configure: + - **Name**: `openclaw` + - **Type**: `stdio` + - **Command**: `openclaw` + - **Arguments**: `mcp serve` + +#### Option 2: Manual Configuration + +Create or edit `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "openclaw": { + "command": "openclaw", + "args": ["mcp", "serve"], + "env": { + "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789" + } + } + } +} +``` + +### Authentication + +If your gateway requires authentication: + +```json +{ + "mcpServers": { + "openclaw": { + "command": "openclaw", + "args": ["mcp", "serve"], + "env": { + "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789", + "OPENCLAW_GATEWAY_TOKEN": "your-token-here", + "OPENCLAW_GATEWAY_PASSWORD": "your-password-here" + } + } + } +} +``` + +## Available Tools + +The MCP server exposes these tools to Cursor: + +| Tool | Description | +|------|-------------| +| `openclaw_chat` | Chat with the OpenClaw AI agent | +| `openclaw_list_sessions` | List all active chat sessions | +| `openclaw_get_session` | Get details about a specific session | +| `openclaw_clear_session` | Clear a session's conversation history | +| `openclaw_execute_command` | Execute OpenClaw control commands | +| `openclaw_send_message` | Send messages through channels | +| `openclaw_get_status` | Get gateway and channel status | +| `openclaw_list_models` | List available AI models | + +### Tool Examples + +#### Chat with OpenClaw + +``` +User: Ask OpenClaw to explain this Python code +Cursor Agent: [Uses openclaw_chat tool] +``` + +#### Send a Message + +``` +User: Send "Build completed" to my Telegram channel +Cursor Agent: [Uses openclaw_send_message tool] +``` + +## Available Resources + +Access OpenClaw data via MCP resources: + +| URI | Description | +|-----|-------------| +| `openclaw://status` | Gateway and channel status | +| `openclaw://models` | Available AI models | +| `openclaw://sessions` | Active chat sessions | +| `openclaw://config` | Current configuration (sanitized) | + +## Available Prompts + +Built-in prompts for common development tasks: + +| Prompt | Description | +|--------|-------------| +| `code_review` | Review code for issues and improvements | +| `explain_code` | Explain how code works | +| `generate_tests` | Generate tests for code | +| `refactor_code` | Suggest refactoring improvements | +| `debug_help` | Help debug issues | +| `send_notification` | Send notification via channels | + +## CLI Commands + +```bash +# Start MCP server manually (usually done by Cursor) +openclaw mcp serve + +# Show configuration help +openclaw mcp info + +# Custom options +openclaw mcp serve --url ws://localhost:18789 --session agent:main:cursor +``` + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `OPENCLAW_GATEWAY_URL` | Gateway WebSocket URL | `ws://127.0.0.1:18789` | +| `OPENCLAW_GATEWAY_TOKEN` | Authentication token | - | +| `OPENCLAW_GATEWAY_PASSWORD` | Authentication password | - | +| `OPENCLAW_SESSION_KEY` | Default session key | `agent:main:cursor` | + +## Architecture + +``` +┌─────────────────┐ MCP Protocol ┌──────────────────┐ +│ Cursor IDE │◄───────────────────►│ OpenClaw MCP │ +│ (MCP Client) │ (stdio) │ Server │ +└─────────────────┘ └────────┬─────────┘ + │ + │ WebSocket + ▼ + ┌──────────────────┐ + │ OpenClaw │ + │ Gateway │ + └────────┬─────────┘ + │ + ┌─────────────────────────────┼─────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ + │ AI Models │ │ Channels │ │ Sessions │ + │ (Anthropic, │ │ (WhatsApp, │ │ │ + │ OpenAI...) │ │ Telegram...) │ │ │ + └───────────────┘ └───────────────┘ └───────────────┘ +``` + +## Troubleshooting + +### Gateway Connection Failed + +1. Ensure the OpenClaw gateway is running: + ```bash + openclaw gateway run + ``` + +2. Check the gateway URL in your configuration + +3. Verify authentication credentials if required + +### Tools Not Appearing + +1. Restart Cursor after adding the MCP server +2. Check Cursor's MCP logs for errors +3. Ensure `openclaw` is in your system PATH + +### Session Issues + +Clear and restart a session using the `openclaw_clear_session` tool or: + +```bash +openclaw sessions clear agent:main:cursor +``` + +## See Also + +- [Gateway Configuration](/gateway/configuration) +- [Model Providers](/concepts/model-providers) +- [Sessions](/concepts/sessions) +- [Messaging Channels](/channels) diff --git a/extensions/cursor-mcp/CHANGELOG.md b/extensions/cursor-mcp/CHANGELOG.md new file mode 100644 index 000000000..88ae55f44 --- /dev/null +++ b/extensions/cursor-mcp/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +## 2026.1.29 + +Initial release of OpenClaw Cursor MCP integration. + +### Features + +- **MCP Server**: Full Model Context Protocol server implementation for Cursor IDE +- **Tools**: + - `openclaw_chat`: Chat with OpenClaw AI agent + - `openclaw_list_sessions`: List active sessions + - `openclaw_get_session`: Get session details + - `openclaw_clear_session`: Clear session history + - `openclaw_execute_command`: Execute OpenClaw commands + - `openclaw_send_message`: Send messages through channels + - `openclaw_get_status`: Get gateway status + - `openclaw_list_models`: List available models +- **Resources**: + - `openclaw://status`: Gateway status + - `openclaw://models`: Available models + - `openclaw://sessions`: Active sessions + - `openclaw://config`: Configuration (sanitized) +- **Prompts**: + - `code_review`: Code review assistance + - `explain_code`: Code explanation + - `generate_tests`: Test generation + - `refactor_code`: Refactoring suggestions + - `debug_help`: Debugging assistance + - `send_notification`: Channel notifications +- **CLI Commands**: + - `openclaw mcp serve`: Start MCP server + - `openclaw mcp info`: Show configuration help diff --git a/extensions/cursor-mcp/README.md b/extensions/cursor-mcp/README.md new file mode 100644 index 000000000..52e205f98 --- /dev/null +++ b/extensions/cursor-mcp/README.md @@ -0,0 +1,221 @@ +# OpenClaw Cursor MCP Integration + +This extension provides Model Context Protocol (MCP) server integration for [Cursor IDE](https://cursor.com), enabling OpenClaw as an AI backend within Cursor's Composer Agent. + +## Features + +- **Chat with OpenClaw Agent**: Use OpenClaw's AI capabilities directly in Cursor +- **Session Management**: Create, list, and manage conversation sessions +- **Multi-Channel Messaging**: Send messages through WhatsApp, Telegram, Discord, Slack, and more +- **Model Selection**: Choose from any AI model configured in OpenClaw +- **Code Assistance Prompts**: Built-in prompts for code review, debugging, test generation, and more + +## Quick Start + +### Prerequisites + +1. OpenClaw installed and gateway running: + ```bash + npm install -g openclaw + openclaw gateway run + ``` + +2. Cursor IDE installed + +### Setup in Cursor + +#### Option 1: Cursor Settings UI + +1. Open **Cursor Settings** → **Features** → **MCP** +2. Click **"+ Add New MCP Server"** +3. Configure: + - **Name**: `openclaw` + - **Type**: `stdio` + - **Command**: `openclaw` + - **Arguments**: `mcp serve` + +#### Option 2: Manual Configuration + +Create or edit `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "openclaw": { + "command": "openclaw", + "args": ["mcp", "serve"], + "env": { + "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789" + } + } + } +} +``` + +### Authentication (Optional) + +If your gateway requires authentication, add credentials to the environment: + +```json +{ + "mcpServers": { + "openclaw": { + "command": "openclaw", + "args": ["mcp", "serve"], + "env": { + "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789", + "OPENCLAW_GATEWAY_TOKEN": "your-token-here", + "OPENCLAW_GATEWAY_PASSWORD": "your-password-here" + } + } + } +} +``` + +## Available Tools + +| Tool | Description | +|------|-------------| +| `openclaw_chat` | Chat with the OpenClaw AI agent | +| `openclaw_list_sessions` | List all active chat sessions | +| `openclaw_get_session` | Get details about a specific session | +| `openclaw_clear_session` | Clear a session's conversation history | +| `openclaw_execute_command` | Execute OpenClaw control commands | +| `openclaw_send_message` | Send messages through channels | +| `openclaw_get_status` | Get gateway and channel status | +| `openclaw_list_models` | List available AI models | + +## Available Resources + +| URI | Description | +|-----|-------------| +| `openclaw://status` | Gateway and channel status | +| `openclaw://models` | Available AI models | +| `openclaw://sessions` | Active chat sessions | +| `openclaw://config` | Current configuration (sanitized) | + +## Available Prompts + +| Prompt | Description | +|--------|-------------| +| `code_review` | Review code for issues and improvements | +| `explain_code` | Explain how code works | +| `generate_tests` | Generate tests for code | +| `refactor_code` | Suggest refactoring improvements | +| `debug_help` | Help debug issues | +| `send_notification` | Send notification via channels | + +## Usage Examples + +### Chat with OpenClaw + +In Cursor's Composer, the Agent can use OpenClaw tools: + +``` +User: Use openclaw to help me debug this Python code +Agent: [Uses openclaw_chat tool to send your code to OpenClaw] +``` + +### Send a Message + +``` +User: Send "Build completed successfully" to my Telegram +Agent: [Uses openclaw_send_message with target and channel] +``` + +### Check Status + +``` +User: What's the status of my OpenClaw channels? +Agent: [Uses openclaw_get_status tool] +``` + +## CLI Commands + +```bash +# Start MCP server (usually done automatically by Cursor) +openclaw mcp serve + +# Show configuration help +openclaw mcp info + +# With custom options +openclaw mcp serve --url ws://localhost:18789 --session agent:main:cursor +``` + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `OPENCLAW_GATEWAY_URL` | Gateway WebSocket URL | `ws://127.0.0.1:18789` | +| `OPENCLAW_GATEWAY_TOKEN` | Authentication token | - | +| `OPENCLAW_GATEWAY_PASSWORD` | Authentication password | - | +| `OPENCLAW_SESSION_KEY` | Default session key | `agent:main:cursor` | + +## Troubleshooting + +### Gateway Connection Failed + +1. Ensure the OpenClaw gateway is running: + ```bash + openclaw gateway run + ``` + +2. Check the gateway URL in your configuration + +3. Verify authentication credentials if required + +### Tools Not Appearing in Cursor + +1. Restart Cursor after adding the MCP server +2. Check Cursor's MCP logs for errors +3. Ensure `openclaw` is in your PATH + +### Session Issues + +Clear and restart a session: +``` +User: Clear my OpenClaw session +Agent: [Uses openclaw_clear_session tool] +``` + +## Development + +```bash +# Install dependencies +cd extensions/cursor-mcp +pnpm install + +# Build +pnpm build + +# Test locally +node bin/server.js +``` + +## Architecture + +``` +┌─────────────────┐ MCP Protocol ┌──────────────────┐ +│ Cursor IDE │◄───────────────────►│ OpenClaw MCP │ +│ (MCP Client) │ (stdio) │ Server │ +└─────────────────┘ └────────┬─────────┘ + │ + │ WebSocket + ▼ + ┌──────────────────┐ + │ OpenClaw │ + │ Gateway │ + └────────┬─────────┘ + │ + ┌─────────────────────────────┼─────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ + │ WhatsApp │ │ Telegram │ │ Discord │ + └───────────────┘ └───────────────┘ └───────────────┘ +``` + +## License + +MIT - Part of the OpenClaw project. diff --git a/extensions/cursor-mcp/index.ts b/extensions/cursor-mcp/index.ts new file mode 100644 index 000000000..fbfd89a4a --- /dev/null +++ b/extensions/cursor-mcp/index.ts @@ -0,0 +1,148 @@ +/** + * OpenClaw Cursor MCP Plugin + * + * This plugin integrates OpenClaw with Cursor IDE through the Model Context Protocol (MCP). + * It allows Cursor to use OpenClaw as an AI backend and provides tools for managing + * conversations, sessions, and messaging channels. + * + * Usage in Cursor: + * 1. Add to Cursor's MCP configuration (~/.cursor/mcp.json or Cursor Settings > Features > MCP) + * 2. Use the openclaw_* tools in Cursor's Composer Agent + * + * Configuration example for mcp.json: + * { + * "mcpServers": { + * "openclaw": { + * "command": "openclaw", + * "args": ["mcp", "serve"], + * "env": { + * "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789", + * "OPENCLAW_GATEWAY_TOKEN": "your-token" + * } + * } + * } + * } + */ + +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; + +export type CursorMcpPluginConfig = { + enabled?: boolean; + port?: number; + autoApproveTools?: string[]; +}; + +const cursorMcpPlugin = { + id: "cursor-mcp", + name: "Cursor MCP Server", + description: "MCP server integration for Cursor IDE - enables OpenClaw as an AI agent in Cursor", + configSchema: emptyPluginConfigSchema(), + + register(api: OpenClawPluginApi) { + // Register the CLI command for starting the MCP server + api.registerCli( + async (ctx) => { + const mcpCommand = ctx.program + .command("mcp") + .description("MCP server for IDE integration"); + + mcpCommand + .command("serve") + .description("Start the MCP server for Cursor IDE integration") + .option("--url ", "Gateway WebSocket URL", "ws://127.0.0.1:18789") + .option("--token ", "Gateway auth token") + .option("--password ", "Gateway auth password") + .option("--session ", "Default session key", "agent:main:cursor") + .action(async (opts) => { + // Dynamic import to avoid loading MCP SDK unless needed + const { OpenClawMcpServer } = await import("./src/server.js"); + + const server = new OpenClawMcpServer({ + gatewayUrl: opts.url, + gatewayToken: opts.token, + gatewayPassword: opts.password, + defaultSessionKey: opts.session, + }); + + process.on("SIGINT", async () => { + await server.stop(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + await server.stop(); + process.exit(0); + }); + + await server.start(); + }); + + mcpCommand + .command("info") + .description("Show MCP server configuration information for Cursor") + .action(() => { + console.log(` +OpenClaw MCP Server - Cursor IDE Integration + +To use OpenClaw in Cursor, add the following to your Cursor MCP configuration: + +1. Open Cursor Settings > Features > MCP +2. Click "+ Add New MCP Server" +3. Configure: + - Name: openclaw + - Type: stdio + - Command: openclaw mcp serve + +Or manually edit ~/.cursor/mcp.json: + +{ + "mcpServers": { + "openclaw": { + "command": "openclaw", + "args": ["mcp", "serve"], + "env": { + "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789" + } + } + } +} + +Environment Variables: +- OPENCLAW_GATEWAY_URL: Gateway WebSocket URL (default: ws://127.0.0.1:18789) +- OPENCLAW_GATEWAY_TOKEN: Authentication token +- OPENCLAW_GATEWAY_PASSWORD: Authentication password +- OPENCLAW_SESSION_KEY: Default session key (default: agent:main:cursor) + +Available Tools: +- openclaw_chat: Chat with the OpenClaw AI agent +- openclaw_list_sessions: List active sessions +- openclaw_get_session: Get session details +- openclaw_clear_session: Clear session history +- openclaw_execute_command: Execute OpenClaw commands +- openclaw_send_message: Send messages through channels +- openclaw_get_status: Get gateway status +- openclaw_list_models: List available models + +Available Resources: +- openclaw://status: Gateway status +- openclaw://models: Available models +- openclaw://sessions: Active sessions +- openclaw://config: Configuration (sanitized) + +Available Prompts: +- code_review: Review code for issues +- explain_code: Explain how code works +- generate_tests: Generate tests +- refactor_code: Suggest refactoring +- debug_help: Help debug issues +- send_notification: Send notification via channels +`); + }); + }, + { commands: ["mcp"] }, + ); + }, +}; + +export default cursorMcpPlugin; diff --git a/extensions/cursor-mcp/openclaw.plugin.json b/extensions/cursor-mcp/openclaw.plugin.json new file mode 100644 index 000000000..9e6d24541 --- /dev/null +++ b/extensions/cursor-mcp/openclaw.plugin.json @@ -0,0 +1,25 @@ +{ + "id": "cursor-mcp", + "name": "Cursor MCP Server", + "description": "MCP server integration for Cursor IDE - enables OpenClaw as an AI agent in Cursor", + "providers": [], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable the Cursor MCP server" + }, + "port": { + "type": "number", + "description": "Port for SSE transport (optional, uses stdio by default)" + }, + "autoApproveTools": { + "type": "array", + "items": { "type": "string" }, + "description": "List of tool names to auto-approve without user confirmation" + } + } + } +} diff --git a/extensions/cursor-mcp/package.json b/extensions/cursor-mcp/package.json new file mode 100644 index 000000000..bd6bf4bbd --- /dev/null +++ b/extensions/cursor-mcp/package.json @@ -0,0 +1,29 @@ +{ + "name": "@openclaw/cursor-mcp", + "version": "2026.1.29", + "type": "module", + "description": "OpenClaw MCP server for Cursor IDE integration", + "main": "index.ts", + "openclaw": { + "extensions": [ + "./index.ts" + ] + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", + "ws": "^8.19.0", + "zod": "^3.25.0" + }, + "peerDependencies": { + "openclaw": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/ws": "^8.18.1", + "typescript": "^5.9.0" + }, + "scripts": { + "build": "tsc", + "start": "node --import tsx src/server.ts" + } +} diff --git a/extensions/cursor-mcp/src/gateway-client.ts b/extensions/cursor-mcp/src/gateway-client.ts new file mode 100644 index 000000000..0c2e95d0e --- /dev/null +++ b/extensions/cursor-mcp/src/gateway-client.ts @@ -0,0 +1,302 @@ +/** + * Gateway client for MCP server + * + * This module provides a standalone WebSocket client for communicating + * with the OpenClaw gateway. It implements the gateway's JSON-RPC protocol. + */ + +import { randomUUID } from "node:crypto"; +import { WebSocket } from "ws"; + +import type { CursorMcpConfig } from "./types.js"; + +type RequestFrame = { + type: "req"; + id: string; + method: string; + params?: unknown; +}; + +type ResponseFrame = { + type: "res"; + id: string; + ok: boolean; + payload?: unknown; + error?: { message?: string }; +}; + +type EventFrame = { + type: "evt"; + event: string; + payload?: unknown; + seq?: number; +}; + +type Pending = { + resolve: (value: unknown) => void; + reject: (err: Error) => void; +}; + +const PROTOCOL_VERSION = 6; + +export class McpGatewayClient { + private ws: WebSocket | null = null; + private connected = false; + private config: CursorMcpConfig; + private pending = new Map(); + private eventHandlers: Map void)[]> = new Map(); + private connectResolve: ((value: void) => void) | null = null; + private connectReject: ((err: Error) => void) | null = null; + private connectNonce: string | null = null; + private connectSent = false; + private connectTimer: ReturnType | null = null; + + constructor(config: CursorMcpConfig) { + this.config = config; + } + + async connect(): Promise { + return new Promise((resolve, reject) => { + this.connectResolve = resolve; + this.connectReject = reject; + + const gatewayUrl = this.config.gatewayUrl ?? "ws://127.0.0.1:18789"; + + try { + this.ws = new WebSocket(gatewayUrl, { + maxPayload: 25 * 1024 * 1024, + }); + } catch (err) { + reject(err instanceof Error ? err : new Error(String(err))); + return; + } + + this.ws.on("open", () => { + this.queueConnect(); + }); + + this.ws.on("message", (data) => { + const raw = typeof data === "string" ? data : data.toString("utf8"); + this.handleMessage(raw); + }); + + this.ws.on("close", (code, reason) => { + const reasonText = typeof reason === "string" ? reason : reason.toString("utf8"); + this.ws = null; + this.connected = false; + this.flushPendingErrors(new Error(`Gateway closed (${code}): ${reasonText}`)); + }); + + this.ws.on("error", (err) => { + if (!this.connectSent && this.connectReject) { + this.connectReject(err instanceof Error ? err : new Error(String(err))); + this.connectResolve = null; + this.connectReject = null; + } + }); + }); + } + + disconnect(): void { + if (this.connectTimer) { + clearTimeout(this.connectTimer); + this.connectTimer = null; + } + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.connected = false; + this.flushPendingErrors(new Error("Client disconnected")); + } + + isConnected(): boolean { + return this.connected; + } + + private queueConnect(): void { + this.connectNonce = null; + this.connectSent = false; + if (this.connectTimer) clearTimeout(this.connectTimer); + // Wait a bit for optional challenge before sending connect + this.connectTimer = setTimeout(() => { + this.sendConnect(); + }, 500); + } + + private sendConnect(): void { + if (this.connectSent) return; + this.connectSent = true; + if (this.connectTimer) { + clearTimeout(this.connectTimer); + this.connectTimer = null; + } + + const params = { + minProtocol: PROTOCOL_VERSION, + maxProtocol: PROTOCOL_VERSION, + client: { + id: "mcp", + displayName: "Cursor MCP", + version: "2026.1.29", + platform: process.platform, + mode: "backend", + }, + caps: [], + auth: { + token: this.config.gatewayToken, + password: this.config.gatewayPassword, + }, + role: "operator", + scopes: ["operator.admin"], + }; + + this.request("connect", params) + .then(() => { + this.connected = true; + if (this.connectResolve) { + this.connectResolve(); + this.connectResolve = null; + this.connectReject = null; + } + }) + .catch((err) => { + if (this.connectReject) { + this.connectReject(err instanceof Error ? err : new Error(String(err))); + this.connectResolve = null; + this.connectReject = null; + } + this.ws?.close(1008, "connect failed"); + }); + } + + private handleMessage(raw: string): void { + try { + const parsed = JSON.parse(raw); + + // Handle events + if (parsed.type === "evt" || parsed.event) { + const evt = parsed as EventFrame; + + // Handle connect challenge + if (evt.event === "connect.challenge") { + const payload = evt.payload as { nonce?: unknown } | undefined; + const nonce = payload && typeof payload.nonce === "string" ? payload.nonce : null; + if (nonce) { + this.connectNonce = nonce; + this.sendConnect(); + } + return; + } + + // Dispatch to event handlers + const handlers = this.eventHandlers.get(evt.event); + if (handlers) { + for (const handler of handlers) { + try { + handler(evt.payload); + } catch (err) { + console.error(`Event handler error for ${evt.event}:`, err); + } + } + } + return; + } + + // Handle responses + if (parsed.type === "res" || (parsed.id && (parsed.ok !== undefined || parsed.error))) { + const res = parsed as ResponseFrame; + const pending = this.pending.get(res.id); + if (!pending) return; + + this.pending.delete(res.id); + if (res.ok) { + pending.resolve(res.payload); + } else { + pending.reject(new Error(res.error?.message ?? "Unknown error")); + } + } + } catch (err) { + console.error(`Gateway message parse error: ${String(err)}`); + } + } + + private flushPendingErrors(err: Error): void { + for (const [, p] of this.pending) { + p.reject(err); + } + this.pending.clear(); + } + + onEvent(eventName: string, handler: (payload: unknown) => void): () => void { + const handlers = this.eventHandlers.get(eventName) ?? []; + handlers.push(handler); + this.eventHandlers.set(eventName, handlers); + + return () => { + const idx = handlers.indexOf(handler); + if (idx >= 0) handlers.splice(idx, 1); + }; + } + + async request(method: string, params?: unknown): Promise { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error("Gateway not connected"); + } + + const id = randomUUID(); + const frame: RequestFrame = { type: "req", id, method, params }; + + return new Promise((resolve, reject) => { + this.pending.set(id, { + resolve: (value) => resolve(value as T), + reject, + }); + this.ws!.send(JSON.stringify(frame)); + }); + } + + // Convenience methods for common operations + + async chat(params: { + message: string; + sessionKey?: string; + model?: string; + deliver?: boolean; + }): Promise { + return this.request("chat.run", { + message: params.message, + sessionKey: params.sessionKey ?? this.config.defaultSessionKey ?? "agent:main:cursor", + model: params.model, + deliver: params.deliver ?? false, + }); + } + + async listSessions(): Promise { + return this.request("sessions.list", {}); + } + + async getSessionInfo(sessionKey: string): Promise { + return this.request("sessions.get", { sessionKey }); + } + + async clearSession(sessionKey: string): Promise { + return this.request("sessions.clear", { sessionKey }); + } + + async getChannelStatus(): Promise { + return this.request("channels.status", {}); + } + + async getHealth(): Promise { + return this.request("health", {}); + } + + async getModels(): Promise { + return this.request("models.list", {}); + } + + async executeCommand(command: string): Promise { + return this.request("command", { command }); + } +} diff --git a/extensions/cursor-mcp/src/server.ts b/extensions/cursor-mcp/src/server.ts new file mode 100644 index 000000000..aa3091448 --- /dev/null +++ b/extensions/cursor-mcp/src/server.ts @@ -0,0 +1,818 @@ +#!/usr/bin/env node +/** + * OpenClaw MCP Server for Cursor IDE Integration + * + * This server exposes OpenClaw functionality as an MCP server that can be used + * within Cursor IDE. It provides tools for chatting with the OpenClaw agent, + * managing sessions, and executing commands. + * + * Transport modes: + * - stdio (default): For local process-based integration + * - sse: For HTTP-based SSE integration (set OPENCLAW_MCP_PORT) + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; + +import { McpGatewayClient } from "./gateway-client.js"; +import type { CursorMcpConfig, McpToolResult } from "./types.js"; + +// Tool input schemas +const ChatSchema = z.object({ + message: z.string().describe("The message to send to the OpenClaw agent"), + sessionKey: z + .string() + .optional() + .describe("Session key for conversation continuity (e.g., 'agent:main:cursor')"), + model: z.string().optional().describe("Model to use (e.g., 'anthropic/claude-sonnet-4')"), +}); + +const SessionKeySchema = z.object({ + sessionKey: z.string().describe("The session key to operate on"), +}); + +const CommandSchema = z.object({ + command: z.string().describe("The command to execute (e.g., '/status', '/help')"), +}); + +const SendMessageSchema = z.object({ + target: z.string().describe("Target identifier (phone number, group ID, or channel:target)"), + message: z.string().describe("Message content to send"), + channel: z + .string() + .optional() + .describe("Channel to use (whatsapp, telegram, discord, slack, etc.)"), +}); + +// MCP Server implementation +export class OpenClawMcpServer { + private server: Server; + private gatewayClient: McpGatewayClient; + private config: CursorMcpConfig; + + constructor(config: CursorMcpConfig = {}) { + this.config = { + gatewayUrl: process.env.OPENCLAW_GATEWAY_URL ?? config.gatewayUrl ?? "ws://127.0.0.1:18789", + gatewayToken: process.env.OPENCLAW_GATEWAY_TOKEN ?? config.gatewayToken, + gatewayPassword: process.env.OPENCLAW_GATEWAY_PASSWORD ?? config.gatewayPassword, + defaultSessionKey: config.defaultSessionKey ?? "agent:main:cursor", + autoApproveTools: config.autoApproveTools ?? [], + ...config, + }; + + this.gatewayClient = new McpGatewayClient(this.config); + + this.server = new Server( + { + name: "openclaw-mcp", + version: "2026.1.29", + }, + { + capabilities: { + tools: {}, + resources: {}, + prompts: {}, + }, + }, + ); + + this.setupHandlers(); + } + + private setupHandlers(): void { + this.setupToolHandlers(); + this.setupResourceHandlers(); + this.setupPromptHandlers(); + } + + private setupToolHandlers(): void { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "openclaw_chat", + description: + "Chat with the OpenClaw AI agent. Use this to ask questions, request help with coding, or have a conversation. The agent can help with various tasks including code generation, explanations, and problem-solving.", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", + description: "The message to send to the OpenClaw agent", + }, + sessionKey: { + type: "string", + description: + "Session key for conversation continuity (e.g., 'agent:main:cursor'). Different session keys maintain separate conversation contexts.", + }, + model: { + type: "string", + description: + "Model to use (e.g., 'anthropic/claude-sonnet-4', 'openai/gpt-4o'). Leave empty to use the default model.", + }, + }, + required: ["message"], + }, + }, + { + name: "openclaw_list_sessions", + description: + "List all active OpenClaw chat sessions. Returns information about ongoing conversations including session keys, message counts, and last activity times.", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "openclaw_get_session", + description: + "Get detailed information about a specific OpenClaw session, including conversation history summary and metadata.", + inputSchema: { + type: "object", + properties: { + sessionKey: { + type: "string", + description: "The session key to get information about", + }, + }, + required: ["sessionKey"], + }, + }, + { + name: "openclaw_clear_session", + description: + "Clear a specific OpenClaw session, removing all conversation history. Use this to start fresh with a new context.", + inputSchema: { + type: "object", + properties: { + sessionKey: { + type: "string", + description: "The session key to clear", + }, + }, + required: ["sessionKey"], + }, + }, + { + name: "openclaw_execute_command", + description: + "Execute an OpenClaw control command. Available commands include /status, /help, /models, /config, and more.", + inputSchema: { + type: "object", + properties: { + command: { + type: "string", + description: + "The command to execute (e.g., '/status', '/help', '/models list')", + }, + }, + required: ["command"], + }, + }, + { + name: "openclaw_send_message", + description: + "Send a message through OpenClaw to a specific target on a messaging channel (WhatsApp, Telegram, Discord, Slack, etc.).", + inputSchema: { + type: "object", + properties: { + target: { + type: "string", + description: + "Target identifier (phone number for WhatsApp, username/chat ID for Telegram, channel ID for Discord/Slack)", + }, + message: { + type: "string", + description: "Message content to send", + }, + channel: { + type: "string", + description: + "Channel to use: whatsapp, telegram, discord, slack, imessage, signal, line, etc.", + }, + }, + required: ["target", "message"], + }, + }, + { + name: "openclaw_get_status", + description: + "Get the current status of OpenClaw gateway, including connected channels, active sessions, and health information.", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "openclaw_list_models", + description: + "List all available AI models configured in OpenClaw. Shows provider, model ID, and capabilities.", + inputSchema: { + type: "object", + properties: {}, + }, + }, + ], + })); + + // Handle tool calls + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + // Ensure gateway is connected + if (!this.gatewayClient.isConnected()) { + try { + await this.gatewayClient.connect(); + } catch (err) { + return this.errorResult(`Failed to connect to OpenClaw gateway: ${String(err)}`); + } + } + + switch (name) { + case "openclaw_chat": { + const parsed = ChatSchema.safeParse(args); + if (!parsed.success) { + return this.errorResult(`Invalid arguments: ${parsed.error.message}`); + } + const result = await this.gatewayClient.chat({ + message: parsed.data.message, + sessionKey: parsed.data.sessionKey ?? this.config.defaultSessionKey, + model: parsed.data.model, + deliver: false, + }); + return this.formatChatResult(result); + } + + case "openclaw_list_sessions": { + const result = await this.gatewayClient.listSessions(); + return this.formatJsonResult(result); + } + + case "openclaw_get_session": { + const parsed = SessionKeySchema.safeParse(args); + if (!parsed.success) { + return this.errorResult(`Invalid arguments: ${parsed.error.message}`); + } + const result = await this.gatewayClient.getSessionInfo(parsed.data.sessionKey); + return this.formatJsonResult(result); + } + + case "openclaw_clear_session": { + const parsed = SessionKeySchema.safeParse(args); + if (!parsed.success) { + return this.errorResult(`Invalid arguments: ${parsed.error.message}`); + } + const result = await this.gatewayClient.clearSession(parsed.data.sessionKey); + return this.formatJsonResult(result); + } + + case "openclaw_execute_command": { + const parsed = CommandSchema.safeParse(args); + if (!parsed.success) { + return this.errorResult(`Invalid arguments: ${parsed.error.message}`); + } + const result = await this.gatewayClient.executeCommand(parsed.data.command); + return this.formatJsonResult(result); + } + + case "openclaw_send_message": { + const parsed = SendMessageSchema.safeParse(args); + if (!parsed.success) { + return this.errorResult(`Invalid arguments: ${parsed.error.message}`); + } + const target = parsed.data.channel + ? `${parsed.data.channel}:${parsed.data.target}` + : parsed.data.target; + const result = await this.gatewayClient.request("message.send", { + target, + text: parsed.data.message, + }); + return this.formatJsonResult(result); + } + + case "openclaw_get_status": { + const [health, channels] = await Promise.all([ + this.gatewayClient.getHealth(), + this.gatewayClient.getChannelStatus(), + ]); + return this.formatJsonResult({ health, channels }); + } + + case "openclaw_list_models": { + const result = await this.gatewayClient.getModels(); + return this.formatJsonResult(result); + } + + default: + return this.errorResult(`Unknown tool: ${name}`); + } + } catch (err) { + return this.errorResult(`Tool execution failed: ${String(err)}`); + } + }); + } + + private setupResourceHandlers(): void { + // List available resources + this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [ + { + uri: "openclaw://status", + name: "OpenClaw Gateway Status", + description: "Current status of the OpenClaw gateway and connected channels", + mimeType: "application/json", + }, + { + uri: "openclaw://models", + name: "Available Models", + description: "List of AI models configured in OpenClaw", + mimeType: "application/json", + }, + { + uri: "openclaw://sessions", + name: "Active Sessions", + description: "List of active chat sessions", + mimeType: "application/json", + }, + { + uri: "openclaw://config", + name: "OpenClaw Configuration", + description: "Current OpenClaw configuration (sanitized, no secrets)", + mimeType: "application/json", + }, + ], + })); + + // Read resource content + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + + try { + if (!this.gatewayClient.isConnected()) { + await this.gatewayClient.connect(); + } + + switch (uri) { + case "openclaw://status": { + const [health, channels] = await Promise.all([ + this.gatewayClient.getHealth(), + this.gatewayClient.getChannelStatus(), + ]); + return { + contents: [ + { + uri, + mimeType: "application/json", + text: JSON.stringify({ health, channels }, null, 2), + }, + ], + }; + } + + case "openclaw://models": { + const models = await this.gatewayClient.getModels(); + return { + contents: [ + { + uri, + mimeType: "application/json", + text: JSON.stringify(models, null, 2), + }, + ], + }; + } + + case "openclaw://sessions": { + const sessions = await this.gatewayClient.listSessions(); + return { + contents: [ + { + uri, + mimeType: "application/json", + text: JSON.stringify(sessions, null, 2), + }, + ], + }; + } + + case "openclaw://config": { + const config = await this.gatewayClient.request("config.get", {}); + return { + contents: [ + { + uri, + mimeType: "application/json", + text: JSON.stringify(config, null, 2), + }, + ], + }; + } + + default: + throw new Error(`Unknown resource: ${uri}`); + } + } catch (err) { + return { + contents: [ + { + uri, + mimeType: "text/plain", + text: `Error reading resource: ${String(err)}`, + }, + ], + }; + } + }); + } + + private setupPromptHandlers(): void { + // List available prompts + this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ + prompts: [ + { + name: "code_review", + description: "Ask OpenClaw to review code for issues, improvements, and best practices", + arguments: [ + { + name: "code", + description: "The code to review", + required: true, + }, + { + name: "language", + description: "Programming language (optional, will be auto-detected)", + required: false, + }, + ], + }, + { + name: "explain_code", + description: "Ask OpenClaw to explain how code works", + arguments: [ + { + name: "code", + description: "The code to explain", + required: true, + }, + ], + }, + { + name: "generate_tests", + description: "Ask OpenClaw to generate tests for code", + arguments: [ + { + name: "code", + description: "The code to generate tests for", + required: true, + }, + { + name: "framework", + description: "Testing framework to use (e.g., jest, pytest, vitest)", + required: false, + }, + ], + }, + { + name: "refactor_code", + description: "Ask OpenClaw to suggest refactoring improvements", + arguments: [ + { + name: "code", + description: "The code to refactor", + required: true, + }, + { + name: "goal", + description: "Refactoring goal (e.g., 'improve readability', 'optimize performance')", + required: false, + }, + ], + }, + { + name: "debug_help", + description: "Ask OpenClaw to help debug an issue", + arguments: [ + { + name: "code", + description: "The code with the issue", + required: true, + }, + { + name: "error", + description: "Error message or description of the problem", + required: true, + }, + ], + }, + { + name: "send_notification", + description: "Send a notification message through OpenClaw channels", + arguments: [ + { + name: "message", + description: "The notification message to send", + required: true, + }, + { + name: "channel", + description: "Channel to use (whatsapp, telegram, discord, slack)", + required: false, + }, + ], + }, + ], + })); + + // Get prompt content + this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case "code_review": { + const code = args?.code ?? ""; + const language = args?.language ?? ""; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please review the following${language ? ` ${language}` : ""} code for: +- Bugs and potential issues +- Code quality and readability +- Performance considerations +- Security concerns +- Best practices and improvements + +Code to review: +\`\`\`${language} +${code} +\`\`\``, + }, + }, + ], + }; + } + + case "explain_code": { + const code = args?.code ?? ""; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please explain how the following code works, step by step: + +\`\`\` +${code} +\`\`\` + +Include: +- What the code does overall +- Key components and their purposes +- Control flow and logic +- Any important patterns or techniques used`, + }, + }, + ], + }; + } + + case "generate_tests": { + const code = args?.code ?? ""; + const framework = args?.framework ?? ""; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please generate comprehensive tests for the following code${framework ? ` using ${framework}` : ""}: + +\`\`\` +${code} +\`\`\` + +Include: +- Unit tests for individual functions/methods +- Edge cases and boundary conditions +- Error handling scenarios +- Mock/stub setup where needed`, + }, + }, + ], + }; + } + + case "refactor_code": { + const code = args?.code ?? ""; + const goal = args?.goal ?? "improve code quality"; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please refactor the following code to ${goal}: + +\`\`\` +${code} +\`\`\` + +Provide: +- The refactored code +- Explanation of changes made +- Benefits of the refactoring`, + }, + }, + ], + }; + } + + case "debug_help": { + const code = args?.code ?? ""; + const error = args?.error ?? ""; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `I'm encountering an issue with this code: + +\`\`\` +${code} +\`\`\` + +Error/Problem: +${error} + +Please help me: +1. Identify the root cause +2. Explain why this is happening +3. Provide a fix with explanation`, + }, + }, + ], + }; + } + + case "send_notification": { + const message = args?.message ?? ""; + const channel = args?.channel ?? "default"; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please send this notification through OpenClaw${channel !== "default" ? ` via ${channel}` : ""}: + +"${message}" + +Use the openclaw_send_message tool if a specific target is configured, or inform me that I need to specify a target.`, + }, + }, + ], + }; + } + + default: + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Unknown prompt: ${name}`, + }, + }, + ], + }; + } + }); + } + + private formatChatResult(result: unknown): McpToolResult { + if (!result || typeof result !== "object") { + return { + content: [{ type: "text", text: "No response from OpenClaw agent" }], + }; + } + + const response = result as { payloads?: Array<{ text?: string }>; error?: string }; + + if (response.error) { + return this.errorResult(response.error); + } + + const payloads = response.payloads; + if (!Array.isArray(payloads) || payloads.length === 0) { + return { + content: [{ type: "text", text: "No response from OpenClaw agent" }], + }; + } + + const text = payloads + .map((p) => (typeof p.text === "string" ? p.text : "")) + .filter(Boolean) + .join("\n\n"); + + return { + content: [{ type: "text", text: text || "No response from OpenClaw agent" }], + }; + } + + private formatJsonResult(result: unknown): McpToolResult { + return { + content: [ + { + type: "text", + text: typeof result === "string" ? result : JSON.stringify(result, null, 2), + }, + ], + }; + } + + private errorResult(message: string): McpToolResult { + return { + content: [{ type: "text", text: `Error: ${message}` }], + isError: true, + }; + } + + async start(): Promise { + // Connect to gateway + try { + await this.gatewayClient.connect(); + console.error("[openclaw-mcp] Connected to OpenClaw gateway"); + } catch (err) { + console.error(`[openclaw-mcp] Warning: Could not connect to gateway: ${String(err)}`); + console.error("[openclaw-mcp] Will attempt to connect when tools are called"); + } + + // Start stdio transport + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error("[openclaw-mcp] MCP server started on stdio transport"); + } + + async stop(): Promise { + this.gatewayClient.disconnect(); + await this.server.close(); + } +} + +// Main entry point for standalone execution +async function main() { + const config: CursorMcpConfig = { + gatewayUrl: process.env.OPENCLAW_GATEWAY_URL, + gatewayToken: process.env.OPENCLAW_GATEWAY_TOKEN, + gatewayPassword: process.env.OPENCLAW_GATEWAY_PASSWORD, + defaultSessionKey: process.env.OPENCLAW_SESSION_KEY ?? "agent:main:cursor", + }; + + const server = new OpenClawMcpServer(config); + + process.on("SIGINT", async () => { + console.error("[openclaw-mcp] Shutting down..."); + await server.stop(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + console.error("[openclaw-mcp] Shutting down..."); + await server.stop(); + process.exit(0); + }); + + await server.start(); +} + +// Run if this is the main module +const isMain = + typeof process !== "undefined" && + process.argv[1] && + (process.argv[1].endsWith("server.ts") || + process.argv[1].endsWith("server.js") || + process.argv[1].includes("cursor-mcp")); + +if (isMain) { + main().catch((err) => { + console.error(`[openclaw-mcp] Fatal error: ${String(err)}`); + process.exit(1); + }); +} + +export { main }; diff --git a/extensions/cursor-mcp/src/types.ts b/extensions/cursor-mcp/src/types.ts new file mode 100644 index 000000000..a22fc40ba --- /dev/null +++ b/extensions/cursor-mcp/src/types.ts @@ -0,0 +1,55 @@ +/** + * Type definitions for the Cursor MCP server + */ + +export type CursorMcpConfig = { + enabled?: boolean; + port?: number; + autoApproveTools?: string[]; + gatewayUrl?: string; + gatewayToken?: string; + gatewayPassword?: string; + defaultSessionKey?: string; +}; + +export type McpToolResult = { + content: Array<{ + type: "text" | "image" | "resource"; + text?: string; + data?: string; + mimeType?: string; + uri?: string; + }>; + isError?: boolean; +}; + +export type ChatRequest = { + message: string; + sessionKey?: string; + model?: string; + stream?: boolean; +}; + +export type SessionInfo = { + sessionKey: string; + agentId: string; + messageCount: number; + lastActivity?: string; + model?: string; +}; + +export type ChannelStatus = { + channelId: string; + accountId: string; + status: "connected" | "disconnected" | "error"; + lastHeartbeat?: string; + error?: string; +}; + +export type GatewayHealth = { + status: "healthy" | "degraded" | "unhealthy"; + uptime: number; + version: string; + channels: ChannelStatus[]; + activeSessionCount: number; +}; diff --git a/extensions/cursor-mcp/tsconfig.json b/extensions/cursor-mcp/tsconfig.json new file mode 100644 index 000000000..c2ec4c815 --- /dev/null +++ b/extensions/cursor-mcp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": ".", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["*.ts", "src/**/*.ts", "bin/**/*.ts"], + "exclude": ["node_modules", "dist"] +}