Skip to content

Elicitation bridge — server asks the user

Rust · crate toolnexus · SPEC §2 · rust/src/mcp/elicit.rs

pub fn elicitation_to_request(params: &Value) -> Request // toolnexus::mcp
pub fn answer_to_elicit_result(answer: &Answer) -> Value
// enable it: ToolkitOptions { wait_for: Some(..), .. } (or McpLoadOptions { wait_for, .. })

When the toolkit is built with a wait_for, every MCP client advertises the elicitation capability and answers elicitation/create through it — inline, without re-executing the tool. Form mode becomes kind: "input" with the requested schema in data.schema; URL mode becomes kind: "authorization" with url. The Answer maps back: ok → accept (content = answer.data, or {}), reason: "declined" → decline, anything else → cancel. Without a wait_for the capability is not advertised, so a compliant server never elicits.

Mostly you do not call these yourself: give the toolkit (or McpLoadOptions) a wait_for and the bridge runs inline whenever a server elicits. Call the two functions directly when you host your own MCP client loop, or when you test how your wait_for will see a server’s request.

1. Form-mode elicitation becomes a kind:"input" Request

Section titled “1. Form-mode elicitation becomes a kind:"input" Request”
use serde_json::json;
use toolnexus::mcp::elicitation_to_request;
fn main() {
let params = json!({
"mode": "form",
"message": "What is your deployment region?",
"requestedSchema": {"type": "object", "properties": {"region": {"type": "string"}}},
});
let req = elicitation_to_request(&params);
assert_eq!(req.kind, "input");
assert_eq!(req.prompt, "What is your deployment region?");
assert!(req.id.starts_with("elc-"), "a generated, unique request id");
let schema = req.data.as_ref().and_then(|d| d.get("schema"));
assert!(schema.is_some(), "the requested schema rides under data.schema");
println!("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”
use serde_json::json;
use toolnexus::mcp::elicitation_to_request;
fn main() {
let params = json!({
"mode": "url",
"message": "Sign in to continue",
"url": "https://example.com/oauth/authorize",
});
let req = elicitation_to_request(&params);
assert_eq!(req.kind, "authorization");
assert_eq!(req.url, "https://example.com/oauth/authorize");
println!("ok: {} | {}", req.kind, req.url);
}

3. Round-tripping an Answer back to an elicitation result

Section titled “3. Round-tripping an Answer back to an elicitation result”

answer_to_elicit_result covers three outcomes: accepted (with data), explicitly declined, and everything else treated as a cancellation — a dismissal, a timeout, an unspecified reason.

use serde_json::json;
use toolnexus::Answer;
use toolnexus::mcp::answer_to_elicit_result;
fn main() {
let data = json!({"region": "apac"}).as_object().cloned();
let accepted = answer_to_elicit_result(&Answer { ok: true, data, ..Default::default() });
assert_eq!(accepted, json!({"action": "accept", "content": {"region": "apac"}}));
let declined = answer_to_elicit_result(&Answer { reason: "declined".into(), ..Default::default() });
assert_eq!(declined["action"], "decline");
let cancelled = answer_to_elicit_result(&Answer { reason: "expired".into(), ..Default::default() });
assert_eq!(cancelled["action"], "cancel");
println!("ok: {} {} {}", accepted["action"], declined["action"], cancelled["action"]);
}
elicitation params Request
mode == "url" kind: "authorization", url: params.url
mode != "url" (form, default) kind: "input", data["schema"]: params.requestedSchema
message prompt
— id — freshly generated (elc-…), unique per call
Answer result
ok: true {"action":"accept","content": answer.data} (or {} if None)
ok: false, reason: "declined" {"action":"decline"}
ok: false, any other reason {"action":"cancel"}