From 9c19d5f7f8c2e32e274ff7a6fca865ae0c1d0d09 Mon Sep 17 00:00:00 2001 From: weiconghe <13976098570@163.com> Date: Tue, 26 May 2026 15:30:52 +0800 Subject: [PATCH 01/17] fix: allow non-existent paths in ignore check The _is_ignored_relative_path method used to raise FileNotFoundError when checking a path that does not exist on disk. This prevented editing tools (replace_content, insert_after_symbol, etc.) from operating on newly created files. Since a non-existent file cannot be matched by ignore patterns, returning False (not ignored) is semantically correct and safe. Changes: - _is_ignored_relative_path: return False instead of raising for non-existent paths, with a debug log message - validate_relative_path docstring: updated to reflect new behavior - Added 3 test cases covering non-existent path scenarios --- CHANGELOG.md | 3 +++ src/serena/project.py | 14 ++++++----- .../config/test_global_ignored_paths.py | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c99726..4411a500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Not existing paths return `False` on is ignored checks (instead of raising an error) + # v1.5.1 (2026-05-18) * General: diff --git a/src/serena/project.py b/src/serena/project.py index f202af96..2c1558a8 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -191,8 +191,8 @@ class Project(ToStringMixin): def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: """ - Determine whether an existing path should be ignored based on file type and ignore patterns. - Raises `FileNotFoundError` if the path does not exist. + Determine whether a path should be ignored based on file type and ignore patterns. + Returns False for non-existent paths since they cannot be matched by ignore patterns. :param relative_path: Relative path to check :param ignore_non_source_files: whether files that are not source files (according to the file masks @@ -208,7 +208,8 @@ class Project(ToStringMixin): abs_path = os.path.join(self.project_root, relative_path) if not os.path.exists(abs_path): - raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") + log.debug(f"Path {abs_path} does not exist, skipping ignore check") + return False # Check file extension if it's a file is_file = os.path.isfile(abs_path) @@ -284,10 +285,11 @@ class Project(ToStringMixin): def validate_relative_path(self, relative_path: str, require_not_ignored: bool = False) -> None: """ - Validates that the given relative path to an existing file/dir is safe to read or edit, - meaning it's inside the project directory. + Validates that the given relative path is safe to read or edit, + meaning it's inside the project directory and not ignored. - Passing a path to a non-existing file will lead to a `FileNotFoundError`. + Non-existent paths are allowed (not considered ignored) to support + editing tools that create new files. :param relative_path: the path to validate, relative to the project root :param require_not_ignored: if True, the path must not be ignored according to the project's ignore settings diff --git a/test/serena/config/test_global_ignored_paths.py b/test/serena/config/test_global_ignored_paths.py index af33ceea..da9fadb9 100644 --- a/test/serena/config/test_global_ignored_paths.py +++ b/test/serena/config/test_global_ignored_paths.py @@ -102,6 +102,31 @@ class TestGlobalIgnoredPaths: assert project.is_ignored_path(str(self.project_path / "debug.log")) assert not project.is_ignored_path(str(self.project_path / "main.py")) + def test_nonexistent_path_not_ignored(self) -> None: + """Non-existent paths should return False (not ignored), not raise FileNotFoundError.""" + project = _create_test_project(self.project_path) + nonexistent = str(self.project_path / "src" / "new_file.py") + assert not os.path.exists(nonexistent) + assert not project.is_ignored_path(nonexistent) + + def test_nonexistent_path_with_ignore_patterns(self) -> None: + """Non-existent paths matching ignore patterns still return False (cannot check).""" + project = _create_test_project( + self.project_path, + global_ignored_paths=["*.log"], + ) + nonexistent = str(self.project_path / "nonexistent.log") + assert not os.path.exists(nonexistent) + # Should not raise, should not match the *.log pattern + assert not project.is_ignored_path(nonexistent) + + def test_validate_relative_path_allows_nonexistent(self) -> None: + """validate_relative_path with require_not_ignored should not raise for non-existent paths.""" + project = _create_test_project(self.project_path) + nonexistent = "src/new_file.py" + # Should not raise + project.validate_relative_path(nonexistent, require_not_ignored=True) + class TestRegisteredProjectGlobalIgnoredPaths: """RegisteredProject.get_project_instance() correctly passes global patterns to Project.""" From 016ccbe1c095a3eed7967737ac1d4df2754f5d96 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 11:44:27 +0200 Subject: [PATCH 02/17] Dashboard: validate host and port on each request --- src/serena/dashboard.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 66c93827..f5e4ec0c 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self import psutil -from flask import Flask, Response, redirect, request, send_from_directory +from flask import Flask, Response, abort, redirect, request, send_from_directory from PIL import Image from pydantic import BaseModel from sensai.util import logging @@ -776,7 +776,15 @@ class SerenaDashboardAPI: cli.show_server_banner = lambda *args, **kwargs: None + # Verify host and port on each request to prevent DNS-rebinding-based attacks + @self._app.before_request + def check_host() -> None: + allowed = {f"127.0.0.1:{port}", f"localhost:{port}"} + if request.host not in allowed: + abort(403) + self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) + return port def run_in_thread(self, host: str) -> tuple[threading.Thread, int]: From 6941af6aabefa7220a2289c9024f89aa9b7cfe88 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 14:58:54 +0200 Subject: [PATCH 03/17] Bumped pythonnet, added serena-agent cli alias command The latter enables running uvx serena-agent as entrypoint, otherwise one needs uvx --from serena-agent serena --- pyproject.toml | 3 ++- uv.lock | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9b53049..a544f193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "filelock==3.25.2", "cryptography==46.0.7", "regex==2026.2.28", - "pythonnet==3.1.0-rc0 ; sys_platform == 'win32'", + "pythonnet==3.1.0 ; sys_platform == 'win32'", ] @@ -64,6 +64,7 @@ explicit = true [project.scripts] serena = "serena.cli:top_level" +serena-agent = "serena.cli:top_level" serena-hooks = "serena.hooks:hook_commands" [project.license] diff --git a/uv.lock b/uv.lock index 41fab003..7c3141fb 100644 --- a/uv.lock +++ b/uv.lock @@ -421,14 +421,14 @@ wheels = [ [[package]] name = "clr-loader" -version = "0.2.10" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/46/7eea92b6aa2d68af78e049cbecec5f757f1aad44ecdecdc16bbad7eead51/clr_loader-0.3.1.tar.gz", hash = "sha256:2e073e9aaf49d1ae2f56ecba27987ad5fb68be4bcd9dd34a5bed8f0e4e128366", size = 86805, upload-time = "2026-04-18T17:49:44.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/ec1a6e36624000b6df0dd61183c42342ee5814c073315e802cadaad04d2f/clr_loader-0.3.1-py3-none-any.whl", hash = "sha256:cbad189de20d202a7d621956b0fc38049e13c9bf7ca2923441eff725cd121aa1", size = 55730, upload-time = "2026-04-18T17:49:42.99Z" }, ] [[package]] @@ -2347,14 +2347,15 @@ wheels = [ [[package]] name = "pythonnet" -version = "3.1.0rc0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "clr-loader" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/59/4295652a9a93a2bd6755ecce2dc32e3d3c704e7b8ef301946ee65c818a59/pythonnet-3.1.0rc0.tar.gz", hash = "sha256:5f0f505e100233d70864b00dc5f5dd760603cfd94ac64da5c8b0e15ed95af939", size = 241025, upload-time = "2026-02-25T10:46:53.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/57/da1992e44663b71365c6e842c8d7fa453d4ec45fb99a68cfee5b7e944d3c/pythonnet-3.1.0.tar.gz", hash = "sha256:7b34c382905d10a371509ffafd64cae0416305c28817738a9cd138336f4e9991", size = 250599, upload-time = "2026-05-23T20:30:21.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/3d/a378aa74085fc6f72923d5e39fc30efbc97541cfd49b3fe0a35f20d09ff7/pythonnet-3.1.0rc0-py3-none-any.whl", hash = "sha256:286ccf23d08e1cc2b5200bb91d0ee649436d2e8386fec36a85193c66d0ff8b5b", size = 293150, upload-time = "2026-02-25T10:46:48.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4b/52414f442624d2589f5374a48c08d5ae94f24bea67fc13a20a752884e5b7/pythonnet-3.1.0-cp310.cp311.cp312.cp313.cp314-none-any.whl", hash = "sha256:698dd88edc198819ad63b624a6ebe76208c7b46e4fe13626f65e484f0358d6ba", size = 217578, upload-time = "2026-05-23T20:30:19.527Z" }, + { url = "https://files.pythonhosted.org/packages/db/67/031124fdcb937c266a3265118525bbf6dc13b8c79786d6a7290aecb6e7bb/pythonnet-3.1.0-cp310.cp311.cp312.cp313.cp314-none-win32.win_amd64.whl", hash = "sha256:7bdd4de03df3547a48122a3989265c8b31d5be0d19dadffa009eec7df8085e0b", size = 1644898, upload-time = "2026-05-23T20:30:16.213Z" }, ] [[package]] @@ -2962,7 +2963,7 @@ requires-dist = [ { name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" }, { name = "python-dotenv", specifier = "==1.2.2" }, { name = "python-multipart", specifier = "==0.0.27" }, - { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, + { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0" }, { name = "pywebview", specifier = "==6.2" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "regex", specifier = "==2026.2.28" }, From 1122166b7cfa8458fafcad99651ba40997f98be3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 15:31:56 +0200 Subject: [PATCH 04/17] Remove pyright and fortls from serena-agent package, instead install with uvx on the fly Added a dedicated LSDependencyProvider for that --- CHANGELOG.md | 2 + pyproject.toml | 2 - src/solidlsp/language_servers/common.py | 37 ++++++++++++++ .../fortran_language_server.py | 38 +++++++-------- .../language_servers/pyright_server.py | 22 +++++---- src/solidlsp/language_servers/ty_server.py | 34 ++++--------- src/solidlsp/ls.py | 45 ++++++++++++++++- uv.lock | 48 ------------------- 8 files changed, 124 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4411a500..088d77d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Not existing paths return `False` on is ignored checks (instead of raising an error) + - Add `serena-agent` CLI command so that `uvx serena-agent` can be used as entrypoint. + - Fortls and pyright are now installed on the fly instead of being bundled in the serena-agent package. # v1.5.1 (2026-05-18) diff --git a/pyproject.toml b/pyproject.toml index a544f193..5d856691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,6 @@ classifiers = [ ] dependencies = [ "requests==2.33.0", - "pyright==1.1.403", - "fortls==3.2.2", "overrides==7.7.0", "python-dotenv==1.2.2", "mcp==1.27.0", diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 20017630..16a97411 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import os import platform +import shutil import subprocess from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, replace @@ -154,6 +155,42 @@ class RuntimeDependencyCollection: ) +DEFAULT_UVX_PYTHON_VERSION = "3.13" + + +def build_uvx_launch_command( + package: str, + version: str, + entrypoint: str, + extra_args: Sequence[str] = (), + python_version: str = DEFAULT_UVX_PYTHON_VERSION, +) -> list[str]: + """Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``. + + Resolution order: + 1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup). + 2. Fall back to ``uv x`` if only ``uv`` is on PATH. + 3. Raise ``RuntimeError`` if neither is available. + + :param package: PyPI package name (e.g. ``"pyright"``). + :param version: Pinned package version. + :param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``). + :param extra_args: Arguments appended after the entrypoint (e.g. ``("--stdio",)``). + :param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing). + """ + base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint, *extra_args] + + uvx_path = os.environ.get("UVX") or shutil.which("uvx") + if uvx_path is not None: + return [uvx_path, *base_args] + + uv_path = shutil.which("uv") + if uv_path is not None: + return [uv_path, "x", *base_args] + + raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).") + + def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]: """Build a pinned npm install command for a package in a Serena-managed install directory.""" command = ["npm", "install", "--prefix", "./"] diff --git a/src/solidlsp/language_servers/fortran_language_server.py b/src/solidlsp/language_servers/fortran_language_server.py index a80efc68..45d7071b 100644 --- a/src/solidlsp/language_servers/fortran_language_server.py +++ b/src/solidlsp/language_servers/fortran_language_server.py @@ -6,19 +6,26 @@ import logging import os import pathlib import re -import shutil from overrides import override from solidlsp import ls_types -from solidlsp.ls import DocumentSymbols, LSPConstants, LSPFileBuffer, SolidLanguageServer +from solidlsp.ls import ( + DocumentSymbols, + LanguageServerDependencyProvider, + LanguageServerDependencyProviderUvx, + LSPConstants, + LSPFileBuffer, + SolidLanguageServer, +) from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +FORTLS_VERSION = "3.2.2" + class FortranLanguageServer(SolidLanguageServer): """Fortran Language Server implementation using fortls.""" @@ -170,24 +177,17 @@ class FortranLanguageServer(SolidLanguageServer): return DocumentSymbols(fixed_root_symbols) - @staticmethod - def _check_fortls_installation() -> str: - """Check if fortls is available.""" - fortls_path = shutil.which("fortls") - if fortls_path is None: - raise RuntimeError("fortls is not installed or not in PATH.\nInstall it with: pip install fortls") - return fortls_path - def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): - # Check fortls installation - fortls_path = self._check_fortls_installation() + super().__init__(config, repository_root_path, None, "fortran", solidlsp_settings) - # Command to start fortls language server - # fortls uses stdio for LSP communication by default - fortls_cmd = f"{fortls_path}" - - super().__init__( - config, repository_root_path, ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path), "fortran", solidlsp_settings + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return LanguageServerDependencyProviderUvx( + self._custom_settings, + self._ls_resources_dir, + package="fortls", + entrypoint="fortls", + default_version=FORTLS_VERSION, + version_setting_key="fortls_version", ) @staticmethod diff --git a/src/solidlsp/language_servers/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py index 7f774836..d4aa1265 100644 --- a/src/solidlsp/language_servers/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -6,19 +6,20 @@ import logging import os import pathlib import re -import sys import threading from typing import cast from overrides import override -from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +PYRIGHT_VERSION = "1.1.403" + class PyrightServer(SolidLanguageServer): """ @@ -44,14 +45,15 @@ class PyrightServer(SolidLanguageServer): self.found_source_files = False def _create_dependency_provider(self) -> LanguageServerDependencyProvider: - return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - - class DependencyProvider(LanguageServerDependencyProviderSinglePath): - def _get_or_install_core_dependency(self) -> str: - return sys.executable - - def _create_launch_command(self, core_path: str) -> list[str]: - return [core_path, "-m", "pyright.langserver", "--stdio"] + return LanguageServerDependencyProviderUvx( + self._custom_settings, + self._ls_resources_dir, + package="pyright", + entrypoint="pyright-langserver", + default_version=PYRIGHT_VERSION, + version_setting_key="pyright_version", + extra_args=("--stdio",), + ) @override def is_ignored_dirname(self, dirname: str) -> bool: diff --git a/src/solidlsp/language_servers/ty_server.py b/src/solidlsp/language_servers/ty_server.py index 0adb8f64..1e0832f0 100644 --- a/src/solidlsp/language_servers/ty_server.py +++ b/src/solidlsp/language_servers/ty_server.py @@ -10,12 +10,11 @@ You can pass the following entries in ``ls_specific_settings["python_ty"]``: import logging import os import pathlib -import shutil from typing import cast from typing_extensions import override -from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings @@ -44,28 +43,15 @@ class TyLanguageServer(SolidLanguageServer): ) def _create_dependency_provider(self) -> LanguageServerDependencyProvider: - return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - - class DependencyProvider(LanguageServerDependencyProvider): - def create_launch_command(self) -> list[str]: - # respecting an explicit override - ls_path = self._custom_settings.get("ls_path") - if ls_path is not None: - return [ls_path, "server"] - - ty_version = self._custom_settings.get("ty_version", TY_VERSION) - - # preferring uvx for on-demand execution - uvx_path = os.environ.get("UVX") or shutil.which("uvx") - if uvx_path is not None: - return [uvx_path, "--from", f"ty=={ty_version}", "ty", "server"] - - # falling back to uv's uvx-compatible subcommand when only `uv` is available - uv_path = shutil.which("uv") - if uv_path is not None: - return [uv_path, "x", "--from", f"ty=={ty_version}", "ty", "server"] - - raise RuntimeError("Could not find 'uvx' or 'uv' in PATH.\nInstall uv or provide ls_specific_settings.python_ty.ls_path.") + return LanguageServerDependencyProviderUvx( + self._custom_settings, + self._ls_resources_dir, + package="ty", + entrypoint="ty", + default_version=TY_VERSION, + version_setting_key="ty_version", + extra_args=("server",), + ) @override def is_ignored_dirname(self, dirname: str) -> bool: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 64bf112c..ecf1dea7 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -8,7 +8,7 @@ import shutil import threading from abc import ABC, abstractmethod from collections import defaultdict -from collections.abc import Callable, Hashable, Iterator +from collections.abc import Callable, Hashable, Iterator, Sequence from contextlib import contextmanager from copy import copy from pathlib import Path, PurePath @@ -22,6 +22,7 @@ from sensai.util.string import ToStringMixin from serena.util.file_system import match_path from serena.util.text_utils import MatchedConsecutiveLines from solidlsp import ls_types +from solidlsp.language_servers.common import build_uvx_launch_command from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_process import LanguageServerInterface, StdioLanguageServer @@ -333,6 +334,48 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide """ +class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider): + """ + Dependency provider for language servers distributed as a PyPI package, run on demand via ``uvx`` / ``uv x``. + + The pinned package version can be overridden by the user via the LS-specific setting given by + ``version_setting_key``. Alternatively, the LS-specific setting "ls_path" can be set to the path of an + already-installed language server executable, in which case it is launched directly, bypassing uv entirely. + """ + + def __init__( + self, + custom_settings: "SolidLSPSettings.CustomLSSettings", + ls_resources_dir: str, + *, + package: str, + entrypoint: str, + default_version: str, + version_setting_key: str, + extra_args: Sequence[str] = (), + ): + """ + :param package: the PyPI package name (e.g. ``"pyright"``) + :param entrypoint: the console script provided by the package (e.g. ``"pyright-langserver"``) + :param default_version: the package version to pin unless overridden + :param version_setting_key: the LS-specific setting key through which the user can override the version + :param extra_args: arguments appended after the entrypoint (e.g. ``("--stdio",)``) + """ + super().__init__(custom_settings, ls_resources_dir) + self._package = package + self._entrypoint = entrypoint + self._default_version = default_version + self._version_setting_key = version_setting_key + self._extra_args = tuple(extra_args) + + def create_launch_command(self) -> list[str]: + ls_path = self._custom_settings.get("ls_path") + if ls_path is not None: + return [ls_path, *self._extra_args] + version = self._custom_settings.get(self._version_setting_key, self._default_version) + return build_uvx_launch_command(self._package, version, self._entrypoint, self._extra_args) + + class SolidLanguageServer(ABC): """ High-level abstraction for language server interaction, which wraps the underlying low-level LSP interface diff --git a/uv.lock b/uv.lock index 7c3141fb..953f8a9c 100644 --- a/uv.lock +++ b/uv.lock @@ -671,19 +671,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] -[[package]] -name = "fortls" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "json5" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/2b/db1e5cd07fc9e74a2e4fb8f65946f8fd79ef72211001af00982a04d977d5/fortls-3.2.2.tar.gz", hash = "sha256:b43b2b8cbd447ae848c63b8f008c2df96fd48c3a967b33f6ed64b3421496883b", size = 7085222, upload-time = "2025-03-16T18:03:11.377Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/7e/41a55a49e764243fd72b1ed202a95d9a9193586d8665bdc94e4df92050e4/fortls-3.2.2-py3-none-any.whl", hash = "sha256:b7ae8378ce9741064b8340d3f1fbbbf7ab697285e174a0208fed98485c4edfe3", size = 278359, upload-time = "2025-03-16T18:03:09.969Z" }, -] - [[package]] name = "gitdb" version = "4.0.12" @@ -1135,15 +1122,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, ] -[[package]] -name = "json5" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" @@ -1616,15 +1594,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - [[package]] name = "overrides" version = "7.7.0" @@ -2235,19 +2204,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680, upload-time = "2025-11-14T10:07:23.331Z" }, ] -[[package]] -name = "pyright" -version = "1.1.403" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, -] - [[package]] name = "pystray" version = "0.19.5" @@ -2867,7 +2823,6 @@ dependencies = [ { name = "dotenv" }, { name = "filelock" }, { name = "flask" }, - { name = "fortls" }, { name = "jinja2" }, { name = "joblib" }, { name = "lsprotocol" }, @@ -2877,7 +2832,6 @@ dependencies = [ { name = "psutil" }, { name = "pydantic" }, { name = "pygls" }, - { name = "pyright" }, { name = "pystray" }, { name = "python-dotenv" }, { name = "python-multipart" }, @@ -2938,7 +2892,6 @@ requires-dist = [ { name = "dotenv", specifier = "==0.9.9" }, { name = "filelock", specifier = "==3.25.2" }, { name = "flask", specifier = "==3.1.3" }, - { name = "fortls", specifier = "==3.2.2" }, { name = "google-genai", marker = "extra == 'google'", specifier = "==1.27.0" }, { name = "jinja2", specifier = "==3.1.6" }, { name = "jinja2", marker = "extra == 'dev'", specifier = "==3.1.6" }, @@ -2956,7 +2909,6 @@ requires-dist = [ { name = "pygls", specifier = "==2.1.1" }, { name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" }, { name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.12.0" }, - { name = "pyright", specifier = "==1.1.403" }, { name = "pystray", specifier = "==0.19.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.4.1" }, { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, From 75738f48386bd2a5fd2c9f1287149c6e8dc5d4e1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 15:33:16 +0200 Subject: [PATCH 05/17] Add identifier as comment to readme Needed for Serena to be listed in GitHub's mcp registry --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 30d7c297..e733d4ec 100644 --- a/README.md +++ b/README.md @@ -261,3 +261,5 @@ Please refer to the [user guide](https://oraios.github.io/serena/02-usage/000_in A significant part of Serena, especially support for various languages, was contributed by the open source community. We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. + + From f5c707aca43a93f7f96623136cb131836f1fcdac Mon Sep 17 00:00:00 2001 From: Joe Black Date: Tue, 26 May 2026 07:42:50 -0400 Subject: [PATCH 06/17] feat(hooks): expand _CODE_FILE_EXTENSIONS to include config/schema formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add structured non-source file types (.yaml, .json, .toml, .graphql, .proto, .sql, .tf, .hcl, etc.) to the read-file nudge set. These extensions are common in real-world projects and repeated raw reads of them are equally well served by `search_for_pattern` or other Serena tools — the same rationale that applies to .py/.ts already applies to config, schema, and query files. Note added to the docstring: `search_for_pattern` works regardless of extension and is always a valid alternative to repeated raw reads. --- CHANGELOG.md | 3 +++ src/serena/hooks.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 088d77d9..e68a27d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Add `serena-agent` CLI command so that `uvx serena-agent` can be used as entrypoint. - Fortls and pyright are now installed on the fly instead of being bundled in the serena-agent package. +* Hooks: + - Extend list of extensions that are considered code files (affects the reminder hook counter). + # v1.5.1 (2026-05-18) * General: diff --git a/src/serena/hooks.py b/src/serena/hooks.py index d9a9b03f..ef5e2106 100644 --- a/src/serena/hooks.py +++ b/src/serena/hooks.py @@ -272,6 +272,8 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook): #: file suffixes for source-like files where symbolic tools are usually more #: appropriate than repeated raw reads. Lowercase and extension-only. + #: Note: ``search_for_pattern`` is always available regardless of extension and + #: is a better alternative to repeated raw reads for any structured file type. _CODE_FILE_EXTENSIONS: frozenset[str] = frozenset( ( ".al", @@ -289,14 +291,19 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook): ".fs", ".fsx", ".go", + ".graphql", + ".gql", ".groovy", ".h", + ".hcl", ".hpp", ".hs", ".html", ".java", ".jl", ".js", + ".json", + ".jsonc", ".jsx", ".kt", ".kts", @@ -305,6 +312,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook): ".m", ".matlab", ".php", + ".proto", ".ps1", ".py", ".r", @@ -313,11 +321,17 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook): ".scala", ".sh", ".sol", + ".sql", ".svelte", ".swift", + ".tf", + ".tfvars", + ".toml", ".ts", ".tsx", ".vue", + ".yaml", + ".yml", ".zig", ) ) From 2ee8f100b25c4d8c8c9b416c3175bb4be413a19c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 17:05:07 +0200 Subject: [PATCH 07/17] Release v1.5.2 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e68a27d5..05952530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +# v1.5.2 (2026-05-26) + * General: - Not existing paths return `False` on is ignored checks (instead of raising an error) - Add `serena-agent` CLI command so that `uvx serena-agent` can be used as entrypoint. diff --git a/pyproject.toml b/pyproject.toml index 5d856691..35fe7914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.5.2.dev0" +version = "1.5.2" description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 2be474ad..f0bd9b23 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.5.2.dev0" +__version__ = "1.5.2" import logging diff --git a/uv.lock b/uv.lock index 953f8a9c..8c8d8746 100644 --- a/uv.lock +++ b/uv.lock @@ -2813,7 +2813,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.5.2.dev0" +version = "1.5.2" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 209ea9588167bf27b26c4d8a2a1d27749ee00962 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 17:05:07 +0200 Subject: [PATCH 08/17] Set version to v1.5.3.dev0 --- pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35fe7914..1d8fe429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.5.2" +version = "1.5.3.dev0" description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index f0bd9b23..5c8f9e0c 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.5.2" +__version__ = "1.5.3.dev0" import logging diff --git a/uv.lock b/uv.lock index 8c8d8746..4abe69cb 100644 --- a/uv.lock +++ b/uv.lock @@ -2813,7 +2813,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.5.2" +version = "1.5.3.dev0" source = { editable = "." } dependencies = [ { name = "anthropic" }, From d0dbed37793963bda3cbc400107dc883fa65cb0a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 17:10:14 +0200 Subject: [PATCH 09/17] Shorten command in installation instructions --prerelease=allow is no longer necessary (pythonnet was bumped) and @latest is not needed either (same effect as without it) --- README.md | 2 +- docs/02-usage/010_installation.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e733d4ec..5a47b32f 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ To achieve this, Serena offers multiple levels of (composable) configuration: **Install Serena**. Serena is installed via uv as follows: ```bash -uv tool install -p 3.13 serena-agent@latest --prerelease=allow +uv tool install -p 3.13 serena-agent ``` After successful installation, the command `serena` should be available in your shell. diff --git a/docs/02-usage/010_installation.md b/docs/02-usage/010_installation.md index 5b0f216b..dff3c1c6 100644 --- a/docs/02-usage/010_installation.md +++ b/docs/02-usage/010_installation.md @@ -20,7 +20,7 @@ to be provided manually, this is mentioned in the notes below the respective lan With `uv` installed and on your PATH, install Serena with this command: - uv tool install -p 3.13 serena-agent@latest --prerelease=allow + uv tool install -p 3.13 serena-agent Upon completion, the command `serena` should be available in your terminal. @@ -37,7 +37,7 @@ Note that you can switch backends at any time via Serena's [configuration](050_c To update Serena to the latest version, run: - uv tool upgrade serena-agent --prerelease=allow + uv tool upgrade serena-agent :::{tip} To keep informed about updates, make sure you regularly open [Serena's Dashboard](060_dashboard), From 084721574049dba4b5002626d5f015e086665c58 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 20:56:52 +0200 Subject: [PATCH 10/17] Prepare release to GitHub's mcp registry --- README.md | 2 +- server.json | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 server.json diff --git a/README.md b/README.md index 5a47b32f..44cdee0f 100644 --- a/README.md +++ b/README.md @@ -262,4 +262,4 @@ A significant part of Serena, especially support for various languages, was cont We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. - + diff --git a/server.json b/server.json new file mode 100644 index 00000000..18fe98d0 --- /dev/null +++ b/server.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.oraios/serena", + "title": "Serena MCP: the IDE for your agent", + "description": "A powerful toolkit for coding, providing semantic retrieval and editing capabilities.", + "version": "1.5.3", + "repository": { + "source": "github", + "url": "https://github.com/oraios/serena" + }, + "packages": [ + { + "registryType": "pypi", + "identifier": "serena-agent", + "version": "1.5.3", + "transport": { + "type": "stdio" + }, + "runtimeHint": "uvx", + "runtimeArguments": [ + { + "description": "Select the python version", + "name": "-p", + "value": "3.13", + "type": "named" + }, + { + "type": "positional", + "value": "start-mcp-server" + }, + { + "type": "positional", + "value": "--project-from-cwd", + "isRequired": false + }, + { + "type": "named", + "name": "--context", + "isRequired": false, + "default": "desktop-app", + "description": "The name of a markdown file representing Serena's execution context, typically related to the executing client. Allowed values are: agent, antigravity, chatgpt, claude-code, codex, context.template, copilot-cli, desktop-app, ide, jb-ai-assistant, jb-copilot-plugin, junie, oaicompat-agent, vscode " + } + ] + } + ] +} \ No newline at end of file From 2449313c0d7427275c4c66aedff7d4881782f713 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 21:03:36 +0200 Subject: [PATCH 11/17] Release v1.5.3 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05952530..cb02d092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +# v1.5.3 (2026-05-26) + # v1.5.2 (2026-05-26) * General: diff --git a/pyproject.toml b/pyproject.toml index 1d8fe429..2ccd86b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.5.3.dev0" +version = "1.5.3" description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 5c8f9e0c..f55f6e86 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.5.3.dev0" +__version__ = "1.5.3" import logging diff --git a/uv.lock b/uv.lock index 4abe69cb..b803527d 100644 --- a/uv.lock +++ b/uv.lock @@ -2813,7 +2813,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.5.3.dev0" +version = "1.5.3" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 9b292a6c8b03c8306f117efeabb0ea7afdb0b3c0 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 26 May 2026 21:03:36 +0200 Subject: [PATCH 12/17] Set version to v1.5.4.dev0 --- pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2ccd86b7..9a03e553 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.5.3" +version = "1.5.4.dev0" description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index f55f6e86..eaa65f49 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.5.3" +__version__ = "1.5.4.dev0" import logging diff --git a/uv.lock b/uv.lock index b803527d..0f97a31d 100644 --- a/uv.lock +++ b/uv.lock @@ -2813,7 +2813,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.5.3" +version = "1.5.4.dev0" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 7bf30080f6aa8fc1f983e1d72bc91dd790f28d29 Mon Sep 17 00:00:00 2001 From: Petr Devyaterikov <22612354+utrumo@users.noreply.github.com> Date: Wed, 27 May 2026 14:31:57 +0300 Subject: [PATCH 13/17] typescript_vts: add initialization_options pass-through (#1507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * typescript_vts: add initialization_options pass-through Extends ls_specific_settings["typescript_vts"] with an optional initialization_options key — a dict forwarded to vtsls via all three LSP configuration channels: - the initialize request (initializationOptions field), - a workspace/didChangeConfiguration notification right after initialize, - and responses to workspace/configuration pulls, looked up per section (e.g. "typescript", "vtsls", "javascript", or dotted paths like "typescript.tsdk"). All three channels are necessary because different parts of vtsls read settings from different places — notably typescript.tsdk, which vtsls pulls via workspace/configuration rather than reading from initializationOptions. Without the pull-answer, setting tsdk had no effect. Primary use case: Yarn Plug'n'Play projects. Running `yarn dlx @yarnpkg/sdks vscode` in the project root generates .yarn/sdks/typescript/lib/tsserver.js, a PnP-aware tsserver. The user then points vtsls at it with: ls_specific_settings: typescript_vts: initialization_options: typescript: tsdk: "<...>/.yarn/sdks/typescript/lib" vtsls: autoUseWorkspaceTsdk: true autoUseWorkspaceTsdk is required alongside tsdk because, in a headless LSP context, there is no UI prompt to confirm switching to the workspace TypeScript version. See yioneko/vtsls#169 for the recipe. Default behaviour is unchanged — the field is optional, omitted from the initialize params and didChangeConfiguration when unset or empty, and workspace/configuration continues to answer empty sections. --- CHANGELOG.md | 6 ++ docs/02-usage/050_configuration.md | 24 +++++++ .../language_servers/vts_language_server.py | 63 ++++++++++++++++--- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb02d092..0884b617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Language Servers: + - `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`. + The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`, + and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing + at the Yarn-generated SDK. + # v1.5.3 (2026-05-26) # v1.5.2 (2026-05-26) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 03731604..983924bb 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -1022,6 +1022,30 @@ Supported settings: |---|---|---| | `vtsls_version` | `0.2.9` | Override the `@vtsls/language-server` npm package version Serena installs. | | `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | +| `initialization_options` | `null` | Dict forwarded to vtsls on three LSP channels: the `initializationOptions` field of the `initialize` request, a `workspace/didChangeConfiguration` notification sent right after initialize, and as the response to `workspace/configuration` pull requests (section-scoped). Typical use is Yarn PnP: point `typescript.tsdk` at the Yarn-generated SDK and enable `vtsls.autoUseWorkspaceTsdk`. | + +Example (Yarn PnP project with TypeScript in a subdirectory; run `yarn dlx @yarnpkg/sdks vscode` in the project once to generate the SDK): + +```yaml +ls_specific_settings: + typescript_vts: + initialization_options: + typescript: + tsdk: "project/.yarn/sdks/typescript/lib" + vtsls: + autoUseWorkspaceTsdk: true +``` + +vtsls reads `typescript.tsdk` through the `workspace/configuration` pull, not through `initializationOptions`, so Serena answers those pulls from the same dict (and also pushes it on `workspace/didChangeConfiguration` for compatibility with servers that expect the notification). Without `autoUseWorkspaceTsdk: true`, vtsls falls back to its bundled TypeScript and ignores `tsdk` (there is no UI prompt to confirm the switch in a headless LSP). + +The dict is forwarded to vtsls verbatim — Serena does not validate its structure. For the list of supported keys and their expected types, refer to the vtsls [configuration schema](https://github.com/yioneko/vtsls/blob/main/packages/service/configuration.schema.json) and the underlying [VS Code TypeScript settings](https://code.visualstudio.com/docs/languages/typescript). `null` (the default) and `{}` are both treated as "unset": no `initializationOptions` are sent and no `workspace/didChangeConfiguration` notification is pushed. A non-dict value (e.g. a string or list) raises an error at server start. + +**Troubleshooting:** + +- *vtsls keeps using its bundled TypeScript and ignores `tsdk`* — ensure `vtsls.autoUseWorkspaceTsdk: true` is set alongside `typescript.tsdk`. Without it vtsls does not auto-switch to the workspace TS in a headless LSP. +- *tsserver fails to start after pointing at a custom `tsdk`* — verify the path resolves to a directory containing `tsserver.js` (e.g. `.yarn/sdks/typescript/lib`, not `.yarn/sdks/typescript`). Relative paths are interpreted relative to the project root. +- *Setting appears in `solidlsp` logs but vtsls does not react* — cross-check the key against the vtsls configuration schema linked above. The dict is forwarded as-is, so an unknown or wrong-typed key is silently ignored by vtsls. +- *Need to inspect what Serena is actually forwarding* — the dict is logged at INFO level via the `Forwarding user-provided initializationOptions to vtsls: …` line at language server startup. #### Vue diff --git a/src/solidlsp/language_servers/vts_language_server.py b/src/solidlsp/language_servers/vts_language_server.py index 2464338a..5525809b 100644 --- a/src/solidlsp/language_servers/vts_language_server.py +++ b/src/solidlsp/language_servers/vts_language_server.py @@ -34,7 +34,20 @@ DEFAULT_VTSLS_VERSION = "0.2.9" class VtsLanguageServer(SolidLanguageServer): """ Provides TypeScript specific instantiation of the LanguageServer class using vtsls. - Contains various configurations and settings specific to TypeScript via vtsls wrapper. + + Supported entries in ``ls_specific_settings["typescript_vts"]``: + - ``vtsls_version``: version of ``@vtsls/language-server`` to install (default: ``"0.2.9"``). + - ``npm_registry``: custom npm registry for the managed install. + - ``initialization_options``: optional dict forwarded verbatim as LSP + ``initializationOptions``. Useful for Yarn PnP projects, e.g.:: + + initialization_options: + typescript: + tsdk: "project/.yarn/sdks/typescript/lib" + vtsls: + autoUseWorkspaceTsdk: true + + See https://github.com/yioneko/vtsls/issues/169 for the PnP recipe. """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -113,13 +126,30 @@ class VtsLanguageServer(SolidLanguageServer): assert os.path.exists(vts_executable_path), "vtsls executable not found. Please install @vtsls/language-server and try again." return f"{vts_executable_path} --stdio" - @staticmethod - def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + @property + def _initialization_options(self) -> dict: + """ + Validated user-provided ``initializationOptions``. + + :raises ValueError: if ``ls_specific_settings.typescript_vts.initialization_options`` + is set to a value that is not a dict. + """ + opts = self._custom_settings.get("initialization_options") + if opts is None: + return {} + if not isinstance(opts, dict): + raise ValueError(f"ls_specific_settings.typescript_vts.initialization_options must be a dict, got {type(opts).__name__}") + return opts + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the VTS Language Server. + + If ``initialization_options`` is set in ``ls_specific_settings["typescript_vts"]``, + it is forwarded verbatim as LSP ``initializationOptions``. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() - initialize_params = { + initialize_params: dict = { "locale": "en", "capabilities": { "textDocument": { @@ -152,6 +182,11 @@ class VtsLanguageServer(SolidLanguageServer): } ], } + + if self._initialization_options: + log.info("Forwarding user-provided initializationOptions to vtsls: %s", self._initialization_options) + initialize_params["initializationOptions"] = self._initialization_options + return cast(InitializeParams, initialize_params) def _start_server(self) -> None: @@ -179,12 +214,12 @@ class VtsLanguageServer(SolidLanguageServer): def execute_client_command_handler(params: dict) -> list: return [] - def workspace_configuration_handler(params: dict) -> list[dict] | dict: - # VTS may request workspace configuration - # Return empty configuration for each requested item - if "items" in params: - return [{}] * len(params["items"]) - return {} + init_options = self._initialization_options + + def workspace_configuration_handler(params: dict) -> list[object]: + # vtsls pulls settings for sections like "typescript", "vtsls", "javascript". + # Return the matching sub-dicts from the user-provided initialization_options. + return [init_options.get(item.get("section", ""), {}) for item in params["items"]] def do_nothing(params: dict) -> None: return @@ -227,6 +262,14 @@ class VtsLanguageServer(SolidLanguageServer): log.debug(f"completionProvider: {init_response['capabilities']['completionProvider']}") self.server.notify.initialized({}) + + # vtsls also reads settings via workspace/didChangeConfiguration (in addition + # to initializationOptions and workspace/configuration pulls). Push the same + # user-provided settings on all three channels for maximum compatibility, + # e.g. so that `typescript.tsdk` is honoured for Yarn PnP projects. + if init_options: + self.server.notify.workspace_did_change_configuration({"settings": init_options}) + if self.server_ready.wait(timeout=1.0): log.info("VTS server is ready") else: From 3240403ab1c2a8af09ca976770184db84c4521c3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 28 May 2026 11:29:10 +0200 Subject: [PATCH 14/17] Dashboard: configure trusted hosts directly in flask app --- src/serena/dashboard.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index f5e4ec0c..914b28e5 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self import psutil -from flask import Flask, Response, abort, redirect, request, send_from_directory +from flask import Flask, Response, redirect, request, send_from_directory from PIL import Image from pydantic import BaseModel from sensai.util import logging @@ -202,6 +202,9 @@ class SerenaDashboardAPI: self._tool_names = tool_names self._agent = agent self._app = Flask(__name__) + + self._app.config["TRUSTED_HOSTS"] = ["127.0.0.1", "localhost"] + self._tool_usage_stats = tool_usage_stats self._loaded_news: dict[str, str] = {} self._news_ready = threading.Event() @@ -775,16 +778,7 @@ class SerenaDashboardAPI: from flask import cli cli.show_server_banner = lambda *args, **kwargs: None - - # Verify host and port on each request to prevent DNS-rebinding-based attacks - @self._app.before_request - def check_host() -> None: - allowed = {f"127.0.0.1:{port}", f"localhost:{port}"} - if request.host not in allowed: - abort(403) - self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) - return port def run_in_thread(self, host: str) -> tuple[threading.Thread, int]: From 09430b0fb682cd40fcd5e1943e6096c98b8a94ec Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 28 May 2026 12:51:39 +0200 Subject: [PATCH 15/17] Fix: Host validation required a local host even if the listen address was set to a non-local address (regression introduced in 1.5.2) --- CHANGELOG.md | 7 +++++++ src/serena/agent.py | 8 ++++++-- src/serena/dashboard.py | 20 +++++++++++--------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0884b617..da2f032d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Status of the `main` branch. Changes prior to the next official version change w and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing at the Yarn-generated SDK. +* Dashboard: + - Fix: Host validation required a local host regardless of the listen address (regression introduced in v1.5.2), + preventing remote connections + # v1.5.3 (2026-05-26) # v1.5.2 (2026-05-26) @@ -17,6 +21,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Add `serena-agent` CLI command so that `uvx serena-agent` can be used as entrypoint. - Fortls and pyright are now installed on the fly instead of being bundled in the serena-agent package. +* Dashboard: + - Add host validation + * Hooks: - Extend list of extensions that are considered code files (affects the reminder hook counter). diff --git a/src/serena/agent.py b/src/serena/agent.py index c8e0d1e5..ce4c1191 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -664,8 +664,12 @@ class SerenaAgent: # may access various parts of the agent if self.serena_config.web_dashboard: self._dashboard_thread, port = SerenaDashboardAPI( - get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats - ).run_in_thread(host=self.serena_config.web_dashboard_listen_address) + get_memory_log_handler(), + tool_names, + agent=self, + tool_usage_stats=self._tool_usage_stats, + host=self.serena_config.web_dashboard_listen_address, + ).run_in_thread() self._dashboard_manager = DashboardManager( port, self.serena_config.web_dashboard_listen_address, diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 914b28e5..f7c5c3cc 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -197,14 +197,16 @@ class SerenaDashboardAPI: tool_names: list[str], agent: "SerenaAgent", tool_usage_stats: ToolUsageStats | None = None, + host: str = "127.0.0.1", ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._agent = agent + self._host = host self._app = Flask(__name__) - - self._app.config["TRUSTED_HOSTS"] = ["127.0.0.1", "localhost"] - + local_hosts = ["127.0.0.1", "localhost"] + if self._host in local_hosts: + self._app.config["TRUSTED_HOSTS"] = local_hosts self._tool_usage_stats = tool_usage_stats self._loaded_news: dict[str, str] = {} self._news_ready = threading.Event() @@ -770,7 +772,7 @@ class SerenaDashboardAPI: raise RuntimeError(f"No free ports found starting from {start_port}") - def run(self, host: str, port: int) -> int: + def run(self, port: int) -> int: """ Runs the dashboard on the given host and port and returns the port number. """ @@ -778,13 +780,13 @@ class SerenaDashboardAPI: from flask import cli cli.show_server_banner = lambda *args, **kwargs: None - self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) + self._app.run(host=self._host, port=port, debug=False, use_reloader=False, threaded=True) return port - def run_in_thread(self, host: str) -> tuple[threading.Thread, int]: - port = self._find_first_free_port(self.BASE_PORT, host) - log.info("Starting dashboard (listen_address=%s, port=%d)", host, port) - thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True) + def run_in_thread(self) -> tuple[threading.Thread, int]: + port = self._find_first_free_port(self.BASE_PORT, self._host) + log.info("Starting dashboard (listen_address=%s, port=%d)", self._host, port) + thread = threading.Thread(target=lambda: self.run(port=port), daemon=True) thread.start() return thread, port From c57b7f2b7d83b8923abfab908646263a0a964d4d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 28 May 2026 12:53:44 +0200 Subject: [PATCH 16/17] Add missing changelog entry for v1.5.3 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da2f032d..2872553c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Status of the `main` branch. Changes prior to the next official version change w # v1.5.3 (2026-05-26) +Add meta-data for the GitHub MCP registry + # v1.5.2 (2026-05-26) * General: From d5033c404dc565fb7e0f9edffb98745610df52d7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 28 May 2026 14:31:41 +0200 Subject: [PATCH 17/17] Make dashboard trusted hosts configurable --- CHANGELOG.md | 6 +++--- src/serena/agent.py | 1 + src/serena/config/serena_config.py | 1 + src/serena/dashboard.py | 6 +++--- src/serena/resources/serena_config.template.yml | 8 ++++++++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2872553c..7230a56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ Status of the `main` branch. Changes prior to the next official version change w and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing at the Yarn-generated SDK. -* Dashboard: - - Fix: Host validation required a local host regardless of the listen address (regression introduced in v1.5.2), - preventing remote connections +* Dashboard: + - Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only + default local hostnames, effectively preventing remote connections # v1.5.3 (2026-05-26) diff --git a/src/serena/agent.py b/src/serena/agent.py index ce4c1191..9542d37f 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -669,6 +669,7 @@ class SerenaAgent: agent=self, tool_usage_stats=self._tool_usage_stats, host=self.serena_config.web_dashboard_listen_address, + trusted_hosts=self.serena_config.web_dashboard_trusted_hosts, ).run_in_thread() self._dashboard_manager = DashboardManager( port, diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index ff63f523..7207a9b1 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -720,6 +720,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): web_dashboard_open_on_launch: bool = True web_dashboard_interface: str | None = None web_dashboard_listen_address: str = "127.0.0.1" + web_dashboard_trusted_hosts: list[str] = field(default_factory=lambda: ["127.0.0.1", "localhost"]) jetbrains_plugin_server_address: str = "127.0.0.1" tool_timeout: float = DEFAULT_TOOL_TIMEOUT diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index f7c5c3cc..8b15166b 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -198,15 +198,15 @@ class SerenaDashboardAPI: agent: "SerenaAgent", tool_usage_stats: ToolUsageStats | None = None, host: str = "127.0.0.1", + trusted_hosts: list[str] | None = None, ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._agent = agent self._host = host self._app = Flask(__name__) - local_hosts = ["127.0.0.1", "localhost"] - if self._host in local_hosts: - self._app.config["TRUSTED_HOSTS"] = local_hosts + if trusted_hosts: + self._app.config["TRUSTED_HOSTS"] = trusted_hosts self._tool_usage_stats = tool_usage_stats self._loaded_news: dict[str, str] = {} self._news_ready = threading.Event() diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index a24ed313..8664fea3 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -67,6 +67,14 @@ web_dashboard_interface: # the address the web dashboard will listen on (bind address). web_dashboard_listen_address: 127.0.0.1 +# trusted hosts used to access the web dashboard. +# By default, only allow access via local addresses for security reasons. +# If you want to allow access from remote machines, add the hostname used to access the dashboard to this list. +# If the list is empty/undefined, all hosts are trusted. +web_dashboard_trusted_hosts: + - 127.0.0.1 + - localhost + # address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) jetbrains_plugin_server_address: 127.0.0.1