> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# LSP Auto-Detect

> Zero-config language, workspace root, and server-availability detection for LSP-driven agents

Zero-config helpers that pick the right language server, its real project root, and tell you plainly when it isn't installed.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.tools import lsp_definition

agent = Agent(
    name="Coder",
    instructions="Navigate the codebase with LSP accuracy.",
    tools=[lsp_definition],
)

agent.start("Where is `authenticate` defined in this monorepo?")
# The LSP client now finds the nearest pyproject.toml / go.mod / Cargo.toml
# on its own; no rootUri configuration needed.
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Zero-Config LSP"
        File[📄 file.py] --> Detect[🔍 detect_language]
        Detect --> Lang[python]
        File --> Root[📁 detect_root_uri]
        Root --> Rootdir[pyproject.toml dir]
        Lang --> Probe[⚡ probe]
        Probe --> Avail{Installed?}
        Avail -->|Yes| Start[🚀 start server]
        Avail -->|No| Hint[💡 install hint]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#8B0000,stroke:#7C90A0,color:#fff

    class File,Lang,Rootdir input
    class Detect,Root,Probe,Avail process
    class Start ok
    class Hint warn
```

## Quick Start

**1. Let the agent do it (nothing to configure)**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.tools import lsp_definition, lsp_references

agent = Agent(
    name="Coder",
    instructions="Find definitions and references before editing.",
    tools=[lsp_definition, lsp_references],
)
agent.start("What calls `parse_config`?")
```

**2. Detect language and root yourself (advanced)**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.lsp import detect_language, detect_root_uri

detect_language("services/auth/main.go")     # → "go"
detect_root_uri("services/auth/main.go")      # → "file:///abs/path/to/repo"  (nearest go.mod)
```

**3. Probe availability before you use LSP**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.lsp import probe

available, command, install_hint = probe("rust")
# → (False, "rust-analyzer", "rustup component add rust-analyzer")
if not available:
    print(f"Install `{command}` first: {install_hint}")
```

**4. Talk to the client directly with a workspace-aware root**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents.lsp import LSPClient

client = LSPClient(language="python", workspace_file="services/api/mod.py")
started = asyncio.run(client.start())
if not started:
    print(client.last_error)
    # "language server `pylsp` not found on PATH; install with `pip install python-lsp-server`"
```

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Client as LSPClient
    participant Cfg as lsp.config
    participant Path as PATH

    Agent->>Client: LSPClient(language, workspace_file=...)
    Agent->>Client: await client.start()
    Client->>Path: shutil.which(configured command)
    alt Missing
        Path-->>Client: None
        Client->>Cfg: probe(language) — for install hint
        Client-->>Agent: False (last_error populated)
    else Present
        Client->>Cfg: detect_root_uri(workspace_file)
        Cfg-->>Client: file:// URI of nearest root marker
        Client->>Client: spawn server with correct rootUri
        Client-->>Agent: True
    end
```

Each language maps to a default server, its file extensions, and the root markers that pin a project root — the nearest marker wins.

| Language | Extensions | Root markers (nearest wins) | Command | Install hint |
| - | - | - | - | - |
| `python` | `.py`, `.pyi` | `pyproject.toml`, `setup.py`, `setup.cfg`, `requirements.txt`, `.git` | `pylsp` | `pip install python-lsp-server` |
| `javascript` | `.js`, `.jsx`, `.mjs`, `.cjs` | `package.json`, `tsconfig.json`, `jsconfig.json`, `.git` | `typescript-language-server` | `npm install -g typescript-language-server typescript` |
| `typescript` | `.ts`, `.tsx` | `tsconfig.json`, `package.json`, `jsconfig.json`, `.git` | `typescript-language-server` | `npm install -g typescript-language-server typescript` |
| `rust` | `.rs` | `Cargo.toml`, `Cargo.lock`, `.git` | `rust-analyzer` | `rustup component add rust-analyzer` |
| `go` | `.go` | `go.mod`, `go.sum`, `.git` | `gopls` | `go install golang.org/x/tools/gopls@latest` |

***

## Adding or Overriding a Language Server

Register a new language or swap a built-in server by passing `servers=` — no package source edits needed.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents.lsp import LSPClient

# Register Java once; the LSP layer honours the extension, root markers,
# install hint, and initialization options from this entry.
JAVA_SERVERS = {
    "java": {
        "command": "jdtls",
        "args": ["--stdio"],
        "extensions": [".java"],
        "root_markers": ["pom.xml", ".git"],
        "install_hint": "install eclipse.jdt.ls",
    }
}

client = LSPClient(language="java", servers=JAVA_SERVERS)
started = asyncio.run(client.start())
if not started:
    print(client.last_error)
    # "language server `jdtls` not found on PATH; install with `install eclipse.jdt.ls`"
```

> [!NOTE]
>
> The `Agent(tools=[lsp_*])` path spawns its own `LSPClient` and does **not** forward a `servers=` mapping today. Register custom languages with the direct-client pattern shown here — `LSPClient(language=..., servers=...)`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Req[Agent asks for language X] --> Merge[resolve_servers - merge user over DEFAULT_SERVERS]
    Merge --> Q{X in merged registry?}
    Q -->|Yes, built-in| BuiltIn[Use DEFAULT_SERVERS entry]
    Q -->|Yes, user override| Over[Use user entry - args dropped if command replaced]
    Q -->|Yes, new language| New[Use user entry as-is]
    Q -->|No| Raise[ValueError: No default server for language X]

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#8B0000,stroke:#7C90A0,color:#fff

    class Req input
    class Merge,Q process
    class BuiltIn,Over,New ok
    class Raise warn
```

### Four Ways to Configure

> [tab] Add a language
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.lsp import LSPClient
>
> LSPClient(language="java", servers={
>     "java": {
>         "command": "jdtls",
>         "args": ["--stdio"],
>         "extensions": [".java"],
>         "root_markers": ["pom.xml"],
>     }
> })
> ```

> [tab] Override a built-in
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.lsp import LSPClient
>
> # Swap the Python server for Pyright.
> LSPClient(language="python", servers={
>     "python": {"command": "pyright-langserver", "args": ["--stdio"]}
> })
> # Extensions, root markers, and install hint fall back to the built-in `python` row.
> ```

> [tab] With init options
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.lsp import LSPClient
>
> # `initialization_options` are picked up automatically when you don't pass your own.
> LSPClient(language="java", servers={
>     "java": {
>         "command": "jdtls",
>         "args": ["--stdio"],
>         "extensions": [".java"],
>         "root_markers": ["pom.xml"],
>         "initialization_options": {"bundles": []},
>     }
> })
> ```

> [tab] Compose the registry
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.lsp import resolve_servers
>
> merged = resolve_servers({
>     "java": {"command": "jdtls", "args": ["--stdio"], "extensions": [".java"]}
> })
> merged["python"]["command"]   # → "pylsp"   (built-in kept)
> merged["java"]["command"]     # → "jdtls"   (added)
> ```

### How the Merge Works

User entries override built-ins per language key; unset fields fall back to the built-in row.

| Scenario | What the merge does |
| - | - |
| No user config (`None` or `{}`) | Returns `DEFAULT_SERVERS` unchanged (identity return) |
| New language key (e.g. `"java"`) | Added to the merged registry; built-ins untouched |
| Same key, `command` differs, no `args` given | Built-in `args` are dropped (never leaked to a replacement binary) |
| Same key, `command` same, other fields changed | Built-in `args` retained; other fields updated |
| Non-dict user entry (`{"bogus": "not-a-dict"}`) | Silently ignored |

`DEFAULT_SERVERS` is never mutated by a merge — `resolve_servers({...})` always returns a fresh dict.

### Which Configuration Style?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[I need a different server] --> Q1{Language shipped by default?}
    Q1 -->|No, brand new| Add[Add a new key with servers=]
    Q1 -->|Yes, wrong binary| Q2{Keep the built-in metadata?}
    Q2 -->|Yes, just swap command| Over[Override command only - args reset]
    Q2 -->|No, full control| Full[Provide command, args, extensions, root_markers]
    Start --> Q3{Need the merged dict directly?}
    Q3 -->|Yes| Compose[Call resolve_servers to inspect]

    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef choice fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class Start start
    class Q1,Q2,Q3 choice
    class Add,Over,Full,Compose result
```

### User Interaction Flow

A registered language behaves exactly like a built-in one.

> **User:** "Where is `AuthService.login` defined in this Maven repo?"
>
> **Agent** uses a Java LSP client registered via `servers=`, which finds the nearest `pom.xml` for the root and queries `jdtls`.
>
> **Tool returns:** `src/main/java/com/acme/AuthService.java:42:5`
>
> **Agent replies:** "`AuthService.login` is defined in `src/main/java/com/acme/AuthService.java` at line 42."

***

## Which Helper Should I Use?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[I want to work with LSP] --> Q1{Just using the tools?}
    Q1 -->|Yes| Tools[Use lsp_definition / lsp_references / ...]
    Q1 -->|No, custom code| Q2{What do I need to know?}
    Q2 -->|What language is this file?| A[detect_language]
    Q2 -->|Where is the project root?| B[detect_root_uri]
    Q2 -->|Is the server installed?| C[probe]
    Q2 -->|Full client control| D[LSPClient with workspace_file]

    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef choice fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class Start start
    class Q1,Q2 choice
    class Tools,A,B,C,D result
```

***

## Public API Surface

These are pure helpers plus one new keyword and one new attribute on `LSPClient`.

| Symbol | Kind | Import path | Returns | Notes |
| - | - | - | - | - |
| `detect_language(file_path, servers=None)` | function | `from praisonaiagents.lsp import detect_language` | `Optional[str]` | Case-insensitive extension lookup; honours custom extensions from `servers`; `None` for unknown extensions |
| `detect_root_uri(file_path, language=None, servers=None)` | function | `from praisonaiagents.lsp import detect_root_uri` | `Optional[str]` | Walks up from the file; honours custom root markers from `servers`; nearest match wins; `None` if none found |
| `probe(language, servers=None)` | function | `from praisonaiagents.lsp import probe` | `Tuple[bool, Optional[str], Optional[str]]` | `(available, command, install_hint)`; honours custom install hints from `servers` |
| `resolve_servers(user_servers=None)` | function | `from praisonaiagents.lsp import resolve_servers` | `Dict[str, Dict]` | Merges user servers over `DEFAULT_SERVERS`; identity return when input is falsy |
| `path_to_uri(path)` | function | `from praisonaiagents.lsp.config import path_to_uri` | `str` | Percent-encodes spaces, `#`, and other reserved characters |
| `DEFAULT_SERVERS` | constant | `from praisonaiagents.lsp import DEFAULT_SERVERS` | `Dict[str, Dict]` | Rows contain `command`, `args`, `extensions`, `root_markers`, `install_hint` |
| `LSPClient(..., workspace_file=None, servers=None)` | class | `from praisonaiagents.lsp import LSPClient` | — | Root auto-detected from `workspace_file`; `servers=` threads a custom registry through detect / probe / start |
| `LSPClient.last_error` | attribute | (on any client) | `Optional[str]` | Structured "not found on PATH" message; `None` on success |

***

## Common Patterns

**Guarded startup logging** — log what the LSP layer would do for each language:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.lsp import probe

for lang in ("python", "typescript", "go", "rust"):
    ok, cmd, hint = probe(lang)
    print(f"{lang}: {'ok' if ok else 'missing'} ({cmd or 'no server'})")
```

**Monorepo-aware navigation** — prove you're pointing at the right project root:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.lsp import detect_root_uri

assert detect_root_uri("services/api/mod.py").endswith("/services/api")
```

**Fallback to grep with a real reason** — surface `client.last_error` so the model picks a fallback intelligently:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents.lsp import LSPClient

client = LSPClient(language="go", workspace_file="services/auth/main.go")
if not asyncio.run(client.start()):
    print(f"LSP unavailable — {client.last_error}. Falling back to grep.")
```

***

## Best Practices

> [accordion] Prefer probe() over try/except
>
> `LSPClient.start()` no longer raises on a missing binary — it sets `last_error` and returns `False`. Reserve `try/except` for real IO failures.

> [accordion] Pass workspace\_file= to LSPClient in monorepos
>
> Without it, the client falls back to `os.getcwd()`, which is almost never the file's real project root. `workspace_file` lets `detect_root_uri` initialise the server against the nearest root marker.

> [accordion] Do not parse the phrases
>
> Error text like `install with \`...\``is for humans. For machine-readable data, call`probe(language)`and read the`(available, command, install\_hint)\` tuple.

> [accordion] Treat the 'not found on PATH' note as a call to action
>
> The `edit_tools` note appears once per language per run. When you see it, install the server rather than ignore the diagnostic signal for the rest of the session.

> [accordion] Pin the server your team uses
>
> Define a `servers=` mapping in your project bootstrap and pass it to every `LSPClient`, so contributors get the same server without installing an alternative. New teammates inherit the choice instead of debugging a mismatch.

> [accordion] Set install\_hint for internal tooling
>
> When you register a private or in-house server, include `install_hint`. It flows into `client.last_error` so a teammate missing the binary sees exactly how to install it.

> [accordion] Provide initialization\_options in the registry
>
> Put server-specific tuning in the registry entry's `initialization_options`. It is auto-picked when the caller didn't set its own — cleaner than passing the same options to every client.

***

## Related

> [card] [LSP Navigation Tools](https://praison.ai/docs/features/lsp-navigation-tools.md)
>
> Go-to-definition, find-references, hover, and symbol search

> [card] [LSP Tools (reference)](https://praison.ai/docs/tools/lsp_tools.md)
>
> Per-tool parameters and output format

> [card] [Post-Edit Formatter](https://praison.ai/docs/features/post-edit-formatter.md)
>
> Produces the new "diagnostics unavailable" note

> [card] [Built-in Tool Registry](https://praison.ai/docs/features/tools.md)
>
> How `Agent(tools=[…])` resolves tool names


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.