Merge pull request #1570 from oraios/mcp-tool-arg-rename

Support aliases of tool parameters, adding class SerenaFastMCPTool
This commit is contained in:
Dr. Dominik Jain authored and GitHub committed 2026-06-12 19:18:00 +02:00
commit e776e7988a
5 files changed
+113 -69

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ Status of the `main` branch. Changes prior to the next official version change w
- Fix: JetBrains mode prompt was not provided to agents; The mode is now treated as a (background) base mode
in `ActiveModes` which reduces the surface for issues pertaining to custom handling of modes.
- Improve quoting/escaping of arguments in shell executions on Windows (via `oslex` dependency)
- Add tool parameter alias support, adding `name_path` as an alias for `name_path_pattern` in `find_symbol` tools
* Language Servers:
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
+96 -69
View File
@@ -11,8 +11,10 @@ from typing import Any, Literal, cast
import docstring_parser
from mcp.server.fastmcp import server
from mcp.server.fastmcp.server import FastMCP, Settings
from mcp.server.fastmcp.tools.base import Tool as MCPTool
from mcp.server.fastmcp.server import Context, FastMCP, Settings
from mcp.server.fastmcp.tools.base import Tool as FastMCPTool
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
from mcp.types import ToolAnnotations
from pydantic_settings import SettingsConfigDict
from sensai.util import logging
@@ -47,6 +49,94 @@ class SerenaMCPRequestContext:
agent: SerenaAgent
class SerenaFastMCPTool(FastMCPTool):
def __init__(self, tool: Tool, openai_tool_compatible: bool):
"""
: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.
"""
func_name = tool.get_name()
func_doc = tool.get_apply_docstring() or ""
func_arg_metadata = tool.get_apply_fn_metadata()
is_async = False
parameters = func_arg_metadata.arg_model.model_json_schema()
if openai_tool_compatible:
parameters = SerenaMCPFactory._sanitize_for_openai_tools(parameters)
docstring = docstring_parser.parse(func_doc)
# Mount the tool description as a combination of the docstring description and
# the return value description, if it exists.
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 ""
func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}."
# Parse the parameter descriptions from the docstring and add pass its description
# to the parameter schema.
docstring_params = {param.arg_name: param for param in docstring.params}
parameters_properties: dict[str, dict[str, Any]] = parameters["properties"]
for parameter, properties in parameters_properties.items():
if (param_doc := docstring_params.get(parameter)) and param_doc.description:
param_desc = f"{param_doc.description.strip().strip('.') + '.'}"
properties["description"] = param_desc[0].upper() + param_desc[1:]
def execute_fn(**kwargs) -> str: # type: ignore
return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs)
# Generate human-readable title from snake_case tool name
tool_title = " ".join(word.capitalize() for word in func_name.split("_"))
# Create annotations with appropriate hints based on tool capabilities
can_edit = tool.can_edit()
annotations = ToolAnnotations(
title=tool_title,
readOnlyHint=not can_edit,
destructiveHint=can_edit,
)
super().__init__(
fn=execute_fn,
name=func_name,
description=func_doc,
parameters=parameters,
fn_metadata=func_arg_metadata,
is_async=is_async,
# keep the value in sync with the kwarg name in Tool.apply_ex. The mcp sdk uses reflection to infer this
# when the tool is constructed via from_function (which is a bit crazy IMO, but well...)
context_kwarg="mcp_ctx",
annotations=annotations,
title=tool_title,
)
self._param_aliases = tool.get_param_aliases()
async def run(
self,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
convert_result: bool = False,
) -> Any:
# apply parameter aliases
for param_alias, param_name in self._param_aliases.items():
if param_alias in arguments and param_name not in arguments:
arguments[param_name] = arguments.pop(param_alias)
return await super().run(arguments, context, convert_result)
class SerenaMCPFactory:
"""
Factory for the creation of the Serena MCP server with an associated SerenaAgent.
@@ -181,78 +271,15 @@ class SerenaMCPFactory:
return walk(s)
@staticmethod
def make_mcp_tool(tool: Tool, openai_tool_compatible: bool = True) -> MCPTool:
def make_mcp_tool(tool: Tool, openai_tool_compatible: bool = True) -> SerenaFastMCPTool:
"""
Create an MCP tool from a Serena Tool instance.
Creates an MCP tool from a Serena Tool instance.
:param tool: The Serena Tool instance to convert.
: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.
"""
func_name = tool.get_name()
func_doc = tool.get_apply_docstring() or ""
func_arg_metadata = tool.get_apply_fn_metadata()
is_async = False
parameters = func_arg_metadata.arg_model.model_json_schema()
if openai_tool_compatible:
parameters = SerenaMCPFactory._sanitize_for_openai_tools(parameters)
docstring = docstring_parser.parse(func_doc)
# Mount the tool description as a combination of the docstring description and
# the return value description, if it exists.
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 ""
func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}."
# Parse the parameter descriptions from the docstring and add pass its description
# to the parameter schema.
docstring_params = {param.arg_name: param for param in docstring.params}
parameters_properties: dict[str, dict[str, Any]] = parameters["properties"]
for parameter, properties in parameters_properties.items():
if (param_doc := docstring_params.get(parameter)) and param_doc.description:
param_desc = f"{param_doc.description.strip().strip('.') + '.'}"
properties["description"] = param_desc[0].upper() + param_desc[1:]
def execute_fn(**kwargs) -> str: # type: ignore
return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs)
# Generate human-readable title from snake_case tool name
tool_title = " ".join(word.capitalize() for word in func_name.split("_"))
# Create annotations with appropriate hints based on tool capabilities
can_edit = tool.can_edit()
annotations = ToolAnnotations(
title=tool_title,
readOnlyHint=not can_edit,
destructiveHint=can_edit,
)
return MCPTool(
fn=execute_fn,
name=func_name,
description=func_doc,
parameters=parameters,
fn_metadata=func_arg_metadata,
is_async=is_async,
# keep the value in sync with the kwarg name in Tool.apply_ex. The mcp sdk uses reflection to infer this
# when the tool is constructed via from_function (which is a bit crazy IMO, but well...)
context_kwarg="mcp_ctx",
annotations=annotations,
title=tool_title,
)
return SerenaFastMCPTool(tool, openai_tool_compatible)
def _iter_tools(self) -> Iterator[Tool]:
assert self.agent is not None
+4
View File
@@ -122,6 +122,10 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
result = self._to_json(grouped_symbols)
return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_shortened_result])
@classmethod
def get_param_aliases(cls) -> dict[str, str]:
return {"name_path": "name_path_pattern"}
class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta):
"""
+4
View File
@@ -234,6 +234,10 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
result = self._to_json(grouped_symbol_dicts)
return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_short_result_relative_path_to_name_paths])
@classmethod
def get_param_aliases(cls) -> dict[str, str]:
return {"name_path": "name_path_pattern"}
class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
"""
+8
View File
@@ -306,6 +306,14 @@ class Tool(Component):
def is_symbolic(self) -> bool:
return issubclass(self.__class__, ToolMarkerSymbolicRead) or issubclass(self.__class__, ToolMarkerSymbolicEdit)
@classmethod
def get_param_aliases(cls) -> dict[str, str]:
"""
:return: a mapping of parameter aliases for the apply method, where the key is the alias and the value is the actual parameter name.
This can be used to define alternative parameter names for the same parameter.
"""
return {}
def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, mcp_ctx: Context | None = None, **kwargs) -> str: # type: ignore
"""
Applies the tool with logging and exception handling, using the given keyword arguments