Skip to content

McpSource.elicitationToRequest

Java · package io.github.muthuishere:toolnexus · SPEC §2 · McpSource.java

// package-private in the Java port — see the note below
static Request elicitationToRequest(McpSchema.ElicitRequest params)
static McpSchema.ElicitResult answerToElicitResult(Answer answer)

When a connected MCP server calls elicitation/create mid-tools/call — asking the human for a value, or to complete a login at a URL — McpSource.load bridges that reverse-request onto the one §10 suspension contract: Function<Request, Answer> waitFor. Form-mode elicitation becomes a Request with kind:"input" (the JSON Schema in data.schema); URL-mode becomes kind:"authorization" (the link in url). Your waitFor’s Answer is mapped back: ok:true → accept (content = answer.data()), reason:"declined" → decline, anything else (cancelled, expired, timed out) → cancel.

You do not call this mapping directly. You use it by passing a waitFor to McpSource.load or Toolkit.Options.waitFor — the same resolver you already use for tool-level suspensions (§10). Read this page when you need to know exactly what shape of Request your waitFor will see for an MCP elicitation, and exactly how your Answer is interpreted.

Omit waitFor (or pass null) and the client degrades cleanly: the elicitation capability is never advertised to the server, so a server that requires it will simply not offer that interaction — no crash, no dangling suspension.

1. The Request shapes the bridge produces — form vs. URL

Section titled “1. The Request shapes the bridge produces — form vs. URL”

The mapping is pure data-shuffling: kind, prompt, url, and data.schema are the exact fields SPEC §10 pins across all six ports. This example builds the two shapes by hand to pin the contract your waitFor must handle — the same shapes McpSource.load’s internal bridge produces from a real elicitation/create.

import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
public static void main(String[] args) {
// Form mode: kind:"input", the requested schema travels in data.schema.
Map<String, Object> schema = Map.of(
"type", "object",
"properties", Map.of("name", Map.of("type", "string")),
"required", List.of("name")
);
Request formReq = new Request("elc-1", "input", "Your name?", null, Map.of("schema", schema), null);
if (!formReq.kind().equals("input")) throw new AssertionError(formReq.kind());
if (!formReq.data().get("schema").equals(schema)) throw new AssertionError(formReq.data());
if (formReq.url() != null) throw new AssertionError("form mode carries no url");
// URL mode: kind:"authorization", the link travels in url, no schema.
Request urlReq = new Request("elc-2", "authorization", "Log in", "https://x/auth", null, null);
if (!urlReq.kind().equals("authorization")) throw new AssertionError(urlReq.kind());
if (!urlReq.url().equals("https://x/auth")) throw new AssertionError(urlReq.url());
if (urlReq.data() != null) throw new AssertionError("url mode carries no data");
System.out.println("ok: " + formReq.kind() + " / " + urlReq.kind());
}
}

2. Wiring waitFor into McpSource.load — the real entry point

Section titled “2. Wiring waitFor into McpSource.load — the real entry point”

The mapping function itself is internal; what you actually write is a waitFor and hand it to load. With every server disabled this is fully hermetic — waitFor is never invoked because no server connects to elicit anything, but the wiring compiles and runs exactly as it would with a live, elicitation-capable server.

import io.github.muthuishere.toolnexus.*;
import java.util.Map;
import java.util.function.Function;
public class Example {
public static void main(String[] args) throws Exception {
Map<String, Object> config = Map.of(
"mcpServers", Map.of("off", Map.of("command", java.util.List.of("x"), "enabled", false))
);
// A real host would prompt a human here. This one always accepts.
Function<Request, Answer> waitFor = req -> new Answer(req.id(), true, Map.of("name", "Ada"));
try (McpSource src = McpSource.load(config, waitFor)) {
// No connected server ⇒ elicitation never fires ⇒ status is just "disabled".
if (!"disabled".equals(src.status().get("off"))) throw new AssertionError(src.status());
System.out.println("ok: waitFor wired, " + src.status());
}
}
}

3. How an Answer maps back — accept, decline, cancel

Section titled “3. How an Answer maps back — accept, decline, cancel”

The rule McpSource’s internal answerToElicitResult applies: ok:true → accept; ok:false with reason:"declined" → decline; anything else ok:false (cancelled, expired, unspecified) → cancel. This example asserts the documented rule against the public Answer shape so the contract is checkable without reaching into the package-private function.

import io.github.muthuishere.toolnexus.*;
import java.util.Map;
public class Example {
// Mirrors the rule documented on McpSource.answerToElicitResult (package-private).
static String classify(Answer a) {
if (a.ok()) return "accept";
return "declined".equals(a.reason()) ? "decline" : "cancel";
}
public static void main(String[] args) {
Answer accepted = new Answer("id-1", true, Map.of("name", "Ada"));
Answer declined = new Answer("id-2", false, null, "declined");
Answer expired = new Answer("id-3", false, null, "expired");
Answer noReason = new Answer("id-4", false, null, null);
if (!classify(accepted).equals("accept")) throw new AssertionError();
if (!classify(declined).equals("decline")) throw new AssertionError();
if (!classify(expired).equals("cancel")) throw new AssertionError();
if (!classify(noReason).equals("cancel")) throw new AssertionError();
System.out.println("ok: accept/decline/cancel classification matches the bridge rule");
}
}
Field What it is
Request.kind() "input" for form-mode elicitation, "authorization" for URL-mode.
Request.prompt() The server’s message, verbatim (empty string if absent).
Request.data() {"schema": <requestedSchema>} for form mode; null for URL mode.
Request.url() The link for URL mode; null for form mode.
Answer.ok() true → MCP accept (content = answer.data(), or {}).
Answer.reason() "declined" → MCP decline; any other non-ok reason (or none) → MCP cancel.
  • McpSource.load — the entry point that actually wires this bridge via waitFor.
  • McpSource.loadWith — the same waitFor argument, plus cancellation.
  • McpSource.listMcpTools — list what each configured server would expose, plus per-server status, without wiring it into a toolkit.