> ## 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

<Steps>
  <Step title="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`?")
    ```
  </Step>

  <Step title="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)
    ```
  </Step>

  <Step title="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}")
    ```
  </Step>

  <Step title="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`"
    ```
  </Step>
</Steps>

***

## 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=...)`.
</Note>

```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

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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>

  <Tab title="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)
    ```
  </Tab>
</Tabs>

### 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

<AccordionGroup>
  <Accordion title="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>

  <Accordion title="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>

  <Accordion title="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>

  <Accordion title="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>

  <Accordion title="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>

  <Accordion title="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>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="LSP Navigation Tools" icon="magnifying-glass-code" href="/docs/features/lsp-navigation-tools">
    Go-to-definition, find-references, hover, and symbol search
  </Card>

  <Card title="LSP Tools (reference)" icon="compass" href="/docs/tools/lsp_tools">
    Per-tool parameters and output format
  </Card>

  <Card title="Post-Edit Formatter" icon="wand-magic-sparkles" href="/docs/features/post-edit-formatter">
    Produces the new "diagnostics unavailable" note
  </Card>

  <Card title="Built-in Tool Registry" icon="wrench" href="/docs/features/tools">
    How `Agent(tools=[…])` resolves tool names
  </Card>
</CardGroup>
