feat: add Go server implementation in go-server/

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)
This commit is contained in:
Brandon Zhang
2026-03-27 15:45:26 +08:00
parent 4db402f258
commit 8a0dffbcae
20 changed files with 2284 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
// 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()
}