mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 22:14:54 +00:00
Merge pull request #1605 from oraios/structured-output
Allow to configure structured tool output on a per-context basis Resolves #1042
This commit is contained in:
6 files changed
+37
-13
No files matched your search
@@ -22,6 +22,8 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Make tool call errors surface explicitly as errors at the MCP protocol level
|
||||
- Fix: a registered project whose root directory was deleted while Serena was already running could break
|
||||
`activate_project`/project lookup, raising `FileNotFoundError` in `RegisteredProject.matches_root_path`
|
||||
- Allow structured tool output to be configured on a per-context basis, disabling it for Claude Code
|
||||
(which does not correctly unpack structured output) #1042
|
||||
|
||||
* Language Servers:
|
||||
- C/C++ (clangd): add Unreal Engine 5 fixture and tests verifying that reflection-macro
|
||||
|
||||
@@ -184,6 +184,11 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
|
||||
The `activate_project` tool will, therefore, be disabled in this case, as project switching is not allowed.
|
||||
"""
|
||||
|
||||
structured_tool_output: bool | None = None
|
||||
"""
|
||||
whether to use structured output for tools (None = auto)
|
||||
"""
|
||||
|
||||
def _tostring_includes(self) -> list[str]:
|
||||
return ["name"]
|
||||
|
||||
|
||||
+18
-8
@@ -51,15 +51,16 @@ class SerenaMCPRequestContext:
|
||||
|
||||
|
||||
class SerenaFastMCPTool(FastMCPTool):
|
||||
def __init__(self, tool: Tool, openai_tool_compatible: bool):
|
||||
def __init__(self, tool: Tool, openai_tool_compatible: bool, structured_output: bool | None):
|
||||
"""
|
||||
:param tool: the Serena tool
|
||||
:param openai_tool_compatible: whether to process the tool schema to be compatible with OpenAI tools
|
||||
(doesn't accept integer, needs number instead, etc.). This allows using Serena MCP within Codex.
|
||||
:param structured_output: whether to use structured output for the tool (None = auto)
|
||||
"""
|
||||
func_name = tool.get_name()
|
||||
func_doc = tool.get_apply_docstring() or ""
|
||||
func_arg_metadata = tool.get_apply_fn_metadata()
|
||||
func_arg_metadata = tool.get_apply_fn_metadata(structured_output=structured_output)
|
||||
is_async = False
|
||||
parameters = func_arg_metadata.arg_model.model_json_schema()
|
||||
if openai_tool_compatible:
|
||||
@@ -275,27 +276,34 @@ class SerenaMCPFactory:
|
||||
return walk(s)
|
||||
|
||||
@staticmethod
|
||||
def make_mcp_tool(tool: Tool, openai_tool_compatible: bool = True) -> SerenaFastMCPTool:
|
||||
def make_mcp_tool(tool: Tool, openai_tool_compatible: bool = True, structured_output: bool | None = None) -> SerenaFastMCPTool:
|
||||
"""
|
||||
Creates an MCP tool from a Serena Tool instance.
|
||||
|
||||
:param tool: the Serena Tool instance to convert.
|
||||
:param openai_tool_compatible: whether to process the tool schema to be compatible with OpenAI tools
|
||||
(doesn't accept integer, needs number instead, etc.). This allows using Serena MCP within codex.
|
||||
:param structured_output: whether to use structured output for the tool (None = auto)
|
||||
"""
|
||||
return SerenaFastMCPTool(tool, openai_tool_compatible)
|
||||
return SerenaFastMCPTool(tool, openai_tool_compatible=openai_tool_compatible, structured_output=structured_output)
|
||||
|
||||
def _iter_tools(self) -> Iterator[Tool]:
|
||||
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:
|
||||
"""Update the tools in the MCP server"""
|
||||
def _set_mcp_tools(self, mcp: FastMCP, openai_tool_compatible: bool, structured_output: bool | None) -> None:
|
||||
"""
|
||||
Update the tools in the MCP server
|
||||
|
||||
:param mcp: The MCP server to update
|
||||
:param openai_tool_compatible: whether to process the tool schema to be compatible with OpenAI tools
|
||||
:param structured_output: whether to use structured output for the tools (None = auto)
|
||||
"""
|
||||
if mcp is not None:
|
||||
mcp._tool_manager._tools = {}
|
||||
for tool in self._iter_tools():
|
||||
mcp_tool = self.make_mcp_tool(tool, openai_tool_compatible=openai_tool_compatible)
|
||||
mcp_tool = self.make_mcp_tool(tool, openai_tool_compatible=openai_tool_compatible, structured_output=structured_output)
|
||||
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())}")
|
||||
|
||||
@@ -389,7 +397,9 @@ class SerenaMCPFactory:
|
||||
:param mcp_server: the MCP server instance to configure
|
||||
"""
|
||||
openai_tool_compatible = self.context.name in ["chatgpt", "codex", "oaicompat-agent"]
|
||||
self._set_mcp_tools(mcp_server, openai_tool_compatible=openai_tool_compatible)
|
||||
assert self.agent is not None
|
||||
context = self.agent.get_context()
|
||||
self._set_mcp_tools(mcp_server, openai_tool_compatible=openai_tool_compatible, structured_output=context.structured_tool_output)
|
||||
log.info("MCP server lifetime setup complete")
|
||||
try:
|
||||
yield
|
||||
|
||||
@@ -50,3 +50,7 @@ tool_description_overrides: {}
|
||||
# Tools explicitly disabled by the project will not be available at all.
|
||||
# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed.
|
||||
single_project: true
|
||||
|
||||
# Claude Code has a bug (it does not unpack structured tool output, causing a lot of unnecessary escaping
|
||||
# owing to the wrapped structure), so we disable it here.
|
||||
structured_tool_output: false
|
||||
@@ -15,4 +15,7 @@ tool_description_overrides: {}
|
||||
# If set to true and 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.
|
||||
# The `activate_project` tool will, therefore, be disabled in this case, as project switching is not allowed.
|
||||
single_project: false
|
||||
single_project: false
|
||||
|
||||
# whether to make MCP tools return structured output (null = auto-detect, true = always, false = never)
|
||||
structured_tool_output: null
|
||||
@@ -246,12 +246,12 @@ class Tool(Component):
|
||||
"""Gets the docstring for the tool application, used by the MCP server."""
|
||||
return self.get_apply_docstring_from_cls()
|
||||
|
||||
def get_apply_fn_metadata(self) -> FuncMetadata:
|
||||
def get_apply_fn_metadata(self, structured_output: bool | None = None) -> FuncMetadata:
|
||||
"""Gets the metadata for the tool application function, used by the MCP server."""
|
||||
return self.get_apply_fn_metadata_from_cls()
|
||||
return self.get_apply_fn_metadata_from_cls(structured_output=structured_output)
|
||||
|
||||
@classmethod
|
||||
def get_apply_fn_metadata_from_cls(cls) -> FuncMetadata:
|
||||
def get_apply_fn_metadata_from_cls(cls, structured_output: bool | None = None) -> FuncMetadata:
|
||||
"""Get the metadata for the apply method from the class (static metadata).
|
||||
Needed for creating MCP tools in a separate process without running into serialization issues.
|
||||
"""
|
||||
@@ -264,7 +264,7 @@ class Tool(Component):
|
||||
if apply_fn is None:
|
||||
raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?")
|
||||
|
||||
return func_metadata(apply_fn, skip_names=["self", "cls", cls.SESSION_ID_PARAM_NAME])
|
||||
return func_metadata(apply_fn, skip_names=["self", "cls", cls.SESSION_ID_PARAM_NAME], structured_output=structured_output)
|
||||
|
||||
def _log_tool_application(self, frame: Any, session_id: str) -> None:
|
||||
params = {}
|
||||
|
||||
Reference in new issue
Block a user