TypeScriptTools
Ask the User for Input
Pause a tool to ask for confirmation or missing details (MRTR)
Some actions shouldn't run until a person agrees, such as deleting data or sending money. A tool can pause, ask the client for structured input, and continue when the answer arrives. NitroStack implements this with multi round-trip requests (MRTR, SEP-2322).
It needs no WebSocket or side channel: the tool simply returns early with a request for input, and the client calls it again with the answer.
Client
Tool handler
1Call drop_table
Needs confirmation, returns inputRequired(...)
2Input requiredForm schema and requestState
The user confirms
3Call again with inputResponsesconfirmed: true
acceptedContent() reads the answer, then the table is dropped
4Success
Protocol Helpers
NitroStack exports two primary helpers from @nitrostack/core:
inputRequired(spec): Returns a protocol marker signaling that the handler needs additional client input before continuing.requests: Array of input requests (e.g., input ID, prompt message, expected JSON schema, and kind'form' | 'url').requestState: Opaque state preserved and echoed back by the client on re-invocation.message: Optional human-readable explanation displayed with the request.
acceptedContent<T>(inputResponses, id): Extracts and unwraps the client-supplied answer for request IDidfromctx.inputResponses. If no answer is present, returnsundefined.
Interactive Confirmation Example
This tool asks for confirmation before it drops a table. The first call returns inputRequired with a form; the client calls again with the user's answer, and acceptedContent() reads it.
import {
Controller,
ToolDecorator as Tool,
ExecutionContext,
inputRequired,
acceptedContent,
z,
} from '@nitrostack/core';
@Controller('database')
export class DatabaseController {
@Tool({
name: 'drop_table',
description: 'Permanently deletes an entire database table with explicit user confirmation',
inputSchema: z.object({
tableName: z.string().describe('Target database table name'),
}),
})
async dropTable(input: { tableName: string }, ctx: ExecutionContext) {
// 1. Check if client previously responded to the confirmation elicitation
const confirmed = acceptedContent<boolean>(
ctx.inputResponses,
'confirm_delete'
);
// 2. If confirmation has not been provided yet, return inputRequired marker
if (confirmed !== true) {
return inputRequired({
requests: [
{
id: 'confirm_delete',
message: `CRITICAL: Permanent deletion of table '${input.tableName}'. Proceed?`,
schema: { type: 'boolean' },
kind: 'form',
},
],
requestState: { table: input.tableName },
message: 'Explicit user confirmation is mandatory before executing drop operation.',
});
}
// 3. User confirmed: execute destructive operation
ctx.logger.warn('Executing destructive table drop', {
table: input.tableName,
user: ctx.auth?.subject,
});
await this.executeDrop(input.tableName);
return {
success: true,
dropped: input.tableName,
timestamp: new Date().toISOString(),
};
}
private async executeDrop(table: string): Promise<void> {
// Database driver execution logic
}
}