---
adr_number: 40
title: Language Server Protocol Resource Types
status_history:
- - '2026-02-21'
  - Proposed
  - Jeffrey Phillips Freeman
- - '2026-02-21'
  - Accepted
  - Jeffrey Phillips Freeman
tier: 4
authors:
- Jeffrey Phillips Freeman
superseded_by: null
related_adrs:
- number: 8
  title: Resource System
  relationship: These types extend the built-in resource type registry
- number: 27
  title: Language Server Protocol
  relationship: ADR-027 defines CleverAgents as an LSP server; this ADR defines external LSP servers as resources
- number: 29
  title: Model Context Protocol (MCP) Adoption
  relationship: Analogous integration pattern — MCP tools as external resources with sandbox path rewriting
- number: 36
  title: Resource DAG Operational Semantics
  relationship: LSP document equivalence creates cross-mechanism coherence scenarios
- number: 37
  title: Tool Reachability and Access Projection
  relationship: LSP documents as preferred read sources in read/write routing
- number: 38
  title: Cross-Mechanism Sandbox Coordination
  relationship: '`cached` coherence between LSP documents and filesystem files'
- number: 39
  title: Container and Execution Environment Resource Types
  relationship: LSP servers may run inside containers, depending on container executables
acceptance:
  votes_for:
  - voter: Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com>
    comment: LSP resource types enable semantic read routing and principled sandbox coordination with language servers
  votes_against: []
  abstentions: []
---
## Context

ADR-027 defines an LSP server as a Presentation-layer adapter — CleverAgents exposes plan-aware diagnostics and code actions to IDEs. But the inverse relationship is equally important: external LSP servers (pylsp, typescript-language-server, rust-analyzer, etc.) are **rich read sources** for resources that CleverAgents operates on. An LSP server provides semantic understanding — type information, go-to-definition, find-references, diagnostics, completions — that far exceeds what raw file reading can offer. The resource DAG currently has no way to represent these LSP servers, their workspaces, or their documents as resources, which means the system cannot reason about:

1. **Which LSP servers are available** for a given workspace or language.
2. **Where an LSP server runs** (host, container, remote) and what executables it depends on.
3. **How LSP documents relate to filesystem files** (they are equivalent physical manifestations of the same virtual file identity, with `cached` coherence).
4. **How to route reads through LSP** for richer semantic access versus raw filesystem reads.

## Decision Drivers

- External LSP servers provide semantic understanding (type info, go-to-definition, diagnostics) that far exceeds raw file reading, but the resource DAG has no way to represent them
- The system must know which LSP servers are available for a given workspace/language and where they run (host, container, remote)
- LSP documents and filesystem files are equivalent physical manifestations of the same virtual file identity, requiring `cached` coherence and cross-mechanism coordination
- Read routing should prefer semantically rich LSP access paths over raw filesystem reads when semantic understanding is needed
- LSP servers read from the real filesystem by default, not from sandboxes; sandbox path rewriting is needed for accurate post-change semantic analysis during plan execution

## Decision

CleverAgents adds **three new built-in physical resource types** for modeling LSP server infrastructure: `lsp-server`, `lsp-workspace`, and `lsp-document`. These integrate into the resource DAG with containment edges (server → workspace → document), reference edges (server → executable dependency, workspace → backing filesystem), and virtual equivalence links (document ↔ filesystem file through shared `file` virtual parent).

## Design

### New Resource Types

##### `lsp-server`

An LSP server definition and (optionally) running process. Represents a specific language server that provides semantic services for one or more languages.

| Field | Value |
|-------|-------|
| **Kind** | physical |
| **Sandbox strategy** | `none` (the server itself is not sandboxed; it reads from the sandboxed workspace or from the original source) |
| **User-addable** | yes |
| **CLI arguments** | `--server-name` (required, e.g., `pylsp`, `typescript-language-server`, `rust-analyzer`), `--command` (required, path to server executable or command), `--args` (optional, command-line arguments), `--language-ids` (required, comma-separated, e.g., `python,python3`), `--transport` (optional: stdio \| tcp \| pipe, default: stdio), `--port` (optional, for tcp transport), `--initialization-options` (optional, JSON string) |
| **Allowed parents** | `container-instance` (0..1, if the server runs inside a container), `container-exec-env` (0..1, more specific — the execution environment where it runs) |
| **Allowed children** | `lsp-workspace` (0..\*) |
| **Auto-discovery** | Not auto-discovered (user-registered). Auto-discovers workspaces when the server is started and initialized. |
| **Capabilities** | read: true, write: false, sandbox: false, checkpoint: false |

Properties: `server_name`, `command` (path to executable), `args` (list of strings), `language_ids` (list of language identifiers per LSP spec), `transport` (stdio \| tcp \| pipe), `port` (for tcp transport), `initialization_options` (JSON), `server_capabilities` (populated after initialization — which LSP features are supported), `state` (registered \| starting \| running \| stopped \| errored).

**Reference edges**:
- `lsp-server` → `references` → `executable`: The server depends on a specific executable (e.g., `pylsp` depends on the `pylsp` binary, which depends on a `python3` interpreter). This enables the query: "Is the executable for this LSP server available? Where?"
- If running in a container: `lsp-server` → `references` → `container-instance` (or more precisely, the `lsp-server`'s parent is `container-exec-env` which is a child of `container-instance`).

##### `lsp-workspace`

A workspace root tracked by an LSP server. Corresponds to a `workspaceFolder` in the LSP specification.

| Field | Value |
|-------|-------|
| **Kind** | physical |
| **Sandbox strategy** | `none` (delegates to the backing filesystem resource's sandbox) |
| **User-addable** | no (auto-discovered from lsp-server when the server is initialized with workspace folders) |
| **Allowed parents** | `lsp-server` (1) |
| **Allowed children** | `lsp-document` (0..\*) |
| **Auto-discovery** | Discovered from `lsp-server` during initialization. Discovers documents as the server reports them (via `textDocument/didOpen` tracking or workspace symbol queries). |
| **Capabilities** | read: true, write: false, sandbox: false, checkpoint: false |

Properties: `root_uri` (the workspace folder URI, e.g., `file:///home/user/projects/api-service`), `name` (workspace folder name).

**Reference edges**:
- `lsp-workspace` → `references` → `fs-directory` or `git-checkout`: The backing workspace on the filesystem. This is a soft dependency — the LSP workspace tracks the same directory that is registered as an `fs-directory` or `git-checkout` resource. This reference enables the system to correlate LSP workspace state with filesystem state.

##### `lsp-document`

A text document tracked by an LSP server. Provides semantic access to the document — types, diagnostics, completions, references, symbols.

| Field | Value |
|-------|-------|
| **Kind** | physical |
| **Sandbox strategy** | `none` (delegates to the backing file's sandbox) |
| **User-addable** | no (auto-discovered from lsp-workspace) |
| **Allowed parents** | `lsp-workspace` (1), `file` virtual (0..1, equivalence link) |
| **Allowed children** | (none) |
| **Auto-discovery** | Discovered as the LSP server opens and tracks documents. Not all files in the workspace are necessarily tracked — only those that the server has indexed. |
| **Capabilities** | read: true, write: true, sandbox: false, checkpoint: false |

Properties: `document_uri` (the document URI, e.g., `file:///home/user/projects/api-service/src/main.py`), `language_id` (python, typescript, rust, etc.), `version` (LSP document version — incremented on each change).

**Write capability**: `lsp-document` is marked writable because LSP supports `workspace/applyEdit` — the server (or client) can push edits through the LSP protocol. However, writes through LSP do not go through the sandbox directly; they must be coordinated with the backing file's sandbox. This is handled by the cross-mechanism coordination protocol (ADR-038).

**Virtual equivalence**: An `lsp-document` shares a `file` virtual parent with the corresponding `fs-file` (or `git-tree-entry`) when the document URI maps to a registered physical file and the content is identical. This is the primary cross-mechanism equivalence for LSP:

- `lsp-document` at `file:///repo/src/main.py` is equivalent to `fs-file` at `/repo/src/main.py`.
- Both are children of the same `file` virtual resource.
- **Coherence: `cached`** — the LSP server maintains an in-memory buffer of the document. When the backing file changes on disk (e.g., through a sandbox-tracked write), the LSP server must be notified via `workspace/didChangeWatchedFiles` to refresh its buffer. Conversely, when the LSP applies an edit (via `workspace/applyEdit`), the backing file must be updated.

### Containment Hierarchy Summary

```
lsp-server ("pylsp")
  ├── [references] ──▷ executable ("pylsp" binary)
  ├── [references] ──▷ executable ("python3" interpreter)
  └── lsp-workspace ("file:///repo")
        ├── [references] ──▷ git-checkout ("local/api-repo")
        ├── lsp-document ("file:///repo/src/main.py")
        │     └── [virtual parent] ──▷ file (virtual: "src/main.py")
        │                                   └── fs-file ("/repo/src/main.py")
        └── lsp-document ("file:///repo/src/utils.py")
              └── [virtual parent] ──▷ file (virtual: "src/utils.py")
                                           └── fs-file ("/repo/src/utils.py")
```

### LSP as a Rich Read Source

The primary value of LSP resource types in the DAG is enabling **read routing** through semantically rich access paths. When the system needs to understand a file:

| Access Path | What It Provides | Use Case |
|-------------|-----------------|----------|
| `fs-file` (raw read) | Raw file content (bytes/text) | Editing, diffing, content hashing |
| `lsp-document` (semantic read) | Type information, symbol table, diagnostics, go-to-definition targets, references, hover info, completions | Understanding code structure, impact analysis, refactoring planning |
| `git-tree-entry` (historical read) | File content at a specific commit | Comparing versions, understanding history |

The tool reachability and routing system (ADR-037) uses this hierarchy to route reads through the richest available path: prefer `lsp-document` when semantic understanding is needed, fall back to `fs-file` for raw access.

### LSP Server Lifecycle in the Resource DAG

1. **Registration**: User registers an `lsp-server` resource via `agents resource add lsp-server pylsp --command pylsp --language-ids python`. The server resource is created in `registered` state.

2. **Executable resolution**: The system checks that the `command` executable is available — either as a registered `executable` resource or by searching registered `container-exec-env` and host `fs-directory` resources. A `references` edge is created from the `lsp-server` to the resolved `executable`.

3. **Server start**: When a plan requires LSP access to a workspace, the `lsp-server` handler starts the server process (via stdio, tcp, or pipe transport), sends `initialize`, and transitions to `running` state.

4. **Workspace discovery**: The handler sends `workspace/didChangeWorkspaceFolders` to register workspace folders. `lsp-workspace` child resources are auto-discovered.

5. **Document discovery**: As documents are opened or indexed, `lsp-document` child resources are auto-discovered under the appropriate workspace. Virtual equivalence links to `file` virtual parents are created when content matches.

6. **Active use**: Tools route semantic queries through `lsp-document` resources (hover, go-to-def, find-references). These are read-only operations that don't go through sandboxes.

7. **Sandbox awareness**: When the backing file is modified in a sandbox, the LSP server must be notified so it can re-index. The coherence property (`cached`) triggers a `workspace/didChangeWatchedFiles` notification. The LSP server then re-analyzes the file and its dependents.

8. **Shutdown**: When the plan completes or the server is no longer needed, the handler sends `shutdown` / `exit` and transitions to `stopped` state. Auto-discovered workspaces and documents are cleaned up.

### Sandbox Interaction: LSP Reads Real Files vs. Sandboxed Files

A critical design point: by default, an LSP server reads from the **real filesystem**, not from the sandbox. This means:

- Before any sandbox writes: LSP provides accurate semantic info (real files == sandbox files).
- After a sandbox write: LSP is stale until notified. It sees the original file, not the sandboxed version.

Two approaches to handle this:

**Approach A: Notify and accept staleness** — After each sandbox write, send `workspace/didChangeWatchedFiles` with the file URI. The LSP server re-reads from disk... but reads the original, not the sandbox version. Semantic info reflects the pre-change state.

**Approach B: Point LSP at the sandbox** — Configure the LSP server's workspace root to point at the sandbox directory (e.g., the git worktree sandbox path instead of the original checkout path). The LSP server then reads sandboxed files directly and provides accurate post-change semantic info.

Approach B is preferred for active plan execution because it provides accurate semantic info for the sandboxed state. The `lsp-server` handler should support re-initializing or reconfiguring the workspace root to point at the sandbox path when a sandbox is created. The `lsp-workspace` resource's `root_uri` is updated to reflect the sandbox path, and the `references` edge to the backing `fs-directory` is updated to point at the sandbox directory.

This sandbox path rewriting for LSP is analogous to the sandbox path rewriting already defined for MCP tools (specification line 20803).

## Constraints

- LSP document discovery is lazy — documents are discovered as the server opens/indexes them, not eagerly for every file in the workspace. This avoids creating thousands of `lsp-document` resources for large workspaces.
- LSP servers that do not support a needed capability (e.g., no `textDocument/hover` support) should not be used for semantic reads that require that capability. The `server_capabilities` property (populated after initialization) must be checked before routing queries.
- LSP server startup may be slow (seconds for large workspaces). The system must handle lazy server startup (start on first semantic query) and async initialization.
- Virtual equivalence between `lsp-document` and `fs-file` requires matching document URIs to registered file resources. Unregistered files (e.g., in directories not part of a registered resource) do not get equivalence links.

## Consequences

### Positive
- LSP servers become first-class resources in the DAG, enabling semantic read routing and tool dependency resolution.
- Virtual equivalence between `lsp-document` and `fs-file` enables the system to provide richer answers to queries like "what do I know about this file?" by combining raw content with semantic information.
- The `references` edges to `executable` resources enable answering "can I start this LSP server in this environment?"
- Sandbox path rewriting for LSP (approach B) provides accurate semantic info during sandboxed plan execution.

### Negative
- Three new resource types add complexity. LSP server lifecycle management (start/stop/initialize) is non-trivial.
- Lazy document discovery means the set of `lsp-document` resources is not known upfront — it grows as the server indexes files.
- Sandbox path rewriting for LSP requires server re-initialization, which may be slow and not supported by all LSP servers.

### Risks
- LSP server crashes during plan execution leave stale resources in the DAG. Crash detection and cleanup are needed.
- Not all LSP servers support workspace folder changes or re-initialization. Some may need to be restarted entirely when the sandbox path changes.
- The semantic information provided by LSP servers depends on the server's analysis quality, which varies significantly across implementations and languages.

## Alternatives Considered

**Model LSP servers as MCP servers** — LSP servers could be wrapped as MCP tool providers, exposing semantic queries as MCP tools. This avoids new resource types but loses the structured relationship between documents, workspaces, and backing files. The equivalence tracking and coherence model would be impossible without dedicated resource types.

**No LSP integration in the resource DAG** — LSP servers are used as external tools without resource tracking. This works for basic tool execution but prevents semantic read routing and makes it impossible to reason about tool dependencies or sandbox coherence for LSP.

## Compliance

- **Type registration tests**: Each new resource type validates correctly (CLI args, parent/child constraints, capabilities).
- **Workspace discovery tests**: Starting an LSP server correctly discovers workspace and document resources.
- **Virtual equivalence tests**: `lsp-document` resources correctly link to `file` virtual parents when content matches the backing `fs-file`.
- **Coherence tests**: Modifying the backing file through a sandbox write triggers LSP notification and document re-indexing.
- **Sandbox path rewriting tests**: LSP workspace root correctly updates to sandbox path when a sandbox is created.
- **Executable dependency tests**: LSP server registration correctly resolves executable dependencies through `references` edges.
