NAME
AnyEvent::MCP::Server::Permissions - action authorization and sandbox grants
SYNOPSIS
my $permissions = AnyEvent::MCP::Server::Permissions->new (
name => "example",
actions => { read => "Read file contents", write => "Modify files" },
sandbox_grants => {
"workdir-read" => [],
"workdir-write" => ["workdir-read"],
},
sandbox_grant_descriptions => {
"workdir-read" => "Read the configured workdir",
},
sandbox_implementation => "bubblewrap",
);
my $grant = $req->require_permission ({
tool => $req->{tool},
action => "read",
subject => $path,
subjects => [{ label => "this file", subject => $path }],
sandbox_grants => ["workdir-read"],
reason => "read $path",
});
DESCRIPTION
This module lets an MCP server check whether a tool operation is allowed before doing the work. It applies ordered permission rules and asks the user when a decision is needed. Users can allow or deny one operation, or save a rule for future requests.
Attach a Permissions object to your server, then have each tool call $req->require_permission before starting its operation. The tool describes what it wants to do and which worker privileges it needs. An allowed request receives the granted sandbox privileges; a denied request must not proceed.
By default, questions go to a local graphical or terminal helper. You can instead configure the server to ask through an MCP client that supports form elicitation. Saved rules can answer later requests without another prompt; see "RULE FILES".
For HTTP, the transport must authenticate callers before dispatching MCP requests. This module then authorizes operations for the accepted caller. The tool must enforce the returned grants, for example by constructing a restricted sandbox. The permission engine does not construct that sandbox or enforce worker isolation itself.
The three separate questions
Authentication establishes who may call the MCP endpoint. The permission engine answers whether a requested action on a subject may proceed and which sandbox grants the user approved. The application's sandbox or other enforcement then determines what the operation can actually do. None of the three substitutes for the others.
For example, a user may approve write on a project but only with workdir-write authority. This does not authorize an arbitrary command with network access, and an approval to write a buffer does not imply approval to save it. Ask before side effects: permission prompts can restart the tool callback. See "Permission checks and retries" in AnyEvent::MCP::Server::Toolset and AnyEvent::MCP::Intro for examples from actual servers.
PERMISSION REQUESTS
A request names an action (a server-defined class of authority such as inspect, read, write, manage or exec), an optional subject, and the sandbox_grants required by its worker. These are independent: authorization to write does not imply authorization to read, although sandbox-level workdir-write may imply workdir-read. tool is optional human-readable context identifying the actual MCP tool; rules do not match it.
An action may have a short human-readable description declared in actions. sandbox_grant_descriptions similarly describes worker privileges, and sandbox_implementation identifies the enforcement environment (for example, bubblewrap or NONE - UNSANDBOXED). These labels are explanatory, not authorization identifiers, and the trusted server supplies them.
details optionally maps plain display labels to Unicode text, for example { "View patch" => $diff }. These details are display-only and never participate in rule matching or persistence. Supply complete text; the UI chooses presentation, escaping and preview limits. Labels carry no UI punctuation such as an ellipsis. The local YAD helper opens a text viewer; terminal and native MCP forms include bounded previews in the prompt.
RULE FILES
Rules are evaluated in order, with the first match deciding. decision is allow, deny or ask; omitted decisions are invalid. action is the server-defined action class, not the decision. A matcher beginning with ^ is interpreted as a Perl regular expression; otherwise it matches exactly. An omitted subject matches any subject. For example:
[rule]
decision=allow
action=read
subject=^/home/example/documents/
sandbox-grants=workdir-read
[rule]
decision=deny
action=^
An allow rule requires sandbox-grants, which may be empty for actions that need no worker grants. It matches only requests whose required grants (including implied grants) are contained in the allowed closure. Omitted sandbox-grants on deny and ask rules matches any requested grants. An explicit ask rule takes precedence over any later allow rules. Deny rules may also specify an exact path matched against any entry in the request's paths array and a human-readable message included in subsequent denial errors. Neither field is permitted on allow rules. Exact-path denials apply even when an operation requires additional sandbox grants for other files. File paths are lexical strings, not a sandbox or inode identity. With no matching rule the user is asked.
An optional expires field is an unsigned integer Unix timestamp. Rules with expires at or before the current time are removed from the returned list on every read, before matching. Omit it for indefinite rules. Reads do not rewrite the file; the next ordinary save removes expired entries from disk. Permission prompts offer indefinite (the default), 900 seconds or 16 hours. Expiry does not interrupt operations already authorized.
Decisions received after asynchronous prompting are rechecked against the current policy. Persistent answers are appended. Rule files are private, updates use locking and atomic rename, and failed persistence is reported as a failed grant rather than a successful permanent authorization.
METHODS
- $permissions = AnyEvent::MCP::Server::Permissions->new (%config)
-
Creates a permission engine. It does not enforce worker isolation. Options are:
- actions => { $action => $description, ... }
-
Maps action names to descriptions, defaulting to an empty hash. When nonempty, unknown actions are rejected.
- sandbox_grants => { $grant => [$implied_grant...], ... }
-
Defines grant names and their implications, defaulting to an empty hash. Every requested or implied grant must be declared.
- sandbox_grant_descriptions => { $grant => $description, ... }
- sandbox_grant_descriptions => $callback->($request)
-
Optional display descriptions. The callback returns a description hash reference. These labels come from the server; request-supplied overrides are ignored.
- sandbox_implementation => $description
-
Optional display label, such as
bubblewrap. It does not select or construct an enforcement mechanism. - rules => [$rule...]
-
Ordered in-memory rule hashes, defaulting to an empty array. See "RULE FILES" for their fields.
- file => $file
-
UTF-8 rule file. When supplied, rules are read from this file on permission checks. A missing file supplies an empty rule list.
- name => $name
-
Optional name used for display and, when
fileis omitted, the default filename$XDG_CONFIG_HOME/anyevent-mcp/rules/NAME.ini, or$HOME/.config/anyevent-mcp/rules/NAME.ini. Without a name or file, policy stays in memory. Applications can supply a differentfile;mcp-fsusesanyevent-mcp/rules/fs-NAME.ini. - ask => $method
-
helper,tty,yad,autoormcp. When omitted, usesPERL_ANYEVENT_MCP_PERMISSION_ASK, defaulting tohelper.helperprompts locally using the configured helper.ttyandyadrun the built-in permission helper with only that interface, ignoring custom helper settings and without interface fallback.autouses modern MCP form elicitation when advertised for the request, otherwise the helper.mcprequires modern form elicitation and fails when unavailable. A declined prompt never falls back to a different approval mechanism. - ask => $callback->($prepared, $done->($success, $decision))
-
Local asynchronous asking callback.
$preparedis the prepared request;$donereceives a decision hash on success or an error on failure. Callback success means a decision was obtained, not that access was granted. - helper => $executable
- helper => [$executable, $arg...]
-
External helper, not a shell command. Defaults to
PERL_ANYEVENT_MCP_PERMISSION_HELPERoranyevent-mcp-askpermission. - timeout => $seconds
-
External helper timeout, defaulting to 300 seconds. Also determines the lifetime of each modern permission continuation. Use a positive value; zero makes continuations expire immediately, while the helper treats zero as its default timeout.
- auto_rules => $rules
-
Ordered process-local permission rules, each
[DECISION, SPEC]whereDECISIONisallowordenyandSPECisTOOL[,ACTION[,CAPABILITY]]. Each field is an exact name or*(any); omitted trailing fields mean*. Tool names treat underscores and hyphens alike. A named capability must be known to this server. Allow matches when all requested grants are implied by the specified capability; deny matches when the requested grants imply that capability.*matches any capability set in either direction. First match wins, before saved rules and prompting. Only tools which call the permission engine are affected. Automatic allowance grants exactly the requested sandbox grants; tool validation and sandbox enforcement still apply.
- $grant = $permissions->check ($request, $tool)
-
$requestcontainsaction,reason, andsandbox_grants(an array reference; it may be empty).toolin the request is optional display context. The separate optional$toolargument is the trusted dispatched MCP tool name for automatic overrides; Server supplies it from the call.subjectis optional; when present,subjectsoffers persistent scope choices as{ label => $label, subject => $matcher }hashes, each matching the requested subject.workdiris an optional exact-match identity separate fromsubject. Rules withworkdirmatch only that configured workdir spelling, and rules without it may match any workdir when the sandbox grants also match. The application decides when a selected workdir matters to the requested sandbox grants. The persistent-rule dialog defaults to the selected workdir but can explicitly offer any current or future workdir by omitting the matcher; this does not change the sandbox grants available to a single operation. The multibroker alias, when supplied by the child's environment, is display-only and does not change rule matching or policy file selection.pathsmay contain a list of affected filenames for display in permission prompts and for optional deny-only exact-file rules. Such matching does not restrict sandbox authority and is not a substitute for sandboxing. Absent apathmatcher, the permission rule is not limited by paths.Returns
{decision => "allow", sandbox_grants => [...]},{decision => "deny", sandbox_grants => []}, or{decision => "ask", request => $prepared}. Denials can also include a savedmessage.checkdoes not prompt or execute the operation. - $grant = $permissions->resolve ($request, $approval)
-
$requestis the original permission request.$approvalis{ request => $prepared, decision => $decision }, where$preparedis the request returned bycheckand$decisionis the helper's decision hash. Resolves the answer to that prepared request. Approval decisions areallow-once,allow-always,deny-once, anddeny-always. Allow-once uses exactly the requested sandbox grants. Allow-always may save a broader or narrower set of grants for future invocations; the present invocation still receives no more than requested. Permanent decisions contain a validated rule restricted to the offered action and subject scope. Denial decisions may include an optional humanmessage; allowed tool results are never modified by permission prompting.This is a trusted application interface. Do not pass unauthenticated client metadata directly to it.
- $grant = $permissions->require_permission ($req, { %request })
-
Synchronous interface used by
$req->require_permission. Returns the granted sandbox-grant array, dies on denial, or throws a permission-needed exception for the server to handle. The worker must enforce the returned grants; never treat merely requested grants as sufficient. - $permissions->require ($req, $request, $done->($success, $grant))
-
Asynchronous interface.
$grantis the allowed sandbox-grant array or an error. Check before starting side effects.With a local prompt,
$doneruns when permission is resolved. Modern MCP prompting instead completes$reqwithinput_requiredwithout calling$done; the client repeats the call with input. Original callbacks are not retained. Continuations are short-lived and bound to the method, parameters, and request, and are consumed once. Approval retries retain earlier one-time grants within the same call. Always decisions and Back use additional protectedinput_requiredrounds, capped at 32, without executing the original tool. Messages in persistent deny rules are returned to subsequent callers, including later sessions. Client-side elicitation is only appropriate when the connected client is the trusted approval authority. - $closure = $permissions->closure ([$grant...])
-
Returns a hash reference whose keys contain the supplied sandbox grants and all grants they imply. Unknown grants or malformed implication lists die. This expands authority names; it does not grant permission.
- $permissions->auto_rules ($rules)
-
Replace the ordered process-local rule chain. Parsing and validation occur once, at installation. An empty list disables automatic rules.
- $rules = $permissions->rules
-
Returns the ordered, uncompiled in-memory or file-backed rule array reference. File-backed rules are reread, so changes apply to subsequent checks.
- $permissions->save ($rules)
-
Validates and replaces the ordered rules, using atomic file replacement if backed by a file.