opencode-cursor-agent/cmd/mcp-server/main.go

242 lines
6.6 KiB
Go

// Command cursor-mcp-server is a Model Context Protocol (MCP) server that
// exposes the cursor-adapter HTTP API as MCP tools for Claude Desktop.
//
// It communicates with Claude Desktop over stdio (JSON-RPC) and forwards
// requests to a running cursor-adapter instance via HTTP.
//
// Usage (standalone):
//
// go run ./cmd/mcp-server
// go run ./cmd/mcp-server --adapter-url http://127.0.0.1:8765
//
// Usage (Claude Desktop config):
//
// {
// "mcpServers": {
// "cursor-bridge": {
// "command": "/path/to/cursor-mcp-server",
// "args": ["--adapter-url", "http://127.0.0.1:8765"]
// }
// }
// }
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
var adapterURL string
func init() {
flag.StringVar(&adapterURL, "adapter-url", "http://127.0.0.1:8765", "cursor-adapter HTTP base URL")
}
// --- Tool input/output types ---
type AskCursorInput struct {
Prompt string `json:"prompt" mcp:"required"`
Model string `json:"model"`
}
type EmptyInput struct{}
type TextOutput struct {
Text string `json:"text"`
}
// --- Tool handlers ---
func askCursor(ctx context.Context, _ *mcp.CallToolRequest, input AskCursorInput) (*mcp.CallToolResult, TextOutput, error) {
model := input.Model
if model == "" {
model = "claude-opus-4-7-high"
}
payload := map[string]interface{}{
"model": model,
"max_tokens": 16384,
"messages": []map[string]string{{"role": "user", "content": input.Prompt}},
"stream": false,
}
body, _ := json.Marshal(payload)
httpReq, err := http.NewRequestWithContext(ctx, "POST", adapterURL+"/v1/messages", bytes.NewReader(body))
if err != nil {
return nil, TextOutput{}, fmt.Errorf("build request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-api-key", "mcp-bridge")
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(httpReq)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "❌ Cannot connect to cursor-adapter at " + adapterURL + ". Make sure it is running."}},
IsError: true,
}, TextOutput{}, nil
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("❌ cursor-adapter HTTP %d: %s", resp.StatusCode, string(respBody))}},
IsError: true,
}, TextOutput{}, nil
}
var data struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(respBody, &data); err != nil {
return nil, TextOutput{Text: string(respBody)}, nil
}
if data.Error != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "❌ Cursor error: " + data.Error.Message}},
IsError: true,
}, TextOutput{}, nil
}
var texts []string
for _, block := range data.Content {
if block.Type == "text" {
texts = append(texts, block.Text)
}
}
result := strings.Join(texts, "\n")
if result == "" {
result = string(respBody)
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("[Model: %s]\n\n%s", model, result)}},
}, TextOutput{Text: result}, nil
}
func listModels(ctx context.Context, _ *mcp.CallToolRequest, _ EmptyInput) (*mcp.CallToolResult, TextOutput, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", adapterURL+"/v1/models", nil)
if err != nil {
return nil, TextOutput{}, err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "❌ Cannot connect to cursor-adapter"}},
IsError: true,
}, TextOutput{}, nil
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var data struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &data); err != nil {
return nil, TextOutput{Text: string(respBody)}, nil
}
var lines []string
lines = append(lines, fmt.Sprintf("Available models (%d total):\n", len(data.Data)))
for _, m := range data.Data {
lines = append(lines, " "+m.ID)
}
text := strings.Join(lines, "\n")
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: text}},
}, TextOutput{Text: text}, nil
}
func checkHealth(ctx context.Context, _ *mcp.CallToolRequest, _ EmptyInput) (*mcp.CallToolResult, TextOutput, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", adapterURL+"/health", nil)
if err != nil {
return nil, TextOutput{}, err
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "❌ cursor-adapter is not running"}},
IsError: true,
}, TextOutput{}, nil
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var pretty bytes.Buffer
text := string(respBody)
if err := json.Indent(&pretty, respBody, "", " "); err == nil {
text = pretty.String()
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: text}},
}, TextOutput{Text: text}, nil
}
func main() {
flag.Parse()
if envURL := os.Getenv("CURSOR_ADAPTER_URL"); envURL != "" {
adapterURL = envURL
}
server := mcp.NewServer(
&mcp.Implementation{
Name: "cursor-bridge",
Version: "1.0.0",
},
&mcp.ServerOptions{
Instructions: "This server provides access to the Cursor AI coding agent via cursor-adapter. " +
"Use ask_cursor to delegate coding tasks, code generation, debugging, or technical questions to Cursor.",
},
)
mcp.AddTool(server, &mcp.Tool{
Name: "ask_cursor",
Description: "Ask the Cursor AI agent a question or delegate a coding task. " +
"Use this when you need code generation, review, debugging, or a second opinion. " +
"The Cursor agent acts as a pure reasoning engine. " +
"Available models: claude-opus-4-7-high (default), claude-opus-4-7-thinking-high, " +
"claude-4.6-opus-high, claude-4.6-sonnet-medium, gpt-5.4-medium, gemini-3.1-pro. " +
"Pass model name in the 'model' field.",
}, askCursor)
mcp.AddTool(server, &mcp.Tool{
Name: "list_cursor_models",
Description: "List all available models from the Cursor adapter.",
}, listModels)
mcp.AddTool(server, &mcp.Tool{
Name: "cursor_health",
Description: "Check the health status of the cursor-adapter service.",
}, checkHealth)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}