lsp-mcp
Integrates the Ruff language server to provide Python linting, diagnostics, and code quality checks.
Integrates the TypeScript language server to provide code navigation, symbol search, definition lookup, and workspace-wide refactoring for TypeScript and JavaScript files.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
lsp-mcp
An MCP server that exposes LSP-backed code navigation and editing tools to LLM agents. Uses a single global config file to route any file extension to any language server(s) — no per-project configuration required.
Why
Serena is excellent but requires per-project .serena/ files to control which language server each project uses. lsp-mcp lets you define the policy once at ~/.config/lsp-mcp/config.yml and apply it to any project — including ones you don't own.
Related MCP server: AI Knowledge Center MCP
Installation
With uvx (recommended — no install needed)
uvx lsp-mcp --helpWith uv run
git clone https://github.com/AFriemann/lsp-mcp
uv run --project /path/to/lsp-mcp lsp-mcpConfiguration
Create ~/.config/lsp-mcp/config.yml (or $XDG_CONFIG_HOME/lsp-mcp/config.yml):
servers:
# Declare every language server you want to use.
# command: list of tokens (no shell quoting needed).
ty:
command: [uvx, ty, server]
ruff:
command: [ruff, server]
typescript-language-server:
command: [npx, --yes, typescript-language-server, --stdio]
# typescript-language-server needs a TypeScript *with tsserver.js* (5.x).
# `npx` alone fetches no TypeScript, and TypeScript 7+ ships no tsserver.js,
# so point it at a 5.x install (e.g. `npm i --prefix ~/.local/share/ts5 typescript@5`):
initialization_options:
tsserver:
path: /home/you/.local/share/ts5/node_modules/typescript/lib/tsserver.js
gopls:
command: [gopls]
file_handlers:
# Map glob patterns to ordered lists of servers.
# Navigation tools: first server returning a non-empty result wins.
# Diagnostics: results from all servers are merged.
"*.py":
- server: ty # type checker — first priority
- server: ruff # linter — second priority (also provides diagnostics)
"*.ts":
- server: typescript-language-server
"*.js":
- server: typescript-language-server
"*.go":
- server: goplsPattern matching
*.ext— matched against the basename only (e.g.*.pymatches any.pyfile in any directory).src/*.py— matched against the full path (use for path-style filtering).Multiple handlers for the same pattern are concatenated in order; duplicates are removed by first occurrence.
initialization_options (optional, per server) is sent verbatim as the LSP initializationOptions.
Behaviour change: earlier versions parsed
initialization_optionsbut never sent it. It now takes effect, so an existing config that already contains this key will start passing it to the server. Remove or correct it if that is not what you want.
Timeouts, deadline and retries
Setting | Default | Override |
Per-request timeout (each LSP request attempt) | 15 s |
|
Server start + initialize timeout | 30 s |
|
Per-call deadline (whole tool call) | 30 s |
|
Push-diagnostics quiet period | 0.3 s |
|
Values must be positive; the CLI rejects zero, negative and non-numeric values, and invalid environment values are ignored with a warning.
Per-call deadline: covers waiting for the server lock, all requests and all retries of one tool call. The deadline is paused while a server is being started, so starting is not counted against it; each start is bounded separately by the start timeout. On expiry, partial results are discarded and the result's
notenames the tool and the deadline and says the result is unknown (not "no matches").Overall bound: a call takes at most about (time spent starting servers) + the call deadline. Starting is bounded per server by the start timeout, a start failure that is not a timeout is retried once (up to about 2 × start timeout for that server), and a call may start several servers: one per matching handler, and
find_symbolwithoutfile_pathstarts every configured server. So the worst case is roughlyN servers × (up to 2 × start timeout) + call deadline; with typical single-server calls and warm servers it is just the deadline. A request that hits the request timeout returns a note naming the server and operation.Retries: LSP errors
-32801(ContentModified) and-32800(RequestCancelled) are retried up to 3 times (4 attempts) with exponential backoff (0.1 s, 0.2 s, 0.4 s) inside the per-call deadline. If retries are exhausted the note says the request failed — never an empty "no matches" result.Serialisation: requests to one language server are serialised (per-server lock). Read tools open the document once per call;
replace_symbol_bodyandrename_symbolhold the locks of every server they notify.Start failures: a server that fails to start (or times out starting) is not respawned for 30 s. The
noteof every result it affects carries the server name and the error. Command arguments and initialization options are not deliberately logged because they may contain credentials.
Diagnostic logs
INFO diagnostics are saved automatically to ${XDG_STATE_HOME:-~/.local/state}/lsp-mcp/lsp-mcp.log. Each file is limited to 5 MiB, with three rotating backups. The application log directory is owner-only (0700), and log, backup, and lock files are owner-only (0600), including existing files. Unsafe ownership, symbolic links, or hard links disable persistence rather than modifying their targets.
To inspect startup failures:
tail -n 100 "${XDG_STATE_HOME:-$HOME/.local/state}/lsp-mcp/lsp-mcp.log"Records include timestamps, process IDs, server names, project roots, startup stages, elapsed times, retries, cooldowns, and failure classifications. Language-server stderr and window/logMessage text are retained without enabling protocol tracing. Request/response bodies and document contents are not deliberately captured, even with --log-level DEBUG. Third-party messages and exception text can themselves contain sensitive data or source excerpts; inspect and redact logs before sharing.
--log-level continues to control stderr verbosity (default WARNING), independently of the INFO file. Storage errors fall back to stderr without changing MCP responses or timeout settings; persistence stays disabled until the next process start. Existing oversized logs or backups are preserved untouched and cause stderr fallback rather than rotation. Concurrent processes coordinate rotation without waiting for a busy log lock: contention sends the affected record to stderr, so file logs can have gaps. Disk operations and stderr writes are synchronous and can delay execution if storage or the stderr consumer stalls. Platforms without POSIX locking use stderr instead.
Empty results that are not "no": an empty document/workspace symbol answer adds a "may still be indexing" hint to the note; a push-diagnostics server that publishes nothing within 2 s yields a note saying the result is unknown, not clean.
Push diagnostics (servers without pull diagnostics, e.g.
ruff): after the document is opened for the call, any earlier cached publish is discarded, so only a publish that arrives after this call'sdidOpenis used. The call waits for the first publish (up to 2 s), then until no further publish arrives for the quiet period (default 0.3 s) and uses the last one — servers typically send syntax diagnostics first and semantic ones later. Publishes for documents that are not open (e.g. the empty one many servers send afterdidClose) are ignored, and a publish whoseversiondiffers from the version we sent is discarded. No publish at all gives the "unknown, not clean" note; an explicitly published empty list after our open means clean. Per-document state is dropped when the call ends, and the document is closed (didClose) even if the call fails, is cancelled or hits the call deadline. If the document is already open when the call starts, the cached diagnostics are returned as they are. Set the quiet period with--diagnostics-quiet-period SECONDSor envLSP_MCP_DIAGNOSTICS_QUIET_PERIOD(positive values only). Residual race: a late publish caused by the previous call'sdidClosethat carries noversionand lands after the new document was opened cannot be told apart from a real one; it can make that call report a stale (typically empty) result if the server's real publish is slower than the quiet period. The quiet period narrows this window but does not close it.context_linesonly reads files inside the project root of the queried file (symlinks resolved), at most 2 MiB per file; other locations are returned withoutcontext.
Server binaries
Binaries are resolved lazily at server start time. A missing binary degrades just that one server — the others still work.
Adding to opencode
In your ~/.config/opencode/opencode.jsonc:
{
"mcp": {
"lsp-mcp": {
"type": "local",
"command": "uvx",
"args": ["lsp-mcp"]
}
}
}Or with a local clone:
{
"mcp": {
"lsp-mcp": {
"type": "local",
"command": "uv",
"args": ["run", "--project", "/home/you/git/lsp-mcp", "lsp-mcp"]
}
}
}Tools
All ten tools accept absolute file paths. All line / character values are 0-based (LSP convention); every Location also has line_1based (= line + 1). Symbol names may be bare (my_func) or name-paths (MyClass/my_method) for nested symbols.
Tool | Description | LSP method |
| List all symbols in a file |
|
| Search for a symbol by name across the workspace |
|
| Find where a symbol is defined |
|
| Find implementations of an interface/abstract method |
|
| Find all references to a symbol |
|
| Replace a symbol's source text in-place |
|
| Rename a symbol across the workspace |
|
| Type / signature / docs at a position |
|
| Callers or callees of the function at a position |
|
| Get merged diagnostics from all configured servers |
|
Tool parameters
get_symbols_overview(file_path)
file_path: absolute path to the source file
find_symbol(query, file_path="", kind=None, limit=50)
query: symbol name substring to search for; results are ranked exact → case-insensitive exact → prefix → substringfile_path: optional context file (selects which servers to query)kind: optional LSP SymbolKind name (Class,Function, …) or number to filter bylimit: max results (default 50, max 500);truncatedistrueandtotal_matchesgives the full count when capped
find_declaration(symbol, file_path, context_lines=0)
symbol: bare name or name-path (MyClass/my_method)file_path: absolute path to the file containing the symbolcontext_lines: source lines before/after each location returned as text incontext(default 0 = off, max 20)
find_implementations(symbol, file_path, context_lines=0) — same parameters as find_declaration
find_referencing_symbols(symbol, file_path, context_lines=0) — same parameters as find_declaration
get_hover(file_path, line, character) (read-only)
line,character: 0-based position. Returnscontents(usually Markdown) and optionalrange.
get_call_hierarchy(file_path, line, character, direction="incoming") (read-only)
direction:incoming(callers) oroutgoing(callees). Returnsrootsandcalls(each withitemandcall_sites).Both new tools report a missing server capability (
hoverProvider/callHierarchyProvider) throughnote.
replace_symbol_body(symbol, new_body, file_path)
symbol: bare name or name-pathnew_body: full replacement source text (including the signature line)file_path: absolute path to the file
rename_symbol(symbol, new_name, file_path)
symbol: bare name or name-pathnew_name: new identifierfile_path: absolute path to the file
get_diagnostics_for_file(file_path)
file_path: absolute path to the source file
Response shape
Every tool returns a JSON object with:
The result data (
symbols,locations,diagnostics,changed_files,success)note: non-empty when the call did not give a clean result.No ... foundmeans nothing matched;Request failed (this is NOT a 'no matches' result): ...means execution failed;Partial result - ...means at least one server completed the operation but another failed, even if the completed answer was emptywarnings: list of per-server error messages
When execution fails without a completed answer, the MCP response sets isError: true and retains the JSON object in both text and structured content. This includes startup failures, exhausted retries, request timeouts, and per-call deadline expiry. Deadline expiry discards partial data. Successful empty answers, missing capabilities, ambiguous or absent symbols, and unknown diagnostics remain successful MCP responses with explanatory notes. Partial results remain successful with warnings. Internal failure metadata is not added to the JSON payload.
Text returned by language servers (hover docs, symbol names, messages) is data, not instructions.
Behaviour changes in this release
initialization_optionsinconfig.ymlis now actually sent to the server (see above).Tools no longer report failures as empty results:
noteisRequest failed …,Partial result — …, a deadline/timeout message, or an indexing / unpublished-diagnostics hint. Callers that treated "empty list" as "no matches" must now checknote.find_symbolranks results and caps them at 50 by default (limit, max 500).New fields:
Location.line_1based,Location.context, find_symboltruncated/total_matches/kind_name/line_1based.New tools
get_hoverandget_call_hierarchy; new optionskind,limit,context_lines.New settings: per-call deadline, request/start timeouts, push-diagnostics quiet period.
get_diagnostics_for_fileno longer returns stale or prematurely "clean" push diagnostics (see "Push diagnostics" above); it waits a short quiet period after the first publish, so calls to push-only servers can take ~0.3 s longer.get_diagnostics_for_file(and every tool): documents are now opened with the correct LSPlanguageIdfor the file extension (python,typescript, …) instead ofplaintext. ty ignores plaintext documents and returned an empty "full" report, so Python type errors were silently reported as clean. A pull-diagnostics reply that is not afullreport with anitemslist (e.g.unchanged, missing items) now yields an "unknown, not clean" note. Anullpull reply is deliberately treated the same way: LSP 3.17 defines thetextDocument/diagnosticresult as a non-nullDocumentDiagnosticReport, and ruff, ty and tsc (observed locally) always answer a clean file with afullreport and an emptyitemslist; gopls and clangd were not installed to observe. An unknown extension still opens asplaintext;.hmaps toc(clangd default), so C++ headers named.hopen as C.
Architecture
Five layers:
lsp_mcp/
__main__.py # CLI entry point
server.py # FastMCP app + 10 tool adapters
types.py # Shared response models
dispatch/ # Routing (first-wins / merge), symbol resolution, edits
lsp/ # GenericLanguageServer, ServerManager pool, capabilities
config/ # Config loading, validation, glob resolverConfig defines the routing policy. One global file, no per-project files.
ServerManager maintains a persistent pool of running server processes keyed by
(server_name, project_root). Servers start on demand, are reused across calls, and are idle-evicted after 15 minutes.GenericLanguageServer subclasses multilspy's
LanguageServerto launch any arbitrary command.Dispatcher routes each tool call: resolves the file's matching servers, filters by capability, applies first-wins or merge semantics.
Edits apply on-disk changes with UTF-16-correct offset handling and notify open servers via
didChange.
Development
git clone https://github.com/AFriemann/lsp-mcp
cd lsp-mcp
uv sync
uv run pytestThis server cannot be deployed
Maintenance
Related MCP Connectors
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Shared memory MCP server for Claude Code, Codex, Gemini CLI and Cursor. Cited, editable.
An MCP server that gives your AI access to the source code and docs of all public github repos
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Related MCP Servers
- AlicenseCqualityDmaintenanceA security-first MCP server that provides LLMs with structured tools for filesystem, process, search, build/test/lint, IDE integration, and more.402PolyForm Noncommercial 1.0.0
- AlicenseNot gradedqualityCmaintenanceLocal-first MCP server that provides project context, verification gates, and structured tools for coding agents to discover knowledge, run diagnostics, and execute allowlisted commands within a repository.41 npmMIT
- AlicenseNot gradedqualityDmaintenanceMCP server for semantic codebase navigation that builds an AST index of symbols, imports, and exports, providing AI agents with tools to search, explore, and understand code.MIT
- AlicenseNot gradedqualityCmaintenanceMCP server that gives AI coding agents codebase navigation intelligence, enabling symbol lookup, reference finding, type inspection, and diagnostics through tools like locate, refs, hover, diagnostics, status, and rename.413 npmMIT