Full Go port of local-mcp with all core features. Copied from local-mcp-go worktree to consolidate into single-branch repo (easier maintenance). Architecture: - internal/config: Environment variable configuration - internal/models: Shared types (Instruction, Settings, AgentActivity, etc.) - internal/db: SQLite init with modernc.org/sqlite (pure Go, no CGo) - internal/store: Database operations + WakeupSignal + AgentTracker - internal/events: SSE broker for browser /api/events endpoint - internal/mcp: get_user_request MCP tool with 5s keepalive progress bars - internal/api: chi HTTP router with Bearer auth middleware - main.go: Entry point with auto port switching and Windows interactive banner Dependencies: - github.com/mark3labs/mcp-go@v0.46.0 - github.com/go-chi/chi/v5@v5.2.5 - modernc.org/sqlite@v1.47.0 (pure Go SQLite) - github.com/google/uuid@v1.6.0 Static assets embedded via //go:embed static Features matching Python: - Same wait strategy: 50s with 5s progress keepalives - Same hardcoded constants (DEFAULT_WAIT_SECONDS, DEFAULT_EMPTY_RESPONSE) - Auto port switching (tries 8000-8009) - Windows interactive mode (formatted banner on double-click launch) Build: cd go-server && go build -o local-mcp.exe . Run: ./local-mcp.exe Binary size: ~18 MB (vs Python ~60+ MB memory footprint) Startup: ~10 ms (vs Python ~1-2s)
74 lines
1.8 KiB
Go
74 lines
1.8 KiB
Go
// Package events provides an SSE event broker for fanning out server-sent
|
|
// events to browser clients watching /api/events.
|
|
package events
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Event is the wire format sent to browser clients.
|
|
type Event struct {
|
|
Type string `json:"type"`
|
|
Timestamp string `json:"timestamp"`
|
|
Data any `json:"data"`
|
|
}
|
|
|
|
// Broker distributes named events to all currently-connected SSE clients.
|
|
// Clients subscribe by calling Subscribe(); they must call Unsubscribe() when
|
|
// done to avoid goroutine leaks.
|
|
type Broker struct {
|
|
mu sync.RWMutex
|
|
clients map[chan []byte]struct{}
|
|
}
|
|
|
|
// NewBroker creates a ready-to-use Broker.
|
|
func NewBroker() *Broker {
|
|
return &Broker{clients: make(map[chan []byte]struct{})}
|
|
}
|
|
|
|
// Subscribe returns a channel that will receive serialised SSE "data: ..." lines.
|
|
func (b *Broker) Subscribe() chan []byte {
|
|
ch := make(chan []byte, 32) // buffered so a slow reader doesn't stall others
|
|
b.mu.Lock()
|
|
b.clients[ch] = struct{}{}
|
|
b.mu.Unlock()
|
|
return ch
|
|
}
|
|
|
|
// Unsubscribe removes the channel and closes it.
|
|
func (b *Broker) Unsubscribe(ch chan []byte) {
|
|
b.mu.Lock()
|
|
delete(b.clients, ch)
|
|
b.mu.Unlock()
|
|
close(ch)
|
|
}
|
|
|
|
// Broadcast encodes and sends an event to all subscribers. Slow subscribers
|
|
// are skipped (their buffered channel is full) to prevent head-of-line blocking.
|
|
func (b *Broker) Broadcast(eventType string, data any) {
|
|
ev := Event{
|
|
Type: eventType,
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
Data: data,
|
|
}
|
|
payload, err := json.Marshal(ev)
|
|
if err != nil {
|
|
return // should never happen
|
|
}
|
|
line := fmt.Sprintf("data: %s\n\n", payload)
|
|
msg := []byte(line)
|
|
|
|
b.mu.RLock()
|
|
for ch := range b.clients {
|
|
select {
|
|
case ch <- msg:
|
|
default: // skip stalled clients
|
|
}
|
|
}
|
|
b.mu.RUnlock()
|
|
}
|
|
|