|
15 | 15 | ``codebase_consultant``) should suggest checking ``conversation_id``. |
16 | 16 | """ |
17 | 17 |
|
| 18 | +import json |
18 | 19 | from dataclasses import dataclass |
19 | | -from typing import Mapping, Optional |
| 20 | +from typing import Any, Mapping, Optional |
20 | 21 |
|
21 | 22 | import httpx |
22 | 23 | from fastmcp import Context |
23 | 24 | from fastmcp.exceptions import ToolError |
24 | 25 |
|
25 | 26 | from core.config import REQUEST_TIMEOUT_SECONDS |
26 | 27 |
|
| 28 | + |
| 29 | +def _parse_problem_details(body: str) -> Optional[dict[str, Any]]: |
| 30 | + """Parse a CodeAlive RFC 9457 / legacy error body, return None on mismatch.""" |
| 31 | + if not body: |
| 32 | + return None |
| 33 | + try: |
| 34 | + d = json.loads(body) |
| 35 | + except (ValueError, TypeError): |
| 36 | + return None |
| 37 | + return d if isinstance(d, dict) else None |
| 38 | + |
| 39 | + |
| 40 | +def _summarise_field_errors(d: dict[str, Any]) -> Optional[str]: |
| 41 | + """Render a one-line ``field: msg; field: msg`` summary from RFC 9457 |
| 42 | + ``errors`` (a dict-of-lists) or the legacy flat ``validationErrors`` list.""" |
| 43 | + errors = d.get("errors") |
| 44 | + if isinstance(errors, dict): |
| 45 | + rendered = [ |
| 46 | + f"{field}: {msg}" |
| 47 | + for field, msgs in errors.items() |
| 48 | + for msg in (msgs or []) |
| 49 | + ] |
| 50 | + if rendered: |
| 51 | + return "; ".join(rendered) |
| 52 | + legacy = d.get("validationErrors") |
| 53 | + if isinstance(legacy, list) and legacy: |
| 54 | + return "; ".join(str(x) for x in legacy) |
| 55 | + return None |
| 56 | + |
27 | 57 | # GitHub Issues URL is verified in README.md and manifest.json — safe to embed. |
28 | 58 | _ISSUES_URL = "https://github.com/CodeAlive-AI/codealive-mcp/issues" |
29 | 59 |
|
@@ -66,6 +96,18 @@ class _ErrorTemplate: |
66 | 96 |
|
67 | 97 |
|
68 | 98 | _ERROR_TEMPLATES: dict[int, _ErrorTemplate] = { |
| 99 | + 400: _ErrorTemplate( |
| 100 | + label="Bad request (400): The CodeAlive service rejected the request", |
| 101 | + retryable=False, |
| 102 | + retry_window=None, |
| 103 | + default_hint=( |
| 104 | + "(1) inspect the field-level errors below and fix the offending parameter, " |
| 105 | + "(2) for conversation_id / message_id, ensure the value is a 24-character hex " |
| 106 | + "Mongo ObjectId taken from a previous response, " |
| 107 | + "(3) if no field errors are surfaced, re-read the tool docstring and verify " |
| 108 | + "the request shape matches" |
| 109 | + ), |
| 110 | + ), |
69 | 111 | 401: _ErrorTemplate( |
70 | 112 | label="Authentication error (401): Invalid API key or insufficient permissions", |
71 | 113 | retryable=False, |
@@ -213,21 +255,39 @@ async def handle_api_error( |
213 | 255 | error_code = error.response.status_code |
214 | 256 | error_detail = error.response.text |
215 | 257 |
|
| 258 | + # Parse the CodeAlive RFC 9457 / legacy error body once. The summary is |
| 259 | + # appended to whichever branch handles this status code, so the LLM sees |
| 260 | + # field-level errors and the requestId without scanning raw JSON. |
| 261 | + problem = _parse_problem_details(error_detail) |
| 262 | + field_summary = _summarise_field_errors(problem) if problem else None |
| 263 | + request_id = problem.get("requestId") if problem else None |
| 264 | + rfc_detail = problem.get("detail") if problem else None |
| 265 | + |
| 266 | + def _enrich(msg: str) -> str: |
| 267 | + extras: list[str] = [] |
| 268 | + if rfc_detail and rfc_detail not in msg: |
| 269 | + extras.append(f"Detail: {rfc_detail}") |
| 270 | + if field_summary: |
| 271 | + extras.append(f"Fields: {field_summary}") |
| 272 | + if request_id: |
| 273 | + extras.append(f"requestId={request_id}") |
| 274 | + return msg if not extras else f"{msg} ({' | '.join(extras)})" |
| 275 | + |
216 | 276 | template = _ERROR_TEMPLATES.get(error_code) |
217 | 277 | if template is not None: |
218 | 278 | hint = (recovery_hints or {}).get(error_code, template.default_hint) |
219 | | - error_msg = _format_error(template, hint) |
| 279 | + error_msg = _enrich(_format_error(template, hint)) |
220 | 280 | elif error_code >= 500: |
221 | 281 | # Unknown 5xx — treat as retryable server error |
222 | | - error_msg = ( |
| 282 | + error_msg = _enrich( |
223 | 283 | f"Server error ({error_code}): The CodeAlive service encountered an issue. " |
224 | 284 | "Retry: yes (retry once after a few seconds). " |
225 | 285 | "Try: (1) retry the call once, " |
226 | 286 | f"(2) report a persistent error at {_ISSUES_URL}" |
227 | 287 | ) |
228 | 288 | else: |
229 | 289 | # Unknown 4xx — keep raw detail (truncated) and a conservative hint |
230 | | - error_msg = ( |
| 290 | + error_msg = _enrich( |
231 | 291 | f"HTTP error: {error_code} - {error_detail[:200]}. " |
232 | 292 | "Retry: no — fix the input. " |
233 | 293 | "Try: review the parameters you passed and try a different value" |
|
0 commit comments