avoigt1121 Claude Opus 4.8 commited on
Commit
3aa4e62
·
1 Parent(s): 5f60092

Fix: raise on MCP tool errors instead of returning error text as a string

Browse files

Root cause of the recurring "'str' object has no attribute 'items'" noise:
when a decoupleR tool raised server-side, MCP returns isError=True with the
message as plain text. _parse_mcp_content ignored isError, tried json.loads
(which fails on a plain message), and handed the agent the error TEXT as a
string. The agent's dict-expecting code then did result.items()/result['k']
on that string and failed with a misleading secondary error that hid the
real cause.

Now _parse_mcp_content checks result.isError and raises RuntimeError with the
message, so the executor reports the actual tool error (e.g. "dataset_id 'foo'
not found") and the agent self-corrects on the real problem. Verified
end-to-end against a raising FastMCP tool and a normal dict tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (1) hide show
  1. src/managers/tools/mcp_manager.py +30 -7
src/managers/tools/mcp_manager.py CHANGED
@@ -119,6 +119,13 @@ class MCPManager:
119
  await session.initialize()
120
  result = await session.call_tool(tool_name, kwargs)
121
  content = result.content[0] if result.content else None
 
 
 
 
 
 
 
122
  if content is None:
123
  return {}
124
  if hasattr(content, "text"):
@@ -286,15 +293,31 @@ class MCPManager:
286
  print(f"Failed to discover remote tools from {url}: {e}")
287
  return []
288
 
289
- def _parse_mcp_content(content) -> Any:
290
- """Extract and JSON-parse the tool result from an MCP content object.
291
 
292
- MCP tools return dicts serialized as JSON in content.text.
293
- Returning a parsed Python dict lets agent code do result["key"]
294
  without hitting 'string indices must be integers, not str'.
 
 
 
 
 
 
 
295
  """
296
  import json
297
- text = content.text if hasattr(content, "text") else str(content)
 
 
 
 
 
 
 
 
 
298
  try:
299
  return json.loads(text)
300
  except (json.JSONDecodeError, TypeError):
@@ -313,7 +336,7 @@ class MCPManager:
313
  async with ClientSession(reader, writer) as session:
314
  await session.initialize()
315
  result = await session.call_tool(tool_name, kwargs)
316
- return _parse_mcp_content(result.content[0])
317
 
318
  try:
319
  loop = asyncio.get_running_loop()
@@ -339,7 +362,7 @@ class MCPManager:
339
  async with ClientSession(read, write) as session:
340
  await session.initialize()
341
  result = await session.call_tool(tool_name, kwargs)
342
- return _parse_mcp_content(result.content[0])
343
 
344
  try:
345
  loop = asyncio.get_running_loop()
 
119
  await session.initialize()
120
  result = await session.call_tool(tool_name, kwargs)
121
  content = result.content[0] if result.content else None
122
+ text = content.text if (content is not None and hasattr(content, "text")) else (
123
+ str(content) if content is not None else "")
124
+ # Surface server-side tool errors as raised exceptions
125
+ # instead of handing the agent the error text as a string
126
+ # (which then breaks dict-expecting code misleadingly).
127
+ if getattr(result, "isError", False):
128
+ raise RuntimeError(text or "tool reported an error")
129
  if content is None:
130
  return {}
131
  if hasattr(content, "text"):
 
293
  print(f"Failed to discover remote tools from {url}: {e}")
294
  return []
295
 
296
+ def _parse_mcp_content(result) -> Any:
297
+ """Extract the tool result from an MCP CallToolResult.
298
 
299
+ MCP tools return dicts serialized as JSON in content[0].text;
300
+ returning a parsed Python dict lets agent code do result["key"]
301
  without hitting 'string indices must be integers, not str'.
302
+
303
+ If the tool raised server-side, MCP sets result.isError and puts the
304
+ error message in content[0].text. We must NOT hand that text back as a
305
+ plain string: the agent's dict-expecting code then does result.items()
306
+ on it and fails with a misleading "'str' object has no attribute
307
+ 'items'" that hides the real cause. Instead we raise, so the executor
308
+ reports the actual tool error and the agent can self-correct on it.
309
  """
310
  import json
311
+
312
+ content = result.content[0] if getattr(result, "content", None) else None
313
+ text = content.text if (content is not None and hasattr(content, "text")) else (
314
+ str(content) if content is not None else "")
315
+
316
+ if getattr(result, "isError", False):
317
+ raise RuntimeError(text or "tool reported an error")
318
+
319
+ if content is None:
320
+ return {}
321
  try:
322
  return json.loads(text)
323
  except (json.JSONDecodeError, TypeError):
 
336
  async with ClientSession(reader, writer) as session:
337
  await session.initialize()
338
  result = await session.call_tool(tool_name, kwargs)
339
+ return _parse_mcp_content(result)
340
 
341
  try:
342
  loop = asyncio.get_running_loop()
 
362
  async with ClientSession(read, write) as session:
363
  await session.initialize()
364
  result = await session.call_tool(tool_name, kwargs)
365
+ return _parse_mcp_content(result)
366
 
367
  try:
368
  loop = asyncio.get_running_loop()