project-lsp
Code intelligence for Claude Code in Go, Python, Rust, TypeScript and JavaScript (go to definition, find references, document and workspace symbols, hover, call hierarchy, diagnostics after each edit) that stays right when a session works on files of several repositories or git worktrees.
Claude Code starts one language server per language for the whole session, rooted at the folder the session started in. A file of another repository, or of a worktree under .claude/worktrees/, then reads as outside that server's project: its imports fail to resolve and every edit comes back with diagnostics that are not true. project-lsp stands in front of the real server and runs one per project instead.
How it routes
- The project of a file is the git worktree that holds it, the folder
git rev-parse --show-toplevelnames from the file's directory. A file outside git belongs to its own directory. - Each project gets its own server, started the first time one of its files is opened, through
mise exec -C <project> -- <server>whenmiseis onPATH. The server then sees the project's own tool versions, environment and virtualenv, as a shell opened in that folder would. - A message about a file (open, change, definition, references, hover, call hierarchy) goes to the server of the file's project, and the diagnostics of that server come back as they are.
- A Python script that declares its own dependencies in a PEP 723 block (
# /// script, the headeruv run --scriptreads) goes to aty serverof its project instead of pyright. ty creates and updates the script's environment from that block through uv, so the script's imports resolve against the dependencies it declares. - A workspace symbol search goes to one server per project, and the answer joins theirs. A project's pyright also finds the symbols of its scripts, so the ty of a project answers only while no pyright runs there.
- Anything else goes to the server of the project the session started in, whose answer to
initializeis the one Claude Code sees.
| Language | Server | Extensions |
|---|---|---|
| Go | gopls | .go |
| Python | pyright-langserver --stdio, and ty server for a script with a PEP 723 block | .py, .pyi |
| Rust | rust-analyzer | .rs |
| TypeScript, JavaScript | tsc --lsp --stdio from the project's node_modules on TypeScript 7 or later, typescript-language-server --stdio before that | .ts, .tsx, .js, .jsx, .mts, .cts, .mjs, .cjs |
TypeScript 7 carries its own language server and no longer ships the tsserver.js that typescript-language-server drives, which is why the TypeScript server is picked per project.
Installation
claude plugin install project-lsp@nexaedge-marketplace
Each server has to be on PATH, or installed by mise. bin/project-lsp runs through uv run --script, so uv has to be on PATH too, at 0.12.3 or later for ty to build the environment of a script.
Claude Code runs one language server per file extension, so disable the plugins this one replaces before enabling it: gopls-lsp, pyright-lsp, rust-analyzer-lsp and typescript-lsp from claude-plugins-official.
Tests
uv run --no-project --python 3.12 --with pytest pytest plugins/project-lsp/tests
They drive bin/project-lsp over stdio against tests/fake_server.py, a server whose answers say which folder it runs in.