Files
local-mcp/go-server/main.go
Brandon Zhang 8a0dffbcae 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)
2026-03-27 15:45:26 +08:00

164 lines
4.2 KiB
Go

// local-mcp-go — localhost MCP server delivering user instructions to agents.
package main
import (
"embed"
"fmt"
"io/fs"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"syscall"
"github.com/mark3labs/mcp-go/server"
"github.com/local-mcp/local-mcp-go/internal/api"
"github.com/local-mcp/local-mcp-go/internal/config"
"github.com/local-mcp/local-mcp-go/internal/db"
"github.com/local-mcp/local-mcp-go/internal/events"
"github.com/local-mcp/local-mcp-go/internal/mcp"
"github.com/local-mcp/local-mcp-go/internal/store"
)
//go:embed static
var staticFS embed.FS
func main() {
cfg := config.Load()
// Logger
level := slog.LevelInfo
switch cfg.LogLevel {
case "DEBUG":
level = slog.LevelDebug
case "WARN", "WARNING":
level = slog.LevelWarn
case "ERROR":
level = slog.LevelError
}
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})))
// Database
database, err := db.Open(cfg.DBPath)
if err != nil {
slog.Error("Failed to open database", "error", err)
os.Exit(1)
}
defer database.Close()
slog.Info("Database initialised", "path", cfg.DBPath)
// Stores
instStore := store.NewInstructionStore(database)
settStore := store.NewSettingsStore(database)
agentStore := store.NewAgentStore(database)
// Event broker
broker := events.NewBroker()
// MCP server
mcpHandler := mcp.New(instStore, settStore, agentStore, broker)
sseServer := server.NewStreamableHTTPServer(
mcpHandler.MCP,
server.WithEndpointPath("/mcp"),
server.WithStateLess(cfg.MCPStateless),
)
// HTTP router
staticSubFS, _ := fs.Sub(staticFS, "static")
router := api.NewRouter(
api.Stores{
Instructions: instStore,
Settings: settStore,
Agents: agentStore,
},
broker,
cfg.APIToken,
staticSubFS,
)
// Combined router: /mcp → MCP StreamableHTTP, /* → REST API
mux := http.NewServeMux()
mux.Handle("/mcp", sseServer)
mux.Handle("/mcp/", http.StripPrefix("/mcp", sseServer))
mux.Handle("/", router)
// Server with auto port switching
port := cfg.HTTPPort
maxAttempts := 10
var srv *http.Server
var addr string
for attempt := 0; attempt < maxAttempts; attempt++ {
addr = fmt.Sprintf("%s:%s", cfg.Host, port)
srv = &http.Server{Addr: addr, Handler: mux}
// Try to listen
ln, err := net.Listen("tcp", addr)
if err == nil {
ln.Close() // Close test listener
break
}
// Port taken, try next
portNum := 8000 + attempt
port = fmt.Sprintf("%d", portNum+1)
if attempt == maxAttempts-1 {
slog.Error("Could not find available port", "tried", maxAttempts)
os.Exit(1)
}
}
if cfg.APIToken != "" {
slog.Info("Token authentication enabled")
} else {
slog.Info("Token authentication disabled (set API_TOKEN to enable)")
}
httpURL := fmt.Sprintf("http://%s", addr)
mcpURL := fmt.Sprintf("http://%s/mcp", addr)
slog.Info("local-mcp-go ready",
"http", httpURL,
"mcp", mcpURL,
"stateless", cfg.MCPStateless,
)
// On Windows, show interactive prompt
if runtime.GOOS == "windows" {
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════════════════╗")
fmt.Printf("║ local-mcp-go ready on port %s%-24s║\n", port, "")
fmt.Println("║ ║")
fmt.Printf("║ Web UI: %-46s║\n", httpURL)
fmt.Printf("║ MCP: %-46s║\n", mcpURL)
fmt.Println("║ ║")
fmt.Println("║ Press Ctrl+C to stop the server ║")
fmt.Println("╚══════════════════════════════════════════════════════════╝")
fmt.Println()
}
// Graceful shutdown on SIGINT / SIGTERM
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go func() {
<-stop
fmt.Println()
slog.Info("Shutting down gracefully...")
_ = srv.Close()
}()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("Server error", "error", err)
os.Exit(1)
}
}