MCP Language Server

This is an MCP server that runs and exposes a language server to LLMs. Not a language server for MCP, whatever that would be.
This is a downstream fork of isaacphi/mcp-language-server with a few extra tools, flags, and stability fixes. See Changes from upstream below.
Demo
mcp-language-server helps MCP enabled clients navigate codebases more easily by giving them access semantic tools like get definition, references, rename, and diagnostics.

Setup
-
Install Go: Follow instructions at https://golang.org/doc/install
-
Install this server: go install github.com/STRd6/mcp-language-server@latest
Or, to use upstream's last release (no longer actively maintained): go install github.com/isaacphi/mcp-language-server@latest.
-
Install a language server: follow one of the guides below
-
Configure your MCP client: follow one of the guides below
Go (gopls)
Install gopls: go install golang.org/x/tools/gopls@latest
Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\ Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": ["--workspace", "/Users/you/dev/yourproject/", "--lsp", "gopls"],
"env": {
"PATH": "/opt/homebrew/bin:/Users/you/go/bin",
"GOPATH": "/users/you/go",
"GOCACHE": "/users/you/Library/Caches/go-build",
"GOMODCACHE": "/Users/you/go/pkg/mod"
}
}
}
}
Note: Not all clients will need these environment variables. For Claude Desktop you will need to update the environment variables above based on your machine and username:
PATH needs to contain the path to go and to gopls. Get this with echo $(which go):$(which gopls)
GOPATH, GOCACHE, and GOMODCACHE may be different on your machine. These are the defaults.
Rust (rust-analyzer)
Install rust-analyzer: rustup component add rust-analyzer
Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\ Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": [
"--workspace",
"/Users/you/dev/yourproject/",
"--lsp",
"rust-analyzer"
]
}
}
}
Python (pyright)
Install pyright: npm install -g pyright
Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\ Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": [
"--workspace",
"/Users/you/dev/yourproject/",
"--lsp",
"pyright-langserver",
"--",
"--stdio"
]
}
}
}
Typescript (typescript-language-server)
Install typescript-language-server: npm install -g typescript typescript-language-server
Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\ Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": [
"--workspace",
"/Users/you/dev/yourproject/",
"--lsp",
"typescript-language-server",
"--",
"--stdio"
]
}
}
}
C/C++ (clangd)
Install clangd: Download prebuilt binaries from the official LLVM releases page or install via your system's package manager (e.g., apt install clangd, brew install clangd).
Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\\ Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": [
"--workspace",
"/Users/you/dev/yourproject/",
"--lsp",
"/path/to/your/clangd_binary",
"--",
"--compile-commands-dir=/path/to/yourproject/build_or_compile_commands_dir"
]
}
}
}
<p><strong>Note</strong>:</p>
<ul>
<li>Replace <code>/path/to/your/clangd_binary</code> with the actual path to your clangd executable.</li>
<li><code>--compile-commands-dir</code> should point to the directory containing your <code>compile_commands.json</code> file (e.g., <code>./build</code>, <code>./cmake-build-debug</code>).</li>
<li>Ensure <code>compile_commands.json</code> is generated for your project for clangd to work effectively.</li>
</ul>
Civet (civet-lsp)
Install civet-lsp: npm install -g @danielx/civet-language-server (provides the civet-lsp binary).
Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\ Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": [
"--workspace",
"/Users/you/dev/yourproject/",
"--lsp",
"civet-lsp",
"--",
"--stdio"
]
}
}
}
Other
I have only tested this repo with the servers above but it should be compatible with many more. Note:
- The language server must communicate over stdio.
- Any aruments after
-- are sent as arguments to the language server.
- Any env variables are passed on to the language server.
Flags
In addition to --workspace and --lsp:
-
--disable-tools=tool1,tool2,... — suppress specific tools after registration. Useful when pairing with another tool (e.g. disabling edit_file when running alongside aider).
-
--idle-timeout=10m — suspend the LSP subprocess and release all file watches after this duration with no MCP traffic; the next tool call restarts the LSP transparently. Default 0 (disabled). Useful when the parent editor keeps idle sessions (and their MCP children) alive indefinitely — each idle session otherwise pins an LSP process plus its inotify watches.
-
--lsp-init-async — initialize the LSP in a background goroutine so ServeStdio starts immediately. Capability-gated tools then register after the handshake via tools/list_changed, so the client must honor that notification (Claude Desktop / Cursor do; some MCP clients only read tools/list once at startup). Default is synchronous init so all tools appear in the first tools/list response.
-
--config=/path/to/init.json — pass per-LSP initializationOptions. The file is a JSON object keyed by LSP binary name; the matching entry becomes the LSP's initializationOptions. Solves cases like rust-analyzer needing linkedProjects or gopls needing specific build flags.
{
"rust-analyzer": { "linkedProjects": ["./Cargo.toml"] },
"gopls": { "buildFlags": ["-tags=integration"] }
}
When --config is absent, falls back to the previous hardcoded gopls-friendly defaults (codelenses, semanticTokens). Other LSPs ignore unknown init options per the LSP spec.
Each tool is annotated with title + ReadOnlyHint / DestructiveHint so MCP clients can gate auto-approval and surface clearer labels. Tools that depend on a specific LSP capability are only registered when the connected LSP advertises support, so the tools/list response reflects what the language server can actually do.
Always-on tools (every LSP):
edit_file (destructive): apply multiple text edits to a file based on line numbers. More reliable and token-efficient than search-and-replace edit tools.
diagnostics (read-only): diagnostic information for a specific file. Waits for textDocument/publishDiagnostics rather than sleeping; works with push-only LSPs that reject textDocument/diagnostic.
Capability-gated tools (registered only if the LSP advertises the capability):
definition (read-only): complete source-code definition of a symbol.
references (read-only): all usages and references of a symbol throughout the codebase.
hover (read-only): documentation, type hints, or other hover information at a position.
rename_symbol (destructive): rename a symbol across the project.
document_symbols (read-only): hierarchical symbol outline of a file (classes, functions, methods, etc.).
code_actions (read-only): available code actions (quick fixes, refactorings, source actions) for a range.
format_document (destructive): format a document (or a range within it) and apply the resulting edits to disk.
semantic_tokens (read-only): full semantic-tokens response, decoded with the server's legend. Intended for debugging LSP semantic-token providers.
Changes from upstream
This fork tracks isaacphi/main and folds in fixes from contributor forks. Notable additions:
- LSP init scheduling — synchronous by default so every capability-gated tool is registered before the first
tools/list response (works with clients that don't honor tools/list_changed). Opt into the original lazy behavior with --lsp-init-async: ServeStdio starts immediately and tool handlers gate per-call on waitForLSP so a slow LSP (Kotlin/Gradle, large rust-analyzer indexes) doesn't stall the MCP handshake. Adapted from upstream PR #127.
- Handshake hardening — server-request handlers registered before
initialize so workspace/configuration etc. don't see "method not found"; duplicate initialized notification removed; cleanup paths guarded with sync.Once.
- Preopen removal — the old "open every matching file on
client/registerCapability" goroutine is gone. None of gopls / typescript-language-server / clangd / civet-lsp / rust-analyzer actually needed it, and it was the source of issue #83 ("too many open files").
- Faster diagnostics — wait for
textDocument/publishDiagnostics instead of a fixed 3 s sleep; soft-fail on -32601 from push-only LSPs.
- LSP-3.17 glob matching — bespoke pattern matcher replaced with the gopls implementation (cached, supports
**, {}, [...], [!...]).
- File URIs — built via
protocol.URIFromPath everywhere; Windows paths no longer produce invalid file://C:\... URIs.
- Richer document_symbols — advertise
HierarchicalDocumentSymbolSupport=true so LSPs return nested children (struct fields, interface methods).
- C++ definition lookback — capture
template<...> and [[attribute]] lines preceding the LSP-reported symbol range.
- Tool annotations — every tool tagged with title + ReadOnly / Destructive hints.
- New always-on tools —
document_symbols, code_actions, format_document, semantic_tokens (each registered only if the LSP advertises the capability).
- New flags —
--disable-tools, --idle-timeout, --config (see Flags).
- Extra language detection —
.civet → civet, .hera → hera, .v → coq.
- Integration-test matrix — clangd and civet-lsp added to CI alongside go / rust / python / typescript; each language runs as a separate CI job.
About
This codebase makes use of edited code from gopls to handle LSP communication. See ATTRIBUTION for details. Everything here is covered by a permissive BSD style license.
mcp-go is used for MCP communication. Thank you for your service.
This is beta software. Please let me know by creating an issue if you run into any problems or have suggestions of any kind.
Contributing
Please keep PRs small and open Issues first for anything substantial. AI slop O.K. as long as it is tested, passes checks, and doesn't smell too bad.
Setup
Clone the repo:
git clone https://github.com/STRd6/mcp-language-server.git
cd mcp-language-server
A justfile is included for convenience:
just -l
Available recipes:
build # Build
check # Run code audit checks
fmt # Format code
generate # Generate LSP types and methods
help # Help
install # Install locally
snapshot # Update snapshot tests
test # Run tests
Configure your Claude Desktop (or similar) to use the local binary:
{
"mcpServers": {
"language-server": {
"command": "/full/path/to/your/clone/mcp-language-server/mcp-language-server",
"args": [
"--workspace",
"/path/to/workspace",
"--lsp",
"language-server-executable"
],
"env": {
"LOG_LEVEL": "DEBUG"
}
}
}
}
Rebuild after making changes.
Logging
Setting the LOG_LEVEL environment variable to DEBUG enables verbose logging to stderr for all components including messages to and from the language server and the language server's logs.
LSP interaction
internal/lsp/methods.go contains generated code to make calls to the connected language server.
internal/protocol/tsprotocol.go contains generated code for LSP types. I borrowed this from gopls's source code. Thank you for your service.
- LSP allows language servers to return different types for the same methods. Go doesn't like this so there are some ugly workarounds in
internal/protocol/interfaces.go.
Local Development and Snapshot Tests
There is a snapshot test suite that makes it a lot easier to try out changes to tools. These run actual language servers on mock workspaces and capture output and logs.
You will need the language servers installed locally to run them. The matrix covers gopls, rust-analyzer, basedpyright-langserver, typescript-language-server, clangd, and civet-lsp. Each runs as its own CI job (see .github/workflows/go.yml).
integrationtests/
├── tests/ # Tests are in this folder
├── snapshots/ # Snapshots of tool outputs
├── test-output/ # Gitignored folder showing the final state of each workspace and logs after each test run
└── workspaces/ # Mock workspaces that the tools run on
To update snapshots, run UPDATE_SNAPSHOTS=true go test ./integrationtests/...