From c97ec273a96ddfc0c0e488471ccb4ca5f97f18b3 Mon Sep 17 00:00:00 2001 From: Hamza Khyari Date: Thu, 19 Mar 2026 04:03:43 +0000 Subject: [PATCH] feat(lsp): add missing LspServerConfig model fields 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 --- CHANGELOG.md | 7 + features/lsp_config_fields.feature | 113 +++++++++ features/steps/lsp_config_fields_steps.py | 285 ++++++++++++++++++++++ robot/helper_lsp_config_fields.py | 122 +++++++++ robot/lsp_config_fields.robot | 39 +++ src/cleveragents/cli/commands/lsp.py | 11 + src/cleveragents/lsp/__init__.py | 3 + src/cleveragents/lsp/models.py | 49 ++++ vulture_whitelist.py | 3 + 9 files changed, 632 insertions(+) create mode 100644 features/lsp_config_fields.feature create mode 100644 features/steps/lsp_config_fields_steps.py create mode 100644 robot/helper_lsp_config_fields.py create mode 100644 robot/lsp_config_fields.robot diff --git a/CHANGELOG.md b/CHANGELOG.md index e540b5bfd..52fcb8985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/features/lsp_config_fields.feature b/features/lsp_config_fields.feature new file mode 100644 index 000000000..98620ef9c --- /dev/null +++ b/features/lsp_config_fields.feature @@ -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 diff --git a/features/steps/lsp_config_fields_steps.py b/features/steps/lsp_config_fields_steps.py new file mode 100644 index 000000000..19da7e20b --- /dev/null +++ b/features/steps/lsp_config_fields_steps.py @@ -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 diff --git a/robot/helper_lsp_config_fields.py b/robot/helper_lsp_config_fields.py new file mode 100644 index 000000000..71c3a4fbf --- /dev/null +++ b/robot/helper_lsp_config_fields.py @@ -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) diff --git a/robot/lsp_config_fields.robot b/robot/lsp_config_fields.robot new file mode 100644 index 000000000..cae808949 --- /dev/null +++ b/robot/lsp_config_fields.robot @@ -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 diff --git a/src/cleveragents/cli/commands/lsp.py b/src/cleveragents/cli/commands/lsp.py index 494f7c122..221dc91fe 100644 --- a/src/cleveragents/cli/commands/lsp.py +++ b/src/cleveragents/cli/commands/lsp.py @@ -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()): diff --git a/src/cleveragents/lsp/__init__.py b/src/cleveragents/lsp/__init__.py index 48647ce3e..db943ff98 100644 --- a/src/cleveragents/lsp/__init__.py +++ b/src/cleveragents/lsp/__init__.py @@ -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", ] diff --git a/src/cleveragents/lsp/models.py b/src/cleveragents/lsp/models.py index d16bad843..27a59da0e 100644 --- a/src/cleveragents/lsp/models.py +++ b/src/cleveragents/lsp/models.py @@ -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", ] diff --git a/vulture_whitelist.py b/vulture_whitelist.py index c2304c6b9..b939e8c08 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -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