Skip to content

Commit f65a0c8

Browse files
committed
Better tool exclusion handling at mcp level
Some tools will never be activated during a session, so it's better to not expose them
1 parent 3fddeb4 commit f65a0c8

2 files changed

Lines changed: 75 additions & 12 deletions

File tree

src/serena/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
from copy import copy
77
from dataclasses import asdict, dataclass, field
8+
from enum import Enum
89
from pathlib import Path
910
from typing import TYPE_CHECKING, Self
1011

@@ -167,3 +168,35 @@ def print_overview(self) -> None:
167168
print(f"{self.name}:\n {self.description}")
168169
if self.excluded_tools:
169170
print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools)))
171+
172+
173+
class RegisteredContext(Enum):
174+
"""A registered context."""
175+
176+
IDE_ASSISTANT = "ide-assistant"
177+
"""For Serena running within an assistant that already has basic tools, like Claude Code, Cline, Cursor, etc."""
178+
DESKTOP_APP = "desktop-app"
179+
"""For Serena running within Claude Desktop or a similar app which does not have built-in tools for code editing."""
180+
AGENT = "agent"
181+
"""For Serena running as a standalone agent, e.g. through agno."""
182+
183+
def load(self) -> SerenaAgentContext:
184+
"""Load the context."""
185+
return SerenaAgentContext.from_name(self.value)
186+
187+
188+
class RegisteredMode(Enum):
189+
"""A registered mode."""
190+
191+
INTERACTIVE = "interactive"
192+
"""Interactive mode, for multi-turn interactions."""
193+
EDITING = "editing"
194+
"""Editing tools are activated."""
195+
PLANNING = "planning"
196+
"""Editing tools are deactivated."""
197+
ONE_SHOT = "one-shot"
198+
"""Non-interactive mode, where the goal is to finish a task autonomously."""
199+
200+
def load(self) -> SerenaAgentMode:
201+
"""Load the mode."""
202+
return SerenaAgentMode.from_name(self.value)

src/serena/mcp.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,16 @@
2424
from sensai.util import logging
2525
from sensai.util.helper import mark_used
2626

27-
from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe
28-
from serena.config import SerenaAgentContext, SerenaAgentMode
27+
from serena.agent import (
28+
ActivateProjectTool,
29+
Project,
30+
SerenaAgent,
31+
ToolInterface,
32+
ToolRegistry,
33+
create_serena_config,
34+
show_fatal_exception_safe,
35+
)
36+
from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode
2937
from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES
3038
from serena.process_isolated_agent import (
3139
ProcessIsolatedDashboard,
@@ -137,25 +145,48 @@ def create_mcp_server_and_agent(
137145
context_instance = SerenaAgentContext.load(context)
138146
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
139147

148+
is_ide_assistant = context_instance.name == RegisteredContext.IDE_ASSISTANT.value
149+
150+
project_instance: Project | None = None
140151
if project is not None:
141-
log.info(f"Will use project at mcp server startup: {project}")
152+
# Fail early if the project cannot be loaded
153+
log.info(f"Will activate project {project} at mcp server startup")
154+
try:
155+
project_instance = Project.load(project)
156+
except Exception as e:
157+
log.error(f"Failed to load or generate project config for {project}: {e}")
158+
raise
159+
else:
160+
log.warning("No project passed at startup, you will have to activate a project yourself (by asking the agent to do so).")
161+
162+
tools_excluded_in_this_session = context_instance.get_excluded_tool_classes()
163+
if is_ide_assistant and project_instance is not None:
164+
tools_excluded_in_this_session.extend(project_instance.project_config.get_excluded_tool_classes())
165+
# if a project has been loaded, it will be activated at startup and in ide-assistant context,
166+
# we assume that no other project will be activated in this session.
167+
# Therefore, we exclude the activate project tool
168+
tools_excluded_in_this_session.append(ActivateProjectTool)
169+
tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session}
170+
171+
all_tool_names = set(ToolRegistry.get_tool_names())
172+
tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session
142173

143174
try:
175+
# Start agent and dashboard processes (the latter only if enabled)
176+
serena_dashboard_process = None
144177
serena_config = create_serena_config(
145178
enable_web_dashboard=enable_web_dashboard,
146179
enable_gui_log_window=enable_gui_log_window,
147180
log_level=log_level,
148181
trace_lsp_communication=trace_lsp_communication,
149182
tool_timeout=tool_timeout,
150183
)
184+
if serena_config.web_dashboard:
185+
serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(tool_names_included_in_this_session))
151186
serena_agent_process = ProcessIsolatedSerenaAgent(
152187
project=project, serena_config=serena_config, modes=modes_instances, context=context_instance
153188
)
154189

155-
# Start process-isolated dashboard if enabled
156-
serena_dashboard_process = None
157-
if serena_config.web_dashboard:
158-
serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[])
159190
except Exception as e:
160191
show_fatal_exception_safe(e)
161192
raise
@@ -164,15 +195,14 @@ def update_tools() -> None:
164195
"""Update the tools in the MCP server - adapted for process isolation."""
165196
nonlocal mcp, serena_agent_process
166197

167-
# Get tool names from process-isolated agent
168-
# Tools may change as a result of project activation.
198+
# Get tool names from agent
199+
# Tools may change as a result of project or mode activation.
169200
# NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not,
170201
# unfortunately, query for changed tools. It only queries for changed resources and prompts regularly,
171-
# so we need to register all tools at startup, unfortunately.
172-
tool_names = serena_agent_process.get_exposed_tool_names()
202+
# so we need to register all potentially active tools at startup, unfortunately.
173203
if mcp is not None:
174204
mcp._tool_manager._tools = {}
175-
for tool_name in tool_names:
205+
for tool_name in tool_names_included_in_this_session:
176206
process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name)
177207
mcp_tool = make_tool(process_isolated_tool)
178208
mcp._tool_manager._tools[tool_name] = mcp_tool

0 commit comments

Comments
 (0)