Skip to content

Add -open flag to trigger initial opening of workspace files #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
<div>
<p><strong>Install clangd</strong>: Download prebuilt binaries from the <a href="https://github.com/clangd/clangd/releases">official LLVM releases page</a> or install via your system's package manager (e.g., <code>apt install clangd</code>, <code>brew install clangd</code>).</p>
<p><strong>Configure your MCP client</strong>: This will be different but similar for each client. For Claude Desktop, add the following to <code>~/Library/Application\\ Support/Claude/claude_desktop_config.json</code></p>
<p><strong>NOTE</strong>: clangd will not resolve symbols until the first file is opened. Use the `-open` argument to trigger indexing.</p>

<pre>
{
Expand All @@ -140,6 +141,8 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
"/Users/you/dev/yourproject/",
"--lsp",
"/path/to/your/clangd_binary",
"--open",
"/Users/you/dev/yourproject/main.cpp",
"--",
"--compile-commands-dir=/path/to/yourproject/build_or_compile_commands_dir"
]
Expand Down
13 changes: 7 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ module github.com/isaacphi/mcp-language-server
go 1.24.0

require (
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/davecgh/go-spew v1.1.1
github.com/fsnotify/fsnotify v1.9.0
github.com/mark3labs/mcp-go v0.25.0
github.com/mark3labs/mcp-go v0.32.0
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/stretchr/testify v1.10.0
golang.org/x/text v0.25.0
golang.org/x/text v0.26.0
)

require (
Expand All @@ -21,11 +22,11 @@ require (
github.com/spf13/cast v1.7.1 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
26 changes: 14 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -24,8 +26,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mark3labs/mcp-go v0.25.0 h1:UUpcMT3L5hIhuDy7aifj4Bphw4Pfx1Rf8mzMXDe8RQw=
github.com/mark3labs/mcp-go v0.25.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
Expand All @@ -42,18 +44,18 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
2 changes: 1 addition & 1 deletion integrationtests/snapshots/go/hover/struct-type.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
```go
type SharedStruct struct { // size=56 (0x38)
type SharedStruct struct { // size=56 (0x38), class=64 (0x40)
ID int
Name string
Value float64
Expand Down
2 changes: 1 addition & 1 deletion integrationtests/tests/rust/internal/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func GetTestSuite(t *testing.T) *common.TestSuite {
Command: "rust-analyzer",
Args: []string{},
WorkspaceDir: filepath.Join(repoRoot, "integrationtests/workspaces/rust"),
InitializeTimeMs: 3000,
InitializeTimeMs: 5000,
}

// Create a test suite
Expand Down
10 changes: 2 additions & 8 deletions internal/lsp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
return nil, fmt.Errorf("initialize failed: %w", err)
}

if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
return nil, fmt.Errorf("initialized notification failed: %w", err)
if err := c.Initialized(ctx, protocol.InitializedParams{}); err != nil {
return nil, fmt.Errorf("initialized failed: %w", err)
}

// Register handlers
Expand All @@ -208,12 +208,6 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
c.RegisterNotificationHandler("textDocument/publishDiagnostics",
func(params json.RawMessage) { HandleDiagnostics(c, params) })

// Notify the LSP server
err := c.Initialized(ctx, protocol.InitializedParams{})
if err != nil {
return nil, fmt.Errorf("initialization failed: %w", err)
}

// LSP sepecific Initialization
path := strings.ToLower(c.Cmd.Path)
switch {
Expand Down
49 changes: 49 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/bmatcuk/doublestar/v4"
"github.com/isaacphi/mcp-language-server/internal/logging"
"github.com/isaacphi/mcp-language-server/internal/lsp"
"github.com/isaacphi/mcp-language-server/internal/watcher"
Expand All @@ -23,6 +25,7 @@ var coreLogger = logging.NewLogger(logging.Core)
type config struct {
workspaceDir string
lspCommand string
openGlobs StringArrayFlag
lspArgs []string
}

Expand All @@ -35,10 +38,25 @@ type mcpServer struct {
workspaceWatcher *watcher.WorkspaceWatcher
}

// StringArrayFlag is a custom flag type to handle an array of strings
type StringArrayFlag []string

// Set appends a new value to the custom flag value
func (s *StringArrayFlag) Set(value string) error {
*s = append(*s, value)
return nil
}

// String returns the string representation of the custom flag value
func (s *StringArrayFlag) String() string {
return strings.Join(*s, ",")
}

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.Var(&cfg.openGlobs, "open", "Glob of files to open by default (can specify more than once)")
flag.Parse()

// Get remaining args after -- as LSP arguments
Expand Down Expand Up @@ -99,10 +117,41 @@ func (s *mcpServer) initializeLSP() error {

coreLogger.Debug("Server capabilities: %+v", initResult.Capabilities)

if len(s.config.openGlobs) > 0 {
s.openInitialFiles()
}

go s.workspaceWatcher.WatchWorkspace(s.ctx, s.config.workspaceDir)
return client.WaitForServerReady(s.ctx)
}

func (s *mcpServer) openInitialFiles() {

filepath.WalkDir(s.config.workspaceDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}

if !d.IsDir() {
for _, pattern := range s.config.openGlobs {
match, err := doublestar.PathMatch(pattern, path)
if err != nil {
return err
}

if match {
if err := s.lspClient.OpenFile(s.ctx, path); err != nil {
coreLogger.Error("Failed to open file %s: %v", path, err)
}
break
}
}
}

return nil
})
}

func (s *mcpServer) start() error {
if err := s.initializeLSP(); err != nil {
return err
Expand Down
99 changes: 36 additions & 63 deletions tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ func (s *mcpServer) registerTools() error {

s.mcpServer.AddTool(applyTextEditTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
filePath, ok := request.Params.Arguments["filePath"].(string)
if !ok {
return mcp.NewToolResultError("filePath must be a string"), nil
filePath, err := request.RequireString("filePath")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Extract edits array
editsArg, ok := request.Params.Arguments["edits"]
editsArg, ok := request.GetArguments()["edits"]
if !ok {
return mcp.NewToolResultError("edits is required"), nil
}
Expand Down Expand Up @@ -105,9 +105,9 @@ func (s *mcpServer) registerTools() error {

s.mcpServer.AddTool(readDefinitionTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
symbolName, ok := request.Params.Arguments["symbolName"].(string)
if !ok {
return mcp.NewToolResultError("symbolName must be a string"), nil
symbolName, err := request.RequireString("symbolName")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

coreLogger.Debug("Executing definition for symbol: %s", symbolName)
Expand All @@ -129,9 +129,9 @@ func (s *mcpServer) registerTools() error {

s.mcpServer.AddTool(findReferencesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
symbolName, ok := request.Params.Arguments["symbolName"].(string)
if !ok {
return mcp.NewToolResultError("symbolName must be a string"), nil
symbolName, err := request.RequireString("symbolName")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

coreLogger.Debug("Executing references for symbol: %s", symbolName)
Expand Down Expand Up @@ -161,20 +161,13 @@ func (s *mcpServer) registerTools() error {

s.mcpServer.AddTool(getDiagnosticsTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
filePath, ok := request.Params.Arguments["filePath"].(string)
if !ok {
return mcp.NewToolResultError("filePath must be a string"), nil
}

contextLines := 5 // default value
if contextLinesArg, ok := request.Params.Arguments["contextLines"].(int); ok {
contextLines = contextLinesArg
filePath, err := request.RequireString("filePath")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

showLineNumbers := true // default value
if showLineNumbersArg, ok := request.Params.Arguments["showLineNumbers"].(bool); ok {
showLineNumbers = showLineNumbersArg
}
contextLines := request.GetInt("contextLines", 5)
showLineNumbers := request.GetBool("showLineNumbers", true)

coreLogger.Debug("Executing diagnostics for file: %s", filePath)
text, err := tools.GetDiagnosticsForFile(s.ctx, s.lspClient, filePath, contextLines, showLineNumbers)
Expand Down Expand Up @@ -268,29 +261,19 @@ func (s *mcpServer) registerTools() error {

s.mcpServer.AddTool(hoverTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
filePath, ok := request.Params.Arguments["filePath"].(string)
if !ok {
return mcp.NewToolResultError("filePath must be a string"), nil
filePath, err := request.RequireString("filePath")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Handle both float64 and int for line and column due to JSON parsing
var line, column int
switch v := request.Params.Arguments["line"].(type) {
case float64:
line = int(v)
case int:
line = v
default:
return mcp.NewToolResultError("line must be a number"), nil
line, err := request.RequireInt("line")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

switch v := request.Params.Arguments["column"].(type) {
case float64:
column = int(v)
case int:
column = v
default:
return mcp.NewToolResultError("column must be a number"), nil
column, err := request.RequireInt("column")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

coreLogger.Debug("Executing hover for file: %s line: %d column: %d", filePath, line, column)
Expand Down Expand Up @@ -324,34 +307,24 @@ func (s *mcpServer) registerTools() error {

s.mcpServer.AddTool(renameSymbolTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
filePath, ok := request.Params.Arguments["filePath"].(string)
if !ok {
return mcp.NewToolResultError("filePath must be a string"), nil
filePath, err := request.RequireString("filePath")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

newName, ok := request.Params.Arguments["newName"].(string)
if !ok {
return mcp.NewToolResultError("newName must be a string"), nil
newName, err := request.RequireString("newName")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Handle both float64 and int for line and column due to JSON parsing
var line, column int
switch v := request.Params.Arguments["line"].(type) {
case float64:
line = int(v)
case int:
line = v
default:
return mcp.NewToolResultError("line must be a number"), nil
line, err := request.RequireInt("line")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

switch v := request.Params.Arguments["column"].(type) {
case float64:
column = int(v)
case int:
column = v
default:
return mcp.NewToolResultError("column must be a number"), nil
column, err := request.RequireInt("column")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

coreLogger.Debug("Executing rename_symbol for file: %s line: %d column: %d newName: %s", filePath, line, column, newName)
Expand Down