Toolnexus.Mcp.Protocol.elicitation_to_request
Elixir · package toolnexus · SPEC §2 · elixir/lib/toolnexus/mcp/protocol.ex
@spec elicitation_to_request(map()) :: Toolnexus.Request.t()def elicitation_to_request(params)
@spec answer_to_elicit_result(Toolnexus.Answer.t()) :: map()def answer_to_elicit_result(answer)The MCP spec lets a server pause a tools/call and ask the connecting client something —
fill in a form, or go complete an OAuth flow at a URL. toolnexus doesn’t invent a second
suspension mechanism for that: elicitation_to_request/1 maps the server’s elicitation/create
request onto the exact same §10 Toolnexus.Request your own tools use to suspend (e.g. the
question builtin), and answer_to_elicit_result/1 maps the resolved Toolnexus.Answer back
onto what the MCP server expects.
When to use it
Section titled “When to use it”- Writing your own
:wait_forforMcp.load/2— your function receives aRequestbuilt by this mapping; understanding the two shapes ("input"vs"authorization") tells you what to branch on. - Building a host that surfaces MCP elicitation the same way it surfaces your own tool
suspensions — a form UI, a Telegram/Slack prompt, a CLI question — one code path handles both,
because both arrive as the same
Requeststruct. - Testing — construct a fake
elicitation/createpayload and assert whatRequestit becomes, without a real server or network connection.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. Form-mode elicitation becomes a kind: "input" request
Section titled “1. Form-mode elicitation becomes a kind: "input" request”alias Toolnexus.Mcp.Protocol
params = %{ "message" => "What is your deployment target?", "requestedSchema" => %{ "type" => "object", "properties" => %{"target" => %{"type" => "string", "enum" => ["staging", "prod"]}} }}
req = Protocol.elicitation_to_request(params)
true = req.kind == "input"true = req.prompt == "What is your deployment target?"true = req.url == niltrue = req.data["schema"]["properties"]["target"]["enum"] == ["staging", "prod"]true = String.starts_with?(req.id, "elc-")
IO.puts("ok: #{req.kind} — #{req.prompt}")2. URL-mode elicitation becomes a kind: "authorization" request
Section titled “2. URL-mode elicitation becomes a kind: "authorization" request”alias Toolnexus.Mcp.Protocol
params = %{ "mode" => "url", "message" => "Authorize toolnexus to access your calendar", "url" => "https://example.com/oauth/authorize?state=abc123"}
req = Protocol.elicitation_to_request(params)
true = req.kind == "authorization"true = req.prompt == "Authorize toolnexus to access your calendar"true = req.url == "https://example.com/oauth/authorize?state=abc123"true = req.data == nil
IO.puts("ok: #{req.kind} — #{req.url}")3. Round-tripping an Answer back to the MCP result shape
Section titled “3. Round-tripping an Answer back to the MCP result shape”Three outcomes: accepted (with data), declined, or anything else treated as a cancel.
alias Toolnexus.{Answer, Mcp.Protocol}
accepted = Protocol.answer_to_elicit_result(%Answer{id: "a1", ok: true, data: %{"target" => "prod"}})true = accepted == %{"action" => "accept", "content" => %{"target" => "prod"}}
# ok: true with no data still accepts, with empty content.accepted_empty = Protocol.answer_to_elicit_result(%Answer{id: "a2", ok: true})true = accepted_empty == %{"action" => "accept", "content" => %{}}
declined = Protocol.answer_to_elicit_result(%Answer{id: "a3", ok: false, reason: "declined"})true = declined == %{"action" => "decline"}
# Anything that isn't an explicit decline is a cancel — e.g. a timeout upstream.cancelled = Protocol.answer_to_elicit_result(%Answer{id: "a4", ok: false, reason: "timeout"})true = cancelled == %{"action" => "cancel"}
IO.puts("ok: accept/decline/cancel all mapped")Mapping
Section titled “Mapping”MCP elicitation/create params |
§10 Request |
|---|---|
message |
prompt |
mode == "url" |
kind: "authorization", and url carries params["url"] |
mode != "url" (form) |
kind: "input", and data.schema carries params["requestedSchema"] (when present) |
| — | id is freshly generated ("elc-<ts36>-<seq>") |
§10 Answer |
MCP elicit result |
|---|---|
ok: true |
%{"action" => "accept", "content" => data || %{}} |
ok: false, reason: "declined" |
%{"action" => "decline"} |
ok: false, any other reason |
%{"action" => "cancel"} |
See also
Section titled “See also”Toolnexus.Mcp.load(ctx-aware form) — where:wait_forreceives these requestsToolnexus.Mcp.load— the base connect callToolnexus.Request/Toolnexus.Answer— the §10 suspension contract this bridges onto