elicitationToRequest
JavaScript · package toolnexus · SPEC §10 · §2 · js/src/mcp.ts
function elicitationToRequest(params: ElicitRequest["params"]): Requestfunction answerToElicitResult(answer: Answer): ElicitResultThe two pure functions behind the MCP elicitation bridge: an MCP server can pause a tools/call
mid-flight to ask you for something — a form of input, or a URL to visit and authorize. Both
directions of that reverse-request collapse onto toolnexus’s own suspension contract (§10), so a
host only ever implements one waitFor — never a separate code path per tool source.
When to use it
Section titled “When to use it”- You are implementing a host resolver (
waitFor) yourself, outsidecreateToolkit/loadMcp’s built-in wiring, and need to turn a raw MCPelicitation/createrequest into theRequestshape your resolver already understands. - You are inspecting or logging elicitation traffic and want the readable §10 shape instead of the
wire-level
ElicitRequest. - You are testing a
waitForimplementation and need to construct realisticRequest/Answervalues without standing up an actual MCP server.
Why this and not the alternative
Section titled “Why this and not the alternative”The mapping is intentionally narrow: MCP’s elicitation has two modes carried in one params shape
(mode: "url" vs. everything else, which is form input). elicitationToRequest reads mode and
produces one of two Request.kinds; answerToElicitResult reads only Answer.ok and, when it is
false, Answer.reason — every other Answer field is ignored by design, so a host resolver never
needs MCP-specific knowledge to satisfy one.
Examples
Section titled “Examples”1. Form-mode elicitation → kind: "input"
Section titled “1. Form-mode elicitation → kind: "input"”The common case: a server wants structured input mid-call. The requested JSON Schema survives
under data.schema, unmodified.
import assert from "node:assert"import { elicitationToRequest } from "toolnexus"
const params = { message: "What is your deployment target?", requestedSchema: { type: "object", properties: { env: { type: "string", enum: ["staging", "production"] } }, required: ["env"], },}
const req = elicitationToRequest(params)
assert.equal(req.kind, "input")assert.equal(req.prompt, "What is your deployment target?")assert.deepEqual(req.data?.schema, params.requestedSchema)assert.ok(req.id.length > 0, "an id is always assigned")assert.equal(req.url, undefined)
console.log("ok:", req.kind, "->", req.prompt)2. URL-mode elicitation → kind: "authorization"
Section titled “2. URL-mode elicitation → kind: "authorization"”An OAuth-style flow: the server wants the human to visit a URL. mode: "url" routes to
"authorization" and carries the url through instead of a schema.
import assert from "node:assert"import { elicitationToRequest } from "toolnexus"
const req = elicitationToRequest({ mode: "url", message: "Authorize access to your GitHub account", url: "https://github.com/login/oauth/authorize?client_id=abc123",})
assert.equal(req.kind, "authorization")assert.equal(req.url, "https://github.com/login/oauth/authorize?client_id=abc123")assert.equal(req.data, undefined, "form-only field, absent in url mode")
console.log("ok:", req.kind, "->", req.url)3. The full round trip — a waitFor resolver, and every Answer outcome
Section titled “3. The full round trip — a waitFor resolver, and every Answer outcome”answerToElicitResult is the other half: it turns whatever a waitFor resolver returns back into
the MCP wire shape the server expects. ok: true accepts with the resolver’s data as content;
ok: false distinguishes an explicit decline from every other non-answer (dismissal, timeout,
expiry), all of which the MCP side treats as "cancel".
import assert from "node:assert"import { elicitationToRequest, answerToElicitResult } from "toolnexus"
// A minimal host resolver, in the shape loadMcp's `waitFor` option expects.async function waitFor(request: { kind: string; prompt: string }) { if (request.kind === "authorization") return { id: "a1", ok: false, reason: "declined" as const } return { id: "a1", ok: true, data: { env: "production" } }}
const inputReq = elicitationToRequest({ message: "Pick an environment", requestedSchema: { type: "object" } })const accepted = answerToElicitResult(await waitFor(inputReq))assert.equal(accepted.action, "accept")assert.deepEqual(accepted.content, { env: "production" })
const authReq = elicitationToRequest({ mode: "url", message: "Authorize", url: "https://example.com/auth" })const declined = answerToElicitResult(await waitFor(authReq))assert.equal(declined.action, "decline")
// Every other falsy outcome (dismissed, timed out, expired — anything but an explicit// decline) maps to "cancel", not "decline". The MCP server can't tell those apart, by design.const timedOut = answerToElicitResult({ id: "a2", ok: false, reason: "expired" })assert.equal(timedOut.action, "cancel")const noReason = answerToElicitResult({ id: "a3", ok: false })assert.equal(noReason.action, "cancel")
// An accept with no data still produces a valid (empty) content object.const emptyAccept = answerToElicitResult({ id: "a4", ok: true })assert.deepEqual(emptyAccept.content, {})
console.log("ok:", accepted.action, declined.action, timedOut.action, noReason.action)Mapping
Section titled “Mapping”MCP params |
§10 Request |
|---|---|
mode: "url" |
kind: "authorization", url set, data absent |
| anything else (form) | kind: "input", data: { schema: requestedSchema } if present, url absent |
message |
prompt ("" if absent) |
| — | id — freshly generated, elc-<time>-<seq> |
§10 Answer |
MCP ElicitResult |
|---|---|
ok: true |
{ action: "accept", content: answer.data ?? {} } |
ok: false, reason: "declined" |
{ action: "decline" } |
ok: false, any other/absent reason |
{ action: "cancel" } |
See also
Section titled “See also”loadMcp— Read an mcp.json, connect every local stdio and remote streamable-HTTP server, expose each server tool as a Tool.loadMcp— The ctx-aware load: bound connection time and cancel a slow or hung server without leaking a child process.listMcpTools— List what each configured server would expose, plus per-server status, without wiring it into a toolkit.parseMcpConfig— Parse and validate config without connecting — the fast fail for a malformed or misspelled server block.