go-ai/docs/ide-bridge.md
Snider 6460301533
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 2m1s
docs: add human-friendly documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:02:39 +00:00

7.1 KiB

title description
IDE Bridge IDE integration bridge connecting the MCP server to Laravel core-agentic via WebSocket.

IDE Bridge

The mcp/ide package implements the IDE subsystem. It bridges the desktop MCP server to the Laravel core-agentic backend via a persistent WebSocket connection, enabling AI agents to interact with agent sessions, builds, and the platform dashboard.

Architecture

MCP Client (Claude, Cursor, etc.)
    |
    v  MCP JSON-RPC
go-ai MCP Server
    |
    v  ide_* tool calls
IDE Subsystem (mcp/ide/)
    |
    +-- Bridge -----------> Laravel core-agentic
    |   (WebSocket)         ws://localhost:9876/ws
    |
    +-- ws.Hub <------------ Bridge dispatch
         |
         v  real-time updates
    Local WebSocket subscribers (browser UIs)

Subsystem Structure

type Subsystem struct {
    cfg    Config
    bridge *Bridge    // nil in headless mode
    hub    *ws.Hub    // local WebSocket hub for real-time forwarding
}

When a ws.Hub is provided, the subsystem creates a Bridge that actively connects to Laravel. Without a hub (hub == nil), the subsystem runs in headless mode: tools are still registered and return stub responses, but no real-time forwarding occurs.

Configuration

type Config struct {
    LaravelWSURL         string        // WebSocket endpoint (default: ws://localhost:9876/ws)
    WorkspaceRoot        string        // local path for workspace context
    Token                string        // Bearer token for Authorization header
    ReconnectInterval    time.Duration // base backoff (default: 2s)
    MaxReconnectInterval time.Duration // cap for exponential backoff (default: 30s)
}

All fields are overridable via functional options:

sub := ide.New(hub,
    ide.WithLaravelURL("ws://custom:9876/ws"),
    ide.WithToken("my-bearer-token"),
    ide.WithWorkspaceRoot("/path/to/project"),
    ide.WithReconnectInterval(5 * time.Second),
)

WebSocket Bridge

The Bridge maintains a persistent WebSocket connection to Laravel and forwards inbound messages to the local ws.Hub.

Connection Lifecycle

StartBridge(ctx)
    +-- go connectLoop(ctx)
           +-- dial(ctx)              <-- WebSocket upgrade with Bearer token
           |      sets b.connected = true
           +-- readLoop(ctx)          <-- blocks reading frames
                  +-- dispatch(msg)   <-- routes to ws.Hub channel
                  [on read error]
                  sets b.connected = false, returns to connectLoop

Exponential Backoff

When the connection drops or fails to establish, the bridge uses exponential backoff:

delay := b.cfg.ReconnectInterval    // starts at 2s
for {
    if err := b.dial(ctx); err != nil {
        // wait delay, then double it up to MaxReconnectInterval
        delay = min(delay*2, b.cfg.MaxReconnectInterval)
        continue
    }
    delay = b.cfg.ReconnectInterval // reset on successful connection
    b.readLoop(ctx)
}

Backoff sequence with defaults: 2s, 4s, 8s, 16s, 30s, 30s, ... The delay resets to 2s on every successful connection.

Authentication

var header http.Header
if b.cfg.Token != "" {
    header = http.Header{}
    header.Set("Authorization", "Bearer "+b.cfg.Token)
}
conn, _, err := dialer.DialContext(ctx, b.cfg.LaravelWSURL, header)

When Token is empty, no Authorization header is sent. This is appropriate for development environments running without authentication.

Message Dispatch

Inbound frames from Laravel are deserialised into BridgeMessage:

type BridgeMessage struct {
    Type      string    `json:"type"`
    Channel   string    `json:"channel,omitempty"`
    SessionID string    `json:"sessionId,omitempty"`
    Data      any       `json:"data,omitempty"`
    Timestamp time.Time `json:"timestamp"`
}

The dispatch method routes messages to the local ws.Hub:

func (b *Bridge) dispatch(msg BridgeMessage) {
    channel := msg.Channel
    if channel == "" {
        channel = "ide:" + msg.Type   // synthetic channel name
    }
    b.hub.SendToChannel(channel, ws.Message{Type: ws.TypeEvent, Data: msg.Data})
}

This allows browser-based UIs connected to the local WebSocket hub to receive real-time updates from Laravel without polling.

Outbound Messages

MCP tool handlers call bridge.Send() to push requests to Laravel:

func (b *Bridge) Send(msg BridgeMessage) error {
    b.mu.Lock()
    defer b.mu.Unlock()
    if b.conn == nil {
        return fmt.Errorf("bridge: not connected")
    }
    msg.Timestamp = time.Now()
    data, _ := json.Marshal(msg)
    return b.conn.WriteMessage(websocket.TextMessage, data)
}

A mutex ensures Send and the readLoop do not race on b.conn. If the bridge is disconnected, Send returns an error which propagates to the MCP client as a JSON-RPC error.

IDE Tool Groups

The subsystem registers 11 tools across three groups.

Chat Tools (tools_chat.go)

Tool Description
ide_chat_send Send a message to an agent chat session
ide_chat_history Retrieve message history for a session
ide_session_list List active agent sessions
ide_session_create Create a new agent session
ide_plan_status Get current plan status for a session

Build Tools (tools_build.go)

Tool Description
ide_build_status Get the status of a specific build
ide_build_list List recent builds, optionally filtered by repository
ide_build_logs Retrieve log output for a build

Dashboard Tools (tools_dashboard.go)

Tool Description
ide_dashboard_overview High-level platform overview (repos, services, sessions, builds, bridge status)
ide_dashboard_activity Recent activity feed
ide_dashboard_metrics Aggregate build and agent metrics for a time period

Tool Behaviour

All IDE tools follow a fire-and-forward pattern: the tool sends a BridgeMessage to Laravel and returns an immediate acknowledgement or stub response. Real data arrives asynchronously via the WebSocket read loop and is forwarded to ws.Hub subscribers.

The ide_dashboard_overview tool is the one exception -- it reads bridge.Connected() synchronously to populate the BridgeOnline field.

Registration

The IDE subsystem is registered during MCP server construction:

svc, err := mcp.New(
    mcp.WithWSHub(hub),
    mcp.WithSubsystem(ide.New(hub, ide.WithToken(token))),
)

The subsystem implements SubsystemWithShutdown, closing the bridge connection gracefully when the MCP server shuts down.

Testing

Bridge tests use net/http/httptest to stand up a real WebSocket server in-process. This keeps tests hermetic while exercising:

  • Reconnection logic and exponential backoff
  • Authentication header injection
  • Message dispatch routing
  • Server shutdown detection

The waitConnected helper polls bridge.Connected() with a deadline rather than using fixed sleeps.

All 11 tool handlers are tested under two conditions:

  • nil bridge -- verifies the error path
  • connected mock bridge -- verifies the success path with JSON round-trip validation