From b29942269955782c4a95c9f98c314bfc28325fda Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" Date: Fri, 22 Aug 2025 01:30:02 +0200 Subject: [PATCH] Fix OpenAI tool compatibility (#509) * Fix OpenAI tool compatibility; add agent mode for OpenAI compatible servers like Llama.cpp; add testcase to verify completeness --- README.md | 2 + src/serena/mcp.py | 13 ++++++- .../config/contexts/oaicompat-agent.yml | 8 ++++ test/serena/test_mcp.py | 2 +- test/serena/test_tool_parameter_types.py | 39 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/serena/resources/config/contexts/oaicompat-agent.yml create mode 100644 test/serena/test_tool_parameter_types.py diff --git a/README.md b/README.md index 071198b2..0bf607e6 100644 --- a/README.md +++ b/README.md @@ -552,6 +552,8 @@ Choose the context that best matches the type of integration you are using. When launching Serena, specify the context using `--context `. Note that for cases where parameter lists are specified (e.g. Claude Desktop), you must add two parameters to the list. +If you are using a local server (such as Llama.cpp) which requires you to use OpenAI-compatible tool descriptions, use context `oaicompat-agent` instead of `agent`. + #### Modes Modes further refine Serena's behavior for specific types of tasks or interaction styles. Multiple modes can be active simultaneously, allowing you to combine their effects. Modes influence the system prompt and can also alter the set of available tools by excluding certain ones. diff --git a/src/serena/mcp.py b/src/serena/mcp.py index fa86a334..f8f75ae0 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -38,7 +38,7 @@ def configure_logging(*args, **kwargs) -> None: # type: ignore # patch the logging configuration function in fastmcp, because it's hard-coded and broken -server.configure_logging = configure_logging +server.configure_logging = configure_logging # type: ignore @dataclass @@ -106,6 +106,15 @@ class SerenaMCPFactory: # ---- simplify anyOf/oneOf if they only differ by integer/number ---- for key in ("oneOf", "anyOf"): if key in node and isinstance(node[key], list): + # Special case: anyOf or oneOf with "type X" and "null" + if len(node[key]) == 2: + types = [sub.get("type") for sub in node[key]] + if "null" in types: + non_null_type = next(t for t in types if t != "null") + if isinstance(non_null_type, str): + node["type"] = non_null_type + node.pop(key, None) + continue simplified = [] changed = False for sub in node[key]: @@ -333,7 +342,7 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory): @asynccontextmanager async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: - openai_tool_compatible = self.context.name in ["chatgpt", "codex"] + 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 diff --git a/src/serena/resources/config/contexts/oaicompat-agent.yml b/src/serena/resources/config/contexts/oaicompat-agent.yml new file mode 100644 index 00000000..cdec1eed --- /dev/null +++ b/src/serena/resources/config/contexts/oaicompat-agent.yml @@ -0,0 +1,8 @@ +description: All tools except InitialInstructionsTool for agent context, uses OpenAI compatible tool definitions +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 + +tool_description_overrides: {} \ No newline at end of file diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index 731a1cc6..58508ba6 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -25,7 +25,7 @@ class BaseMockTool(Tool): """A mock Tool class for testing.""" def __init__(self): - super().__init__(MockAgent()) + super().__init__(MockAgent()) # type: ignore class BasicTool(BaseMockTool): diff --git a/test/serena/test_tool_parameter_types.py b/test/serena/test_tool_parameter_types.py new file mode 100644 index 00000000..ec714be4 --- /dev/null +++ b/test/serena/test_tool_parameter_types.py @@ -0,0 +1,39 @@ +import logging + +import pytest + +from serena.config.serena_config import SerenaConfig +from serena.mcp import SerenaMCPFactorySingleProcess +from serena.tools.tools_base import ToolRegistry + + +@pytest.mark.parametrize("context", ("chatgpt", "codex", "oaicompat-agent")) +def test_all_tool_parameters_have_type(context): + """ + For every tool exposed by Serena, ensure that the generated + Open‑AI schema contains a ``type`` entry for each parameter. + """ + 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) + # Initialize the agent so that the tools are available + factory._instantiate_agent(cfg, []) + tools = list(factory._iter_tools()) + + for tool in tools: + mcp_tool = factory.make_mcp_tool(tool, openai_tool_compatible=True) + params = mcp_tool.parameters + + # Collect any parameter that lacks a type + issues = [] + print(f"Checking tool {tool}") + + if "properties" not in params: + issues.append(f"Tool {tool.get_name()!r} missing properties section") + else: + for pname, prop in params["properties"].items(): + if "type" not in prop: + issues.append(f"Tool {tool.get_name()!r} parameter {pname!r} missing 'type'") + if issues: + raise AssertionError("\n".join(issues))