# Language Server Protocol (/docs/language-server-protocol)

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.

<LspVisual />

## Use cases [#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. The query goes to the language server as is, so the matching rules are those of the server for the language.

- **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. Each file is read once, when it is opened, and later changes on disk are not sent to the server.

- **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. Positions are zero-based, and character offsets count UTF-16 code units within the line.

- **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. Daytona does not install language servers, so the server binary must be in the sandbox image and on its path.

- **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. The TypeScript server also covers JavaScript.

## Workflow [#workflow]

Follow this order when using LSP in a sandbox:

1. Start a server for a language ID and a project path
2. Open a file on the server before requesting its completions or symbols
3. Request completions, document symbols, or workspace symbols from the server
4. Close the file when you finish with it
5. Stop the server when it is no longer needed

## Create LSP servers [#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.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

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

# Write a file for the examples on this page
sandbox.fs.write_text(
    "/workspace/project/main.py",
    "class MyClass:\n    def method_one(self):\n        return 1\n\nMyClass().\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"` or `"typescript"`. `"python"` runs `pylsp`. `"typescript"` runs `typescript-language-server` for TypeScript and JavaScript. |
| `path_to_project` | str    | Sets the project directory. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```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@5 typescript-language-server\nRUN mkdir -p /workspace/project\n',
  ),
)

// Write a file for the examples on this page
await sandbox.fs.writeText(
  '/workspace/project/main.ts',
  'class MyClass {\n  methodOne(): number {\n    return 1\n  }\n}\n\nnew MyClass().\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'` or `'typescript'`. `'python'` runs `pylsp`. `'typescript'` runs `typescript-language-server` for TypeScript and JavaScript. |
| `pathToProject` | string | Sets the project directory. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The image needs TypeScript 5: `typescript-language-server` does not start with TypeScript 7.
</Callout>

</TabItem>
</Tabs>

## Start LSP servers [#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.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

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

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

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

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{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: `python` or `typescript`. 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. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox).
</Callout>

</TabItem>
</Tabs>

## Stop LSP servers [#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. A sandbox runs at most 16 language servers at once.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
# When done with LSP features
server.stop()
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
// When done with LSP features
await server.stop()
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/lsp/stop' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "languageId": "python",
  "pathToProject": "/workspace/project"
}'
```

</TabItem>
</Tabs>

## Code completions [#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.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
completions = server.completions(
    "file:///workspace/project/main.py",
    {"line": 4, "character": 10},
)
for item in completions.items:
    print(f"Completion: {item.label}")
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- Position values are zero-based, so `line: 0` is the first line.
- Call `open_document()` on the file before requesting completions.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const completions = await server.completions(
  'file:///workspace/project/main.ts',
  {
    line: 6,
    character: 14,
  },
)
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. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- Call `openDocument()` on the file before requesting completions.
</Callout>

</TabItem>

<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/lsp/completions' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "languageId": "python",
  "pathToProject": "/workspace/project",
  "position": {
    "character": 10,
    "line": 4
  },
  "uri": "file:///workspace/project/main.py"
}'
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- Position values are zero-based, so `line: 0` is the first line.
- Call `POST /lsp/did-open` on the file before requesting completions.
</Callout>

</TabItem>
</Tabs>

### Completion context [#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.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona.v2 import LspCompletionContext

completions = server.completions(
    "file:///workspace/project/main.py",
    {"line": 4, "character": 10},
    context=LspCompletionContext(trigger_kind=2, trigger_character="."),
)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const completions = await server.completions(
  'file:///workspace/project/main.ts',
  { line: 6, character: 14 },
  { context: { triggerKind: 2, triggerCharacter: '.' } },
)
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{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": 10,
    "line": 4
  },
  "context": {
    "triggerKind": 2,
    "triggerCharacter": "."
  }
}'
```

</TabItem>
</Tabs>

### Completion item fields [#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`. The Python SDK names it `sort_text`.                                               |
| `filterText`    | String to filter by instead of `label`. The Python SDK names it `filter_text`.                                           |
| `insertText`    | Text to insert instead of `label`. The Python SDK names it `insert_text`.                                                |

## File notifications [#file-notifications]

### Open file [#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.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
# Notify server that a file is open
server.open_document("file:///workspace/project/main.py")
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The file must exist in the sandbox and be at most 1 MiB.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
// Notify server that a file is open
await server.openDocument('file:///workspace/project/main.ts')
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The file must exist in the sandbox and be at most 1 MiB.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{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"
}'
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The file must exist in the sandbox and be at most 1 MiB.
</Callout>

</TabItem>
</Tabs>

### Close file [#close-file]

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
# Notify server that a file is closed
server.close_document("file:///workspace/project/main.py")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
// Notify server that a file is closed
await server.closeDocument('file:///workspace/project/main.ts')
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{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"
}'
```

</TabItem>
</Tabs>

## Document symbols [#document-symbols]

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
symbols = server.document_symbols("file:///workspace/project/main.py")
for symbol in symbols:
    print(f"Symbol: {symbol.name}, Kind: {symbol.kind}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const symbols = await server.documentSymbols('file:///workspace/project/main.ts')
symbols.forEach((symbol) => {
  console.log(`Symbol: ${symbol.name}, Kind: ${symbol.kind}`)
})
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/lsp/document-symbols?languageId=python&pathToProject=/workspace/project&uri=file:///workspace/project/main.py' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

## Workspace symbols [#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.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
# pylsp does not support workspace symbols: start a typescript server, in a sandbox
# whose image ships typescript-language-server as in the TypeScript tab of Create LSP servers
ts_server = sandbox.lsp.start("typescript", "/workspace/project")
ts_server.open_document("file:///workspace/project/main.ts")
symbols = ts_server.workspace_symbols("MyClass")
for symbol in symbols:
    print(f"Found: {symbol.name} in {symbol.location.uri}")
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The server must be a `typescript` server: `pylsp` does not support workspace symbols.
- Open a file of the project on the server before searching.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const symbols = await server.workspaceSymbols('MyClass')
symbols.forEach((symbol) => {
  console.log(`Found: ${symbol.name} in ${symbol.location.uri}`)
})
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The server must be a `typescript` server: `pylsp` does not support workspace symbols.
- Open a file of the project on the server before searching.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/lsp/workspacesymbols?query=MyClass&languageId=typescript&pathToProject=/workspace/project' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The server must be a `typescript` server: `pylsp` does not support workspace symbols.
- Open a file of the project on the server before searching.
</Callout>

</TabItem>
</Tabs>

## Symbol fields [#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`.                                  |

## Errors [#errors]

The SDKs raise these errors with the codes in the following table, and the Toolbox API returns them in the `code` field. An error that the language server returns for a request reaches you as `BAD_REQUEST`, with the server's message after the name of the LSP method.

| **Code**                     | **Meaning**                                                                                                                                                                  | **What to do**                                                                                   |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **`LSP_SERVER_UNAVAILABLE`** | The image has no binary for the language server, the binary could not run, or 16 language servers already run in the sandbox.                                                | Install the server in the image, or stop a server you no longer need.                            |
| **`LSP_NOT_STARTED`**        | No server runs for the language and project directory: it was not started, it was stopped, or it exited.                                                                     | Start the server.                                                                                |
| **`BAD_REQUEST`**            | The language ID is not `python` or `typescript` (a stop request returns `LSP_NOT_STARTED` instead), a required query parameter is missing, the file to open cannot be read or is larger than 1 MiB, or the language server answered with an error. | Check the language ID and the file. For a language server error, read the message: it names the LSP method. |
| **`REQUEST_TIMEOUT`**        | The language server did not answer within 30 seconds.                                                                                                                        | Retry the request, or stop and start the server.                                                 |
| **`UNPROCESSABLE_ENTITY`**   | The API request body is missing a required field or has a value of the wrong type, such as a negative position. The SDKs check positions before they send the request.       | Fix the request body.                                                                            |

## Permissions [#permissions]

Language server access runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys).

A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:lsp operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access.

| **Permission**        | **Grants**                                                          |
| --------------------- | ------------------------------------------------------------------- |
| **`write:sandboxes`** | Start and stop language servers and request completions and symbols |
