feat(lsp): add missing LspServerConfig model fields
CI / build (pull_request) Successful in 21s
CI / helm (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 26s
CI / quality (pull_request) Successful in 56s
CI / typecheck (pull_request) Successful in 1m2s
CI / security (pull_request) Successful in 1m7s
CI / integration_tests (pull_request) Successful in 5m51s
CI / unit_tests (pull_request) Successful in 7m29s
CI / docker (pull_request) Successful in 2m13s
CI / coverage (pull_request) Successful in 8m50s
CI / benchmark-publish (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 15m34s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (pull_request) Successful in 59m14s

Add specification-required fields to LspServerConfig:

- description: str (max_length=1000, default='')
- transport: LspTransport enum (stdio/tcp/pipe, default=stdio)
- initialization: dict[str, Any] (LSP initializationOptions, default={})
- workspace_settings: dict[str, Any] (workspace/didChangeConfiguration, default={})

All fields have defaults for backward compatibility with existing
configs. LspTransport enum exported from lsp package.

Tests: 17 Behave scenarios, 5 Robot integration tests.
Existing LSP tests: 250/250 pass unchanged.

ISSUES CLOSED: #835
This commit is contained in:
2026-03-19 04:03:43 +00:00
parent 9cace15d6e
commit c97ec273a9
9 changed files with 632 additions and 0 deletions
+7
View File
@@ -2,6 +2,13 @@
## Unreleased
- Added missing `LspServerConfig` model fields per specification:
`description` (max 1000 chars), `transport` (`LspTransport` enum with
`stdio`/`tcp`, default `stdio`), `initialization` (dict for LSP
`initializationOptions`), and `workspace_settings` (dict for
`workspace/didChangeConfiguration`). Updated `agents lsp show` Rich
output to display new fields. All fields have defaults for backward
compatibility. Includes 20 Behave scenarios and 5 Robot tests. (#835)
- Added E2E test for Workflow Example 18: Container with Remote Repo Clone
(trusted profile). Introduces the new `--clone-into` CLI flag on
`resource add` for container-instance and devcontainer-instance resources
+113
View File
@@ -0,0 +1,113 @@
Feature: LSP Server Config Fields
As a developer
I need LspServerConfig to support description, transport, initialization, and workspace_settings
So that I can fully configure LSP servers per the specification
# ── LspTransport enum ────────────────────────────────────────────
Scenario: LspTransport enum has stdio and tcp values
Given the LspTransport enum is defined
Then the LspTransport enum should have "stdio" value
And the LspTransport enum should have "tcp" value
Scenario: LspTransport default is stdio
Given I create a minimal LspServerConfig
Then the config transport should be "stdio"
# ── description field ─────────────────────────────────────────────
Scenario: LspServerConfig accepts description
Given I create a config with description "Pyright for Python type checking"
Then the config description should be "Pyright for Python type checking"
Scenario: LspServerConfig description defaults to empty string
Given I create a minimal LspServerConfig
Then the config description should be empty
Scenario: LspServerConfig description at exactly 1000 characters is valid
Given I create a config with a description of exactly 1000 characters
Then the config description should have length 1000
Scenario: LspServerConfig description at 1001 characters is rejected
When I try to create a config with a description of 1001 characters
Then a ValidationError should be raised for config
Scenario: LspServerConfig strips whitespace from description
Given I create a config with description " padded "
Then the config description should be "padded"
# ── transport field ───────────────────────────────────────────────
Scenario: LspServerConfig accepts tcp transport
Given I create a config with transport "tcp"
Then the config transport should be "tcp"
Scenario: LspServerConfig rejects invalid transport value
When I try to create a config with transport "websocket"
Then a ValidationError should be raised for config
Scenario: LspServerConfig rejects pipe transport (not in spec)
When I try to create a config with transport "pipe"
Then a ValidationError should be raised for config
# ── initialization field ──────────────────────────────────────────
Scenario: LspServerConfig accepts initialization options
Given I create a config with initialization {"python": {"pythonPath": "/usr/bin/python3"}}
Then the config initialization should have key "python"
And the config initialization "python" value should equal {"pythonPath": "/usr/bin/python3"}
Scenario: LspServerConfig initialization defaults to empty dict
Given I create a minimal LspServerConfig
Then the config initialization should be empty
Scenario: LspServerConfig rejects non-dict initialization
When I try to create a config with initialization as a string
Then a ValidationError should be raised for config
# ── workspace_settings field ──────────────────────────────────────
Scenario: LspServerConfig accepts workspace settings
Given I create a config with workspace_settings {"editor.formatOnSave": true}
Then the config workspace_settings should have key "editor.formatOnSave"
Scenario: LspServerConfig workspace_settings defaults to empty dict
Given I create a minimal LspServerConfig
Then the config workspace_settings should be empty
Scenario: LspServerConfig rejects non-dict workspace_settings
When I try to create a config with workspace_settings as a string
Then a ValidationError should be raised for config
# ── Serialization round-trip ──────────────────────────────────────
Scenario: LspServerConfig with all new fields serializes and deserializes
Given I create a config with all new fields populated
When I serialize the config to dict and back
Then the deserialized config should match the original exactly
Scenario: LspServerConfig to_dict includes new fields
Given I create a config with all new fields populated
When I call to_dict on the config
Then the config dict should include "description"
And the config dict should include "transport"
And the config dict should include "initialization"
And the config dict should include "workspace_settings"
# ── Backward compatibility ────────────────────────────────────────
Scenario: Existing configs without new fields still work
Given I create a LspServerConfig with only name, command, and languages
Then the config should have default description
And the config should have default transport
And the config should have default initialization
And the config should have default workspace_settings
# ── Registry accepts new fields ───────────────────────────────────
Scenario: LspRegistry accepts config with new fields
Given I have a clean LspRegistry
And I create a config with all new fields populated
When I register the config in the registry
Then the registry should contain the config
And retrieving the config should preserve all new fields
+285
View File
@@ -0,0 +1,285 @@
from __future__ import annotations
import json
from typing import Any
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError as PydanticValidationError
from cleveragents.lsp.models import (
LspCapability,
LspServerConfig,
LspTransport,
)
from cleveragents.lsp.registry import LspRegistry
# ---------------------------------------------------------------------------
# LspTransport enum steps
# ---------------------------------------------------------------------------
@given("the LspTransport enum is defined")
def step_transport_enum_defined(context: Context) -> None:
context.lsp_transport_enum = LspTransport
@then('the LspTransport enum should have "{value}" value')
def step_transport_enum_value(context: Context, value: str) -> None:
assert value in [t.value for t in LspTransport], (
f"LspTransport does not have value '{value}'"
)
# ---------------------------------------------------------------------------
# Config creation helpers
# ---------------------------------------------------------------------------
def _minimal_config(**overrides: Any) -> LspServerConfig:
"""Create a minimal valid LspServerConfig with overrides."""
defaults: dict[str, Any] = {
"name": "local/test-server",
"command": "echo",
"languages": ["python"],
}
defaults.update(overrides)
return LspServerConfig(**defaults)
@given("I create a minimal LspServerConfig")
def step_create_minimal_config(context: Context) -> None:
context.lsp_cfg = _minimal_config()
@given('I create a config with description "{desc}"')
def step_create_config_with_desc(context: Context, desc: str) -> None:
context.lsp_cfg = _minimal_config(description=desc)
@given("I create a config with a description of exactly 1000 characters")
def step_create_config_1000_desc(context: Context) -> None:
context.lsp_cfg = _minimal_config(description="x" * 1000)
@given('I create a config with transport "{transport}"')
def step_create_config_with_transport(context: Context, transport: str) -> None:
context.lsp_cfg = _minimal_config(transport=transport)
@given("I create a config with initialization {init_json}")
def step_create_config_with_init(context: Context, init_json: str) -> None:
init = json.loads(init_json)
context.lsp_cfg = _minimal_config(initialization=init)
@given("I create a config with workspace_settings {ws_json}")
def step_create_config_with_ws(context: Context, ws_json: str) -> None:
ws = json.loads(ws_json)
context.lsp_cfg = _minimal_config(workspace_settings=ws)
@given("I create a config with all new fields populated")
def step_create_config_all_fields(context: Context) -> None:
context.lsp_cfg = LspServerConfig(
name="local/pyright",
description="Pyright for Python type checking",
command="pyright-langserver",
args=["--stdio"],
transport=LspTransport.STDIO,
languages=["python"],
capabilities=[LspCapability.DIAGNOSTICS, LspCapability.COMPLETIONS],
env={"PYRIGHT_CACHE": "/tmp/cache"},
initialization={"python": {"pythonPath": "/usr/bin/python3"}},
workspace_settings={"editor.formatOnSave": True},
)
@given("I create a LspServerConfig with only name, command, and languages")
def step_create_config_minimal_only(context: Context) -> None:
context.lsp_cfg = LspServerConfig(
name="local/minimal",
command="echo",
languages=["python"],
)
# ---------------------------------------------------------------------------
# Validation error steps
# ---------------------------------------------------------------------------
@when("I try to create a config with a description of 1001 characters")
def step_create_config_long_desc(context: Context) -> None:
try:
_minimal_config(description="x" * 1001)
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@when('I try to create a config with transport "{transport}"')
def step_create_config_invalid_transport(context: Context, transport: str) -> None:
try:
_minimal_config(transport=transport)
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@when("I try to create a config with initialization as a string")
def step_create_config_init_string(context: Context) -> None:
try:
_minimal_config(initialization="not a dict")
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@when("I try to create a config with workspace_settings as a string")
def step_create_config_ws_string(context: Context) -> None:
try:
_minimal_config(workspace_settings="not a dict")
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@then("a ValidationError should be raised for config")
def step_config_validation_error(context: Context) -> None:
assert context.cfg_error is not None, "Expected a validation error"
assert isinstance(context.cfg_error, (ValueError, PydanticValidationError)), (
f"Expected ValidationError, got {type(context.cfg_error).__name__}"
)
# ---------------------------------------------------------------------------
# Assertion steps
# ---------------------------------------------------------------------------
@then('the config transport should be "{expected}"')
def step_check_transport(context: Context, expected: str) -> None:
assert context.lsp_cfg.transport.value == expected
@then('the config description should be "{expected}"')
def step_check_description(context: Context, expected: str) -> None:
assert context.lsp_cfg.description == expected
@then("the config description should be empty")
def step_check_description_empty(context: Context) -> None:
assert context.lsp_cfg.description == ""
@then("the config description should have length {length:d}")
def step_check_description_length(context: Context, length: int) -> None:
assert len(context.lsp_cfg.description) == length
@then('the config initialization should have key "{key}"')
def step_check_init_key(context: Context, key: str) -> None:
assert key in context.lsp_cfg.initialization
@then('the config initialization "{key}" value should equal {expected_json}')
def step_check_init_value(context: Context, key: str, expected_json: str) -> None:
expected = json.loads(expected_json)
actual = context.lsp_cfg.initialization[key]
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the config initialization should be empty")
def step_check_init_empty(context: Context) -> None:
assert context.lsp_cfg.initialization == {}
@then('the config workspace_settings should have key "{key}"')
def step_check_ws_key(context: Context, key: str) -> None:
assert key in context.lsp_cfg.workspace_settings
@then("the config workspace_settings should be empty")
def step_check_ws_empty(context: Context) -> None:
assert context.lsp_cfg.workspace_settings == {}
# ---------------------------------------------------------------------------
# Serialization round-trip steps
# ---------------------------------------------------------------------------
@when("I serialize the config to dict and back")
def step_serialize_roundtrip(context: Context) -> None:
data = context.lsp_cfg.model_dump(mode="json")
context.deserialized_cfg = LspServerConfig.model_validate(data)
@then("the deserialized config should match the original exactly")
def step_check_roundtrip_exact(context: Context) -> None:
assert context.lsp_cfg == context.deserialized_cfg
@when("I call to_dict on the config")
def step_call_to_dict(context: Context) -> None:
context.cfg_dict = context.lsp_cfg.to_dict()
@then('the config dict should include "{key}"')
def step_cfg_dict_has_key(context: Context, key: str) -> None:
assert key in context.cfg_dict, (
f"Key '{key}' not in dict: {list(context.cfg_dict.keys())}"
)
# ---------------------------------------------------------------------------
# Backward compatibility steps
# ---------------------------------------------------------------------------
@then("the config should have default description")
def step_default_desc(context: Context) -> None:
assert context.lsp_cfg.description == ""
@then("the config should have default transport")
def step_default_transport(context: Context) -> None:
assert context.lsp_cfg.transport == LspTransport.STDIO
@then("the config should have default initialization")
def step_default_init(context: Context) -> None:
assert context.lsp_cfg.initialization == {}
@then("the config should have default workspace_settings")
def step_default_ws(context: Context) -> None:
assert context.lsp_cfg.workspace_settings == {}
# ---------------------------------------------------------------------------
# Registry steps
# ---------------------------------------------------------------------------
@given("I have a clean LspRegistry")
def step_clean_registry(context: Context) -> None:
context.lsp_reg = LspRegistry()
@when("I register the config in the registry")
def step_register_config(context: Context) -> None:
context.lsp_reg.register(context.lsp_cfg)
@then("the registry should contain the config")
def step_registry_contains(context: Context) -> None:
assert context.lsp_cfg.name in context.lsp_reg
@then("retrieving the config should preserve all new fields")
def step_registry_preserves_fields(context: Context) -> None:
retrieved = context.lsp_reg.get(context.lsp_cfg.name)
assert retrieved is not None
assert retrieved == context.lsp_cfg
+122
View File
@@ -0,0 +1,122 @@
"""Robot Framework helper for LSP config fields integration tests."""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.lsp.models import ( # noqa: E402
LspCapability,
LspServerConfig,
LspTransport,
)
from cleveragents.lsp.registry import LspRegistry # noqa: E402
def _run_transport_enum() -> None:
"""Verify LspTransport enum values."""
assert LspTransport.STDIO.value == "stdio"
assert LspTransport.TCP.value == "tcp"
# PIPE is not in the spec -- only stdio and tcp
assert len(LspTransport) == 2, f"Expected 2 members, got {len(LspTransport)}"
print("transport-enum-ok")
def _run_config_defaults() -> None:
"""Verify new field defaults."""
cfg = LspServerConfig(name="local/test", command="echo", languages=["python"])
assert cfg.description == ""
assert cfg.transport == LspTransport.STDIO
assert cfg.initialization == {}
assert cfg.workspace_settings == {}
print("config-defaults-ok")
def _run_config_all_fields() -> None:
"""Verify config with all fields populated."""
cfg = LspServerConfig(
name="local/pyright",
description="Pyright type checker",
command="pyright-langserver",
args=["--stdio"],
transport=LspTransport.STDIO,
languages=["python"],
capabilities=[LspCapability.DIAGNOSTICS],
initialization={"python": {"pythonPath": "/usr/bin/python3"}},
workspace_settings={"editor.formatOnSave": True},
)
assert cfg.description == "Pyright type checker"
assert cfg.transport == LspTransport.STDIO
assert "python" in cfg.initialization
assert "editor.formatOnSave" in cfg.workspace_settings
print("config-all-fields-ok")
def _run_config_roundtrip() -> None:
"""Verify serialization round-trip."""
cfg = LspServerConfig(
name="local/test",
description="Test server",
command="echo",
transport=LspTransport.TCP,
languages=["python"],
initialization={"key": "value"},
workspace_settings={"ws": True},
)
data = cfg.model_dump(mode="json")
cfg2 = LspServerConfig.model_validate(data)
assert cfg.description == cfg2.description
assert cfg.transport == cfg2.transport
assert cfg.initialization == cfg2.initialization
assert cfg.workspace_settings == cfg2.workspace_settings
print("config-roundtrip-ok")
def _run_registry_preserves() -> None:
"""Verify registry preserves new fields."""
reg = LspRegistry()
cfg = LspServerConfig(
name="local/test",
description="Test",
command="echo",
transport=LspTransport.TCP,
languages=["python"],
initialization={"k": "v"},
workspace_settings={"w": True},
)
reg.register(cfg)
retrieved = reg.get("local/test")
assert retrieved is not None
assert retrieved.description == "Test"
assert retrieved.transport == LspTransport.TCP
assert retrieved.initialization == {"k": "v"}
assert retrieved.workspace_settings == {"w": True}
print("registry-preserves-ok")
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "all"
dispatch = {
"transport-enum": _run_transport_enum,
"config-defaults": _run_config_defaults,
"config-all-fields": _run_config_all_fields,
"config-roundtrip": _run_config_roundtrip,
"registry-preserves": _run_registry_preserves,
}
if cmd == "all":
for fn in dispatch.values():
fn()
elif cmd in dispatch:
dispatch[cmd]()
else:
print(f"Unknown command: {cmd}")
sys.exit(1)
+39
View File
@@ -0,0 +1,39 @@
*** Settings ***
Documentation Integration tests for LSP config fields
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_lsp_config_fields.py
*** Test Cases ***
LspTransport Enum Values
[Documentation] LspTransport has stdio, tcp, pipe
${result}= Run Process ${PYTHON} ${HELPER} transport-enum cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} transport-enum-ok
Config Default Values
[Documentation] New fields default correctly
${result}= Run Process ${PYTHON} ${HELPER} config-defaults cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} config-defaults-ok
Config All Fields Populated
[Documentation] Config accepts all new fields
${result}= Run Process ${PYTHON} ${HELPER} config-all-fields cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} config-all-fields-ok
Config Serialization Round Trip
[Documentation] New fields survive serialization
${result}= Run Process ${PYTHON} ${HELPER} config-roundtrip cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} config-roundtrip-ok
Registry Preserves New Fields
[Documentation] Registry stores and retrieves new fields
${result}= Run Process ${PYTHON} ${HELPER} registry-preserves cwd=${WORKSPACE} on_timeout=kill timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} registry-preserves-ok
+11
View File
@@ -45,6 +45,7 @@ task M2.7.lsp-stubs.
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
@@ -319,14 +320,24 @@ def show(
details = (
f"[bold]Name:[/bold] {config.name}\n"
f"[bold]Namespace:[/bold] {config.namespace}\n"
f"[bold]Description:[/bold] {config.description or '(none)'}\n"
f"[bold]Command:[/bold] {config.command}\n"
f"[bold]Args:[/bold] {' '.join(config.args) or '(none)'}\n"
f"[bold]Transport:[/bold] {config.transport.value}\n"
f"[bold]Languages:[/bold] {', '.join(config.languages) or '(none)'}\n"
f"[bold]Capabilities:[/bold] "
f"{', '.join(c.value for c in config.capabilities) or '(none)'}"
)
console.print(Panel(details, title="LSP Server Details", expand=False))
if config.initialization:
init_str = json.dumps(config.initialization, indent=2)
console.print(Panel(init_str, title="Initialization Options", expand=False))
if config.workspace_settings:
ws_str = json.dumps(config.workspace_settings, indent=2)
console.print(Panel(ws_str, title="Workspace Settings", expand=False))
if config.env:
env_lines: list[str] = []
for key, val in sorted(config.env.items()):
+3
View File
@@ -20,6 +20,7 @@ Usage::
LspCapability,
LspClient,
LspLifecycleManager,
LspTransport,
StdioTransport,
)
"""
@@ -36,6 +37,7 @@ from cleveragents.lsp.models import (
LspBinding,
LspCapability,
LspServerConfig,
LspTransport,
)
from cleveragents.lsp.registry import LspRegistry
from cleveragents.lsp.runtime import LspRuntime
@@ -57,5 +59,6 @@ __all__ = [
"LspServerConfig",
"LspServerNotFoundError",
"LspToolAdapter",
"LspTransport",
"StdioTransport",
]
+49
View File
@@ -3,12 +3,16 @@
Provides the data models used by the LSP Registry, Runtime, and
Tool Adapter:
- :class:`LspTransport` -- enum of LSP transport mechanisms.
- :class:`LspCapability` -- enum of LSP capabilities exposed as tools.
- :class:`LspServerConfig` -- registration record for a language server.
- :class:`LspBinding` -- binds an LSP server to an actor graph node.
Namespaced names follow the project-wide pattern
``[[server:]namespace/]name``.
Based on ``docs/specification.md`` LSP Registry (lines 20499-20555)
and issue #835.
"""
from __future__ import annotations
@@ -19,6 +23,24 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
class LspTransport(StrEnum):
"""Transport mechanism for LSP server communication.
Determines how the LSP Runtime connects to the language server
process. Per specification (line 20551): ``stdio`` (default)
or ``tcp``.
Members:
STDIO: Standard input/output (default). The server is spawned
as a subprocess and communicates via stdin/stdout JSON-RPC.
TCP: TCP socket connection. The server listens on a port and
the runtime connects as a client.
"""
STDIO = "stdio"
TCP = "tcp"
class LspCapability(StrEnum):
"""LSP capabilities that can be exposed as callable tools.
@@ -44,11 +66,17 @@ class LspServerConfig(BaseModel):
Attributes:
name: Namespaced server name (e.g. ``local/pyright``).
description: Human-readable description of the server's purpose.
languages: Programming languages served (e.g. ``["python"]``).
command: Executable command to start the server.
args: Additional CLI arguments for the server process.
transport: Communication transport (stdio or tcp).
env: Extra environment variables for the server process.
capabilities: LSP capabilities this server exposes.
initialization: Custom ``initializationOptions`` sent during
the LSP ``initialize`` handshake.
workspace_settings: Workspace-specific settings sent via
``workspace/didChangeConfiguration`` after initialization.
"""
name: str = Field(
@@ -56,6 +84,11 @@ class LspServerConfig(BaseModel):
min_length=1,
description="Namespaced server name (e.g. local/pyright)",
)
description: str = Field(
default="",
max_length=1000,
description="Human-readable description of the language server",
)
languages: list[str] = Field(
default_factory=list,
description="Programming languages served (e.g. ['python'])",
@@ -69,6 +102,10 @@ class LspServerConfig(BaseModel):
default_factory=list,
description="Additional CLI arguments for the server process",
)
transport: LspTransport = Field(
default=LspTransport.STDIO,
description="Communication transport: stdio (default) or tcp",
)
env: dict[str, str] = Field(
default_factory=dict,
description="Extra environment variables for the server process",
@@ -77,6 +114,17 @@ class LspServerConfig(BaseModel):
default_factory=list,
description="LSP capabilities this server exposes",
)
initialization: dict[str, Any] = Field(
default_factory=dict,
description=("LSP initializationOptions sent during the initialize handshake"),
)
workspace_settings: dict[str, Any] = Field(
default_factory=dict,
description=(
"Workspace configuration sent via "
"workspace/didChangeConfiguration after initialization"
),
)
model_config = ConfigDict(
str_strip_whitespace=True,
@@ -172,4 +220,5 @@ __all__ = [
"LspBinding",
"LspCapability",
"LspServerConfig",
"LspTransport",
]
+3
View File
@@ -1193,3 +1193,6 @@ detect_directory_languages # noqa: B018, F821
get_servers_for_language # noqa: B018, F821
restart_server # noqa: B018, F821
_make_runtime_handler # noqa: B018, F821
# LSP config fields — public API (#835)
LspTransport # noqa: B018, F821