MCP Language Server
by isaacphi
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/isaacphi/mcp-language-server/internal/lsp"
"github.com/isaacphi/mcp-language-server/internal/protocol"
"github.com/isaacphi/mcp-language-server/internal/watcher"
"github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
type config struct {
workspaceDir string
lspCommand string
lspArgs []string
}
type server struct {
config config
lspClient *lsp.Client
mcpServer *mcp_golang.Server
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) (*server, error) {
ctx, cancel := context.WithCancel(context.Background())
return &server{
config: *config,
ctx: ctx,
cancelFunc: cancel,
}, nil
}
func (s *server) 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)
}
log.Printf("Server capabilities: %+v\n\n", initResult.Capabilities)
err = client.Initialized(s.ctx, protocol.InitializedParams{})
if err != nil {
return fmt.Errorf("initialized notification failed: %v", err)
}
go s.workspaceWatcher.WatchWorkspace(s.ctx, s.config.workspaceDir)
return client.WaitForServerReady(s.ctx)
}
func (s *server) start() error {
if err := s.initializeLSP(); err != nil {
return err
}
s.mcpServer = mcp_golang.NewServer(stdio.NewStdioServerTransport())
err := s.registerTools()
if err != nil {
return fmt.Errorf("tool registration failed: %v", err)
}
return s.mcpServer.Serve()
}
func main() {
done := make(chan struct{})
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
config, err := parseConfig()
if err != nil {
log.Fatal(err)
}
server, err := newServer(config)
if err != nil {
log.Fatal(err)
}
// Parent process monitoring channel
parentDeath := make(chan struct{})
// Monitor parent process termination
go func() {
ppid := os.Getppid()
log.Printf("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) {
log.Printf("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:
log.Printf("Received signal %v in PID: %d", sig, os.Getpid())
cleanup(server, done)
case <-parentDeath:
log.Printf("Parent death detected, initiating shutdown")
cleanup(server, done)
}
}()
if err := server.start(); err != nil {
log.Printf("Server error: %v", err)
cleanup(server, done)
os.Exit(1)
}
<-done
log.Printf("Server shutdown complete for PID: %d", os.Getpid())
os.Exit(0)
}
func cleanup(s *server, done chan struct{}) {
log.Printf("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 {
log.Printf("Sending shutdown request")
if err := s.lspClient.Shutdown(ctx); err != nil {
log.Printf("Shutdown request failed: %v", err)
}
log.Printf("Sending exit notification")
if err := s.lspClient.Exit(ctx); err != nil {
log.Printf("Exit notification failed: %v", err)
}
log.Printf("Closing LSP client")
if err := s.lspClient.Close(); err != nil {
log.Printf("Failed to close LSP client: %v", err)
}
}
// Send signal to the done channel
select {
case <-done: // Channel already closed
default:
close(done)
}
log.Printf("Cleanup completed for PID: %d", os.Getpid())
}