mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 02:14:49 +00:00
Merge pull request #392 from oraios/feature/chatgpt-support
Connecting Serena to chatgpt
This commit is contained in:
20 files changed
+255
-111
No files matched your search
@@ -0,0 +1,102 @@
|
||||
|
||||
# Connecting Serena MCP Server to ChatGPT via MCPO & Cloudflare Tunnel
|
||||
|
||||
This guide explains how to expose a **locally running Serena MCP server** (powered by MCPO) to the internet using **Cloudflare Tunnel**, and how to connect it to **ChatGPT as a Custom GPT with tool access**.
|
||||
|
||||
Once configured, ChatGPT becomes a powerful **coding agent** with direct access to your codebase, shell, and file system — so **read the security notes carefully**.
|
||||
|
||||
---
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
and [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed.
|
||||
|
||||
## 1. Start the Serena MCP Server via MCPO
|
||||
|
||||
Run the following command to launch Serena as http server (assuming port 8000):
|
||||
|
||||
```bash
|
||||
uvx mcpo --port 8000 --api-key <YOUR_SECRET_KEY> -- \
|
||||
uvx --from git+https://github.com/oraios/serena \
|
||||
serena start-mcp-server --context chatgpt --project $(pwd)
|
||||
```
|
||||
|
||||
- `--api-key` is required to secure the server.
|
||||
- `--project` should point to the root of your codebase.
|
||||
|
||||
You can also use other options, and you don't have to pass `--project` if you want to work on multiple projects
|
||||
or want to activate it later. See
|
||||
|
||||
```shell
|
||||
uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Expose the Server Using Cloudflare Tunnel
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cloudflared tunnel --url http://localhost:8000
|
||||
```
|
||||
|
||||
This will give you a **public HTTPS URL** like:
|
||||
|
||||
```
|
||||
https://serena-agent-tunnel.trycloudflare.com
|
||||
```
|
||||
|
||||
Your server is now securely exposed to the internet.
|
||||
|
||||
---
|
||||
|
||||
## 3. Connect It to ChatGPT (Custom GPT)
|
||||
|
||||
### Steps:
|
||||
|
||||
1. Go to [ChatGPT → Explore GPTs → Create](https://chat.openai.com/gpts/editor)
|
||||
2. During setup, click **“Add APIs”**
|
||||
3. Set up **API Key authentication** with the auth type as **Bearer** and enter the api key you used to start the MCPO server.
|
||||
4. In the **Schema** section, click on **import from URL** and paste `<cloudflared_url>/openapi.json` with the URL you got from the previous step.
|
||||
5. Add the following line to the top of the imported JSON schema:
|
||||
```
|
||||
"servers": ["url": "<cloudflared_url>"],
|
||||
```
|
||||
**Important**: don't include a trailing slash at the end of the URL!
|
||||
|
||||
ChatGPT will read the schema and create functions automatically.
|
||||
|
||||
---
|
||||
|
||||
## Security Warning — Read Carefully
|
||||
|
||||
Depending on your configuration and enabled tools, Serena's MCP server may:
|
||||
- Execute **arbitrary shell commands**
|
||||
- Read, write, and modify **files in your codebase**
|
||||
|
||||
This gives ChatGPT the same powers as a remote developer on your machine.
|
||||
|
||||
### ⚠️ Key Rules:
|
||||
- **NEVER expose your API key**
|
||||
- **Only expose this server when needed**, and monitor its use.
|
||||
|
||||
In your project’s `.serena/project.yml` or global config, you can disable tools like:
|
||||
|
||||
```yaml
|
||||
excluded_tools:
|
||||
- execute_shell_command
|
||||
- ...
|
||||
read_only: true
|
||||
```
|
||||
|
||||
This is strongly recommended if you want a read-only or safer agent.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
With this setup, ChatGPT becomes a coding assistant **running on your local code** — able to index, search, edit, and even run shell commands depending on your configuration.
|
||||
|
||||
Use responsibly, and keep security in mind.
|
||||
@@ -9,7 +9,7 @@ from pprint import pprint
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.tools import FindFileTool, FindReferencingSymbolsTool, SearchForPatternTool
|
||||
from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, SearchForPatternTool
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False))
|
||||
@@ -18,13 +18,9 @@ if __name__ == "__main__":
|
||||
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
|
||||
find_file_tool = agent.get_tool(FindFileTool)
|
||||
search_pattern_tool = agent.get_tool(SearchForPatternTool)
|
||||
overview_tool = agent.get_tool(GetSymbolsOverviewTool)
|
||||
|
||||
result = agent.execute_task(
|
||||
lambda: search_pattern_tool.apply(
|
||||
r"def request_full_.*?\).*?\)",
|
||||
restrict_search_to_code_files=False,
|
||||
relative_path="src/solidlsp",
|
||||
paths_include_glob="**/ls.py",
|
||||
)
|
||||
lambda: overview_tool.apply("src/solidlsp/ls.py"),
|
||||
)
|
||||
pprint(json.loads(result))
|
||||
@@ -227,6 +227,12 @@ class SerenaAgent:
|
||||
except Exception as e:
|
||||
log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e)
|
||||
|
||||
def get_context(self) -> SerenaAgentContext:
|
||||
return self._context
|
||||
|
||||
def get_tool_description_override(self, tool_name: str) -> str | None:
|
||||
return self._context.tool_description_overrides.get(tool_name, None)
|
||||
|
||||
def _check_shell_settings(self) -> None:
|
||||
# On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces),
|
||||
# which causes all sorts of trouble, preventing language servers from being launched correctly.
|
||||
@@ -583,3 +589,7 @@ class SerenaAgent:
|
||||
if self._gui_log_viewer:
|
||||
log.info("Stopping the GUI log window ...")
|
||||
self._gui_log_viewer.stop()
|
||||
|
||||
def get_tool_by_name(self, tool_name: str) -> Tool:
|
||||
tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
|
||||
return self.get_tool(tool_class)
|
||||
+23
-1
@@ -26,7 +26,7 @@ from serena.constants import (
|
||||
USER_CONTEXT_YAMLS_DIR,
|
||||
USER_MODE_YAMLS_DIR,
|
||||
)
|
||||
from serena.mcp import SerenaMCPFactorySingleProcess
|
||||
from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess
|
||||
from serena.project import Project
|
||||
from serena.tools import ToolRegistry
|
||||
from serena.util.logging import MemoryLogHandler
|
||||
@@ -478,6 +478,28 @@ class ToolCommands(AutoRegisteringGroup):
|
||||
else:
|
||||
ToolRegistry().print_tool_overview()
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
"description",
|
||||
help="Print the description of a tool, optionally with a specific context (the latter may modify the default description).",
|
||||
)
|
||||
@click.argument("tool_name", type=str)
|
||||
@click.option("--context", type=str, default=None, help="Context name or path to context file.")
|
||||
def description(tool_name: str, context: str | None = None) -> None:
|
||||
# Load the context
|
||||
serena_context = None
|
||||
if context:
|
||||
serena_context = SerenaAgentContext.load(context)
|
||||
|
||||
agent = SerenaAgent(
|
||||
project=None,
|
||||
serena_config=SerenaConfig(web_dashboard=False, log_level=logging.INFO),
|
||||
context=serena_context,
|
||||
)
|
||||
tool = agent.get_tool_by_name(tool_name)
|
||||
mcp_tool = SerenaMCPFactory.make_mcp_tool(tool)
|
||||
click.echo(mcp_tool.description)
|
||||
|
||||
|
||||
class PromptCommands(AutoRegisteringGroup):
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -3,8 +3,7 @@ Context and Mode configuration loader
|
||||
"""
|
||||
|
||||
import os
|
||||
from copy import copy
|
||||
from dataclasses import asdict, dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Self
|
||||
@@ -44,17 +43,6 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
|
||||
def _tostring_includes(self) -> list[str]:
|
||||
return ["name"]
|
||||
|
||||
def to_json_dict(self) -> dict[str, str | list[str]]:
|
||||
result = asdict(self)
|
||||
result["excluded_tools"] = list(result["excluded_tools"])
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_json_dict(cls, data: dict) -> Self:
|
||||
data = copy(data)
|
||||
data["excluded_tools"] = set(data["excluded_tools"])
|
||||
return cls(**data)
|
||||
|
||||
def print_overview(self) -> None:
|
||||
"""Print an overview of the mode."""
|
||||
print(f"{self.name}:\n {self.description}")
|
||||
@@ -134,27 +122,21 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
|
||||
name: str
|
||||
prompt: str
|
||||
description: str = ""
|
||||
tool_description_overrides: dict[str, str] = field(default_factory=dict)
|
||||
"""Maps tool names to custom descriptions, default descriptions are extracted from the tool docstrings."""
|
||||
|
||||
def _tostring_includes(self) -> list[str]:
|
||||
return ["name"]
|
||||
|
||||
def to_json_dict(self) -> dict[str, str | list[str]]:
|
||||
result = asdict(self)
|
||||
result["excluded_tools"] = list(result["excluded_tools"])
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_json_dict(cls, data: dict) -> Self:
|
||||
data = copy(data)
|
||||
data["excluded_tools"] = set(data["excluded_tools"])
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, yaml_path: str | Path) -> Self:
|
||||
"""Load a context from a YAML file."""
|
||||
with open(yaml_path, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
name = data.pop("name", Path(yaml_path).stem)
|
||||
# Ensure backwards compatibility for tool_description_overrides
|
||||
if "tool_description_overrides" not in data:
|
||||
data["tool_description_overrides"] = {}
|
||||
return cls(name=name, **data)
|
||||
|
||||
@classmethod
|
||||
|
||||
+10
-2
@@ -68,10 +68,17 @@ class SerenaMCPFactory:
|
||||
|
||||
# Mount the tool description as a combination of the docstring description and
|
||||
# the return value description, if it exists.
|
||||
if docstring.description:
|
||||
func_doc = f"{docstring.description.strip().strip('.')}."
|
||||
overridden_description = tool.agent.get_context().tool_description_overrides.get(func_name, None)
|
||||
|
||||
if overridden_description is not None:
|
||||
func_doc = overridden_description
|
||||
elif docstring.description:
|
||||
func_doc = docstring.description
|
||||
else:
|
||||
func_doc = ""
|
||||
func_doc = func_doc.strip().strip(".")
|
||||
if func_doc:
|
||||
func_doc += "."
|
||||
if docstring.returns and (docstring_returns_descr := docstring.returns.description):
|
||||
# Only add a space before "Returns" if func_doc is not empty
|
||||
prefix = " " if func_doc else ""
|
||||
@@ -113,6 +120,7 @@ class SerenaMCPFactory:
|
||||
for tool in self._iter_tools():
|
||||
mcp_tool = self.make_mcp_tool(tool)
|
||||
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:
|
||||
|
||||
@@ -3,4 +3,6 @@ prompt: |
|
||||
You are running in agent context where the system prompt is provided externally. You should use symbolic
|
||||
tools when possible for code understanding and modification.
|
||||
excluded_tools:
|
||||
- initial_instructions
|
||||
- initial_instructions
|
||||
|
||||
tool_description_overrides: {}
|
||||
@@ -0,0 +1,28 @@
|
||||
description: A configuration specific for chatgpt, which has a limit of 30 tools and requires short descriptions.
|
||||
prompt: |
|
||||
You are running in desktop app context where the tools give you access to the code base as well as some
|
||||
access to the file system, if configured. You interact with the user through a chat interface that is separated
|
||||
from the code base. As a consequence, if you are in interactive mode, your communication with the user should
|
||||
involve high-level thinking and planning as well as some summarization of any code edits that you make.
|
||||
For viewing the code edits the user will view them in a separate code editor window, and the back-and-forth
|
||||
between the chat and the code editor should be minimized as well as facilitated by you.
|
||||
If complex changes have been made, advise the user on how to review them in the code editor.
|
||||
If complex relationships that the user asked for should be visualized or explained, consider creating
|
||||
a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering
|
||||
options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions.
|
||||
excluded_tools: []
|
||||
included_optional_tools:
|
||||
- switch_modes
|
||||
|
||||
tool_description_overrides:
|
||||
find_symbol: |
|
||||
Retrieves symbols matching `name_path` in a file.
|
||||
Use `depth > 0` to include children. `name_path` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar"
|
||||
replace_regex: |
|
||||
Replaces text using regular expressions. Preferred for smaller edits where symbol-level tools aren't appropriate.
|
||||
Use wildcards (.*?) to match large sections efficiently: "beginning.*?end" instead of specifying exact content.
|
||||
Essential for multi-line replacements.
|
||||
search_for_pattern: |
|
||||
Flexible pattern search across codebase. Prefer symbolic operations when possible.
|
||||
Uses DOTALL matching. Use non-greedy quantifiers (.*?) to avoid over-matching.
|
||||
Supports file filtering via globs and code-only restriction.
|
||||
@@ -1,4 +1,11 @@
|
||||
# See Serena's documentation for more details on concept of contexts.
|
||||
description: Description of the context, not used in the code.
|
||||
prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context.
|
||||
excluded_tools: []
|
||||
excluded_tools: []
|
||||
|
||||
# several tools are excluded by default and have to be explicitly included by the user
|
||||
included_optional_tools: []
|
||||
|
||||
# mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method).
|
||||
# Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason.
|
||||
tool_description_overrides: {}
|
||||
@@ -10,4 +10,8 @@ prompt: |
|
||||
If complex relationships that the user asked for should be visualized or explained, consider creating
|
||||
a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering
|
||||
options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions.
|
||||
excluded_tools: []
|
||||
excluded_tools: []
|
||||
included_optional_tools:
|
||||
- switch_modes
|
||||
|
||||
tool_description_overrides: {}
|
||||
@@ -19,10 +19,8 @@ prompt: |
|
||||
excluded_tools:
|
||||
- create_text_file
|
||||
- read_file
|
||||
- delete_lines
|
||||
- replace_lines
|
||||
- insert_at_line
|
||||
- execute_shell_command
|
||||
- prepare_for_new_conversation
|
||||
- summarize_changes
|
||||
- get_current_config
|
||||
|
||||
tool_description_overrides: {}
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
description: Description of the mode, not used in the code.
|
||||
prompt: Prompt that will form part of the message sent to the model when activating this mode.
|
||||
excluded_tools: []
|
||||
|
||||
# several tools are excluded by default and have to be explicitly included by the user
|
||||
included_optional_tools: []
|
||||
@@ -19,15 +19,8 @@ class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit):
|
||||
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
|
||||
) -> str:
|
||||
"""
|
||||
Execute a shell command and return its output.
|
||||
|
||||
IMPORTANT: you should always consider the memory about suggested shell commands before using this tool.
|
||||
If this memory was not loaded in the current conversation, you should load it using the `read_memory` tool
|
||||
before using this tool.
|
||||
|
||||
You should have at least once looked at the suggested shell commands from the corresponding memory
|
||||
created during the onboarding process before using this tool.
|
||||
Never execute unsafe shell commands like `rm -rf /` or similar! Generally be very careful with deletions.
|
||||
Execute a shell command and return its output. If there is a memory about suggested commands, read that first.
|
||||
Never execute unsafe shell commands like `rm -rf /` or similar!
|
||||
|
||||
:param command: the shell command to execute
|
||||
:param cwd: the working directory to execute the command in. If None, the project root will be used.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
|
||||
from serena.config.context_mode import SerenaAgentMode
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
|
||||
|
||||
|
||||
class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
@@ -35,7 +35,7 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
return result_str
|
||||
|
||||
|
||||
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional):
|
||||
"""
|
||||
Removes a project from the Serena configuration.
|
||||
"""
|
||||
@@ -50,7 +50,7 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
return f"Successfully removed project '{project_name}' from configuration."
|
||||
|
||||
|
||||
class SwitchModesTool(Tool):
|
||||
class SwitchModesTool(Tool, ToolMarkerOptional):
|
||||
"""
|
||||
Activates modes by providing a list of their names
|
||||
"""
|
||||
@@ -71,7 +71,7 @@ class SwitchModesTool(Tool):
|
||||
return result_str
|
||||
|
||||
|
||||
class GetCurrentConfigTool(Tool):
|
||||
class GetCurrentConfigTool(Tool, ToolMarkerOptional):
|
||||
"""
|
||||
Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,7 @@ from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
|
||||
from serena.text_utils import search_files
|
||||
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit
|
||||
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional
|
||||
from serena.util.file_system import scan_directory
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ class ReadFileTool(Tool):
|
||||
"""
|
||||
Reads the given file or a chunk of it. Generally, symbolic operations
|
||||
like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.
|
||||
Reading the entire file is only recommended if there is no other way to get the content required for the task.
|
||||
|
||||
:param relative_path: the relative path to the file to read
|
||||
:param start_line: the 0-based index of the first line to be retrieved.
|
||||
@@ -60,14 +59,7 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
|
||||
def apply(self, relative_path: str, content: str) -> str:
|
||||
"""
|
||||
Write a new file (or overwrite an existing file). For existing files, it is strongly recommended
|
||||
to use symbolic operations like replace_symbol_body or insert_after_symbol/insert_before_symbol, if possible.
|
||||
You can also use insert_at_line to insert content at a specific line for existing files if the symbolic operations
|
||||
are not the right choice for what you want to do.
|
||||
|
||||
If ever used on an existing file, the content has to be the complete content of that file (so it
|
||||
may never end with something like "The remaining content of the file is left unchanged.").
|
||||
For operations that just replace a part of a file, use the replace_lines or the symbolic editing tools instead.
|
||||
Write a new file or overwrite an existing file.
|
||||
|
||||
:param relative_path: the relative path to the file to create
|
||||
:param content: the (utf-8-encoded) content to write to the file
|
||||
@@ -200,7 +192,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit):
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class DeleteLinesTool(Tool, ToolMarkerCanEdit):
|
||||
class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
"""
|
||||
Deletes a range of lines within a file.
|
||||
"""
|
||||
@@ -228,7 +220,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit):
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class ReplaceLinesTool(Tool, ToolMarkerCanEdit):
|
||||
class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
"""
|
||||
Replaces a range of lines within a file with new content.
|
||||
"""
|
||||
@@ -259,7 +251,7 @@ class ReplaceLinesTool(Tool, ToolMarkerCanEdit):
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertAtLineTool(Tool, ToolMarkerCanEdit):
|
||||
class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
"""
|
||||
Inserts content at a given line in a file.
|
||||
"""
|
||||
@@ -351,7 +343,7 @@ class SearchForPatternTool(Tool):
|
||||
For example, for finding classes or methods from a name pattern.
|
||||
Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files,
|
||||
which is why it is the default.
|
||||
:return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested).
|
||||
:return: A mapping of file paths to lists of matched consecutive lines.
|
||||
"""
|
||||
abs_path = os.path.join(self.get_project_root(), relative_path)
|
||||
if not os.path.exists(abs_path):
|
||||
|
||||
@@ -10,15 +10,8 @@ class WriteMemoryTool(Tool):
|
||||
|
||||
def apply(self, memory_name: str, content: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
|
||||
"""
|
||||
Write some information about this project that can be useful for future tasks to a memory.
|
||||
Use markdown formatting for the content.
|
||||
The information should be short and to the point.
|
||||
The memory name should be meaningful, such that from the name you can infer what the information is about.
|
||||
It is better to have multiple small memories than to have a single large one because
|
||||
memories will be read one by one and we only ever want to read relevant memories.
|
||||
|
||||
This tool is either called during the onboarding process or when you have identified
|
||||
something worth remembering about the project from the past conversation.
|
||||
Write some information about this project that can be useful for future tasks to a memory in md format.
|
||||
The memory name should be meaningful.
|
||||
"""
|
||||
if len(content) > max_answer_chars:
|
||||
raise ValueError(
|
||||
|
||||
@@ -4,6 +4,7 @@ Language server-related tools
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
from copy import copy
|
||||
from typing import Any
|
||||
@@ -33,10 +34,7 @@ class RestartLanguageServerTool(Tool):
|
||||
|
||||
def apply(self) -> str:
|
||||
"""Use this tool only on explicit user request or after confirmation.
|
||||
It may be necessary to restart the language server if the user performs edits
|
||||
not through Serena, so the language server state becomes outdated and further editing attempts lead to errors.
|
||||
|
||||
If such editing errors happen, you should suggest using this tool.
|
||||
It may be necessary to restart the language server if it hangs.
|
||||
"""
|
||||
self.agent.reset_language_server()
|
||||
return SUCCESS_RESULT
|
||||
@@ -49,24 +47,28 @@ class GetSymbolsOverviewTool(Tool):
|
||||
|
||||
def apply(self, relative_path: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
|
||||
"""
|
||||
Gets an overview of the given file or directory.
|
||||
For each analyzed file, we list the top-level symbols in the file (name_path, kind).
|
||||
Use this tool to get a high-level understanding of the code symbols.
|
||||
Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols.
|
||||
Before requesting a symbol overview, it is usually a good idea to narrow down the scope of the overview
|
||||
by first understanding the basic directory structure of the repository that you can get from memories
|
||||
or by using the `list_dir` and `find_file` tools (or similar).
|
||||
Use this tool to get a high-level understanding of the code symbols in a file.
|
||||
This should be the first tool to call when you want to understand a new file, unless you already know
|
||||
what you are looking for.
|
||||
|
||||
:param relative_path: the relative path to the file or directory to get the overview of
|
||||
:param max_answer_chars: if the overview is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task. If the overview is too long, you should use a smaller directory instead,
|
||||
(e.g. a subdirectory).
|
||||
:return: a JSON object mapping relative paths of all contained files to info about top-level symbols in the file (name_path, kind).
|
||||
:return: a JSON object containing info about top-level symbols in the file
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
result = symbol_retriever.get_symbol_overview(relative_path)
|
||||
result_json_str = json.dumps({k: [dataclasses.asdict(i) for i in l] for k, l in result.items()})
|
||||
file_path = os.path.join(self.project.project_root, relative_path)
|
||||
|
||||
# The symbol overview is capable of working with both files and directories,
|
||||
# but we want to ensure that the user provides a file path.
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"File or directory {relative_path} does not exist in the project.")
|
||||
if os.path.isdir(file_path):
|
||||
raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ")
|
||||
result = symbol_retriever.get_symbol_overview(relative_path)[relative_path]
|
||||
result_json_str = json.dumps([dataclasses.asdict(i) for i in result])
|
||||
return self._limit_length(result_json_str, max_answer_chars)
|
||||
|
||||
|
||||
@@ -132,7 +134,7 @@ class FindSymbolTool(Tool):
|
||||
:param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`.
|
||||
:param substring_matching: If True, use substring matching for the last segment of `name`.
|
||||
:param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned.
|
||||
:return: JSON string: a list of symbols (with locations) matching the name.
|
||||
:return: a list of symbols (with locations) matching the name.
|
||||
"""
|
||||
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
|
||||
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
|
||||
@@ -164,10 +166,8 @@ class FindReferencingSymbolsTool(Tool):
|
||||
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
|
||||
) -> str:
|
||||
"""
|
||||
Finds symbols that reference the symbol at the given `name_path`. The result will contain metadata about the referencing symbols
|
||||
as well as a short code snippet around the reference (unless `include_body` is True, then the short snippet will be omitted).
|
||||
Note that among other kinds of references, this function can be used to find (direct) subclasses of a class,
|
||||
as subclasses are referencing symbols that have the kind class.
|
||||
Finds references to the symbol at the given `name_path`. The result will contain metadata about the referencing symbols
|
||||
as well as a short code snippet around the reference.
|
||||
|
||||
:param name_path: for finding the symbol to find references for, same logic as in the `find_symbol` tool.
|
||||
:param relative_path: the relative path to the file containing the symbol for which to find references.
|
||||
@@ -269,9 +269,9 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit):
|
||||
body: str,
|
||||
) -> str:
|
||||
"""
|
||||
Inserts the given body/content before the beginning of the definition of the given symbol (via the symbol's location).
|
||||
A typical use case is to insert a new class, function, method, field or variable assignment.
|
||||
It also can be used to insert a new import statement before the first symbol in the file.
|
||||
Inserts the given content before the beginning of the definition of the given symbol (via the symbol's location).
|
||||
A typical use case is to insert a new class, function, method, field or variable assignment; or
|
||||
a new import statement before the first symbol in the file.
|
||||
|
||||
:param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply)
|
||||
:param relative_path: the relative path to the file containing the symbol
|
||||
|
||||
@@ -96,7 +96,7 @@ class ThinkAboutWhetherYouAreDoneTool(Tool):
|
||||
return self.prompt_factory.create_think_about_whether_you_are_done()
|
||||
|
||||
|
||||
class SummarizeChangesTool(Tool):
|
||||
class SummarizeChangesTool(Tool, ToolMarkerOptional):
|
||||
"""
|
||||
Provides instructions for summarizing the changes made to the codebase.
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
|
||||
+14
-9
@@ -4,16 +4,28 @@ import pytest
|
||||
from mcp.server.fastmcp.tools.base import Tool as MCPTool
|
||||
|
||||
from serena.agent import Tool, ToolRegistry
|
||||
from serena.config.context_mode import SerenaAgentContext
|
||||
from serena.mcp import SerenaMCPFactory
|
||||
|
||||
make_tool = SerenaMCPFactory.make_mcp_tool
|
||||
|
||||
|
||||
# Create a mock agent for tool initialization
|
||||
class MockAgent:
|
||||
def __init__(self):
|
||||
self.project_config = None
|
||||
self.serena_config = None
|
||||
|
||||
@staticmethod
|
||||
def get_context() -> SerenaAgentContext:
|
||||
return SerenaAgentContext.load_default()
|
||||
|
||||
|
||||
class BaseMockTool(Tool):
|
||||
"""A mock Tool class for testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
def __init__(self):
|
||||
super().__init__(MockAgent())
|
||||
|
||||
|
||||
class BasicTool(BaseMockTool):
|
||||
@@ -274,13 +286,6 @@ def is_test_mock_class(tool_class: type) -> bool:
|
||||
@pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes())
|
||||
def test_make_tool_all_tools(tool_class) -> None:
|
||||
"""Test that make_tool works for all tools in the codebase."""
|
||||
|
||||
# Create a mock agent for tool initialization
|
||||
class MockAgent:
|
||||
def __init__(self):
|
||||
self.project_config = None
|
||||
self.serena_config = None
|
||||
|
||||
# Create an instance of the tool
|
||||
tool_instance = tool_class(MockAgent())
|
||||
|
||||
|
||||
Reference in new issue
Block a user