diff --git a/docs/plugins/cursor-mcp.md b/docs/plugins/cursor-mcp.md new file mode 100644 index 000000000..3ae420a99 --- /dev/null +++ b/docs/plugins/cursor-mcp.md @@ -0,0 +1,254 @@ +--- +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 +``` + +## Using Cursor's Models in OpenClaw + +The integration is bidirectional - you can also use Cursor's AI models (Claude, GPT-4, etc.) as providers for OpenClaw. + +### Setup + +1. **Install Copilot Proxy extension** in Cursor (search for "Copilot Proxy" by AdrianGonz97) + +2. **Check the proxy**: + ```bash + openclaw mcp setup-models --check + ``` + +3. **Configure OpenClaw**: + ```bash + openclaw config set agents.defaults.model cursor/claude-sonnet-4 + ``` + +### Available Models + +| Model | ID | +|-------|-----| +| Claude Sonnet 4 | `cursor/claude-sonnet-4` | +| Claude Sonnet 4 (Thinking) | `cursor/claude-sonnet-4-thinking` | +| GPT-4o | `cursor/gpt-4o` | +| GPT-4o Mini | `cursor/gpt-4o-mini` | +| o1 | `cursor/o1` | +| Gemini 2.5 Pro | `cursor/gemini-2.5-pro` | + +### Usage + +```bash +# Use Cursor's Claude in OpenClaw +openclaw agent --model cursor/claude-sonnet-4 "Help me debug this" + +# In the TUI +openclaw tui --model cursor/gpt-4o +``` + +## 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..de297e011 --- /dev/null +++ b/extensions/cursor-mcp/README.md @@ -0,0 +1,286 @@ +# 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 │ + └───────────────┘ └───────────────┘ └───────────────┘ +``` + +## Using Cursor's Models with OpenClaw + +You can also use Cursor's AI models (Claude, GPT-4, etc.) as providers for OpenClaw. This enables bidirectional integration: + +- **OpenClaw → Cursor**: Use OpenClaw tools in Cursor (MCP server) +- **Cursor → OpenClaw**: Use Cursor's models in OpenClaw + +### Setup Cursor Models + +1. **Install Copilot Proxy extension** in Cursor: + - Search for "Copilot Proxy" by AdrianGonz97 in Extensions + - Install and restart Cursor + +2. **Check the proxy is running**: + ```bash + openclaw mcp setup-models --check + ``` + +3. **Configure OpenClaw** to use Cursor models: + ```bash + # Set a Cursor model as default + openclaw config set agents.defaults.model cursor/claude-sonnet-4 + ``` + + Or manually add to `~/.clawdbot/config.yaml`: + ```yaml + models: + providers: + cursor: + baseUrl: "http://localhost:3000/v1" + apiKey: "cursor-proxy" + api: openai-completions + models: + - id: claude-sonnet-4 + name: Claude Sonnet 4 + contextWindow: 200000 + ``` + +### Available Cursor Models + +| Model ID | Description | +|----------|-------------| +| `cursor/claude-sonnet-4` | Claude Sonnet 4 | +| `cursor/claude-sonnet-4-thinking` | Claude Sonnet 4 with extended thinking | +| `cursor/gpt-4o` | GPT-4o | +| `cursor/gpt-4o-mini` | GPT-4o Mini | +| `cursor/o1` | OpenAI o1 (reasoning) | +| `cursor/o1-mini` | OpenAI o1-mini | +| `cursor/gemini-2.5-pro` | Gemini 2.5 Pro | + +> **Note**: Available models depend on your Cursor subscription tier. + +### Usage Examples + +```bash +# Chat using Cursor's Claude +openclaw agent --model cursor/claude-sonnet-4 "Explain this code" + +# Send message via channels using GPT-4 +openclaw message send --model cursor/gpt-4o "Hello from OpenClaw!" + +# Use in TUI +openclaw tui --model cursor/claude-sonnet-4 +``` + +## 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..13c327130 --- /dev/null +++ b/extensions/cursor-mcp/index.ts @@ -0,0 +1,204 @@ +/** + * 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(); + }); + + // Command to set up Cursor models as a provider + mcpCommand + .command("setup-models") + .description("Configure OpenClaw to use Cursor's AI models via Copilot Proxy") + .option("--url ", "Copilot Proxy base URL", "http://localhost:3000/v1") + .option("--check", "Only check if proxy is running, don't configure") + .action(async (opts) => { + const { + checkCursorProxyHealth, + CURSOR_AVAILABLE_MODELS, + CURSOR_SETUP_INSTRUCTIONS, + } = await import("./src/cursor-models.js"); + + console.log("Checking Cursor Copilot Proxy..."); + const health = await checkCursorProxyHealth(opts.url); + + if (!health.ok) { + console.error(`\n❌ Copilot Proxy not accessible: ${health.error}`); + console.log(CURSOR_SETUP_INSTRUCTIONS); + process.exit(1); + } + + console.log("✓ Copilot Proxy is running"); + + if (opts.check) { + console.log("\nAvailable models:"); + for (const model of CURSOR_AVAILABLE_MODELS) { + console.log(` - cursor/${model.id} (${model.name})`); + } + return; + } + + // Show configuration instructions + console.log("\nTo use Cursor models, add this to your OpenClaw config:\n"); + console.log("```yaml"); + console.log("models:"); + console.log(" providers:"); + console.log(" cursor:"); + console.log(` baseUrl: "${opts.url}"`); + console.log(' apiKey: "cursor-proxy"'); + console.log(" api: openai-completions"); + console.log(" authHeader: false"); + console.log(" models:"); + for (const model of CURSOR_AVAILABLE_MODELS.slice(0, 4)) { + console.log(` - id: ${model.id}`); + console.log(` name: ${model.name}`); + console.log(` contextWindow: ${model.contextWindow}`); + } + console.log("```"); + console.log("\nOr run: openclaw config set agents.defaults.model cursor/claude-sonnet-4"); + console.log("\nAvailable models:"); + for (const model of CURSOR_AVAILABLE_MODELS) { + console.log(` - cursor/${model.id}`); + } + }); + + 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..4abf9366f --- /dev/null +++ b/extensions/cursor-mcp/package.json @@ -0,0 +1,17 @@ +{ + "name": "@openclaw/cursor-mcp", + "version": "2026.1.29", + "type": "module", + "description": "OpenClaw MCP server for Cursor IDE integration", + "openclaw": { + "extensions": [ + "./index.ts" + ] + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3" + }, + "devDependencies": { + "openclaw": "workspace:*" + } +} diff --git a/extensions/cursor-mcp/src/cursor-models.ts b/extensions/cursor-mcp/src/cursor-models.ts new file mode 100644 index 000000000..2ac8ef3a2 --- /dev/null +++ b/extensions/cursor-mcp/src/cursor-models.ts @@ -0,0 +1,233 @@ +/** + * Cursor Model Provider for OpenClaw + * + * This module provides integration with Cursor's Language Model API, + * allowing OpenClaw to use models available through Cursor's subscription + * (Claude, GPT-4, etc.). + * + * How it works: + * 1. Cursor exposes models via a local HTTP API when the Copilot Proxy extension is active + * 2. OpenClaw connects to this API as an OpenAI-compatible endpoint + * 3. You can then use Cursor's models in OpenClaw prompts + * + * Setup: + * 1. Install "Copilot Proxy" extension in Cursor + * 2. Run `openclaw setup cursor` to configure + * 3. Use models like `cursor/claude-sonnet-4` in OpenClaw + */ + +import type { CursorMcpConfig } from "./types.js"; + +// Default Cursor proxy configuration +const DEFAULT_CURSOR_PROXY_URL = "http://localhost:3000/v1"; +const DEFAULT_CURSOR_PROXY_PORT = 3000; + +// Models typically available through Cursor +export const CURSOR_AVAILABLE_MODELS = [ + { + id: "claude-sonnet-4", + name: "Claude Sonnet 4", + provider: "anthropic", + contextWindow: 200000, + reasoning: false, + }, + { + id: "claude-sonnet-4-thinking", + name: "Claude Sonnet 4 (Thinking)", + provider: "anthropic", + contextWindow: 200000, + reasoning: true, + }, + { + id: "gpt-4o", + name: "GPT-4o", + provider: "openai", + contextWindow: 128000, + reasoning: false, + }, + { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + provider: "openai", + contextWindow: 128000, + reasoning: false, + }, + { + id: "o1", + name: "o1", + provider: "openai", + contextWindow: 200000, + reasoning: true, + }, + { + id: "o1-mini", + name: "o1-mini", + provider: "openai", + contextWindow: 128000, + reasoning: true, + }, + { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + provider: "google", + contextWindow: 1000000, + reasoning: false, + }, +] as const; + +export type CursorModelId = (typeof CURSOR_AVAILABLE_MODELS)[number]["id"]; + +export type CursorModelConfig = { + baseUrl: string; + models: string[]; +}; + +/** + * Check if the Cursor proxy is running and accessible + */ +export async function checkCursorProxyHealth( + baseUrl: string = DEFAULT_CURSOR_PROXY_URL, +): Promise<{ ok: boolean; error?: string }> { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(`${baseUrl}/models`, { + method: "GET", + signal: controller.signal, + }); + + clearTimeout(timeout); + + if (response.ok) { + return { ok: true }; + } + return { ok: false, error: `HTTP ${response.status}: ${response.statusText}` }; + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + return { ok: false, error: "Connection timeout - is Copilot Proxy running in Cursor?" }; + } + return { ok: false, error: String(err) }; + } +} + +/** + * Generate OpenClaw configuration patch for Cursor models + */ +export function generateCursorProviderConfig(opts: { + baseUrl?: string; + models?: string[]; +}): Record { + const baseUrl = opts.baseUrl ?? DEFAULT_CURSOR_PROXY_URL; + const modelIds = opts.models ?? CURSOR_AVAILABLE_MODELS.map((m) => m.id); + + return { + models: { + providers: { + cursor: { + baseUrl, + apiKey: "cursor-proxy", // Placeholder - Copilot Proxy handles auth + api: "openai-completions", + authHeader: false, + models: modelIds.map((id) => { + const model = CURSOR_AVAILABLE_MODELS.find((m) => m.id === id); + return { + id, + name: model?.name ?? id, + api: "openai-completions", + reasoning: model?.reasoning ?? false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model?.contextWindow ?? 128000, + maxTokens: 8192, + }; + }), + }, + }, + }, + agents: { + defaults: { + models: Object.fromEntries(modelIds.map((id) => [`cursor/${id}`, {}])), + }, + }, + }; +} + +/** + * Instructions for setting up Cursor model integration + */ +export const CURSOR_SETUP_INSTRUCTIONS = ` +# Using Cursor's Models with OpenClaw + +## Prerequisites +1. Cursor IDE with an active subscription (Pro/Business) +2. The "Copilot Proxy" VS Code extension installed in Cursor + +## Setup Steps + +### Step 1: Install Copilot Proxy Extension +In Cursor, go to Extensions and search for "Copilot Proxy" by AdrianGonz97. +Install it and restart Cursor. + +### Step 2: Start the Proxy Server +The extension should start automatically. Verify it's running: +- Look for "Copilot Proxy" in the status bar +- Or check http://localhost:3000/v1/models in your browser + +### Step 3: Configure OpenClaw +Run the setup command: + openclaw setup cursor + +Or manually add to your config (~/.clawdbot/config.yaml): + +models: + providers: + cursor: + baseUrl: "http://localhost:3000/v1" + apiKey: "cursor-proxy" + api: openai-completions + authHeader: false + models: + - id: claude-sonnet-4 + name: Claude Sonnet 4 + contextWindow: 200000 + - id: gpt-4o + name: GPT-4o + contextWindow: 128000 + +### Step 4: Use Cursor Models +Now you can use Cursor's models in OpenClaw: + +# Set as default model +openclaw config set agents.defaults.model cursor/claude-sonnet-4 + +# Use in a specific message +openclaw message send --model cursor/gpt-4o "Hello!" + +# Use in the TUI +openclaw tui --model cursor/claude-sonnet-4 + +## Available Models (depends on your Cursor subscription) +- cursor/claude-sonnet-4 +- cursor/claude-sonnet-4-thinking +- cursor/gpt-4o +- cursor/gpt-4o-mini +- cursor/o1 +- cursor/o1-mini +- cursor/gemini-2.5-pro + +## Troubleshooting + +### Proxy not responding +- Ensure Cursor is running with the Copilot Proxy extension active +- Check if http://localhost:3000/v1/models returns a response +- Try restarting Cursor + +### Model not available +- Your Cursor subscription may not include all models +- Check Cursor's model selector to see which models you have access to + +### Authentication errors +- Ensure you're logged into Cursor with your subscription account +- The proxy uses your Cursor session for authentication +`; 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/pnpm-lock.yaml b/pnpm-lock.yaml index 95b940c97..5b32d63dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,13 +42,13 @@ importers: version: 1.2.0-beta.3 '@mariozechner/pi-agent-core': specifier: 0.49.3 - version: 0.49.3(ws@8.19.0)(zod@4.3.6) + version: 0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 0.49.3 - version: 0.49.3(ws@8.19.0)(zod@4.3.6) + version: 0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-coding-agent': specifier: 0.49.3 - version: 0.49.3(ws@8.19.0)(zod@4.3.6) + version: 0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': specifier: 0.49.3 version: 0.49.3 @@ -266,6 +266,16 @@ importers: extensions/copilot-proxy: {} + extensions/cursor-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.25.3 + version: 1.25.3(hono@4.11.4)(zod@4.3.6) + devDependencies: + openclaw: + specifier: workspace:* + version: link:../.. + extensions/diagnostics-otel: dependencies: '@opentelemetry/api': @@ -1489,6 +1499,16 @@ packages: '@mistralai/mistralai@1.10.0': resolution: {integrity: sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==} + '@modelcontextprotocol/sdk@1.25.3': + resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mozilla/readability@0.6.0': resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} @@ -3325,6 +3345,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + croner@9.1.0: resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} engines: {node: '>=18.0'} @@ -3531,10 +3555,24 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} @@ -3976,6 +4014,9 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -4002,6 +4043,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -4697,6 +4741,10 @@ packages: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + playwright-core@1.58.0: resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} engines: {node: '>=18'} @@ -6649,10 +6697,12 @@ snapshots: '@glideapps/ts-necessities@2.2.3': {} - '@google/genai@1.34.0': + '@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))': dependencies: google-auth-library: 10.5.0 ws: 8.19.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.4)(zod@4.3.6) transitivePeerDependencies: - bufferutil - supports-color @@ -6700,7 +6750,6 @@ snapshots: '@hono/node-server@1.19.9(hono@4.11.4)': dependencies: hono: 4.11.4 - optional: true '@huggingface/jinja@0.5.3': optional: true @@ -6994,9 +7043,9 @@ snapshots: transitivePeerDependencies: - tailwindcss - '@mariozechner/pi-agent-core@0.49.3(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-agent-core@0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': 0.49.3 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -7007,11 +7056,11 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.49.3(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-ai@0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.972.0 - '@google/genai': 1.34.0 + '@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6)) '@mistralai/mistralai': 1.10.0 '@sinclair/typebox': 0.34.47 ajv: 8.17.1 @@ -7029,12 +7078,12 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.49.3(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-coding-agent@0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: '@mariozechner/clipboard': 0.3.0 '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-agent-core': 0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.49.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': 0.49.3 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -7111,6 +7160,28 @@ snapshots: zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.4) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - hono + - supports-color + '@mozilla/readability@0.6.0': {} '@napi-rs/canvas-android-arm64@0.1.88': @@ -9210,6 +9281,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + croner@9.1.0: {} cross-fetch@4.1.0: @@ -9403,8 +9479,18 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.3.0: {} + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + express@4.22.1: dependencies: accepts: 1.3.8 @@ -9981,6 +10067,8 @@ snapshots: jose@4.15.9: {} + jose@6.1.3: {} + js-base64@3.7.8: {} js-tokens@9.0.1: {} @@ -10002,6 +10090,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json-stringify-safe@5.0.1: {} @@ -10729,6 +10819,8 @@ snapshots: dependencies: pngjs: 7.0.0 + pkce-challenge@5.0.1: {} + playwright-core@1.58.0: {} playwright@1.58.0: