Merge pull request #146 from oraios/uvx-setup

Uvx setup
This commit is contained in:
Michael Panchenko authored and GitHub committed 2025-06-02 22:02:40 +02:00
commit b54777c3ad
32 files changed
+153 -49

No files matched your search

+2 -6
View File
@@ -55,10 +55,6 @@ jobs:
run: uv pip install -e ".[dev]"
- name: Test with pytest
shell: bash
# Currently java Language Server is not working on macos
# Currently, java and rust seem to cause problems in CI
run: |
if [ "${{ matrix.os }}" = "macos-latest" ]; then
uv run pytest -m "not java"
else
uv run poe test
fi
uv run pytest -m "not java and not rust"
+29 -7
View File
@@ -173,19 +173,31 @@ Serena can be used in various ways, below you will find instructions for selecte
### Setup
1. Install `uv` (instructions [here](https://docs.astral.sh/uv/getting-started/installation/))
2. Clone the repository.
3. Copy `serena_config.template.yml` to `serena_config.yml`
Serena is managed by `uv`, so you will need to [install it](https://docs.astral.sh/uv/getting-started/installation/)).
Then you can either
1. Clone the repository and cd into it.
2. Optionally create a config file from the template and adjust it according to your preferences.
```shell
cp serena_config.template.yml serena_config.yml
cp src/serena/resources/serena_config.template.yml serena_config.yml
```
If you just want the default config, you can skip this part, and a config file will be created when you first run Serena.
or use `uvx` to run Serena directly by relying on
```shell
uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe
```
in your MCP config (delete the `.exe` on macOS or Linux).
You can now add Serena to your MCP client as described below for various clients and
[activate your first project](#project-activation).
> In the default configuration, Serena will start a small dashboard on localhost that will display logs and allow shutting down the
> MCP server (since many clients fail to cleanup processes, leaving zombies behind). If you don't want that, simply set `web_dashboard` to `False`
> in your `serena_config.yml`.
> in your `serena_config.yml` or pass
### Configuration
@@ -195,7 +207,7 @@ Serena's behavior (like available projects, active tools and prompts) is configu
2. In the arguments passed to the `serena-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.
See for a detailed explanation.
See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments.
3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever
that project is activated.
4. Through the currently active set of [modes](#modes).
@@ -253,6 +265,16 @@ That's it! Save the config and then restart Claude Desktop. You are ready for ac
uv run serena-mcp-server --help
```
ℹ️ You can use Serena without cloning or configuring it explicitly by
{
"mcpServers": {
"serena": {
"command": "/abs/path/to/uv",
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"]
}
}
}
#### Troubleshooting
+1 -2
View File
@@ -34,7 +34,6 @@ dependencies = [
"dotenv>=0.9.9",
"pathspec>=0.12.1",
"psutil>=7.0.0",
"agno>=1.2.15",
"docstring_parser>=0.16",
"joblib>=1.5.1",
]
@@ -73,7 +72,7 @@ google = [
Homepage = "https://github.com/oraios/serena"
[tool.hatch.build.targets.wheel]
packages = ["src/serena", "src/multilspy"]
packages = ["src/serena", "src/multilspy", "src/interprompt"]
[tool.black]
line-length = 140
-6
View File
@@ -1,12 +1,6 @@
__version__ = "2025-05-21"
def serena_root_path() -> str:
from pathlib import Path
return str(Path(__file__).parent.parent.parent.absolute())
def serena_version() -> str:
"""
:return: the version of the package, including git status if available.
+38 -9
View File
@@ -21,7 +21,7 @@ from functools import cached_property
from logging import Logger
from pathlib import Path
from types import TracebackType
from typing import TYPE_CHECKING, Any, Self, TypeVar, Union
from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast
import yaml
from overrides import override
@@ -34,9 +34,9 @@ from multilspy import SyncLanguageServer
from multilspy.multilspy_config import Language, MultilspyConfig
from multilspy.multilspy_logger import MultilspyLogger
from multilspy.multilspy_types import SymbolKind
from serena import serena_root_path, serena_version
from serena import serena_version
from serena.config import SerenaAgentContext, SerenaAgentMode
from serena.constants import PROJECT_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME
from serena.constants import PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME
from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI
from serena.prompt_factory import PromptFactory, SerenaPromptFactory
from serena.symbol import SymbolManager
@@ -297,17 +297,29 @@ class SerenaConfig(SerenaConfigBase):
CONFIG_FILE = "serena_config.yml"
@classmethod
def get_config_file_path(cls) -> str:
return os.path.join(serena_root_path(), cls.CONFIG_FILE)
def autogenerate(cls) -> None:
log.info("Autogenerating Serena configuration file")
if os.path.exists(cls.get_config_file_path()):
raise FileExistsError(
f"Serena configuration file already exists at {cls.get_config_file_path()}. Please remove it if you want to autogenerate a new one."
)
loaded_commented_yaml = load_yaml(SELENA_CONFIG_TEMPLATE_FILE, preserve_comments=True)
save_yaml(cls.get_config_file_path(), loaded_commented_yaml, preserve_comments=True)
@classmethod
def from_config_file(cls) -> "SerenaConfig":
def get_config_file_path(cls) -> str:
return os.path.join(REPO_ROOT, cls.CONFIG_FILE)
@classmethod
def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
"""
Static constructor to create SerenaConfig from the configuration file
"""
config_file = cls.get_config_file_path()
if not os.path.exists(config_file):
raise FileNotFoundError(f"Serena configuration file not found: {config_file}")
if not generate_if_missing:
raise FileNotFoundError(f"Serena configuration file not found: {config_file}")
cls.autogenerate()
log.info(f"Loading Serena configuration from {config_file}")
try:
@@ -466,6 +478,9 @@ class SerenaAgent:
serena_config: SerenaConfigBase | None = None,
context: SerenaAgentContext | None = None,
modes: list[SerenaAgentMode] | None = None,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
gui_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
):
"""
:param project: the project to load immediately or None to not load any project; may be a path to the project or a name of
@@ -476,9 +491,21 @@ class SerenaAgent:
:param modes: list of modes in which the agent is operating (they will be combined), None for default modes.
The modes may adjust prompts, tool availability, and tool descriptions.
:param serena_config: the Serena configuration or None to read the configuration from the default location.
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
If not specified, will take the value from the serena configuration.
:param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration.
"""
# obtain serena configuration
self.serena_config = serena_config or SerenaConfig.from_config_file()
if enable_web_dashboard is not None:
self.serena_config.web_dashboard = enable_web_dashboard
if enable_gui_log_window is not None:
self.serena_config.gui_log_window_enabled = enable_gui_log_window
if gui_log_level is not None:
gui_log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], gui_log_level.upper())
# transform to int
self.serena_config.gui_log_window_level = logging.getLevelNamesMapping()[gui_log_level]
# open GUI log window if enabled
self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa
@@ -706,13 +733,13 @@ class SerenaAgent:
# Active modes
active_mode_names = [mode.name for mode in self.get_active_modes()]
result_str += "Active modes: {}\n".format(", ".join(active_mode_names))
result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + "\n"
# Available but not active modes
all_available_modes = SerenaAgentMode.list_registered_mode_names()
inactive_modes = [mode for mode in all_available_modes if mode not in active_mode_names]
if inactive_modes:
result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes))
result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes)) + "\n"
# Active tools
result_str += "Active tools (after all exclusions from the project, context, and modes):\n"
@@ -1914,6 +1941,8 @@ class InitialInstructionsTool(Tool):
"""
Get the initial instructions for the current coding project.
You should always call this tool before starting to work (including using any other tool) on any programming task!
The only exception is when a user asks you to activate a project, in which case you should call the `activate_project` first
instead and then call this tool.
"""
return self.agent.create_system_prompt()
+3 -3
View File
@@ -14,9 +14,9 @@ from agno.tools.toolkit import Toolkit
from dotenv import load_dotenv
from sensai.util.logging import LogTime
from serena import serena_root_path
from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe
from serena.config import SerenaAgentContext
from serena.constants import REPO_ROOT
log = logging.getLogger(__name__)
@@ -65,7 +65,7 @@ class SerenaAgnoAgentProvider:
return cls._agent
# change to Serena root
os.chdir(serena_root_path())
os.chdir(REPO_ROOT)
load_dotenv()
@@ -94,7 +94,7 @@ class SerenaAgnoAgentProvider:
# If project file path is relative, make it absolute by joining with project root
if not project_file.is_absolute():
# Get the project root directory (parent of scripts directory)
project_root = Path(serena_root_path())
project_root = Path(REPO_ROOT)
project_file = project_root / args_project_file
# Ensure the path is normalized and absolute
+7 -1
View File
@@ -121,7 +121,13 @@ class SerenaAgentContext:
try:
return cls.from_name(str(name_or_path))
except FileNotFoundError:
return cls.from_yaml(name_or_path)
try:
return cls.from_yaml(name_or_path)
except FileNotFoundError as e:
raise FileNotFoundError(
f"Context {name_or_path} not found in {CONTEXT_YAMLS_DIR}. You can load a custom context by using from_yaml() instead.\n"
f"Available contexts:\n{cls.list_registered_context_names()}"
) from e
@classmethod
def list_registered_context_names(cls) -> list[str]:
+8 -4
View File
@@ -1,15 +1,19 @@
from pathlib import Path
_repo_root_path = Path(__file__).parent.parent.parent.resolve()
_serena_pkg_path = Path(__file__).parent.resolve()
REPO_ROOT = str(_repo_root_path)
PROMPT_TEMPLATES_DIR = str(_repo_root_path / "config" / "prompt_templates")
CONTEXT_YAMLS_DIR = str(_repo_root_path / "config" / "contexts")
MODE_YAMLS_DIR = str(_repo_root_path / "config" / "modes")
PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates")
CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts")
MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "modes")
SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard")
SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons")
SERENA_MANAGED_DIR_NAME = ".serena"
DEFAULT_CONTEXT = "desktop-app"
DEFAULT_MODES = ("interactive", "editing")
PROJECT_TEMPLATE_FILE = str(_repo_root_path / "project.template.yml")
PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml")
SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml")
+2 -3
View File
@@ -10,7 +10,7 @@ from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from sensai.util import logging
from serena import serena_root_path
from serena.constants import SERENA_DASHBOARD_DIR
log = logging.getLogger(__name__)
@@ -75,8 +75,7 @@ class SerenaDashboardAPI:
self._setup_routes()
def _setup_routes(self) -> None:
static_dir = os.path.join(serena_root_path(), "dashboard")
self._app.mount("/dashboard", StaticFiles(directory=static_dir), name="dashboard")
self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard")
self._app.add_api_route("/get_log_messages", self._get_log_messages, methods=["POST"], response_model=ResponseLog)
self._app.add_api_route("/get_tool_names", self._get_tool_names, methods=["GET"], response_model=ResponseToolNames)
+44 -1
View File
@@ -102,6 +102,9 @@ def create_mcp_server_and_agent(
port: int = 8000,
context: str = DEFAULT_CONTEXT,
modes: Sequence[str] = DEFAULT_MODES,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
gui_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
) -> tuple[FastMCP, SerenaAgent]:
"""
Create an MCP server.
@@ -113,6 +116,10 @@ def create_mcp_server_and_agent(
:param port: The port to bind to
:param context: The context name or path to context file
:param modes: List of mode names or paths to mode files
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
If not specified, will take the value from the serena configuration.
:param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration.
"""
mcp: FastMCP | None = None
context_instance = SerenaAgentContext.load(context)
@@ -125,6 +132,9 @@ def create_mcp_server_and_agent(
# project_activation_callback=update_tools
context=context_instance,
modes=modes_instances,
enable_web_dashboard=enable_web_dashboard,
enable_gui_log_window=enable_gui_log_window,
gui_log_level=gui_log_level,
)
except Exception as e:
show_fatal_exception_safe(e)
@@ -237,6 +247,27 @@ PROJECT_TYPE = ProjectType()
default=8000,
help="Port to bind to (for SSE transport).",
)
@click.option(
"--enable-web-dashboard",
type=bool,
is_flag=False,
default=None,
help="Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.",
)
@click.option(
"--enable-gui-log-window",
type=bool,
is_flag=False,
default=None,
help="Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. "
"If not specified, will take the value from the serena configuration.",
)
@click.option(
"--gui-log-level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
default=None,
help="Log level for the GUI log window. If not specified, will take the value from the serena configuration.",
)
def start_mcp_server(
project_file_opt: str | None,
project_file_arg: str | None,
@@ -245,6 +276,9 @@ def start_mcp_server(
transport: Literal["stdio", "sse"] = "stdio",
host: str = "0.0.0.0",
port: int = 8000,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
gui_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
) -> None:
"""Starts the Serena MCP server. By default, will not activate any project at startup.
If you want to start with an already active project, use --project to pass the project name or path.
@@ -256,7 +290,16 @@ def start_mcp_server(
# This is for backward compatibility with the old CLI, should be removed in the future!
project_file = project_file_arg if project_file_arg is not None else project_file_opt
mcp_server, agent = create_mcp_server_and_agent(project=project_file, host=host, port=port, context=context, modes=modes)
mcp_server, agent = create_mcp_server_and_agent(
project=project_file,
host=host,
port=port,
context=context,
modes=modes,
enable_web_dashboard=enable_web_dashboard,
enable_gui_log_window=enable_gui_log_window,
gui_log_level=gui_log_level,
)
# log after server creation such that the log appears in the GUI
if project_file_arg is not None:
File renamed without changes.
@@ -16,7 +16,7 @@ prompt: |
You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file
or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit.
For example, if you are working with python code and alread know that you need to read the body of the constructor of the class Foo, you can directly
For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly
use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
to read the desired methods with `include_body=True`.
@@ -37,12 +37,13 @@ prompt: |
then use your knowledge of the codebase to write the regex, if you haven't collected enough information of this content yet.
You are extremely good at regex, so you never need to check whether the replacement produced the correct result.
In particular, you know what to escape and what not to escape, and you know how to use wildcards.
Also, the regex tool never adds any indentation (contrary to the symbolic editing tools), so you have to take care to add the correct indentation
when using it to insert code.
Moreover, the replacement tool will fail if it can't perform the desired replacement, and this is all the feedback you need.
Your overall goal for replacement operations is to use relatively short regexes, since I want you to minimize the number
of output tokens. For replacements of larger chunks of code, this means you intelligently make use of wildcards for the middle part
and of characteristic snippets for the before/after parts that uniquely identify the chunk.
For small replacements, up to a single line, you follow the following rules:
1. If the snippet to be replaced is likely to be unique within the file, you perform the replacement by directly using the escaped version of the
File renamed without changes.
File renamed without changes.
@@ -8,7 +8,19 @@ prompt: |
it may be enough to read a single file or a small set of symbols.
You can achieve the intelligent reading of code by using the symbolic tools for getting an overview of symbols and
the relations between them, and then only reading the bodies of symbols that are necessary to answer the question
or complete the task. You can also use the standard tools like list_dir and search_for_pattern if you need to.
or complete the task. You can also use the standard tools like list_dir, find_file and search_for_pattern if you need to.
Symbols are identified by their name path and relative file path, see the description of the `find_symbols` tool for more details
on how the `name_path` matches symbols.
You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file
or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit.
For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly
use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
to read the desired methods with `include_body=True`
You can unterstand relationships between symbols by using the `find_referencing_symbols` tool.
You generally have access to memories and it may be useful for you to read them, but also only if they help you
to answer the question or complete the task. You can infer which memories are relevant to the current task by reading
the memory names and descriptions.
@@ -21,3 +33,4 @@ excluded_tools:
- replace_lines
- insert_at_line
- execute_shell_command
- replace_regex
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -629,7 +629,7 @@
# Instance variable with type annotation
self.typed_instance_var: list[str] = ["item1", "item2"]
def modify_instance_var(self):
# This body has been replaced
self.instance_var = "Replaced!"
@@ -694,7 +694,7 @@
constructor(value: number) {
this.value = value;
}
function printValue() {
// This body has been replaced
console.warn("New value: " + this.value);
Generated
-2
View File
@@ -970,7 +970,6 @@ name = "serena"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "agno" },
{ name = "docstring-parser" },
{ name = "dotenv" },
{ name = "fastapi" },
@@ -1016,7 +1015,6 @@ google = [
[package.metadata]
requires-dist = [
{ name = "agno", specifier = ">=1.2.15" },
{ name = "agno", marker = "extra == 'agno'", specifier = ">=1.2.6" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.49.0" },
{ name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" },