rohitg00/ai-engineering-from-scratch · error · ValueError
review_goal must be accepted with a string goal
Error message
review_goal must be accepted with a string goal
What it means
The elicitation step only satisfies the server when the user accepted the form (action == 'accept') AND content.goal is a string. Declines, cancels, or a missing/non-string goal raise this — the server treats an unaccepted goal as an invalid response rather than a retryable input.
Source
Thrown at certifications/claude/lessons/11-mcp-server-design-and-integration/code/main.py:422
workspace_scope = responses["workspace_scope"]
review_sample = responses["review_sample"]
elicitation = responses["review_goal"]
if not all(
isinstance(response, dict)
for response in (workspace_scope, review_sample, elicitation)
):
raise ValueError("inputResponses entries must be objects")
elicitation_content = elicitation.get("content", {})
if not isinstance(elicitation_content, dict):
raise ValueError("review_goal.content must be an object")
roots = workspace_scope.get("roots")
sample_content = review_sample.get("content", {})
goal = elicitation_content.get("goal")
if not isinstance(roots, list) or not isinstance(sample_content, dict):
raise ValueError("inputResponses contain invalid roots or sampling results")
if elicitation.get("action") != "accept" or not isinstance(goal, str):
raise ValueError("review_goal must be accepted with a string goal")
sample = sample_content.get("text")
if not isinstance(sample, str):
raise ValueError("review_sample must contain text")
summary = {
"goal": goal,
"rootCount": len(roots),
"sample": sample,
"topic": arguments["topic"],
}
return self._complete(
content=[{"type": "text", "text": json.dumps(summary, sort_keys=True)}],
isError=False,
)
@staticmethod
def _progress(token: str | int, progress: int, total: int, message: str) -> dict[str, Any]:
return {
"jsonrpc": "2.0",View on GitHub (pinned to 39ea8a1c6d)
Solutions
- Only resume the call after the user accepts the form with a non-empty goal string
- Handle decline/cancel as a user-abort path before resuming tools/call, not as a response payload
- Verify action == 'accept' and isinstance(goal, str) client-side before sending inputResponses
Example fix
# before
review_goal = {"action": "decline", "content": {}}
# after
review_goal = {"action": "accept", "content": {"goal": "tighten auth review"}} Defensive patterns
Strategy: validation
Validate before calling
goal_response = responses["review_goal"]
if goal_response.get("action") != "accept":
raise UserAborted("elicitation not accepted")
if not isinstance(goal_response.get("content", {}).get("goal"), str):
raise ValueError("accepted elicitation must carry a string goal") Type guard
def is_accepted_goal(response: object) -> bool:
return (
isinstance(response, dict)
and response.get("action") == "accept"
and isinstance(response.get("content", {}).get("goal"), str)
) Try / catch
try:
result = server.exchange("tools/call", params, metadata=meta)
except ValueError as exc:
if "review_goal" in str(exc):
rerun_elicitation()
raise Prevention
- Handle decline/cancel as a user abort before resuming
- Map form fields to requestedSchema keys exactly
- Block empty goals in the form UI
When it happens
Trigger: review_goal of {"action": "decline"}, {"action": "cancel"}, or {"action": "accept", "content": {}} where goal is absent or not a string.
Common situations: User cancels the elicitation dialog; the UI sends the goal under a different key ('value', 'text'); a form validation gap lets an empty or null goal through.
Related errors
- inputResponses entries must be objects
- review_goal.content must be an object
- -32600
- -32603
- _meta.{PROTOCOL_VERSION_KEY} is required
AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26).
Data as JSON: /api/errors/2887e87aead83444.
Report an issue: GitHub.