Merge branch 'main' into feature/docstring_and_signature

This commit is contained in:
Michael Panchenko committed 2026-01-03 11:00:58 +07:00
commit 036625e809
14 files changed
+133 -95

No files matched your search

+2
View File
@@ -27,6 +27,8 @@ special settings for Docker usage:
```
# Disable the GUI log window since it's not supported in Docker
gui_log_window: False
# Listen on all interfaces for the web dashboard to be accessible from outside the container
web_dashboard_listen_address: 0.0.0.0
# Disable opening the web dashboard on launch (not possible within the container)
web_dashboard_open_on_launch: False
```
+1
View File
@@ -64,6 +64,7 @@ ENV SERENA_HOME=/workspaces/serena/config
RUN mkdir -p $SERENA_HOME
RUN cp src/serena/resources/serena_config.template.yml $SERENA_HOME/serena_config.yml
RUN sed -i 's/^gui_log_window: .*/gui_log_window: False/' $SERENA_HOME/serena_config.yml
RUN sed -i 's/^web_dashboard_listen_address: .*/web_dashboard_listen_address: 0.0.0.0/' $SERENA_HOME/serena_config.yml
RUN sed -i 's/^web_dashboard_open_on_launch: .*/web_dashboard_open_on_launch: False/' $SERENA_HOME/serena_config.yml
# Create virtual environment and install dependencies
+21 -7
View File
@@ -75,7 +75,8 @@ object, e.g.
Serena is a great way to make Claude Code both cheaper and more powerful!
From your project directory, add serena with a command like this,
**Per-Project Configuration.** To add the Serena MCP server to the current project in the current directory,
use this command:
```shell
claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)"
@@ -88,18 +89,31 @@ Note:
that Serena is configured to work on the current project from the get-go, following
Claude Code's mode of operation.
Alternatively, use `--project-from-cwd` for user-level configuration that works across all projects:
**Global Configuration**. Alternatively, use `--project-from-cwd` for user-level configuration that works across all projects:
```shell
claude mcp add --scope user serena -- <serena> start-mcp-server --context=claude-code --project-from-cwd
```
This searches up from the current directory for `.serena/project.yml` or `.git` markers,
falling back to the current directory if neither is found. This makes it suitable for a
single global MCP configuration. The `--project-from-cwd` option is intended for CLI-based agents
(like Claude Code, Gemina or Codex) that invoke Serena from within project directories.
Whenever you start Claude Code, Serena will search up from the current directory for `.serena/project.yml` or `.git` markers,
activating the current directory as the project if neither is found.
This mechanism makes it suitable for a single global MCP configuration.
Be sure to use at least `v1.0.52` of Claude Code (as earlier versions do not read MCP server system prompts upon startup).
**Maximum Token Efficiency.** To maximize token efficiency, you may want to use Claude Code's
*on-demand tool loading* feature, which is supported since at least v2.0.74 of Claude Code.
This feature avoids sending all tool descriptions to Claude upon startup, thus saving tokens.
Instead, Claude will search for tools as needed (but there are no guarantees that it will
search optimally, of course).
To enable this feature, set the environment variable `ENABLE_TOOL_SEARCH=true`.
Depending on your shell, you can also set this on a per-session basis, e.g. using
```shell
ENABLE_TOOL_SEARCH=true claude
```
in bash/zsh, or using
```shell
set ENABLE_TOOL_SEARCH=true && claude
```
in Windows CMD to launch Claude Code.
## VSCode
+4 -4
View File
@@ -55,18 +55,18 @@ Indexing has to be called only once. During regular usage, Serena will automatic
Project activation makes Serena aware of the project you want to work with.
You can either choose to do this
* while in a conversation, by telling the model to activate a project, e.g.,
* while in a conversation, by telling the LLM to activate a project, e.g.,
* "Activate the project /path/to/my_project" (for first-time activation with auto-creation)
* "Activate the project my_project"
Note that this option requires the `activate_project` tool to be active,
which it isn't in the (default version) of context `ide-assistant` if a project is provided at startup.
(The tool is deactivated because we assume that in the ide-assistant context the user will only work on the open project and have
which it isn't in single-project [contexts](contexts) like `ide` or `claude-code` *if* a project is provided at startup.
(The tool is deactivated, because we assume that in these contexts, user will only work on the single, open project and have
no need to switch it.)
* when the MCP server starts, by passing the project path or name as a command-line argument
(e.g. when working on a fixed project in `ide-assistant` mode): `--project <path|name>`
(e.g. when using a single-project mode like `ide` or `claude-code`): `--project <path|name>`
## Onboarding & Memories
+10 -3
View File
@@ -8,14 +8,15 @@ Serena is configured in four places:
1. The `serena_config.yml` for general settings that apply to all clients and projects.
It is located in your user directory under `.serena/serena_config.yml`.
If you do not explicitly create the file, it will be auto-generated when you first run Serena.
The file will be auto-generated when you first run Serena.
You can edit it directly or use
```shell
uvx --from git+https://github.com/oraios/serena serena config edit
<serena> config edit
```
(or use the `--directory` command version).
where `<serena>` is [your way of running Serena](020_running).
The configuration file can also be accessed through [Serena's dashboard](060_dashboard).
2. In the arguments passed to the `start-mcp-server` in your client's config (see below),
which will apply to all sessions started by the respective client. In particular, the [context](contexts) parameter
should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client.
@@ -55,6 +56,12 @@ Choose the context that best matches the type of integration you are using.
Find the concrete definitions of the above contexts [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/contexts).
Note that the contexts `ide` and `claude-code` are **single-project contexts** (defining `single_project: true`).
For such contexts, if a project is provided at startup, the set of tools is limited to those required by the project's
concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal.
Tools explicitly disabled by the project will not be available at all. Since changing the active project
ceases to be a relevant operation in this case, the project activation tool is disabled.
When launching Serena, specify the context using `--context <context-name>`.
Note that for cases where parameter lists are specified (e.g. Claude Desktop), you must add two parameters to the list.
+1 -1
View File
@@ -25,7 +25,7 @@ dependencies = [
"pydantic>=2.10.6",
"types-pyyaml>=6.0.12.20241230",
"pyyaml>=6.0.2",
"ruamel.yaml>=0.18.0",
"ruamel.yaml==0.18.14",
"jinja2>=3.1.6",
"dotenv>=0.9.9",
"pathspec>=0.12.1",
+29 -20
View File
@@ -107,26 +107,35 @@ class ToolSet:
registry = ToolRegistry()
tool_names = set(self._tool_names)
for definition in tool_inclusion_definitions:
included_tools = []
excluded_tools = []
for included_tool in definition.included_optional_tools:
included_tool = get_updated_tool_name(included_tool)
if not registry.is_valid_tool_name(included_tool):
raise ValueError(f"Invalid tool name '{included_tool}' provided for inclusion")
if included_tool not in tool_names:
tool_names.add(included_tool)
included_tools.append(included_tool)
for excluded_tool in definition.excluded_tools:
excluded_tool = get_updated_tool_name(excluded_tool)
if not registry.is_valid_tool_name(excluded_tool):
raise ValueError(f"Invalid tool name '{excluded_tool}' provided for exclusion")
if excluded_tool in tool_names:
tool_names.remove(excluded_tool)
excluded_tools.append(excluded_tool)
if included_tools:
log.info(f"{definition} included {len(included_tools)} tools: {', '.join(included_tools)}")
if excluded_tools:
log.info(f"{definition} excluded {len(excluded_tools)} tools: {', '.join(excluded_tools)}")
if definition.is_fixed_tool_set():
tool_names = set()
for fixed_tool in definition.fixed_tools:
fixed_tool = get_updated_tool_name(fixed_tool)
if not registry.is_valid_tool_name(fixed_tool):
raise ValueError(f"Invalid tool name '{fixed_tool}' provided for fixed tool set")
tool_names.add(fixed_tool)
log.info(f"{definition} defined a fixed tool set with {len(tool_names)} tools: {', '.join(tool_names)}")
else:
included_tools = []
excluded_tools = []
for included_tool in definition.included_optional_tools:
included_tool = get_updated_tool_name(included_tool)
if not registry.is_valid_tool_name(included_tool):
raise ValueError(f"Invalid tool name '{included_tool}' provided for inclusion")
if included_tool not in tool_names:
tool_names.add(included_tool)
included_tools.append(included_tool)
for excluded_tool in definition.excluded_tools:
excluded_tool = get_updated_tool_name(excluded_tool)
if not registry.is_valid_tool_name(excluded_tool):
raise ValueError(f"Invalid tool name '{excluded_tool}' provided for exclusion")
if excluded_tool in tool_names:
tool_names.remove(excluded_tool)
excluded_tools.append(excluded_tool)
if included_tools:
log.info(f"{definition} included {len(included_tools)} tools: {', '.join(included_tools)}")
if excluded_tools:
log.info(f"{definition} excluded {len(excluded_tools)} tools: {', '.join(excluded_tools)}")
return ToolSet(tool_names)
def without_editing_tools(self) -> "ToolSet":
+2 -2
View File
@@ -27,7 +27,7 @@ from serena.constants import (
SERENAS_OWN_CONTEXT_YAMLS_DIR,
SERENAS_OWN_MODE_YAMLS_DIR,
)
from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess
from serena.mcp import SerenaMCPFactory
from serena.project import Project
from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry
from serena.util.logging import MemoryLogHandler
@@ -237,7 +237,7 @@ class TopLevelCommands(AutoRegisteringGroup):
log.info("Auto-detected project root: %s", project)
project_file = project_file_arg or project
factory = SerenaMCPFactorySingleProcess(context=context, project=project_file, memory_log_handler=memory_log_handler)
factory = SerenaMCPFactory(context=context, project=project_file, memory_log_handler=memory_log_handler)
server = factory.create_mcp_server(
host=host,
port=port,
+27 -3
View File
@@ -5,7 +5,7 @@ The Serena Model Context Protocol (MCP) Server
import dataclasses
import os
import shutil
from collections.abc import Iterable
from collections.abc import Sequence
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime
@@ -95,8 +95,32 @@ class SerenaPaths:
@dataclass
class ToolInclusionDefinition:
excluded_tools: Iterable[str] = ()
included_optional_tools: Iterable[str] = ()
"""
Defines which tools to include/exclude in Serena's operation.
This can mean either
* defining exclusions/inclusions to apply to an existing set of tools [incremental mode], or
* defining a fixed set of tools to use [fixed mode].
"""
excluded_tools: Sequence[str] = ()
"""
the names of tools to exclude from use [incremental mode]
"""
included_optional_tools: Sequence[str] = ()
"""
the names of optional tools to include [incremental mode]
"""
fixed_tools: Sequence[str] = ()
"""
the names of tools to use as a fixed set of tools [fixed mode]
"""
def is_fixed_tool_set(self) -> bool:
num_fixed = len(self.fixed_tools)
num_incremental = len(self.excluded_tools) + len(self.included_optional_tools)
if num_fixed > 0 and num_incremental > 0:
raise ValueError("Cannot use both fixed_tools and excluded_tools/included_optional_tools at the same time.")
return num_fixed > 0
class SerenaConfigError(Exception):
+23 -49
View File
@@ -3,7 +3,6 @@ The Serena Model Context Protocol (MCP) Server
"""
import sys
from abc import abstractmethod
from collections.abc import AsyncIterator, Iterator, Sequence
from contextlib import asynccontextmanager
from copy import deepcopy
@@ -49,15 +48,22 @@ class SerenaMCPRequestContext:
class SerenaMCPFactory:
def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None):
"""
Factory for the creation of the Serena MCP server with an associated SerenaAgent.
"""
def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None, memory_log_handler: MemoryLogHandler | None = None):
"""
:param context: The context name or path to context file
:param project: Either an absolute path to the project directory or a name of an already registered project.
If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name
afterward.
:param memory_log_handler: the in-memory log handler to use for the agent's logging
"""
self.context = SerenaAgentContext.load(context)
self.project = project
self.agent: SerenaAgent | None = None
self.memory_log_handler = memory_log_handler
@staticmethod
def _sanitize_for_openai_tools(schema: dict) -> dict:
@@ -229,9 +235,9 @@ class SerenaMCPFactory:
title=None,
)
@abstractmethod
def _iter_tools(self) -> Iterator[Tool]:
pass
assert self.agent is not None
yield from self.agent.get_exposed_tool_instances()
# noinspection PyProtectedMember
def _set_mcp_tools(self, mcp: FastMCP, openai_tool_compatible: bool = False) -> None:
@@ -243,9 +249,13 @@ class SerenaMCPFactory:
mcp._tool_manager._tools[tool.get_name()] = mcp_tool
log.info(f"Starting MCP server with {len(mcp._tool_manager._tools)} tools: {list(mcp._tool_manager._tools.keys())}")
@abstractmethod
def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None:
pass
def _create_serena_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> SerenaAgent:
return SerenaAgent(
project=self.project, serena_config=serena_config, context=self.context, modes=modes, memory_log_handler=self.memory_log_handler
)
def _create_default_serena_config(self) -> SerenaConfig:
return SerenaConfig.from_config_file()
def create_mcp_server(
self,
@@ -275,7 +285,7 @@ class SerenaMCPFactory:
:param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.
"""
try:
config = SerenaConfig.from_config_file()
config = self._create_default_serena_config()
# update configuration with the provided parameters
if enable_web_dashboard is not None:
@@ -293,7 +303,7 @@ class SerenaMCPFactory:
config.language_backend = language_backend
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
self._instantiate_agent(config, modes_instances)
self.agent = self._create_serena_agent(config, modes_instances)
except Exception as e:
show_fatal_exception_safe(e)
@@ -308,49 +318,13 @@ class SerenaMCPFactory:
return mcp
@asynccontextmanager
@abstractmethod
async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]:
"""Manage server startup and shutdown lifecycle."""
yield None # ensures MyPy understands we yield None
@abstractmethod
def _get_initial_instructions(self) -> str:
pass
class SerenaMCPFactorySingleProcess(SerenaMCPFactory):
"""
MCP server factory where the SerenaAgent and its language server run in the same process as the MCP server
"""
def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None, memory_log_handler: MemoryLogHandler | None = None):
"""
:param context: The context name or path to context file
:param project: Either an absolute path to the project directory or a name of an already registered project.
If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name
afterward.
"""
super().__init__(context=context, project=project)
self.agent: SerenaAgent | None = None
self.memory_log_handler = memory_log_handler
def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None:
self.agent = SerenaAgent(
project=self.project, serena_config=serena_config, context=self.context, modes=modes, memory_log_handler=self.memory_log_handler
)
def _iter_tools(self) -> Iterator[Tool]:
assert self.agent is not None
yield from self.agent.get_exposed_tool_instances()
def _get_initial_instructions(self) -> str:
assert self.agent is not None
# we don't use the tool (which at the time of writing calls this method), since the tool may be disabled by the config
return self.agent.create_system_prompt()
@asynccontextmanager
async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]:
openai_tool_compatible = self.context.name in ["chatgpt", "codex", "oaicompat-agent"]
self._set_mcp_tools(mcp_server, openai_tool_compatible=openai_tool_compatible)
log.info("MCP server lifetime setup complete")
yield
def _get_initial_instructions(self) -> str:
assert self.agent is not None
return self.agent.create_system_prompt()
+5 -1
View File
@@ -41,9 +41,13 @@ class JetBrainsPluginClient(ToStringMixin):
"""
BASE_PORT = 0x5EA2
PLUGIN_REQUEST_TIMEOUT = 300
"""
the timeout used for request handling within the plugin (a constant in the plugin)
"""
last_port: int | None = None
def __init__(self, port: int, timeout: int = 30):
def __init__(self, port: int, timeout: int = PLUGIN_REQUEST_TIMEOUT):
self.base_url = f"http://127.0.0.1:{port}"
self.timeout = timeout
self.session = requests.Session()
+4 -1
View File
@@ -349,12 +349,15 @@ class RegisteredTool:
tool_name: str
tool_packages = ["serena.tools"]
@singleton
class ToolRegistry:
def __init__(self) -> None:
self._tool_dict: dict[str, RegisteredTool] = {}
for cls in iter_subclasses(Tool):
if not cls.__module__.startswith("serena.tools"):
if not any(cls.__module__.startswith(pkg) for pkg in tool_packages):
continue
is_optional = issubclass(cls, ToolMarkerOptional)
name = cls.get_name_from_cls()
+3 -3
View File
@@ -3,7 +3,7 @@ import logging
import pytest
from serena.config.serena_config import SerenaConfig
from serena.mcp import SerenaMCPFactorySingleProcess
from serena.mcp import SerenaMCPFactory
from serena.tools.tools_base import ToolRegistry
@@ -16,9 +16,9 @@ def test_all_tool_parameters_have_type(context):
cfg = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False, log_level=logging.ERROR)
registry = ToolRegistry()
cfg.included_optional_tools = tuple(registry.get_tool_names_optional())
factory = SerenaMCPFactorySingleProcess(context=context)
factory = SerenaMCPFactory(context=context)
# Initialize the agent so that the tools are available
factory._instantiate_agent(cfg, [])
factory.agent = factory._create_serena_agent(cfg, [])
tools = list(factory._iter_tools())
for tool in tools:
Generated
+1 -1
View File
@@ -2014,7 +2014,7 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.0.0,<2" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "requests", specifier = ">=2.32.3,<3" },
{ name = "ruamel-yaml", specifier = ">=0.18.0" },
{ name = "ruamel-yaml", specifier = "==0.18.14" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.0.285" },
{ name = "sensai-utils", specifier = ">=1.5.0" },
{ name = "sphinx", marker = "extra == 'dev'", specifier = ">=7,<8" },