SERVER bridge (mcp_bridge.go): - HTTP server on :9877 exposing 24 MCP tools - Window management: list, get, position, size, bounds, maximize, minimize, restore, focus, visibility, title, fullscreen, create, close - Webview: eval JS, navigate, list - System: clipboard read/write, tray control - Endpoints: /mcp, /mcp/tools, /mcp/call, /health, /ws, /claude CLIENT bridge (claude_bridge.go): - WebSocket relay between GUI clients and MCP core on :9876 - Auto-reconnect with backoff - Bidirectional message forwarding (claude_message type) Moved HTTP server from IDEService to MCPBridge for unified endpoint. Co-Authored-By: Virgil <virgil@lethean.io>
171 lines
4.1 KiB
Go
171 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
var wsUpgrader = websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
}
|
|
|
|
// ClaudeBridge forwards messages between GUI clients and the MCP core WebSocket.
|
|
// This is the CLIENT bridge — it connects to the MCP core process on port 9876
|
|
// and relays messages bidirectionally with connected GUI WebSocket clients.
|
|
type ClaudeBridge struct {
|
|
mcpConn *websocket.Conn
|
|
mcpURL string
|
|
clients map[*websocket.Conn]bool
|
|
clientsMu sync.RWMutex
|
|
broadcast chan []byte
|
|
reconnectMu sync.Mutex
|
|
connected bool
|
|
}
|
|
|
|
// NewClaudeBridge creates a new bridge to the MCP core WebSocket.
|
|
func NewClaudeBridge(mcpURL string) *ClaudeBridge {
|
|
return &ClaudeBridge{
|
|
mcpURL: mcpURL,
|
|
clients: make(map[*websocket.Conn]bool),
|
|
broadcast: make(chan []byte, 256),
|
|
}
|
|
}
|
|
|
|
// Connected reports whether the bridge is connected to MCP core.
|
|
func (cb *ClaudeBridge) Connected() bool {
|
|
cb.reconnectMu.Lock()
|
|
defer cb.reconnectMu.Unlock()
|
|
return cb.connected
|
|
}
|
|
|
|
// Start connects to the MCP WebSocket and starts the bridge.
|
|
func (cb *ClaudeBridge) Start() {
|
|
go cb.connectToMCP()
|
|
go cb.broadcastLoop()
|
|
}
|
|
|
|
// connectToMCP establishes connection to the MCP core WebSocket.
|
|
func (cb *ClaudeBridge) connectToMCP() {
|
|
for {
|
|
cb.reconnectMu.Lock()
|
|
if cb.mcpConn != nil {
|
|
cb.mcpConn.Close()
|
|
}
|
|
|
|
log.Printf("ide bridge: connect to MCP at %s", cb.mcpURL)
|
|
conn, _, err := websocket.DefaultDialer.Dial(cb.mcpURL, nil)
|
|
if err != nil {
|
|
log.Printf("ide bridge: connect failed: %v", err)
|
|
cb.connected = false
|
|
cb.reconnectMu.Unlock()
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
|
|
cb.mcpConn = conn
|
|
cb.connected = true
|
|
cb.reconnectMu.Unlock()
|
|
log.Println("ide bridge: connected to MCP core")
|
|
|
|
// Read messages from MCP and broadcast to GUI clients
|
|
for {
|
|
_, message, err := conn.ReadMessage()
|
|
if err != nil {
|
|
log.Printf("ide bridge: MCP read error: %v", err)
|
|
break
|
|
}
|
|
cb.broadcast <- message
|
|
}
|
|
|
|
cb.reconnectMu.Lock()
|
|
cb.connected = false
|
|
cb.reconnectMu.Unlock()
|
|
|
|
// Connection lost, retry after delay
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
}
|
|
|
|
// broadcastLoop sends messages from MCP core to all connected GUI clients.
|
|
func (cb *ClaudeBridge) broadcastLoop() {
|
|
for message := range cb.broadcast {
|
|
cb.clientsMu.RLock()
|
|
for client := range cb.clients {
|
|
if err := client.WriteMessage(websocket.TextMessage, message); err != nil {
|
|
log.Printf("ide bridge: client write error: %v", err)
|
|
}
|
|
}
|
|
cb.clientsMu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// HandleWebSocket handles WebSocket connections from GUI clients.
|
|
func (cb *ClaudeBridge) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
log.Printf("ide bridge: upgrade error: %v", err)
|
|
return
|
|
}
|
|
|
|
cb.clientsMu.Lock()
|
|
cb.clients[conn] = true
|
|
cb.clientsMu.Unlock()
|
|
|
|
// Send connected message
|
|
connMsg, _ := json.Marshal(map[string]any{
|
|
"type": "system",
|
|
"data": "Connected to Claude bridge",
|
|
"timestamp": time.Now(),
|
|
})
|
|
conn.WriteMessage(websocket.TextMessage, connMsg)
|
|
|
|
defer func() {
|
|
cb.clientsMu.Lock()
|
|
delete(cb.clients, conn)
|
|
cb.clientsMu.Unlock()
|
|
conn.Close()
|
|
}()
|
|
|
|
// Read messages from GUI client and forward to MCP core
|
|
for {
|
|
_, message, err := conn.ReadMessage()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
// Parse the message to check type
|
|
var msg map[string]any
|
|
if err := json.Unmarshal(message, &msg); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Forward claude_message to MCP core
|
|
if msgType, ok := msg["type"].(string); ok && msgType == "claude_message" {
|
|
cb.sendToMCP(message)
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendToMCP sends a message to the MCP WebSocket.
|
|
func (cb *ClaudeBridge) sendToMCP(message []byte) {
|
|
cb.reconnectMu.Lock()
|
|
defer cb.reconnectMu.Unlock()
|
|
|
|
if cb.mcpConn == nil {
|
|
log.Println("ide bridge: MCP not connected, dropping message")
|
|
return
|
|
}
|
|
|
|
if err := cb.mcpConn.WriteMessage(websocket.TextMessage, message); err != nil {
|
|
log.Printf("ide bridge: MCP write error: %v", err)
|
|
}
|
|
}
|