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.
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.
Per root
Each server chooses a root from lockfiles, language manifests, or the current instance directory. Clients are reused per server plus root pair.
Configurable
Built-in servers can be disabled or overridden, and custom servers can be added when their file extensions are declared.
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.
- Resolve config from global, project, and remote sources through
Config.defaultLayer. - Build the server map from
packages/nikcli/src/lsp/server.ts. - Apply experimental filtering:
tyreplacespyrightonly when the Python flag is set. - Merge overrides: disabled entries are removed; command overrides replace spawn behavior.
- Spawn on demand when
touchFile,hasClients, or an LSP operation needs a matching extension. - Initialize JSON-RPC with root URI, workspace folders, diagnostics, watched-file support, and configuration support.
- Cache clients by root plus server id, mark broken pairs, and reuse in-flight spawns.
- Dispose cleanly by ending the JSON-RPC connection and killing the child process.
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 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.
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 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
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. |