diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 0165d1c4..fdc55656 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -56,6 +56,7 @@ class ResponseConfigOverview(BaseModel): jetbrains_mode: bool languages: list[str] encoding: str | None + current_client: str | None class ResponseAvailableLanguages(BaseModel): @@ -370,6 +371,7 @@ class SerenaDashboardAPI: def _get_config_overview(self) -> ResponseConfigOverview: from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode + from serena.tools.tools_base import Tool # Get active project info project = self._agent.get_active_project() @@ -491,6 +493,7 @@ class SerenaDashboardAPI: jetbrains_mode=self._agent.serena_config.language_backend == LanguageBackend.JETBRAINS, languages=languages, encoding=encoding, + current_client=Tool.get_last_tool_call_client_str(), ) def _shutdown(self) -> None: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index b6f70ce4..2e885cf9 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -239,7 +239,9 @@ class SerenaMCPFactory: parameters=parameters, fn_metadata=func_arg_metadata, is_async=is_async, - context_kwarg=None, + # 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, ) diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index b7cd3c56..934fd8b9 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -619,6 +619,10 @@ class Dashboard { html += '
File Encoding:
'; html += '
' + (config.encoding || 'N/A') + '
'; + // Current Client info + html += '
Current Client:
'; + html += '
' + (config.current_client || 'None') + '
'; + html += ''; // Active tools - collapsible diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 16f3f517..de4f0c39 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -4,8 +4,10 @@ from abc import ABC from collections.abc import Iterable from dataclasses import dataclass from types import TracebackType -from typing import TYPE_CHECKING, Any, Protocol, Self, TypeVar +from typing import TYPE_CHECKING, Any, Protocol, Self, TypeVar, cast +from mcp import Implementation +from mcp.server.fastmcp import Context from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from sensai.util import logging from sensai.util.string import dict_string @@ -115,6 +117,17 @@ class Tool(Component): # (which is use by the LLM, so a good description is important) # and to validate the tool call arguments. + _last_tool_call_client_str: str | None = None + """We can only get the client info from within a tool call. Each tool call will update this variable.""" + + @classmethod + def set_last_tool_call_client_str(cls, client_str: str | None) -> None: + cls._last_tool_call_client_str = client_str + + @classmethod + def get_last_tool_call_client_str(cls) -> str | None: + return cls._last_tool_call_client_str + @classmethod def get_name_from_cls(cls) -> str: name = cls.__name__ @@ -219,10 +232,21 @@ class Tool(Component): def is_active(self) -> bool: return self.agent.tool_is_active(self.__class__) - def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs) -> str: # type: ignore + 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 """ + if mcp_ctx is not None: + try: + client_params = mcp_ctx.session.client_params + if client_params is not None: + client_info = cast(Implementation, client_params.clientInfo) + client_str = client_info.title if client_info.title else client_info.name + " " + client_info.version + if client_str != self.get_last_tool_call_client_str(): + log.debug(f"Updating client info: {client_info}") + self.set_last_tool_call_client_str(client_str) + except BaseException as e: + log.info(f"Failed to get client info: {e}.") def task() -> str: apply_fn = self.get_apply_fn()