NAME

AnyEvent::MCP::Permissions::Ask - pluggable external permission prompts

DESCRIPTION

An MCP tool may need to ask a human for permission, but its server should not have to know whether the answer comes from a terminal, a graphical dialog or a desktop service. This module runs a separate permission helper so the prompting mechanism can be changed without changing the tool.

The helper receives one UTF-8 JSON request on stdin, terminated by EOF, and writes one JSON decision to stdout. helper is an executable or an argument array. timeout limits elapsed time; max_size limits output, defaulting to 1 MiB. PERL_ANYEVENT_MCP_PERMISSION_HELPER selects the default executable.

ask ($prepared, $done) calls $done->($success, $decision) once. cancel terminates the helper and completes with failure. Helpers are reaped by AnyEvent::Util even after cancellation.

The default helper tries YAD, letting GTK select its display backend, and asks on /dev/tty if YAD cannot start. If neither is available, the request is denied rather than silently allowed. Applications can substitute a desktop-daemon client. Rule management is available separately via anyevent-mcp manage-rules NAME.

AnyEvent::MCP::Permissions::Ask->elicitation ($req, %config) handles a permission-tagged Client::Request with the external helper and replies to it. Use this in the client's on_request callback for local permission dialogs. It preserves cancellation and validates the selected sandbox_grants and subject scope.

The functions schema, message, decision and build implement the shared form/helper representation. The initial form offers allow/deny, once/always, and an optional denial message. Persistent choices are omitted when the request has a subject but offers no persistent subject scopes. Approval never attaches that message to a successful tool result. Only the always choices lead to a second form for persistent rule grants, scopes and lifetime; back returns to the first form. The local graphical helper can complete this two-screen flow without a network round-trip; the terminal helper offers a m shortcut to edit the denial reason. Decisions are validated again by the permission engine before use.

The typical place to use elicitation is in an "on_request" in AnyEvent::MCP::Client callback:

on_request => sub {
   my ($client, $req) = @_;
   AnyEvent::MCP::Permissions::Ask->elicitation ($req);
},

Load this module before using the callback. It handles permission-tagged requests; the application must route or reject unrelated form requests. The server-side authorization logic lives in AnyEvent::MCP::Server::Permissions.

METHODS

$ask = AnyEvent::MCP::Permissions::Ask->new (%config)

Create a helper runner with these options:

helper => $executable
helper => [$executable, $arg...]

Executable or argument array, not a shell command. Defaults to PERL_ANYEVENT_MCP_PERMISSION_HELPER or anyevent-mcp-askpermission.

timeout => $seconds

Maximum elapsed time for a prompt, defaulting to 300 seconds. Zero also selects the default; it does not disable the timer.

max_size => $bytes

Maximum helper output size, defaulting to 1 MiB.

$ask->ask ($prepared, $done->($success, $decision))

Send a prepared permission request to the external helper. $done receives the decoded decision hash on successful completion, or an error message when $success is false. Process success does not mean permission was granted: the permission engine validates the decision separately. Only one request may be active per runner.

$ask->cancel ($reason)

Terminate an active helper, completing its callback with failure. $reason is optional. The child is reaped asynchronously.

$job = AnyEvent::MCP::Permissions::Ask->elicitation ($req, %config)

Handle a permission-tagged elicitation/create Client::Request using the local helper. %config is passed to the runner constructor. Cancellation of $req cancels the helper. Other elicitation requests are rejected.

FORM UTILITIES

These functions are called with fully qualified names, not as methods. $prepared is the prepared request returned by the permission engine. $stage is choice for the initial form or rule for persistent rule details. $previous holds the selection from the preceding form.

$schema = AnyEvent::MCP::Permissions::Ask::schema $prepared, $stage, $previous

Returns the JSON Schema hash reference for the selected form stage.

$text = AnyEvent::MCP::Permissions::Ask::message $prepared, $stage

Formats the request and, for the rule stage, available grants and scopes as readable text.

$text = AnyEvent::MCP::Permissions::Ask::detail_text $label, $text

Formats one display-only detail with its size and a preview of at most 65536 Unicode characters. Control and bidi characters are visibly escaped; tabs and newlines are preserved. Longer text carries an explicit warning.

$decision = AnyEvent::MCP::Permissions::Ask::build $prepared, $answer, $stage, $previous

Validates an answer hash and constructs a decision hash. An initial always choice selects the next stage; the rule stage completes it or returns back. Grant selections here are arrays, unlike the comma-separated grant string in the native MCP form. Persistent answers may select lifetime: indefinite (the default), 900 or 57600, all strings. A finite lifetime produces an absolute expires Unix timestamp in the rule; the selected lifetime is retained when forwarding native forms. When a workdir-scoped request is presented, workdir_scope selects selected (the default) or all configured workdirs, including future ones.

$decision = AnyEvent::MCP::Permissions::Ask::decision $prepared, $response, $stage, $previous

Converts the MCP elicitation response hash to a decision, including decoding its grant string. A declined or cancelled form becomes deny-once. Invalid answers die. The permission engine still validates and resolves the decision against its current policy.