{"record":{"id":"750814912210030a","repo":"BerriAI/litellm","slug":"internal-server-error","errorCode":"internal_server_error","errorMessage":"An unexpected error occurred: {e}","messagePattern":"An unexpected error occurred: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/rest_endpoints.py","lineNumber":1089,"sourceCode":"                    \"message\": str(e),\n                    \"guardrail_name\": getattr(e, \"guardrail_name\", None),\n                },\n            )\n        except MCPUpstreamAuthError as e:\n            # A client-forwarded pass-through upstream 401 from either the direct or the virtual call\n            # branch. Relay it as a 401 + WWW-Authenticate so the MCP client can re-run upstream OAuth,\n            # and log at info: an expected caller-must-reauth signal, not an operator-actionable error.\n            verbose_logger.info(\"MCP tool call relaying upstream HTTP %s\", e.status_code)\n            raise _relay_upstream_auth_http_exception(e, request)\n        except HTTPException as e:\n            # Locally generated denials (tool/server permission, IP filtering, BYOK) stay at error level\n            # so restriction probing keeps full monitoring visibility; the relayed upstream 401 above is\n            # the only status demoted to info.\n            verbose_logger.error(\"HTTPException in MCP tool call: %s\", e)\n            raise e\n        except Exception as e:\n            verbose_logger.exception(\"Unexpected error in MCP tool call: %s\", e)\n            raise HTTPException(\n                status_code=500,\n                detail={\n                    \"error\": \"internal_server_error\",\n                    \"message\": f\"An unexpected error occurred: {e}\",\n                },\n            )\n\n    ########################################################\n    # MCP Connection testing routes\n    # /health -> Test if we can connect to the MCP server\n    # /health/tools/list -> List tools from MCP server\n    # For these routes users will dynamically pass the MCP connection params, they don't need to be on the MCP registry\n    ########################################################\n    from litellm.proxy.management_endpoints.mcp_management_endpoints import (\n        NewMCPServerRequest,\n        _inherit_credentials_from_existing_server,\n    )\n","sourceCodeStart":1071,"sourceCodeEnd":1107,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/rest_endpoints.py#L1071-L1107","documentation":"This is the catch-all handler at the bottom of POST /mcp-rest/tools/call: any exception the typed handlers (permissions, guardrails, BYOK, upstream auth) did not claim becomes HTTP 500 with error code internal_server_error. The proxy logs the full traceback via verbose_logger.exception(\"Unexpected error in MCP tool call\"); the f-string message embeds the original exception text and is the starting point for diagnosis.","triggerScenarios":"Bugs in tool dispatch or response serialization; arguments containing objects that cannot be JSON-serialized; database or network failures mid-call; exceptions raised by custom callbacks or the upstream MCP SDK that no earlier handler catches.","commonSituations":"Right after upgrading litellm; passing datetime/set/custom objects inside arguments; flaky upstream connections during a call.","solutions":["Read the proxy logs for the \"Unexpected error in MCP tool call\" traceback — the response message alone is rarely enough.","Reproduce once with debug logging (LITELLM_LOG=DEBUG) to capture the full stack.","Sanitize arguments to JSON primitives (strings, numbers, booleans, lists, dicts) before sending.","If the traceback points inside litellm itself, open a GitHub issue with the stack trace and the tool-call shape."],"exampleFix":"# before\nawait client.post(\"/mcp-rest/tools/call\", json={\n    \"server_id\": sid, \"name\": \"create_event\",\n    \"arguments\": {\"when\": datetime.now()},  # datetime is not JSON-serializable\n})\n\n# after\nawait client.post(\"/mcp-rest/tools/call\", json={\n    \"server_id\": sid, \"name\": \"create_event\",\n    \"arguments\": {\"when\": datetime.now().isoformat()},\n})","handlingStrategy":"try-catch","validationCode":"import json\n\ndef ensure_serializable(arguments: dict) -> dict:\n    try:\n        json.dumps(arguments)\n    except TypeError as e:\n        raise ValueError(f\"arguments are not JSON-serializable: {e}\") from e\n    return arguments\n\nensure_serializable(payload[\"arguments\"])","typeGuard":null,"tryCatchPattern":"try:\n    resp = await client.post(f\"{base}/mcp-rest/tools/call\", json=payload)\n    resp.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 500:\n        d = e.response.json().get(\"detail\", {})\n        # the message embeds the original exception; correlate with proxy logs,\n        # retry at most once for transient-looking causes (network/DB)\n        capture_for_postmortem(d.get(\"message\"))\n        return await call_with_backoff(payload, retries=1)\n    raise","preventionTips":["JSON-encode arguments (json.dumps round-trip) before sending — non-serializable objects are a common 500 cause.","Ship the proxy request ID / timestamp with every 500 so the traceback in proxy logs can be found.","Pin litellm versions in production and read the changelog before upgrading MCP features."],"tags":["mcp","http-500","unhandled-exception","debugging"],"backgroundTag":"unhandled-server-exception","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}