LSP Navigation Tools
Zero-config, language-server-accurate go-to-definition, find-references, hover, symbol search, call hierarchy, and go-to-implementation for agents
Give your agent language-server-accurate go-to-definition, find-references, call hierarchy, implementation lookup, and symbol search that works out of the box โ the right server is picked from the file extension, and a missing server degrades to an actionable install hint instead of a silent no-op.
Quick Start
- Simple Usage
Pass tool names as strings โ the agent resolves them from the built-in registry:
python from praisonaiagents import Agent agent = Agent( name="Coder", instructions="Navigate the codebase using LSP-accurate tools before editing.", tools=[ "lsp_definition", "lsp_references", "lsp_hover", "lsp_document_symbols", "lsp_workspace_symbols", "lsp_implementations", "lsp_incoming_calls", "lsp_outgoing_calls", ], ) agent.start("Where is `compute` defined, who calls it, and which classes implement `Storage`?") - Pass Tools Directly
Import the functions and pass them to
Agent:python from praisonaiagents import Agent from praisonaiagents.tools import lsp_definition, lsp_references agent = Agent(name="coder", tools=[lsp_definition, lsp_references]) agent.start("Find the definition of `compute` and every call site.") - Direct Call (No Agent)
Call tools directly without an agent:
python from praisonaiagents.tools import lsp_references print(lsp_references("src/mod.py", symbol="compute"))
How It Works
Each tool call spawns a fresh LSPClient, opens the document (didOpen), runs the query, and closes cleanly. There is no shared long-lived server process โ every call is self-contained.
Call-hierarchy tools first issue callHierarchy/prepareCallHierarchy to resolve a CallHierarchyItem, then fan out to callHierarchy/incomingCalls or callHierarchy/outgoingCalls. Multiple prepared items are queried and duplicates are collapsed automatically.
Monorepos
When a file_path is passed, the client initialises the language server against the nearest root marker (pyproject.toml, go.mod, Cargo.toml, tsconfig.json, .git, โฆ) rather than the current working directory. Nested-project searches resolve against the file’s real project root. See LSP Auto-Detect for the full marker table and the helpers you can call yourself.
Choose the Right Tool
| Tool | Signature | Purpose | LSP Method |
|---|---|---|---|
lsp_definition | (file_path, line=None, character=None, symbol=None) | Go to definition of a symbol | textDocument/definition |
lsp_references | (file_path, line=None, character=None, symbol=None, include_declaration=True) | Find all references to a symbol | textDocument/references |
lsp_implementations | (file_path, line=None, character=None, symbol=None) | Concrete implementations behind an interface / abstract method | textDocument/implementation |
lsp_incoming_calls | (file_path, line=None, character=None, symbol=None) | Callers of a symbol (who calls this?) | callHierarchy/incomingCalls |
lsp_outgoing_calls | (file_path, line=None, character=None, symbol=None) | Callees of a symbol (what does this call?) | callHierarchy/outgoingCalls |
lsp_hover | (file_path, line, character) | Type / signature / doc at a position | textDocument/hover |
lsp_document_symbols | (file_path) | List symbols defined in a file | textDocument/documentSymbol |
lsp_workspace_symbols | (query, file_path=None) | Search symbols across the workspace | workspace/symbol |
Supported Languages
The language server is chosen automatically from the file extension โ you never wire up a server yourself.
| Language | File Extensions | Default Server Binary | Install Hint |
|---|---|---|---|
| Python | .py, .pyi | pylsp | pip install python-lsp-server |
| JavaScript | .js, .jsx, .mjs, .cjs | typescript-language-server | npm install -g typescript-language-server typescript |
| TypeScript | .ts, .tsx | typescript-language-server | npm install -g typescript-language-server typescript |
| Rust | .rs | rust-analyzer | rustup component add rust-analyzer |
| Go | .go | gopls | go install golang.org/x/tools/gopls@latest |
The server binary must be on your PATH; when it is missing the tools return the install hint above instead of failing silently. lsp_workspace_symbols without a file_path defaults to the Python language server.
Zero-config: pick a server automatically
detect_language maps a file to its language by extension, so the correct server is spawned with no setup.
from praisonaiagents.lsp import detect_language
detect_language("src/mod.py") # "python"
detect_language("app.tsx") # "typescript"
detect_language("main.go") # "go"
detect_language("lib.rs") # "rust"
detect_language("index.jsx") # "javascript"
detect_language("notes.txt") # NoneUnsupported extensions return None, and the tools degrade explicitly.
When the server is missing
A missing language server produces an actionable message telling you exactly what to install โ never a silent no-op.
Check availability before spawning with probe, which returns (available, command, install_hint):
from praisonaiagents.lsp import probe
available, command, install_hint = probe("go")
# (False, "gopls", "go install golang.org/x/tools/gopls@latest")Every lsp_* navigation tool embeds the hint in its error string:
Error: go language server `gopls` not installed (install with `go install golang.org/x/tools/gopls@latest`); install it to use lsp navigation (falling back to grep is advised)Post-edit diagnostics surface a one-time-per-language note when a known server is absent:
Diagnostics (lsp:python): unavailable โ language server `pylsp` not found on PATH; install with `pip install python-lsp-server`Monorepos and multi-root workspaces
detect_root_uri walks up from the file to the nearest root marker, so the server initialises against the real project root instead of the current working directory.
| Language | Root markers (nearest wins) |
|---|---|
python | pyproject.toml, setup.py, setup.cfg, requirements.txt, .git |
javascript | package.json, tsconfig.json, jsconfig.json, .git |
typescript | tsconfig.json, package.json, jsconfig.json, .git |
rust | Cargo.toml, Cargo.lock, .git |
go | go.mod, go.sum, .git |
from praisonaiagents.lsp import detect_root_uri
detect_root_uri("packages/api/src/mod.py")
# "file:///abs/path/to/monorepo/packages/api" (nearest pyproject.toml)Nested-project workspace-symbol searches now return results from the file’s own project, not the outer repo.
Helpers Reference
Import these directly from praisonaiagents.lsp for zero-config detection and availability checks.
| Helper | Signature | Returns | Example | ||
|---|---|---|---|---|---|
detect_language | detect_language(file_path) | `str \ | None` | detect_language("m.py") โ "python" | |
detect_root_uri | detect_root_uri(file_path, language=None) | `str \ | None` | detect_root_uri("api/m.py") โ "file:///โฆ/api" | |
probe | probe(language) | `(bool, str \ | None, str \ | None)` | probe("go") โ (False, "gopls", "go install โฆ") |
path_to_uri | path_to_uri(path) | str | path_to_uri("/tmp/a b#1/m.py") โ "file:///tmp/a%20b%231/m.py" |
from praisonaiagents.lsp import detect_language, detect_root_uri, probe
from praisonaiagents.lsp.config import path_to_uri
detect_language("app.tsx") # "typescript"
detect_root_uri("packages/api/src/m.py") # "file:///โฆ/packages/api"
probe("python") # (True, "pylsp", "pip install python-lsp-server")
path_to_uri("/tmp/my project#1/mod.py") # "file:///tmp/my%20project%231/mod.py"path_to_uri percent-encodes reserved characters (spaces, #), so projects whose paths contain them no longer break โ the old failure mode where URIs truncated at those characters is gone.
LSPClient Updates
LSPClient now auto-detects the workspace root and degrades gracefully instead of raising.
| Change | What it does |
|---|---|
workspace_file= kwarg | Detects the workspace root from the file’s nearest root marker when root_uri is not given |
last_error attribute | Holds a structured, actionable message when the server binary is missing |
start() no longer raises | Returns False on a missing binary after a PATH pre-flight, instead of FileNotFoundError |
from praisonaiagents.lsp import LSPClient
client = LSPClient(language="go")
ok = await client.start() # False, no exception
print(client.last_error)
# language server `gopls` not found on PATH; install with `go install golang.org/x/tools/gopls@latest`Pass workspace_file= so a monorepo file initialises against its own project root:
client = LSPClient(language="python", workspace_file="packages/api/src/mod.py")
await client.start() # rootUri = packages/api, not os.getcwd()Addressing: Position or Symbol
Position-taking tools (lsp_definition, lsp_references, lsp_hover, lsp_implementations, lsp_incoming_calls, lsp_outgoing_calls) accept two addressing styles:
- Explicit position โ pass
lineandcharacteras 0-indexed integers (LSP convention). The output uses 1-indexed numbers so results are human-readable. - Symbol name โ pass
symbol="my_func"and the tool locates the first word-boundary occurrence of that name in the file, converting it to a position automatically.
lsp_hover requires an explicit (line, character) โ it does not accept a symbol name.
Configuration Options
Common Patterns
Investigate a symbol before refactoring
Use lsp_definition to find where something is defined, then lsp_references to see every call site before changing anything:
from praisonaiagents.tools import lsp_definition, lsp_references
print(lsp_definition("src/utils.py", symbol="compute"))
print(lsp_references("src/utils.py", symbol="compute"))Before renaming a function, see every real caller
Call hierarchy resolves the symbol first, so it catches dynamic dispatch and skips same-named identifiers a grep would over- or under-match:
from praisonaiagents.tools import lsp_incoming_calls, lsp_outgoing_calls
print(lsp_incoming_calls("src/utils.py", symbol="compute"))
print(lsp_outgoing_calls("src/utils.py", symbol="compute"))Find every concrete implementation of an interface
from praisonaiagents.tools import lsp_implementations
print(lsp_implementations("src/storage/base.py", symbol="Storage"))Explore an unfamiliar file
List what a file exports, then hover over interesting names to understand their signatures:
from praisonaiagents.tools import lsp_document_symbols, lsp_hover
print(lsp_document_symbols("src/parser.py"))
print(lsp_hover("src/parser.py", line=42, character=8))Locate a function whose name you half-remember
Search the whole workspace with a partial name:
from praisonaiagents.tools import lsp_workspace_symbols
print(lsp_workspace_symbols("parse_conf"))Best Practices
Install the language server when prompted
Detection is automatic, but the server binary still needs to be on PATH. When one is missing, the tools tell you exactly what to install:
# Python
pip install python-lsp-server
# JavaScript / TypeScript
npm install -g typescript-language-server typescript
# Rust
rustup component add rust-analyzer
# Go
go install golang.org/x/tools/gopls@latestIf a server is missing the tool returns Error: <lang> language server \<command>\ not installed (install with \<install_hint>\); install it to use lsp navigation (falling back to grep is advised) โ it never raises an exception. The exact error names the missing binary and the install command so you can copy-paste it.
Prefer symbol= over line/character when position is unknown
If you don’t already know the exact line number, pass symbol="my_func" instead of guessing a position. The tool locates the first word-boundary occurrence for you, so results are always accurate.
Narrow workspace-symbol queries
lsp_workspace_symbols caps output at 100 results and prints ... (N more; narrow your query) when the cap is hit. Use specific substrings โ "parse_config" rather than "parse" โ to stay inside the limit.
Graceful degradation is by design
When a language server is not installed the tools return a clear error string rather than raising. The error names the missing binary and its install command โ e.g. Error: python language server \pylsp\ not installed (install with \pip install python-lsp-server\); โฆ. Check the return value for not installed and fall back to grep-based tools (ast_grep, shell search) when needed.
Paths with spaces or # just work
Files whose paths contain reserved characters (spaces, #) are handled correctly โ path_to_uri percent-encodes them into valid file:// URIs, and results are decoded back on the way in, so no more truncation at those characters.