165 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|