# LSP

A practical reference for nikcli language-server discovery, startup, diagnostics, and the gated LSP tool.

## Overview

nikcli uses Language Server Protocol servers as an optional code-intelligence layer for tools, diagnostics, symbols, definitions, references, hover information, implementations, and call hierarchy. The service lives under `packages/nikcli/src/lsp` and is loaded through an Effect service, so every project instance gets its own LSP state, root detection, client list, and shutdown finalizer.

Startup

### Lazy by file

Servers are not launched globally at boot. A file touch or operation selects matching servers by extension and then starts the needed client.

Workspace

### Per root

Each server chooses a root from lockfiles, language manifests, or the current instance directory. Clients are reused per server plus root pair.

Config

### Configurable

Built-in servers can be disabled or overridden, and custom servers can be added when their file extensions are declared.

Agent

### Tool gated

The agent-facing `lsp` tool is experimental and is only registered when the LSP tool flag is enabled.

## Runtime lifecycle

The service state is created from the current instance context and current config. If `lsp` is set to `false`, the service keeps an empty server map and all operations become unavailable. Otherwise nikcli loads every built-in server from `LSPServer`, filters experimental servers, applies config overrides, and records a finalizer that shuts down every connected client when the instance is disposed.

1.  **Resolve config** from global, project, and remote sources through `Config.defaultLayer`.
2.  **Build the server map** from `packages/nikcli/src/lsp/server.ts`.
3.  **Apply experimental filtering**: `ty` replaces `pyright` only when the Python flag is set.
4.  **Merge overrides**: disabled entries are removed; command overrides replace spawn behavior.
5.  **Spawn on demand** when `touchFile`, `hasClients`, or an LSP operation needs a matching extension.
6.  **Initialize JSON-RPC** with root URI, workspace folders, diagnostics, watched-file support, and configuration support.
7.  **Cache clients** by root plus server id, mark broken pairs, and reuse in-flight spawns.
8.  **Dispose cleanly** by ending the JSON-RPC connection and killing the child process.

When a server starts

A server starts the first time a matching file is touched by a debug command, status sync, or the agent-facing LSP tool. The service does not pre-launch every supported language.

## Supported servers

The built-in catalog is declared in `packages/nikcli/src/lsp/server.ts`. Each entry has an id, extension list, root strategy, and spawn function.

| Area | Server ids | Notes |
| --- | --- | --- |
| JavaScript and web | `typescript`, `deno`, `eslint`, `oxlint`, `biome`, `vue`, `svelte`, `astro` | TypeScript uses the workspace `typescript/lib/tsserver.js`. Deno only activates when a Deno config file is found. |
| Python | `pyright`, `ty` | `ty` is hidden unless `NIKCLI_EXPERIMENTAL_LSP_TY` is enabled; when enabled it disables `pyright`. |
| Systems | `rust`, `clangd`, `zls`, `terraform` | Rust requires `rust-analyzer` in PATH. Several binary servers can auto-download into the nikcli bin directory. |
| JVM and .NET | `jdtls`, `kotlin-ls`, `csharp`, `fsharp` | Java requires Java 21 or newer for JDTLS. .NET languages install tools through `dotnet tool` when allowed. |
| Other languages | `gopls`, `ruby-lsp`, `elixir-ls`, `sourcekit-lsp`, `yaml-ls`, `lua-ls`, `php intelephense`, `prisma`, `dart`, `ocaml-lsp`, `bash`, `texlab`, `dockerfile`, `gleam`, `clojure-lsp`, `nixd`, `tinymist`, `haskell-language-server` | Many use PATH first, then nikcli-managed binaries, then optional auto-install depending on the server. |

## Root detection

Root selection is server-specific. Most entries use `NearestRoot`, which walks upward from the file directory toward the current nikcli instance directory and returns the directory containing a matching marker file. If no marker exists, some servers fall back to the current instance directory; others return no root and do not start.

| Pattern | Examples | Behavior |
| --- | --- | --- |
| Package roots | `bun.lock`, `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock` | Used by TypeScript, web framework servers, YAML, and format/lint servers. |
| Language manifests | `Cargo.toml`, `go.mod`, `pyproject.toml`, `mix.exs`, `composer.json` | Keep language servers scoped to the real project or module root. |
| Workspace-aware roots | `go.work`, Rust workspace `Cargo.toml`, Gradle settings files | Prefer multi-module roots before module-local fallbacks. |
| Fixed instance roots | `bash`, `dockerfile` | Use the active nikcli directory directly. |

## Agent tool

The `lsp` tool in `packages/nikcli/src/tool/lsp.ts` exposes editor-like navigation to agents. It accepts a file path, one-based line and character values, asks for `lsp` permission, opens the file through the LSP client, waits briefly for diagnostics, and then runs the selected operation.

| Operation | Protocol request | Use |
| --- | --- | --- |
| `goToDefinition` | `textDocument/definition` | Find the declaration or implementation target for a symbol. |
| `findReferences` | `textDocument/references` | Return all references, including declarations. |
| `hover` | `textDocument/hover` | Read type or documentation information at a position. |
| `documentSymbol` | `textDocument/documentSymbol` | List symbols for one document. |
| `workspaceSymbol` | `workspace/symbol` | Search project symbols and keep the top ten class/function-like results. |
| `goToImplementation` | `textDocument/implementation` | Find implementations of interfaces or abstract members. |
| `prepareCallHierarchy` | `textDocument/prepareCallHierarchy` | Prepare the call hierarchy item for a function or method. |
| `incomingCalls` | `callHierarchy/incomingCalls` | Find callers after preparing call hierarchy. |
| `outgoingCalls` | `callHierarchy/outgoingCalls` | Find callees after preparing call hierarchy. |

The tool is experimental

The agent tool is registered only when `NIKCLI_EXPERIMENTAL_LSP_TOOL` is enabled, or when global experimental mode enables experimental tools. Without that gate the service can still power debug/status flows, but the agent cannot call `lsp`.

## Debug commands

The debug command group touches files and prints raw LSP JSON responses. Use it to check whether root detection, server startup, and diagnostics are working before relying on agent calls.

Terminal

bash 

```
nikcli debug lsp diagnostics packages/nikcli/src/lsp/index.ts
nikcli debug lsp symbols Session
nikcli debug lsp document-symbols file:///absolute/path/to/file.ts
```

$ shell · paste into your terminal

Diagnostics waits for a publish event after opening the file, sleeps briefly, and then prints the collected diagnostics map. Symbol commands call the service directly and print JSON.

## Configuration

The `lsp` config field accepts `false` to disable all language servers, or a server-id map to disable, override, or add servers. Custom server ids must declare `extensions`, because nikcli otherwise cannot know which files should start them.

| Field | Applies to | Meaning |
| --- | --- | --- |
| `lsp = false` | All servers | Disable the LSP service for the current config scope. |
| `lsp.SERVER.disabled = true` | Built-in or custom | Remove a server from the enabled map. |
| `lsp.SERVER.command` | Override or custom | Command array used to spawn the server process. |
| `lsp.SERVER.extensions` | Override or custom | File extensions that activate the server. Required for unknown ids. |
| `lsp.SERVER.env` | Override or custom | Environment variables merged into the server child process. |
| `lsp.SERVER.initialization` | Override or custom | Initialization options passed during initialize and configuration requests. |

### Dot-path config sketch

text 

1

2

3

4

5

```
lsp.typescript.disabled = true
lsp.my-language.command = my-language-server --stdio
lsp.my-language.extensions = .my, .mine
lsp.my-language.env.MY_LANGUAGE_MODE = strict
lsp.my-language.initialization.workspaceMode = full
```

## Downloads and binaries

Some servers require a system binary in PATH. Others install language-server packages or release binaries into `Global.Path.bin`, which resolves to the `bin` directory under nikcli's data root. Auto-downloads are skipped when `NIKCLI_DISABLE_LSP_DOWNLOAD` is set.

| Resolution path | Examples | Operational note |
| --- | --- | --- |
| PATH only | `deno`, `rust-analyzer`, `dart`, `gleam`, `ocamllsp` | Install the binary yourself before using the file type. |
| Bun package install | `pyright`, `svelte-language-server`, `@astrojs/language-server`, `bash-language-server` | nikcli installs into its data bin area when downloads are allowed. |
| Release archive | `clangd`, `zls`, `lua-ls`, `terraform-ls`, `texlab`, `tinymist` | Archives are fetched, extracted, marked executable, and reused. |
| Language toolchain | `gopls`, `rubocop`, `csharp-ls`, `fsautocomplete` | Requires the matching language toolchain, such as Go, Ruby, or .NET. |

## Diagnostics and symbols

When a file is opened, nikcli sends `workspace/didChangeWatchedFiles` and `textDocument/didOpen`. If the same file is touched again, it sends a watched-file change plus `textDocument/didChange` with an incremented version. Published diagnostics are normalized to filesystem paths and stored per client.

-   **Diagnostics debounce** waits for the matching path and server id, then resolves after a short quiet period.
-   **TypeScript first diagnostic quirk** suppresses the first TypeScript publish event when the file had no diagnostic entry yet.
-   **Workspace symbols** filter to classes, functions, methods, interfaces, variables, constants, structs, and enums.
-   **Document symbols** accept a file URI and return raw document or location symbols after filtering empty values.

## Failure modes

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `No LSP server available for this file type.` | No enabled server matched the extension, or root detection returned no root. | Check the extension, language manifest, and `lsp` config overrides. |
| Server never starts | Required binary is missing, download is disabled, or the language toolchain is absent. | Install the binary or unset `NIKCLI_DISABLE_LSP_DOWNLOAD` for supported auto-download servers. |
| Server starts once and then stays unavailable | The root plus server id was marked broken after spawn or initialize failed. | Fix the binary/config issue and dispose or restart the nikcli instance. |
| Diagnostics are empty | The server did not publish diagnostics within the wait window, or the file opened before analysis completed. | Run the diagnostics debug command again, or check the language server logs and project root. |
| Tool call denied | The agent-facing tool asks for `lsp` permission. | Approve the request or update the session permission rules. |
| Python server differs from expected | `NIKCLI_EXPERIMENTAL_LSP_TY` switches Python from `pyright` to `ty`. | Unset the flag for pyright or install `ty` for the experimental path. |

## Source anchors

| File | What to inspect |
| --- | --- |
| `packages/nikcli/src/lsp/index.ts` | Effect service, config merging, client cache, operations, and status. |
| `packages/nikcli/src/lsp/client.ts` | JSON-RPC connection, initialize payload, open/change notifications, diagnostics handling, shutdown. |
| `packages/nikcli/src/lsp/server.ts` | Built-in server catalog, root detection, spawn commands, downloads, experimental Python server selection. |
| `@nikcli-ai/util/language` | File extension to language id mapping used when opening documents. |
| `packages/nikcli/src/tool/lsp.ts` | Agent tool parameters, permission request, one-based to zero-based position conversion, and operation dispatch. |
| `packages/nikcli/src/cli/cmd/debug/lsp.ts` | Diagnostics and symbol debug commands. |
| `packages/nikcli/src/config/config.ts` | `lsp` schema, validation, and custom-server extension requirement. |

---

Source: https://nikcli-ai.dev/docs/lsp
