Fixing Model Context Protocol Elicitation Failures in Headless Claude Code
Running Model Context Protocol tools that invoke ctx.elicit() fails abruptly inside headless Claude Code sessions like claude -p and CI pipelines. Shifting from interactive prompting to optional tool parameters with docstring fallback guidance keeps automated agent tasks running reliably.

Why it matters
Update your Model Context Protocol server tools today to prevent silent exceptions from crashing scheduled and automated Claude Code workflows.
TL;DR
- 01Headless execution (claude -p) lacks a transport back-channel, causing ctx.elicit() to raise exceptions instead of returning a decline status.
- 02Provide optional function parameters with fallback docstrings so models provide values automatically during headless runs.
- 03Python MCP SDK 2.3.0 migrates FastMCP to MCPServer and utilizes requested_schema naming internally.
Key facts
- Tested Claude Code Version
- 2.1.310
- Tested Python MCP SDK Version
- 2.3.0
- Key Import Change
- FastMCP renamed to MCPServer in mcp.server.mcpserver
The Headless Elicitation Trap
When developing tools using the Model Context Protocol (MCP) Python SDK, invoking ctx.elicit() prompts users for missing arguments. While functional in interactive terminal sessions, running non-interactive commands such as claude -p immediately fails:
Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.Because headless execution provides no user interface or server-initiated back-channel, the transport context raises a runtime exception. Defensive logic checking result.action != 'accept' is bypassed completely.
The Dual-Mode Fix: Schema Parameters First
To make an MCP tool compatible with both manual chats and headless CI/CD runs, expose required fields as optional parameters in the function definition rather than exclusively inside ctx.elicit():
1. Define the input parameter as optional (e.g., environment: str | None = None). 2. Add explicit docstring instructions: "Pass environment; if omitted, the user is asked." 3. Wrap ctx.elicit() in a narrow try/except block. 4. When an exception occurs, return actionable guidance text instructing the language model to invoke the tool again with the specified parameter.
Python SDK 2.x Architecture Details
In Python MCP SDK 2.3.0, FastMCP has transitioned to from mcp.server.mcpserver import MCPServer. Furthermore, when validating schema payloads, the elicitation parameter key in the Python implementation is requested_schema (snake_case) rather than the camelCase format found in the original specification. Avoid generic except Exception blocks, as they mask client-side schema rejections and request timeouts.
Try it in 2 minutes
from mcp.server.mcpserver import MCPServer, Context
from pydantic import BaseModel, Field
class Target(BaseModel):
environment: str = Field(description="staging or production")
async def deploy(service: str, ctx: Context, environment: str | None = None) -> str:
"""Deploy a service. Pass `environment` (staging or production); if omitted, the user is asked."""
if not environment:
try:
r = await ctx.elicit(message=f"Deploy {service} where?", schema=Target)
if r.action != "accept":
return f"Cancelled ({r.action})."
environment = r.data.environment
except Exception:
return ("Error: no environment given and this client can't prompt the user. "
"Call again with environment='staging' or 'production'.")
return f"Deployed {service} to {environment}."python
✓ When to use
- When running Claude Code or MCP agent tools via headless scripts, cron jobs, or CI/CD pipelines.
- When designing MCP tools intended to support both interactive chatting and background agent invocation.
✕ When NOT to use
- Do not apply this pattern to interactive-only tools that require mandatory human biometric or hardware key approval.
- Do not replace pure client-side parameter validation when arguments can be determined deterministically ahead of time.
What to do today
- Audit existing custom MCP servers for naked ctx.elicit() calls inside tool definitions.
- Refactor tool signatures to accept parameters directly with None defaults and descriptive docstrings.
- Test unattended runs using claude -p --mcp-config mcp.json --allowedTools <tool_name> before shipping to CI.