Skip to main content
The Approval Plugin provides Claude Code-style permission management for FrontMCP servers, enabling fine-grained tool authorization with PKCE webhook security.

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 metadata
2

Approval Check Hook

Before tool execution, the plugin checks if approval exists. ApprovalPlugin.init() registers this check itself; you do not list ApprovalCheckPlugin separately
3

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 alike
4

Grant/Revoke

Approvals are granted via this.approval methods or external webhooks

Quick Start

Basic Setup

Upgrade to 1.8.1 or later. In 1.8.0 and earlier, ApprovalPlugin.init() did not register the check hook, so tools marked approval ran without any approval. Listing ApprovalCheckPlugin next to it is no longer needed; if you already do, the check still runs once per call.

Require Approval on Tools

Using the Approval Service

The plugin extends all execution contexts with this.approval:

How a Call Is Decided

For a tool with approval set, the check runs before the tool executes and decides in this order:
  1. skipApproval: true (or approval not required): the tool runs.
  2. 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.
  3. The call runs in one of the tool’s preApprovedContexts: the tool runs.
  4. alwaysPrompt: true: the call is refused with state pending.
  5. A valid (unexpired) approval for the caller: the tool runs.
  6. Otherwise the call is refused with state pending, or expired when the approval has lapsed.
A refused call throws ApprovalRequiredError, which the client receives as an error result:
The text is the tool’s 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).
Session approvals belong to the caller’s session, and only to a session the server verified (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_specific
ApprovalScope[]
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, admin
string
Risk level hint: low, medium, high, critical
string
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:
The sessionId is never sent to external webhooks. PKCE ensures only the original requester can complete the approval flow.

Storage Options

The storage option uses the StorageConfig type from @frontmcp/utils and supports memory, redis, vercel-kv, upstash, and auto.

Auto-Detect (Default)

Memory Storage

Memory storage resets when the process restarts.

Redis Storage

Use Existing Storage Instance


Best Practices

  • 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
Mark tools with appropriate risk levels to help users make informed decisions:
When integrating with external approval systems, always use webhook mode with PKCE to prevent session hijacking.
Always provide meaningful reason and grantedBy information for compliance and debugging:

Complete Example


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