error formatter
frdel committed
Jun 5, 2025 at 20:05 UTC
bc88d882d7aa16ead11569de778943fd195f77e7
2 files changed
+47
-35
python/helpers/errors.py
+4
-2
@@ -14,7 +14,8 @@ def error_text(e: Exception):
14
15
16
def format_error(e: Exception, start_entries=6, end_entries=4):
17
- traceback_text = traceback.format_exc()
17
+ # format traceback from the provided exception instead of the most recent one
18
+ traceback_text = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
19
# Split the traceback into lines
20
lines = traceback_text.split("\n")
21
@@ -44,7 +45,8 @@ def format_error(e: Exception, start_entries=6, end_entries=4):
45
# Find the error message at the end
46
error_message = ""
47
for line in reversed(lines):
47
- if re.match(r"\w+Error:", line):
48
+ # match both simple errors and module.path.Error patterns
49
+ if re.match(r"[\w\.]+Error:", line):
50
error_message = line
51
break
52
python/helpers/mcp_handler.py
+43
-33
@@ -168,26 +168,26 @@ class MCPTool(Tool):
168
user_message_text[:max_user_context_len] + "... (truncated)"
169
)
170
171
-# commented out for now, output should be unified between tools and MCPs
171
+ # commented out for now, output should be unified between tools and MCPs
172
173
-# contextual_block = f"""
174
-# \n--- End of Results for MCP Tool: {self.name} ---
173
+ # contextual_block = f"""
174
+ # \n--- End of Results for MCP Tool: {self.name} ---
175
176
-# **Original Tool Call Details:**
177
-# * **Tool:** `{self.name}`
178
-# * **Arguments Given:**
179
-# ```json
180
-# {json.dumps(self.args, indent=2)}
181
-# ```
176
+ # **Original Tool Call Details:**
177
+ # * **Tool:** `{self.name}`
178
+ # * **Arguments Given:**
179
+ # ```json
180
+ # {json.dumps(self.args, indent=2)}
181
+ # ```
182
183
-# **Related User Request Context:**
184
-# {user_message_text}
183
+ # **Related User Request Context:**
184
+ # {user_message_text}
185
186
-# **Next Steps Reminder for {self.name}:**
187
-# If this action is part of an ongoing sequence, consider the next step with this tool or another appropriate tool. If the sequence is complete or this was a one-off action, analyze the final output and report to the user or proceed with the overall plan.
188
-# """
186
+ # **Next Steps Reminder for {self.name}:**
187
+ # If this action is part of an ongoing sequence, consider the next step with this tool or another appropriate tool. If the sequence is complete or this was a one-off action, analyze the final output and report to the user or proceed with the overall plan.
188
+ # """
189
190
-# final_text_for_agent = raw_tool_response + contextual_block
190
+ # final_text_for_agent = raw_tool_response + contextual_block
191
192
final_text_for_agent = raw_tool_response
193
@@ -270,8 +270,8 @@ class MCPServerRemote(BaseModel):
270
if key == "name":
271
value = normalize_name(value)
272
if key == "serverUrl":
273
- key = "url" # remap serverUrl to url
274
-
273
+ key = "url" # remap serverUrl to url
274
+
275
setattr(self, key, value)
276
# We already run in an event loop, dont believe Pylance
277
return asyncio.run(self.__on_update())
@@ -306,7 +306,7 @@ class MCPServerLocal(BaseModel):
306
def get_error(self) -> str:
307
with self.__lock:
308
return self.__client.error # type: ignore
309
-
309
+
310
def get_log(self) -> str:
311
with self.__lock:
312
return self.__client.get_log() # type: ignore
@@ -612,7 +612,7 @@ class MCPConfig(BaseModel):
612
with self.__lock:
613
for server in self.servers:
614
if server.name == server_name:
615
- return server.get_log() # type: ignore
615
+ return server.get_log() # type: ignore
616
return ""
617
618
def get_servers_status(self) -> list[dict[str, Any]]:
@@ -626,7 +626,7 @@ class MCPConfig(BaseModel):
626
# get tool count
627
tool_count = len(server.get_tools())
628
# check if server is connected
629
- connected = True # tool_count > 0
629
+ connected = True # tool_count > 0
630
# get error message if any
631
error = server.get_error()
632
# get log bool
@@ -717,7 +717,9 @@ class MCPConfig(BaseModel):
717
)
718
719
tool_args = ""
720
- input_schema = json.dumps(tool["input_schema"]) if tool["input_schema"] else ""
720
+ input_schema = (
721
+ json.dumps(tool["input_schema"]) if tool["input_schema"] else ""
722
+ )
723
# properties: dict[str, Any] = tool["input_schema"]["properties"]
724
# for key, value in properties.items():
725
# optional = False
@@ -847,7 +849,9 @@ class MCPClientBase(ABC):
849
ClientSession(
850
stdio, # type: ignore
851
write, # type: ignore
850
- read_timeout_seconds=timedelta(seconds=read_timeout_seconds),
852
+ read_timeout_seconds=timedelta(
853
+ seconds=read_timeout_seconds
854
+ ),
855
)
856
)
857
await session.initialize()
@@ -857,7 +861,7 @@ class MCPClientBase(ABC):
861
return result
862
except Exception as e:
863
# Store the original exception and raise a dummy exception
860
- excs = getattr(e, "exceptions", None) # Python 3.11+ ExceptionGroup
864
+ excs = getattr(e, "exceptions", None) # Python 3.11+ ExceptionGroup
865
if excs:
866
original_exception = excs[0]
867
else:
@@ -875,10 +879,10 @@ class MCPClientBase(ABC):
879
f"MCPClientBase ({self.server.name} - {operation_name}): Error during operation: {type(e).__name__}: {e}"
880
)
881
raise e # Re-raise the original exception
878
- finally:
879
- PrintStyle(font_color="cyan").print(
880
- f"MCPClientBase ({self.server.name} - {operation_name}): Session and transport will be closed by AsyncExitStack."
881
- )
882
+ # finally:
883
+ # PrintStyle(font_color="cyan").print(
884
+ # f"MCPClientBase ({self.server.name} - {operation_name}): Session and transport will be closed by AsyncExitStack."
885
+ # )
886
# This line should ideally be unreachable if the try/except/finally logic within the 'async with' is exhaustive.
887
# Adding it to satisfy linters that might not fully trace the raise/return paths through async context managers.
888
raise RuntimeError(
@@ -905,7 +909,11 @@ class MCPClientBase(ABC):
909
910
try:
911
set = settings.get_settings()
908
- await self._execute_with_session(list_tools_op, read_timeout_seconds=self.server.init_timeout or set["mcp_client_init_timeout"])
912
+ await self._execute_with_session(
913
+ list_tools_op,
914
+ read_timeout_seconds=self.server.init_timeout
915
+ or set["mcp_client_init_timeout"],
916
+ )
917
except Exception as e:
918
# e = eg.exceptions[0]
919
error_text = errors.format_error(e, 0, 0)
@@ -917,7 +925,7 @@ class MCPClientBase(ABC):
925
)
926
with self.__lock:
927
self.tools = [] # Ensure tools are cleared on failure
920
- self.error = f"Failed to initialize. {error_text}" # store error from tools fetch
928
+ self.error = f"Failed to initialize. {error_text[:200]}{'...' if len(error_text) > 200 else ''}" # store error from tools fetch
929
return self
930
931
def has_tool(self, tool_name: str) -> bool:
@@ -957,7 +965,9 @@ class MCPClientBase(ABC):
965
set = settings.get_settings()
966
# PrintStyle(font_color="cyan").print(f"MCPClientBase ({self.server.name}): Executing 'call_tool' for '{tool_name}' via MCP session...")
967
response: CallToolResult = await current_session.call_tool(
960
- tool_name, input_data, read_timeout_seconds=timedelta(seconds=set["mcp_client_tool_timeout"])
968
+ tool_name,
969
+ input_data,
970
+ read_timeout_seconds=timedelta(seconds=set["mcp_client_tool_timeout"]),
971
)
972
# PrintStyle(font_color="green").print(f"MCPClientBase ({self.server.name}): Tool '{tool_name}' call successful via session.")
973
return response
@@ -977,7 +987,7 @@ class MCPClientBase(ABC):
987
988
def get_log(self):
989
# read and return lines from self.log_file, do not close it
980
- if not hasattr(self, 'log_file') or self.log_file is None:
990
+ if not hasattr(self, "log_file") or self.log_file is None:
991
return ""
992
self.log_file.seek(0)
993
try:
@@ -990,7 +1000,7 @@ class MCPClientBase(ABC):
1000
class MCPClientLocal(MCPClientBase):
1001
def __del__(self):
1002
# close the log file if it exists
993
- if hasattr(self, 'log_file') and self.log_file is not None:
1003
+ if hasattr(self, "log_file") and self.log_file is not None:
1004
try:
1005
self.log_file.close()
1006
except Exception:
@@ -1022,7 +1032,7 @@ class MCPClientLocal(MCPClientBase):
1032
import tempfile
1033
1034
# use a temporary file for error logging (text mode) if not already present
1025
- if not hasattr(self, 'log_file') or self.log_file is None:
1035
+ if not hasattr(self, "log_file") or self.log_file is None:
1036
self.log_file = tempfile.TemporaryFile(mode="w+", encoding="utf-8")
1037
1038
# use the stdio_client with our error log file