Skip to main content
Glama

MCP Language Server

main.go6.26 kB
package main import ( "context" "flag" "fmt" "os" "os/exec" "os/signal" "path/filepath" "syscall" "time" "github.com/isaacphi/mcp-language-server/internal/logging" "github.com/isaacphi/mcp-language-server/internal/lsp" "github.com/isaacphi/mcp-language-server/internal/watcher" "github.com/mark3labs/mcp-go/server" ) // Create a logger for the core component var coreLogger = logging.NewLogger(logging.Core) type config struct { workspaceDir string lspCommand string lspArgs []string } type mcpServer struct { config config lspClient *lsp.Client mcpServer *server.MCPServer ctx context.Context cancelFunc context.CancelFunc workspaceWatcher *watcher.WorkspaceWatcher } func parseConfig() (*config, error) { cfg := &config{} flag.StringVar(&cfg.workspaceDir, "workspace", "", "Path to workspace directory") flag.StringVar(&cfg.lspCommand, "lsp", "", "LSP command to run (args should be passed after --)") flag.Parse() // Get remaining args after -- as LSP arguments cfg.lspArgs = flag.Args() // Validate workspace directory if cfg.workspaceDir == "" { return nil, fmt.Errorf("workspace directory is required") } workspaceDir, err := filepath.Abs(cfg.workspaceDir) if err != nil { return nil, fmt.Errorf("failed to get absolute path for workspace: %v", err) } cfg.workspaceDir = workspaceDir if _, err := os.Stat(cfg.workspaceDir); os.IsNotExist(err) { return nil, fmt.Errorf("workspace directory does not exist: %s", cfg.workspaceDir) } // Validate LSP command if cfg.lspCommand == "" { return nil, fmt.Errorf("LSP command is required") } if _, err := exec.LookPath(cfg.lspCommand); err != nil { return nil, fmt.Errorf("LSP command not found: %s", cfg.lspCommand) } return cfg, nil } func newServer(config *config) (*mcpServer, error) { ctx, cancel := context.WithCancel(context.Background()) return &mcpServer{ config: *config, ctx: ctx, cancelFunc: cancel, }, nil } func (s *mcpServer) initializeLSP() error { if err := os.Chdir(s.config.workspaceDir); err != nil { return fmt.Errorf("failed to change to workspace directory: %v", err) } client, err := lsp.NewClient(s.config.lspCommand, s.config.lspArgs...) if err != nil { return fmt.Errorf("failed to create LSP client: %v", err) } s.lspClient = client s.workspaceWatcher = watcher.NewWorkspaceWatcher(client) initResult, err := client.InitializeLSPClient(s.ctx, s.config.workspaceDir) if err != nil { return fmt.Errorf("initialize failed: %v", err) } coreLogger.Debug("Server capabilities: %+v", initResult.Capabilities) go s.workspaceWatcher.WatchWorkspace(s.ctx, s.config.workspaceDir) return client.WaitForServerReady(s.ctx) } func (s *mcpServer) start() error { if err := s.initializeLSP(); err != nil { return err } s.mcpServer = server.NewMCPServer( "MCP Language Server", "v0.0.2", server.WithLogging(), server.WithRecovery(), ) err := s.registerTools() if err != nil { return fmt.Errorf("tool registration failed: %v", err) } return server.ServeStdio(s.mcpServer) } func main() { coreLogger.Info("MCP Language Server starting") done := make(chan struct{}) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) config, err := parseConfig() if err != nil { coreLogger.Fatal("%v", err) } server, err := newServer(config) if err != nil { coreLogger.Fatal("%v", err) } // Parent process monitoring channel parentDeath := make(chan struct{}) // Monitor parent process termination // Claude desktop does not properly kill child processes for MCP servers go func() { ppid := os.Getppid() coreLogger.Debug("Monitoring parent process: %d", ppid) ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: currentPpid := os.Getppid() if currentPpid != ppid && (currentPpid == 1 || ppid == 1) { coreLogger.Info("Parent process %d terminated (current ppid: %d), initiating shutdown", ppid, currentPpid) close(parentDeath) return } case <-done: return } } }() // Handle shutdown triggers go func() { select { case sig := <-sigChan: coreLogger.Info("Received signal %v in PID: %d", sig, os.Getpid()) cleanup(server, done) case <-parentDeath: coreLogger.Info("Parent death detected, initiating shutdown") cleanup(server, done) } }() if err := server.start(); err != nil { coreLogger.Error("Server error: %v", err) cleanup(server, done) os.Exit(1) } <-done coreLogger.Info("Server shutdown complete for PID: %d", os.Getpid()) os.Exit(0) } func cleanup(s *mcpServer, done chan struct{}) { coreLogger.Info("Cleanup initiated for PID: %d", os.Getpid()) // Create a context with timeout for shutdown operations ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if s.lspClient != nil { coreLogger.Info("Closing open files") s.lspClient.CloseAllFiles(ctx) // Create a shorter timeout context for the shutdown request shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 500*time.Millisecond) defer shutdownCancel() // Run shutdown in a goroutine with timeout to avoid blocking if LSP doesn't respond shutdownDone := make(chan struct{}) go func() { coreLogger.Info("Sending shutdown request") if err := s.lspClient.Shutdown(shutdownCtx); err != nil { coreLogger.Error("Shutdown request failed: %v", err) } close(shutdownDone) }() // Wait for shutdown with timeout select { case <-shutdownDone: coreLogger.Info("Shutdown request completed") case <-time.After(1 * time.Second): coreLogger.Warn("Shutdown request timed out, proceeding with exit") } coreLogger.Info("Sending exit notification") if err := s.lspClient.Exit(ctx); err != nil { coreLogger.Error("Exit notification failed: %v", err) } coreLogger.Info("Closing LSP client") if err := s.lspClient.Close(); err != nil { coreLogger.Error("Failed to close LSP client: %v", err) } } // Send signal to the done channel select { case <-done: // Channel already closed default: close(done) } coreLogger.Info("Cleanup completed for PID: %d", os.Getpid()) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/isaacphi/mcp-language-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server