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.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@lsp-mcplist symbols in /home/user/project/main.py"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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. The command line is deliberately not included (it may contain credentials); it is written to the log (--log-level WARNING).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 a server failed to start, timed out, or kept returning ContentModified;Partial result — ...means data is returned but some server failedwarnings: list of per-server error messages
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.
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
- FlicenseCqualityCmaintenanceA security-first MCP server that provides LLMs with structured tools for filesystem, process, search, build/test/lint, IDE integration, and more.402-
- 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.16 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
- FlicenseAqualityDmaintenanceAn MCP server that exposes filesystem operations — listing directories, reading, writing, and searching files — as tools an LLM can discover and invoke at runtime.4-