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