Skip to content

Elicitation

Elicitation is sampling's human sibling. Instead of the LLM generating the next value, the user is prompted through the agent UI and submits the answer themselves.

Tesseron exposes two verbs on ctx, mapping onto MCP elicit's two orthogonal return fields (action, content):

  • ctx.confirm({ question }) returns Promise<boolean>. For yes/no safety gates. No schema.
  • ctx.elicit({ question, schema, jsonSchema? }) returns Promise<T | null>. For structured content.

Pick by intent: a destructive-op gate is a confirm; a "which warehouse?" is an elicit.

tesseron.action('clearCompleted')
.annotate({ destructive: true, requiresConfirmation: true })
.handler(async (_input, ctx) => {
const ok = await ctx.confirm({
question: 'Remove 5 completed todos? This cannot be undone.',
});
if (!ok) return { removed: 0, cancelled: true };
// ... proceed
});

Returns true only on explicit accept. Decline, cancel, and absence of elicitation capability all collapse to false - the safe default for destructive ops. You don't need to guard with ctx.agentCapabilities.elicitation; confirm returns false when the client can't prompt.

Under the hood, ctx.confirm sends an elicit request with an empty-properties JSON Schema ({ type: 'object', properties: {}, required: [] }), so MCP clients render a pure Accept/Decline prompt with no input field.

import { z } from 'zod';
const warehouseSchema = z.object({ warehouseId: z.string() });
tesseron.action('checkStock')
.handler(async (_input, ctx) => {
const answer = await ctx.elicit({
question: 'Which warehouse should I check?',
schema: warehouseSchema,
jsonSchema: z.toJSONSchema(warehouseSchema),
});
if (answer === null) return { cancelled: true };
return stock.lookup(answer.warehouseId);
});

Returns the validated value on accept, null on decline or cancel. Throws ElicitationNotAvailableError (code -32007) when the client didn't advertise elicitation - structured data has no safe default, so the handler must branch explicitly.

jsonSchema is technically optional; if you omit it, the SDK sends a permissive text-only fallback ({ response: string }), which Claude Code renders as a single text input. For good UX, always derive it from your validator - Zod 4 has z.toJSONSchema(schema) built in.

MCP elicit constrains requestedSchema. In protocol 1.2.0, the host validator rejects these shapes with -32602 InvalidParams:

  • A schema that is not a JSON object.
  • A top-level type whose value is anything other than "object".
  • A truthy top-level oneOf, anyOf, allOf, or not keyword.
  • A property whose checked type is "object".
  • A property whose checked type is "array".
  • A property whose checked type is any other non-primitive value. Present property types must be "string", "number", "integer", or "boolean".

Two lenient cases are part of the 1.2.0 validator behavior:

  • A property without a type is accepted. The validator does not infer a type from the property's other keywords.
  • When a property's type is an array, only its first entry is checked. A primitive first entry is accepted even when a later entry is unsupported; an unsupported first entry is rejected.

The lenient cases preserve the current 1.2.0 behavior. Tightening either rule should wait for a future minor, because it would reject schemas that currently pass.

The SDK enforces these rules on send and surfaces the InvalidParams error at the ctx.elicit call site. The gateway checks the same schema again when it receives the request.

ctx.confirm and ctx.elicit share the same wire flow - the difference is the requestedSchema they send. SDK HANDLER ctx.confirm / ctx.elicit MCP GATEWAY AGENT USER 1 elicitation/request { question, schema } 2 MCP elicitation/elicit 3 shows form or Accept/Decline 4 submits or declines 5 elicitation result 6 { action, value? }
ctx.confirm and ctx.elicit share the same wire flow - the difference is the requestedSchema they send.

Request:

{
"jsonrpc": "2.0",
"id": 11,
"method": "elicitation/request",
"params": {
"invocationId": "inv_abc",
"question": "Which warehouse should I check?",
"schema": {
"type": "object",
"properties": { "warehouseId": { "type": "string" } },
"required": ["warehouseId"]
}
}
}

Response (accept):

{
"jsonrpc": "2.0",
"id": 11,
"result": { "action": "accept", "value": { "warehouseId": "WH-7" } }
}

Response (decline / cancel):

{ "jsonrpc": "2.0", "id": 11, "result": { "action": "decline" } }

The SDK maps action: 'accept' to the validated value, decline / cancel to null (for ctx.elicit) or false (for ctx.confirm).

ctx.agentCapabilities.elicitation reflects what the connected MCP client advertised during initialize. Claude Code advertises elicitation; earlier clients may not.

  • ctx.confirm is safe in any handler: missing capability returns false, which destructive-op guards treat correctly.
  • ctx.elicit throws ElicitationNotAvailableError when capability is missing - catch it or pre-check the flag and provide a non-interactive fallback.
  • One question per call. Don't pack a wizard into a schema - chain actions instead.
  • Use annotations in tandem. { destructive: true, requiresConfirmation: true } tells the agent to warn upfront; ctx.confirm is what gates.
  • Avoid chained elicitations in one handler - latency accumulates. If you need multi-step input, build a dedicated action per step.

Next: resources - state the agent can read and subscribe to.