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.

Diagram

available

missing

๐Ÿ“„ mod.py

๐Ÿ”Ž detect_language

๐Ÿงช probe

๐Ÿ“ detect_root_uri

๐Ÿ–ฅ๏ธ LSPClient.start

๐Ÿ’ก install with pip install python-lsp-server

Quick Start

  1. 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`?")
  2. 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.")
  3. 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

Diagram
LanguageServerLSPClientlsp_definitionAgentUserLanguageServerLSPClientlsp_definitionAgentUser“Where is X defined?”file_path, symbolstart(), didOpen, textDocument/definitionJSON-RPC requestlocations arrayLocation objects“path:line:col snippet”natural-language answer

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

Diagram

Workspace Symbols

Document Symbols

Hover

Implementations

Outgoing Calls

Incoming Calls

References

Definition

What do you need?

Where is X defined?

Where is X used?

Who calls X?

What does X call?

Which classes implement X?

What is X’s type or signature?

What does this file expose?

Find X anywhere in the repo

lsp_definition

lsp_references

lsp_incoming_calls

lsp_outgoing_calls

lsp_implementations

lsp_hover

lsp_document_symbols

lsp_workspace_symbols

ToolSignaturePurposeLSP Method
lsp_definition(file_path, line=None, character=None, symbol=None)Go to definition of a symboltextDocument/definition
lsp_references(file_path, line=None, character=None, symbol=None, include_declaration=True)Find all references to a symboltextDocument/references
lsp_implementations(file_path, line=None, character=None, symbol=None)Concrete implementations behind an interface / abstract methodtextDocument/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 positiontextDocument/hover
lsp_document_symbols(file_path)List symbols defined in a filetextDocument/documentSymbol
lsp_workspace_symbols(query, file_path=None)Search symbols across the workspaceworkspace/symbol

Supported Languages

The language server is chosen automatically from the file extension โ€” you never wire up a server yourself.

LanguageFile ExtensionsDefault Server BinaryInstall Hint
Python.py, .pyipylsppip install python-lsp-server
JavaScript.js, .jsx, .mjs, .cjstypescript-language-servernpm install -g typescript-language-server typescript
TypeScript.ts, .tsxtypescript-language-servernpm install -g typescript-language-server typescript
Rust.rsrust-analyzerrustup component add rust-analyzer
Go.gogoplsgo 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.

Diagram

available

missing

๐Ÿ“„ file

๐Ÿ”Ž detect_language

๐Ÿงช probe

๐Ÿ–ฅ๏ธ spawn server

๐Ÿ’ก actionable install note

python
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")    # None

Unsupported 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):

python
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:

text
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:

text
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.

LanguageRoot markers (nearest wins)
pythonpyproject.toml, setup.py, setup.cfg, requirements.txt, .git
javascriptpackage.json, tsconfig.json, jsconfig.json, .git
typescripttsconfig.json, package.json, jsconfig.json, .git
rustCargo.toml, Cargo.lock, .git
gogo.mod, go.sum, .git
python
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.

HelperSignatureReturnsExample
detect_languagedetect_language(file_path)`str \None`detect_language("m.py") โ†’ "python"
detect_root_uridetect_root_uri(file_path, language=None)`str \None`detect_root_uri("api/m.py") โ†’ "file:///โ€ฆ/api"
probeprobe(language)`(bool, str \None, str \None)`probe("go") โ†’ (False, "gopls", "go install โ€ฆ")
path_to_uripath_to_uri(path)strpath_to_uri("/tmp/a b#1/m.py") โ†’ "file:///tmp/a%20b%231/m.py"
python
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.

ChangeWhat it does
workspace_file= kwargDetects the workspace root from the file’s nearest root marker when root_uri is not given
last_error attributeHolds a structured, actionable message when the server binary is missing
start() no longer raisesReturns False on a missing binary after a PATH pre-flight, instead of FileNotFoundError
python
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:

python
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 line and character as 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:

python
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:

python
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

python
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:

python
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:

python
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:

bash
# 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@latest

If 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.


Related