Why Use Approval?
Tool Permissions
Claude Code-style approval system for sensitive tool execution
Multiple Scopes
Session, user, time-limited, and context-specific approvals
PKCE Security
RFC 7636 PKCE webhooks for secure external approval systems
Audit Trail
Full audit log with grantor/revoker tracking
Installation
How It Works
1
Tool Configuration
Mark tools requiring approval with
approval: { required: true } in metadata2
Approval Check Hook
Before tool execution, the plugin checks if approval exists.
ApprovalPlugin.init() registers this
check itself; you do not list ApprovalCheckPlugin separately3
Approval Request
If not approved, throws
ApprovalRequiredError. The client receives an error result whose text is exactly the
tool’s approvalMessage (or the default prompt) and whose _meta.code is APPROVAL_REQUIRED, in production and
outside it alike4
Grant/Revoke
Approvals are granted via
this.approval methods or external webhooksQuick Start
Basic Setup
Require Approval on Tools
Using the Approval Service
The plugin extends all execution contexts withthis.approval:
How a Call Is Decided
For a tool withapproval set, the check runs before the tool executes and decides in this order:
skipApproval: true(orapprovalnot required): the tool runs.- A recorded denial for the caller, in session or user scope: the call is refused with state
denied. A denial outranks everything below, including pre-approved contexts and a session approval. - The call runs in one of the tool’s
preApprovedContexts: the tool runs. alwaysPrompt: true: the call is refused with statepending.- A valid (unexpired) approval for the caller: the tool runs.
- Otherwise the call is refused with state
pending, orexpiredwhen the approval has lapsed.
ApprovalRequiredError, which the client receives as an error result:
approvalMessage when it sets one, and Tool "<full name>" execution denied. for a recorded
denial. The approval errors are public MCP errors (ApprovalError extends PublicMcpError), so the message is never
replaced by an internal-error notice in production and never carries a stack trace.
Releases up to 1.8.5 wrapped a refusal as an internal server error: outside production its text carried a stack
trace, and in production the client saw
Internal FrontMCP error instead of the approval message.Whose Approval It Is
Approvals are looked up by the tool’s full name,<owner id>:<tool name>, so pass that name to the this.approval
grant and check methods. The owner is the app that declares the tool, or the adapter or plugin that provides it: a
tool declared on app my-app is my-app:file_write, and one the app’s github-api adapter provides is
github-api:create_issue.
Which Tools a Plugin Gates
Installed on an app,ApprovalPlugin gates that app’s tools, including those the app’s adapters and plugins
provide, against its own store. It never judges the tools of another app that has its own ApprovalPlugin, so two
apps can each install it with separate stores. It does gate, against its store, the approval tools of apps with no
approval plugin of their own, so a tool that asks for approval never runs ungated because the plugin sits on a
different app. Installed on the server (@FrontMcp({ plugins })), it gates every tool. A tool both gate must pass
each store’s check, and a denial in either one refuses the call.
Releases up to 1.8.2 let the
approval tools of an app without the plugin run for anyone when the plugin was
installed on another app.@Agent({ approval }) gates the agent’s invoke_<agent> tool the same way. A server where a tool or agent declares
approval and no ApprovalPlugin reaches it refuses to start with UnenforcedMetadataError, naming the entries. This
includes a server with no ApprovalPlugin at all, a splitByApp app whose own scope has none, and a tool declared
inside an @Agent, which only a plugin installed on that agent gates.
Up to 1.8.2 such a server started, and those tools ran without approval.
@Agent({ approval }) was never gated.this.approval resolves the ApprovalService of the ApprovalPlugin nearest to the tool: the one its own app
installed, otherwise the one on the server. With two apps that each install ApprovalPlugin with its own store, a
grant or check made through this.approval in one app’s tool uses that app’s store.
Releases up to 1.8.1 resolved
this.approval to the store of the app registered last, whichever app the tool
belonged to (#600).FrontMcpContext.verifiedSessionId). A stateless request has no such session: it carries the shared stateless
session id, or no mcp-session-id at all and runs under a fresh per-request id. Such a request is keyed by the
authenticated principal instead: authInfo.extra.userId, then authInfo.extra.sub, then authInfo.clientId
(the token’s sub), so an approval granted in one stateless request is found by the next request of the same
principal and by no other principal. A stateless call with no authenticated principal cannot hold a session
approval, and a tool that requires approval stays refused for it.
Releases up to 1.8.1 keyed a stateless request that sent no
mcp-session-id by its per-request id, so a session
approval granted in one request was never found by the next and the tool stayed refused
(#597).Approval Scopes
A context-specific approval opens the gate only for calls whose session carries that context
(
authInfo.extra.approvalContext, set by the server while authenticating), never from a context the caller names.
Plugin Options
Basic Configuration
Recheck Mode (Default)
In recheck mode, the plugin polls an external API for approval status:Webhook Mode with PKCE
For secure external approval systems using PKCE (RFC 7636):Tool Approval Options
boolean
default:"true"
Whether this tool requires approval before execution
ApprovalScope
default:"'session'"
Default scope for approvals:
session, user, time_limited, tool_specific, context_specificApprovalScope[]
Restrict which scopes are allowed for this tool. Granting another scope through
this.approval throws
ApprovalScopeNotAllowedError, and an approval of another scope, however it was stored, does not open the gate.number
Maximum lifetime of an approval of this tool (milliseconds). A longer
grantTimeLimitedApproval() throws
ApprovalOperationError, grants without a TTL get maxTtlMs, and no approval counts for longer than maxTtlMs
after it was granted.string
Category for grouping:
read, write, delete, execute, adminstring
Risk level hint:
low, medium, high, criticalstring
Message shown to user when prompting for approval
boolean
default:"false"
Prompt every time, even if previously approved (for highly sensitive operations)
boolean
default:"false"
Skip approval entirely (for safe, read-only operations)
ApprovalContext[]
Contexts that are pre-approved (bypass approval check).The context a call runs in is read only from the session, at
authInfo.extra.approvalContext, which your authentication layer sets. A context
field in the tool’s own arguments is ignored: the caller of a gated tool must not be
able to name the context that lets it skip the gate. A recorded denial for the caller
still wins over a pre-approved context.API Reference
ApprovalService Methods
Every grant method records who granted the approval in
grantedBy. When you pass none, it is the signed-in caller
whose tool made the grant, i.e. { source: 'user', identifier: '<user id>', method: 'implicit' }: a tool that grants
through this.approval asked no one, so the grant is not recorded as an 'interactive' answer. A tool that did ask passes
grantedBy: userGrantor(<user id>), whose method is 'interactive'. Without a signed-in user (no principal, or an anonymous anon: subject) it is { source: 'user' }
with no identifier. Pass grantedBy to record something else, e.g. policyGrantor('policy:read-only-safe') for an
automatic grant. revokeApproval() records revokedBy by the same rule, and getRevocations(toolId) reads it back. Releases up to
1.8.5 recorded every such grant and revocation as 'policy'; 1.8.6 recorded every default grant as 'interactive' and
kept no revokedBy.Promise<boolean>
Check if a tool is approved for execution
Promise<void>
Grant session-scoped approval
Promise<void>
Grant user-scoped approval (persists across sessions)
Promise<void>
Grant time-limited approval to the caller.
ttlMs must be a positive number.Promise<boolean>
Revoke the caller’s approvals of the tool: session, user, time-limited and context approvals. Returns whether
anything was revoked. Recorded denials are kept.
Promise<ApprovalRecord[]>
The caller’s recent revocations of the tool (kept for 24 hours), newest last. Each is the approval that was revoked,
with
revokedBy, revokedAt and revocationReason. Empty when the store keeps no revocations.Promise<ApprovalRecord | undefined>
Get the current approval record for a tool
Approval Audit Trail
Every approval records who granted it and how:Grantor Factory Functions
Create typed grantors for audit trails.userGrantor(userId, displayName?, options?) takes the method and origin in
its third argument ({ method?, origin? }, method defaulting to 'interactive'):
PKCE Webhook Flow
For external approval systems, the plugin implements RFC 7636 PKCE:Webhook Request
The plugin sends to your webhook URL:Callback Response
Your approval system responds to the callback URL:Storage Options
Thestorage option uses the StorageConfig type from @frontmcp/utils and supports memory, redis, vercel-kv, upstash, and auto.
Auto-Detect (Default)
Memory Storage
Redis Storage
Use Existing Storage Instance
Best Practices
1. Use Appropriate Scopes
1. Use Appropriate Scopes
- Session: Default, most restrictive - good for sensitive operations
- User: For tools the user has explicitly trusted
- Time-limited: For temporary elevated access
- Context-specific: For repository/project-specific permissions
2. Configure Risk Levels
2. Configure Risk Levels
Mark tools with appropriate risk levels to help users make informed decisions:
3. Use PKCE for External Approvals
3. Use PKCE for External Approvals
When integrating with external approval systems, always use webhook mode with PKCE to prevent session hijacking.
4. Track Audit Trails
4. Track Audit Trails
Always provide meaningful
reason and grantedBy information for compliance and debugging:Complete Example
Links & Resources
Source Code
View the approval plugin source code
Remember Plugin
For session memory storage
Plugin Guide
Learn more about FrontMCP plugins
PKCE RFC 7636
PKCE specification