Files
local-mcp/go-server/main.go
2026-03-27 18:16:30 +08:00

165 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",
"version", config.AppVersion,
"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 v%-6s ready on port %s%-14s║\n", config.AppVersion, 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)
}
}