242 lines
6.6 KiB
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)
|
|
}
|
|
}
|