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_HELPERoranyevent-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.
$donereceives the decoded decision hash on successful completion, or an error message when$successis 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.
$reasonis optional. The child is reaped asynchronously. - $job = AnyEvent::MCP::Permissions::Ask->elicitation ($req, %config)
-
Handle a permission-tagged
elicitation/createClient::Request using the local helper.%configis passed to the runner constructor. Cancellation of$reqcancels 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 selectlifetime:indefinite(the default),900or57600, all strings. A finite lifetime produces an absoluteexpiresUnix timestamp in the rule; the selected lifetime is retained when forwarding native forms. When a workdir-scoped request is presented,workdir_scopeselectsselected(the default) orallconfigured 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.