Skip to content

elicitationToRequest

JavaScript · package toolnexus · SPEC §10 · §2 · js/src/mcp.ts

function elicitationToRequest(params: ElicitRequest["params"]): Request
function answerToElicitResult(answer: Answer): ElicitResult

The 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.

  • You are implementing a host resolver (waitFor) yourself, outside createToolkit/loadMcp’s built-in wiring, and need to turn a raw MCP elicitation/create request into the Request shape 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 waitFor implementation and need to construct realistic Request/Answer values without standing up an actual MCP server.

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.

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)
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" }
  • 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.