
An ask-the-user tool for MCP::Server, built on genuine server-side elicitation.
Every other tool a model can call is something it does to the world. This is the one that goes the other way: the model has run out of information, and the person who asked for the work is the only one who has it. The pack registers a single tool — ask — which renders a form, blocks until the user fills it in, and hands the answers back to the model as JSON.
Underneath it is the 2026-07-28 multi round-trip request pattern: the tool call is answered with resultType: "input_required", the client shows the user a form, and the call resumes exactly where it stopped. Nothing is polled, nothing is faked client-side, and the handler is never re-run.
Synopsis
Plug it into a server you are building by hand:
use MCP::Server;
use MCP::Server::Tool::Ask;
my $server = MCP::Server.new(:name<assistant>, :version<1.0>);
$server.plug: MCP::Server::Tool::Ask.new; # registers user_ask
$server.run; # stdin/stdout
Or name it in a one-shot server, with no use statement at all — the toolkit is loaded at runtime:
use MCP::Server;
MCP::Server.new(
:name<assistant>,
:tools['Ask' => { timeout => 120 }],
).run;
Or hand raku-mcp a config file:
{
"name": "assistant",
"version": "1.0",
"instructions": "Ask before guessing.",
"tools": {
"Ask": { "timeout": 120 },
"FileSystem": { "root": "/srv/notes" }
}
}
raku-mcp --config=assistant.json
# Or without a file:
raku-mcp --tool='Ask={"timeout":120}'
Configuration
| Key | Type | Default | Meaning |
|---|
| timeout | Real | unset | Seconds to wait for one answer before treating it as a cancellation. Unset means the wait is bounded only by the server's own C<elicitation-ttl> (five minutes by default). Zero or negative dies. |
prefix is reserved by the framework rather than the pack: pass it to .plug as :prefix<...> , or put it beside the toolkit's settings in a :tools entry or config file. Without one, the pack's default prefix user applies, so the tool is user_ask; an explicit empty prefix registers it bare.
What the client has to support
The tool needs somebody to ask. That means one of:
A 2026-07-28 client that declared the elicitation capability (elicitation: { form: {} } in its _meta clientCapabilities). This is the real path: the question goes to the client, the client asks the human.
A server configured with :&on-elicit, which answers questions locally. That covers legacy-era clients, the LLM tool bridge (execute-tool-calls), and anything driving handlers directly.
With neither, calling the tool comes back as an error result naming which of the two is missing — it does not hang, and it does not invent an answer. See the "Elicitation" section of MCP::Server's documentation for the whole picture.
ask(message, questions?)
| Parameter | Type | Required | Meaning |
|---|
| message | string | yes | The framing: why the model is asking. Shown above the form. |
| questions | array | no | The fields to ask for. Omit it (or send an empty list) to ask one free-form question. |
Each entry of questions is an object:
| Key | Type | Default | Meaning |
|---|
| name | string | required | The key the answer comes back under. Must be unique within the call. |
| prompt | string | — | What to show the user for this field. Becomes the property's C<description>. |
| type | string | "string" | One of C<string>, C<number>, C<boolean>. |
| options | array | — | Strings to choose between; becomes an C<enum>. Only valid on a string question. |
| required | boolean | true | Whether the user must fill this field in. |
A call with three fields:
{
"message": "I need a few details before I can file the ticket.",
"questions": [
{ "name": "component", "prompt": "Which component?", "options": ["api", "ui", "docs"] },
{ "name": "severity", "prompt": "How bad is it?", "type": "number" },
{ "name": "notes", "prompt": "Anything else?", "required": false }
]
}
...becomes this requestedSchema on the wire:
{
"type": "object",
"properties": {
"component": { "type": "string", "description": "Which component?",
"enum": ["api", "ui", "docs"] },
"severity": { "type": "number", "description": "How bad is it?" },
"notes": { "type": "string", "description": "Anything else?" }
},
"required": ["component", "severity"]
}
...and comes back to the model as text:
{"component":"api","notes":"","severity":2}
Keys are sorted and the JSON is not pretty-printed: this text goes into a model's context, where stability is worth more than insertion order and newlines are worth nothing at all.
With no questions, the form has a single required string field called answer, which is what a model reaching for "just ask them" wants nine times out of ten:
{"answer":"go ahead, but stage it first"}
Refusal is an answer
A user who declines or cancels gets a plain, non-error result:
Neither carries isError, and that is deliberate. isError tells a model that something went wrong and that trying again might work — so an error result here would send it straight back to ask the same person the same question. A person saying no is the system working. The tool's description tells the model as much: carry on without the answer, or stop and explain why it cannot.
The same goes for a client that answers with something that is not an ElicitResult at all: that is read as a decline rather than as a fault.
Errors
These are error results, because each is a call the model can fix and retry:
A missing, non-string or empty message.
questions that is not a list, or an entry that is not an object.
A question with no name, or two questions sharing one — answers come back keyed by name, so a duplicate would silently lose one.
An unsupported type, options on a non-string question, an empty options list, or a required flag that is not a boolean.
An unknown key in a question object, listing the valid ones.
And, as above, calling the tool at all when there is nobody to ask.
Security and etiquette
This tool interrupts a human. It is not free the way reading a file is. A model that asks about everything is worse than one that asks about nothing, and the tool description is written to push it toward asking only when it genuinely cannot proceed. If you find a model over-using it, say so in the server's instructions.
An answer is user input, not truth. It arrives as JSON straight from whatever the client's form produced. Validate it before acting on it, exactly as you would any other input, and do not treat "the user typed it" as authorisation for something the user did not understand they were authorising.
A parked call holds server state. Each unanswered question occupies a place in the server's elicitation table (64 by default) until it is answered or the TTL expires. Set timeout if you would rather give up sooner than the server-wide TTL.
Asking is inherently consented. A permission layer in front of the server — Claude Code's rules, MCP::Client::Policy — should normally allow user_ask outright: prompting the user for permission to prompt the user helps nobody.
Testing
prove6 -Ilib t/ # with MCP::Server installed
prove6 -Ilib -I../MCP-Server/lib t/ # against a sibling checkout
t/02-tool.rakutest drives every question shape through a server with a scripted :on-elicit behind it; t/03-mrtr.rakutest does the same thing the long way round, over a real multi round-trip with no local callback anywhere.
Examples
examples/ask-server.raku — a Streamable HTTP server with a terminal-prompt :on-elicit fallback, so both paths can be watched side by side: a client that declares the capability gets the form over the wire, and one that does not sends the question to the terminal the server was started from. (HTTP rather than stdio because a stdio server has already spent stdin and stdout on the protocol.)
Author
Matt Doughty
License
Artistic-2.0