generated from coulomb/repo-seed
fix: harden MCP write tool errors
This commit is contained in:
@@ -92,6 +92,54 @@ def _delete(path: str) -> None:
|
||||
return {"error": f"Request failed: {e}"}
|
||||
|
||||
|
||||
def _mcp_error(tool_name: str, message: str, response: Any | None = None) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"error": message, "tool": tool_name}
|
||||
if response is not None:
|
||||
payload["response"] = response
|
||||
return payload
|
||||
|
||||
|
||||
def _response_error(
|
||||
tool_name: str,
|
||||
response: Any,
|
||||
required_fields: tuple[str, ...] = (),
|
||||
) -> dict[str, Any] | None:
|
||||
"""Return an MCP-visible error payload for failed or malformed API results."""
|
||||
if isinstance(response, dict) and isinstance(response.get("error"), str):
|
||||
return _mcp_error(tool_name, response["error"], response)
|
||||
if not isinstance(response, dict):
|
||||
return _mcp_error(tool_name, "API returned a non-object response", response)
|
||||
|
||||
missing = [field for field in required_fields if response.get(field) is None]
|
||||
if missing:
|
||||
return _mcp_error(
|
||||
tool_name,
|
||||
f"API response missing required field(s): {', '.join(missing)}",
|
||||
response,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _emit_progress_event(
|
||||
tool_name: str,
|
||||
write_result: dict[str, Any],
|
||||
body: dict[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
progress = _post("/progress", body)
|
||||
error = _response_error(f"{tool_name}.progress_event", progress, ("id",))
|
||||
if error:
|
||||
return _mcp_error(
|
||||
tool_name,
|
||||
"Primary write succeeded, but automatic progress_event failed",
|
||||
{"write_result": write_result, "progress_error": error},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _json_result(result: Any) -> str:
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resources
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -449,7 +497,10 @@ def create_workstream(
|
||||
"planning_priority": planning_priority,
|
||||
"planning_order": planning_order,
|
||||
})
|
||||
_post("/progress", {
|
||||
if error := _response_error("create_workstream", ws, ("id",)):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("create_workstream", ws, {
|
||||
"topic_id": topic_id,
|
||||
"workstream_id": ws["id"],
|
||||
"event_type": "workstream_created",
|
||||
@@ -457,7 +508,9 @@ def create_workstream(
|
||||
"author": "custodian",
|
||||
"detail": {"owner": owner, "slug": slug},
|
||||
})
|
||||
return json.dumps(ws, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(ws)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -487,7 +540,10 @@ def create_task(
|
||||
"assignee": assignee,
|
||||
"due_date": due_date,
|
||||
})
|
||||
_post("/progress", {
|
||||
if error := _response_error("create_task", task, ("id",)):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("create_task", task, {
|
||||
"workstream_id": workstream_id,
|
||||
"task_id": task["id"],
|
||||
"event_type": "task_created",
|
||||
@@ -495,7 +551,9 @@ def create_task(
|
||||
"author": "custodian",
|
||||
"detail": {"priority": priority, "assignee": assignee},
|
||||
})
|
||||
return json.dumps(task, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(task)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -558,7 +616,10 @@ def update_task_status(
|
||||
body["token_note"] = note
|
||||
|
||||
task = _patch(f"/tasks/{task_id}", body)
|
||||
_post("/progress", {
|
||||
if error := _response_error("update_task_status", task, ("id", "title")):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("update_task_status", task, {
|
||||
"task_id": task_id,
|
||||
"workstream_id": task.get("workstream_id"),
|
||||
"event_type": "task_status_changed",
|
||||
@@ -566,8 +627,10 @@ def update_task_status(
|
||||
"author": "custodian",
|
||||
"detail": {"blocking_reason": blocking_reason},
|
||||
})
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
|
||||
return json.dumps(task, indent=2)
|
||||
return _json_result(task)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -585,7 +648,10 @@ def flag_for_human(task_id: str, note: str) -> str:
|
||||
"needs_human": True,
|
||||
"intervention_note": note,
|
||||
})
|
||||
_post("/progress", {
|
||||
if error := _response_error("flag_for_human", task, ("id", "title")):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("flag_for_human", task, {
|
||||
"task_id": task_id,
|
||||
"workstream_id": task.get("workstream_id"),
|
||||
"event_type": "task_flagged_human",
|
||||
@@ -593,7 +659,9 @@ def flag_for_human(task_id: str, note: str) -> str:
|
||||
"author": "custodian",
|
||||
"detail": {"intervention_note": note},
|
||||
})
|
||||
return json.dumps(task, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(task)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -609,14 +677,19 @@ def clear_human_flag(task_id: str) -> str:
|
||||
task = _patch(f"/tasks/{task_id}", {
|
||||
"needs_human": False,
|
||||
})
|
||||
_post("/progress", {
|
||||
if error := _response_error("clear_human_flag", task, ("id", "title")):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("clear_human_flag", task, {
|
||||
"task_id": task_id,
|
||||
"workstream_id": task.get("workstream_id"),
|
||||
"event_type": "task_flag_cleared",
|
||||
"summary": f"Human-intervention flag cleared: {task['title']}",
|
||||
"author": "custodian",
|
||||
})
|
||||
return json.dumps(task, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(task)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -671,7 +744,10 @@ def record_decision(
|
||||
"decided_by": decided_by,
|
||||
"deadline": deadline,
|
||||
})
|
||||
_post("/progress", {
|
||||
if error := _response_error("record_decision", decision, ("id",)):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("record_decision", decision, {
|
||||
"topic_id": topic_id,
|
||||
"workstream_id": workstream_id,
|
||||
"decision_id": decision["id"],
|
||||
@@ -680,7 +756,9 @@ def record_decision(
|
||||
"author": "custodian",
|
||||
"detail": {"status": decision.get("status"), "escalation_note": decision.get("escalation_note")},
|
||||
})
|
||||
return json.dumps(decision, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(decision)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -703,7 +781,10 @@ def resolve_decision(
|
||||
"decided_by": decided_by,
|
||||
"decided_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
})
|
||||
_post("/progress", {
|
||||
if error := _response_error("resolve_decision", decision, ("id", "title")):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("resolve_decision", decision, {
|
||||
"topic_id": decision.get("topic_id"),
|
||||
"workstream_id": decision.get("workstream_id"),
|
||||
"decision_id": decision_id,
|
||||
@@ -712,7 +793,9 @@ def resolve_decision(
|
||||
"author": "custodian",
|
||||
"detail": {"rationale": rationale},
|
||||
})
|
||||
return json.dumps(decision, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(decision)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -748,7 +831,9 @@ def add_progress_event(
|
||||
"author": "custodian",
|
||||
"detail": detail,
|
||||
})
|
||||
return json.dumps(event, indent=2)
|
||||
if error := _response_error("add_progress_event", event, ("id",)):
|
||||
return _json_result(error)
|
||||
return _json_result(event)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -760,14 +845,19 @@ def update_workstream_status(workstream_id: str, status: str) -> str:
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||
"""
|
||||
ws = _patch(f"/workstreams/{workstream_id}", {"status": status})
|
||||
_post("/progress", {
|
||||
if error := _response_error("update_workstream_status", ws, ("id", "title")):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("update_workstream_status", ws, {
|
||||
"workstream_id": workstream_id,
|
||||
"topic_id": ws.get("topic_id"),
|
||||
"event_type": "workstream_status_changed",
|
||||
"summary": f"Workstream status → {status}: {ws['title']}",
|
||||
"author": "custodian",
|
||||
})
|
||||
return json.dumps(ws, indent=2)
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(ws)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
Reference in New Issue
Block a user