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()