--- title: Language Server Protocol description: Interact with language server protocol servers in your sandboxes. --- Language Server Protocol (LSP) support is available through LSP server instances created from a sandbox. A language server runs inside the sandbox and analyzes the project's code, so your application gets IDE-grade code intelligence for code that never leaves the sandbox. An LSP server covers code completions, file open and close notifications, document symbols, and project-wide symbol search. Each server instance is scoped to one language and one project directory. Python and TypeScript are the supported language IDs; the matching server binary (pylsp or typescript-language-server) must be installed in the sandbox image, otherwise starting the server fails with an LSP server unavailable error. ## Use cases LSP servers are used when an application needs code intelligence for code that stays in a sandbox. - **Coding agents**: an agent searches workspace symbols to find where a function or class is defined before it edits code. Each match carries the file URI and the line range, so the agent opens the right file at the right line. - **Browser-based editors**: an editor in a web application shows completions and an outline of the open file. The language server computes both in the sandbox, next to the code. - **Code navigation**: document symbols list the functions, classes, and variables of a file, with their kind and position. An application builds an outline or jumps to a definition from the list. - **Completions for generated code**: an agent or an editor requests completions at a position to fill in names that exist in the project. A trigger character, such as a dot, tells the server how the completion started. - **Signatures and documentation**: completion items carry details such as a type signature and documentation. An agent reads how to call a function before it writes the call. - **Repository overviews**: an agent lists the document symbols of each file to learn the structure of an unfamiliar project, without reading every file in full. - **Code that stays in the sandbox**: the language server reads files from the sandbox filesystem. Your application gets code intelligence without copying the source code out of the sandbox. - **Projects with several languages**: each server covers one language and one project directory. A sandbox runs a Python server for one directory and a TypeScript server for another. ## Workflow Follow this order when using LSP in a sandbox: 1. Call `sandbox.lsp.start()` with a language ID and project path to get its server 2. Call `didOpen()` on a file before requesting its completions or symbols 3. Use completions, document symbols, or workspace symbols on the server 4. Call `didClose()` when you finish with a file 5. Call `stop()` on the server when it is no longer needed ## Create LSP servers Create a sandbox whose image includes the language server, then start the server for a project directory. Daytona does not install language servers: the binary must be in the image and on its path. Create the project directory in the image or before starting the server. ```python from daytona import Daytona from daytona.v2 import ImageBuilder # Create a sandbox from an image that ships pylsp daytona = Daytona() sandbox = daytona.v2.create_from_build( ImageBuilder.from_dockerfile( "FROM python:3.12-slim\n" "RUN pip install --no-cache-dir python-lsp-server\n" "RUN mkdir -p /workspace/project\n" ) ) # Start the Python language server for the project server = sandbox.lsp.start("python", "/workspace/project") ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `language_id` | `str` | Sets the language of the server. `"python"` runs `pylsp`. `"typescript"` runs `typescript-language-server` for TypeScript and JavaScript. | | `path_to_project` | `str` | Sets the project directory. | ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' // Create a sandbox from an image that ships typescript-language-server const daytona = new Daytona() const sandbox = await daytona.v2.createFromBuild( ImageBuilder.fromDockerfile( 'FROM node:20\nRUN npm install -g typescript typescript-language-server\nRUN mkdir -p /workspace/project\n', ), ) // Start the TypeScript language server for the project const server = await sandbox.lsp.start('typescript', '/workspace/project') ``` | **Parameter** | **Type** | **Description** | | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `languageId` | `string` | Sets the language of the server. `'python'` runs `pylsp`. `'typescript'` runs `typescript-language-server` for TypeScript and JavaScript. | | `pathToProject` | `string` | Sets the project directory. | ## Start LSP servers Start a language server for a language and a project directory. Starting a server for a language and directory that already has a running server reuses that server. The other operations never start a server implicitly; until the server is started, they fail with a not started error. ```python server = sandbox.lsp.start("python", "/workspace/project") # Now ready for LSP operations ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ------------------------------------------------------------------------------------- | | `path_to_project` | `str` | Sets the absolute path of the project directory. The server uses the path as its workspace root. | ```typescript const server = await sandbox.lsp.start('typescript', '/workspace/project') // Now ready for LSP operations ``` | **Parameter** | **Type** | **Description** | | --------------- | -------- | ------------------------------------------------------------------------------------- | | `pathToProject` | `string` | Sets the absolute path of the project directory. The server uses the path as its workspace root. | ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/start' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project" }' ``` | **Field** | **Type** | **Description** | | --------------- | -------- | ----------------------------------------------------------------------------------------- | | `languageId` | `string` | Sets the language of the server. This field is required. | | `pathToProject` | `string` | Sets the absolute path of the project directory. The server uses the path as its workspace root. This field is required. | ## Stop LSP servers Stop a language server. Stopping sends the server a shutdown request and ends its process, which frees one of the sandbox's server slots. ```python # When done with LSP features server.stop() ``` ```typescript // When done with LSP features await server.stop() ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/stop' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project" }' ``` ## Code completions Get completion candidates at a position in an open file, computed from the content sent when the file was opened. Character offsets count UTF-16 code units within the line. ```python completions = server.completions( "file:///workspace/project/main.py", {"line": 10, "character": 15}, ) for item in completions.items: print(f"Completion: {item.label}") ``` - Position values are zero-based, so `line: 0` is the first line. - Call `did_open()` on the file before requesting completions. - The result is an LSP completion list with `isIncomplete` and `items`. ```typescript const completions = await server.completions( 'file:///workspace/project/main.ts', { line: 10, character: 15, }, ) completions.items.forEach((item) => { console.log(`Completion: ${item.label}`) }) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | | `position` | `LspPosition` | Sets the position in the file. Position values are zero-based, so `line: 0` is the first line. `line` and `character` must be non-negative integers. Other values throw `BadRequestError` before the request is sent. | - Call `didOpen()` on the file before requesting completions. - The result is an LSP completion list with `isIncomplete` and `items`. ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/completions' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "position": { "character": 15, "line": 10 }, "uri": "file:///workspace/project/main.py" }' ``` - Position values are zero-based, so `line: 0` is the first line. - Call `POST /lsp/did-open` on the file before requesting completions. - The result is an LSP completion list with `isIncomplete` and `items`. ### Completion context Pass a completion context to tell the language server how the completion was triggered. The trigger kind is the completion trigger kind number from the Language Server Protocol specification. Set the trigger character when a typed character, such as a dot, opened the completion. Without a context, the server receives none. ```python completions = server.completions( "file:///workspace/project/main.py", {"line": 10, "character": 15}, context={"triggerKind": 2, "triggerCharacter": "."}, ) ``` ```typescript const completions = await server.completions( 'file:///workspace/project/main.ts', { line: 10, character: 15 }, { context: { triggerKind: 2, triggerCharacter: '.' } }, ) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/completions' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "uri": "file:///workspace/project/main.py", "position": { "character": 15, "line": 10 }, "context": { "triggerKind": 2, "triggerCharacter": "." } }' ``` ### Completion item fields | **Field** | **Description** | | ------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `label` | Text shown for the item. Always present. | | `kind` | LSP `CompletionItemKind` number, for example `3` for a function or `6` for a variable. | | `detail` | Extra information such as a type signature. | | `documentation` | Either a plain string or a `MarkupContent` object with `kind` (`plaintext` or `markdown`) and `value`. | | `sortText` | String to sort by instead of `label`. | | `filterText` | String to filter by instead of `label`. | | `insertText` | Text to insert instead of `label`. | ## File notifications ### Open file Open a file on the language server. The file's content is read from the sandbox filesystem and sent to the server, which works from it: later changes on disk are not sent. ```python # Notify server that a file is open server.did_open("file:///workspace/project/main.py") ``` ```typescript // Notify server that a file is open await server.didOpen('file:///workspace/project/main.ts') ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/did-open' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "uri": "file:///workspace/project/main.py" }' ``` ### Close file ```python # Notify server that a file is closed server.did_close("file:///workspace/project/main.py") ``` ```typescript // Notify server that a file is closed await server.didClose('file:///workspace/project/main.ts') ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/did-close' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "uri": "file:///workspace/project/main.py" }' ``` ## Document symbols ```python symbols = server.document_symbols("file:///workspace/project/main.py") for symbol in symbols: print(f"Symbol: {symbol.name}, Kind: {symbol.kind}") ``` ```typescript const symbols = await server.documentSymbols('file:///workspace/project/main.ts') symbols.forEach((symbol) => { console.log(`Symbol: ${symbol.name}, Kind: ${symbol.kind}`) }) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/document-symbols?languageId=python&pathToProject=/workspace/project&uri=file:///workspace/project/main.py' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Workspace symbols Search the project for symbols that match a query string. The query is passed to the language server as is, so the matching rules are those of the server for the language. ```python symbols = server.workspace_symbols("MyClass") for symbol in symbols: print(f"Found: {symbol.name} in {symbol.location.uri}") ``` ```typescript const symbols = await server.workspaceSymbols('MyClass') symbols.forEach((symbol) => { console.log(`Found: ${symbol.name} in ${symbol.location.uri}`) }) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/lsp/workspacesymbols?query=MyClass&languageId=python&pathToProject=/workspace/project' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Symbol fields Document symbols and workspace symbols return the same flat symbol shape. Hierarchical document symbol answers from the language server are reduced to the same three fields: nested children are dropped, and a symbol without a location gets an empty URI and a zero range. | **Field** | **Description** | | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | `name` | Symbol name. | | `kind` | LSP `SymbolKind` number, for example `5` for a class, `6` for a method, `12` for a function. | | `location.uri` | `file://` URI of the document that declares the symbol. | | `location.range` | `start` and `end` positions, each with zero-based `line` and `character`. |