Skip to content
Open
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
267 changes: 267 additions & 0 deletions MULTI-LSP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# Multi-LSP Mode

The MCP Language Server supports running multiple language servers simultaneously in a single session.

## Modes of Operation

| Mode | Command | Use Case | Runtime Changes |
|------|---------|----------|-----------------|
| **Single-MCP** | `--workspace <path> --lsp <cmd>` | Single language server for one workspace | Not supported |
| **Unbounded** | `--config <file>` or auto-detected | Multiple LSPs, manage dynamically | All tools available |
| **Session** | `--config <file> --session <file>` or auto-detected config | Multiple LSPs, fixed configuration | Only `lsp_list` and `lsp_select` |

### Single-MCP Mode

Run with `--workspace` and `--lsp` flags:

```bash
mcp-language-server --workspace /path/to/project --lsp gopls
```

Single-MCP mode runs a single language server. LSP management tools are not available.

### Unbounded Mode

Run with `--config` flag:

```bash
mcp-language-server --config config.json
```

Unbounded mode allows you to dynamically start and stop language servers. All LSP management tools are available.

### Session Mode

Run with `--config` and `--session` flags to auto-load a session on startup:

```bash
mcp-language-server --config config.json --session my-session.json
```

Session mode loads a predefined set of LSPs. LSP management tools (`lsp_start`, `lsp_stop`, `lsp_save`, `lsp_load`) are not registered. Only `lsp_list` and `lsp_select` are available. Edit the session file and restart the server to add/remove LSPs.

## Auto-Detection of Config File

The server automatically looks for a config file in standard locations (in order):

1. `~/.mcp-language-server.json`
2. `~/.config/mcp-language-server.json`

This is used when:
- No parameters provided: `mcp-language-server`
- Only `--session` provided: `mcp-language-server --session my-session.json`

If no config file is found in these cases, an error is raised. To use a custom path, explicitly specify `--config`.

## Configuration File

JSON file defining LSP configurations by language:

```json
{
"lsps": {
"go": {
"command": "gopls",
"args": [],
"env": {
"GOPATH": "/home/user/go"
}
},
"python": {
"command": "pyright-langserver",
"args": ["--", "--stdio"]
},
"rust": {
"command": "rust-analyzer",
"args": []
}
}
}
```

### Fields

- **`command`** (required): LSP executable name or full path
- **`args`** (optional): Command-line arguments passed to the LSP
- **`env`** (optional): Environment variables (merged with system environment)

## Tools

### LSP Management

- **`lsp_start(workspace, language)`** - Start a new LSP instance (unbounded mode only)
- **`lsp_stop(id)`** - Stop a running LSP by ID (unbounded mode only)
- **`lsp_list()`** - List all running LSP instances with their selection status
- **`lsp_select(id)`** - Set a different LSP as default
- **`lsp_languages()`** - List available languages from config (unbounded mode only)
- **`lsp_save(filepath)`** - Save current LSP configuration to a session file (unbounded mode only)
- **`lsp_load(filepath)`** - Load LSP configuration from a session file (unbounded mode only)

### Language Tools

All existing tools accept an optional `id` parameter to target a specific LSP:

- `definition(symbolName, id?)`
- `references(symbolName, id?)`
- `diagnostics(filePath, id?)`
- `hover(filePath, line, column, id?)`
- `rename_symbol(filePath, line, column, newName, id?)`
- `edit_file(filePath, edits, id?)`

In unbounded mode with multiple LSPs, either:
- Call `lsp_select(id)` to set the default, then omit `id` parameter in tools, OR
- Provide `id` parameter in every tool call to specify which LSP to use

## Session File Format

Save and restore LSP configurations:

```json
{
"lsps": [
{
"workspace": "/path/to/workspace1",
"language": "go"
},
{
"workspace": "/path/to/workspace2",
"language": "typescript"
}
]
}
```

## Usage Example

### Auto-detected Unbounded Mode
```bash
# Uses ~/.mcp-language-server.json or ~/.config/mcp-language-server.json
mcp-language-server
```

Then dynamically start LSPs:
```
lsp_start(workspace="/project/backend", language="go")
lsp_start(workspace="/project/frontend", language="typescript")
lsp_select(id="<typescript-lsp-id>") # Select an LSP
definition(symbolName="MyFunc") # Uses selected LSP (TypeScript)
definition(symbolName="MyFunc", id="<go-lsp-id>") # Use specific LSP
```

Save the current setup:
```
lsp_save(filepath="~/.config/my-session.json")
```

### Session Mode (Auto-detected config)
```bash
mcp-language-server --session ~/.config/my-session.json
```

Restore a saved session:
```
lsp_load(filepath="~/.config/my-session.json")
lsp_select(id="<lsp-id>") # Switch to a different LSP
```

### Single-MCP Mode
```bash
mcp-language-server --workspace /path/to/project --lsp gopls
```

## Configuration Examples

Edit `config.json` with your system paths. For Go:

```bash
which gopls
go env GOPATH
go env GOCACHE
```

For clangd, verify installation:

```bash
which clangd
```

For Node.js-based LSPs:

```bash
which pyright-langserver
which typescript-language-server
```

## MCP Client Configuration

### Claude Desktop (Auto-detected Unbounded Mode)

```json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server"
}
}
}
```

### Claude Desktop (Explicit Unbounded Mode)

```json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": ["--config", "/path/to/config.json"]
}
}
}
```

### Claude Desktop (Session Mode)

```json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": ["--session", "~/.config/my-session.json"]
}
}
}
```

### Claude Desktop (Single-MCP Mode)

```json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": ["--workspace", "/path/to/project", "--lsp", "gopls"],
"env": {
"GOPATH": "/home/user/go"
}
}
}
}
```

## Error Handling

- **No config file found**: Create `~/.mcp-language-server.json` or use `--config` flag
- **No LSP Started**: Call `lsp_start` before using language tools (unbounded mode)
- **Invalid Language**: Language must be defined in config file
- **Invalid ID**: LSP instance not found or no longer running
- **Session Load Error**: Check workspace paths exist and language names match config

## Notes

- Each LSP instance isolated with its own workspace
- If only one LSP is running, it is automatically selected; if multiple LSPs are running, explicit `lsp_select(id)` or `id` parameter in tools is required
- `lsp_list` shows all running instances (id, language, workspace, status, selected)
- When the selected LSP is stopped, no LSP is selected (must call `lsp_select` again)
- In session mode with multiple LSPs, none are auto-selected on startup (call `lsp_select` to choose)
- In session mode, `lsp_select` switches between pre-loaded LSPs
- Environment variables in config are merged with system environment
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,27 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
</div>
</details>

## Multi-LSP Support

This server supports running multiple language servers simultaneously. See [MULTI-LSP.md](MULTI-LSP.md) for configuration and usage details.

### Quick Example

Configure with multiple languages:

```bash
mcp-language-server --config config.json
```

Then start LSP instances as needed:

```
lsp_start(workspace="/path/to/go-project", language="go")
lsp_start(workspace="/path/to/rust-project", language="rust")
```

See [MULTI-LSP.md](MULTI-LSP.md) for session management, auto-loading, and more.

## Tools

- `definition`: Retrieves the complete source code definition of any symbol (function, type, constant, etc.) from your codebase.
Expand Down
57 changes: 57 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"encoding/json"
"fmt"
"os"
)

// LSPDefaultConfig defines the default configuration for a language server in the config file
type LSPDefaultConfig struct {
Command string `json:"command"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
}

// LSPConfig defines the configuration needed to start a language server instance
// It extends LSPDefaultConfig with the workspace directory for the instance
type LSPConfig struct {
LSPDefaultConfig
Workspace string // Workspace directory for this instance
}

// Config represents the loaded configuration
type Config struct {
// Defaults are the LSP definitions from the config file
Defaults map[string]LSPDefaultConfig
// LSPs maps language -> workspace -> config
// This structure supports multiple instances per language
LSPs map[string]map[string]LSPConfig
}

// ConfigFile represents the structure of the config file on disk
type ConfigFile struct {
LSPs map[string]LSPDefaultConfig `json:"lsps"`
}

// LoadConfigFile loads and parses the configuration file
func LoadConfigFile(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}

var configFile ConfigFile
if err := json.Unmarshal(data, &configFile); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}

if len(configFile.LSPs) == 0 {
return nil, fmt.Errorf("config file must contain at least one LSP definition")
}

return &Config{
Defaults: configFile.LSPs,
LSPs: make(map[string]map[string]LSPConfig),
}, nil
}
36 changes: 36 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"lsps": {
"go": {
"command": "gopls",
"args": [],
"env": {
"GOPATH": "/home/user/go",
"GOCACHE": "/home/user/.cache/go-build"
}
},
"rust": {
"command": "rust-analyzer",
"args": []
},
"python": {
"command": "pyright-langserver",
"args": ["--", "--stdio"]
},
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"]
},
"javascript": {
"command": "typescript-language-server",
"args": ["--stdio"]
},
"c": {
"command": "clangd",
"args": []
},
"cpp": {
"command": "clangd",
"args": []
}
}
}
Loading