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
npm install @frontmcp/plugin-approval
How It Works
Tool Configuration
Mark tools requiring approval with approval: { required: true } in metadata
Approval Check Hook
Before tool execution, the plugin checks if approval exists
Approval Request
If not approved, throws ApprovalRequiredError for client handling
Grant/Revoke
Approvals are granted via this.approval methods or external webhooks
Quick Start
Basic Setup
import { FrontMcp , App } from ' @frontmcp/sdk ' ;
import { ApprovalPlugin } from ' @frontmcp/plugin-approval ' ;
@ App ({
id : ' my-app ' ,
name : ' My App ' ,
plugins : [ ApprovalPlugin . init ()], // Uses memory store by default
tools : [
/* your tools */
],
})
class MyApp {}
@ FrontMcp ({
info : { name : ' My Server ' , version : ' 1.0.0 ' },
apps : [ MyApp ],
})
export default class Server {}
import { Tool , ToolContext } from ' @frontmcp/sdk ' ;
import { z } from ' zod ' ;
@ Tool ({
name : ' file_write ' ,
description : ' Write to file system ' ,
inputSchema : { path : z . string (), content : z . string () },
approval : {
required : true ,
defaultScope : ' session ' ,
category : ' write ' ,
riskLevel : ' medium ' ,
approvalMessage : ' Allow writing to file system for this session? ' ,
},
})
export default class FileWriteTool extends ToolContext {
async execute ( input : { path : string ; content : string }) {
// Tool only executes if approved
return await this . writeFile ( input . path , input . content );
}
}
Using the Approval Service
The plugin extends all execution contexts with this.approval:
@ Tool ({ name : ' dangerous_tool ' })
class DangerousTool extends ToolContext {
async execute ( input ) {
// Check if approved
const isApproved = await this . approval . isApproved ( ' dangerous_tool ' );
if (! isApproved ) {
// Request approval from client
return { needsApproval : true , message : ' Please approve this action ' };
}
// Proceed with dangerous operation
return await this . performDangerousAction ( input );
}
}
Approval Scopes
Scope Description Use Case sessionValid only for current session Default, most restrictive userPersists across sessions for user Trusted tools time_limitedExpires after specified TTL Temporary elevated access tool_specificTied to specific tool instance Fine-grained control context_specificTied to context (e.g., specific repo) Context-aware approvals
Plugin Options
Basic Configuration
ApprovalPlugin . init ({
// Storage configuration
storage : { type : ' auto ' }, // 'auto', 'memory', 'redis', 'vercel-kv'
// Namespace for approval keys
namespace : ' approval ' , // default
// Approval workflow mode
mode : ' recheck ' , // or 'webhook'
// Enable audit logging
enableAudit : true , // default
// Maximum delegation depth
maxDelegationDepth : 3 , // default
// Cleanup interval for expired approvals
cleanupIntervalSeconds : 60 , // default
});
Recheck Mode (Default)
In recheck mode, the plugin polls an external API for approval status:
ApprovalPlugin . init ({
mode : ' recheck ' ,
recheck : {
url : ' https://api.example.com/approval/status ' ,
auth : ' jwt ' , // 'jwt', 'bearer', 'none', 'custom'
interval : 5000 , // ms between checks
maxAttempts : 10 ,
},
});
Webhook Mode with PKCE
For secure external approval systems using PKCE (RFC 7636):
ApprovalPlugin . init ({
mode : ' webhook ' ,
webhook : {
url : ' https://approval.example.com/webhook ' ,
includeJwt : false , // Security: don't expose JWT by default
challengeTtl : 300 , // 5 minutes
callbackPath : ' /approval/callback ' ,
},
});
Whether this tool requires approval before execution
approval.defaultScope
ApprovalScope
default: "'session'"
Default scope for approvals: session, user, time_limited, tool_specific, context_specific
Restrict which scopes are allowed for this tool
Maximum TTL for time-limited approvals (milliseconds)
Category for grouping: read, write, delete, execute, admin
Risk level hint: low, medium, high, critical
Message shown to user when prompting for approval
Prompt every time, even if previously approved (for highly sensitive operations)
Skip approval entirely (for safe, read-only operations)
approval.preApprovedContexts
Contexts that are pre-approved (bypass approval check)
API Reference
ApprovalService Methods
Check if a tool is approved for execution const approved = await this . approval . isApproved ( ' file_write ' );
grantSessionApproval(toolId, options?)
Grant session-scoped approval await this . approval . grantSessionApproval ( ' file_write ' , {
reason : ' User clicked Allow button ' ,
});
grantUserApproval(toolId, options?)
Grant user-scoped approval (persists across sessions) await this . approval . grantUserApproval ( ' api_access ' , {
reason : ' Admin pre-approved ' ,
});
grantTimeLimitedApproval(toolId, ttlMs, options?)
Grant time-limited approval await this . approval . grantTimeLimitedApproval ( ' elevated_access ' , 3600000 , {
reason : ' Temporary elevated access for 1 hour ' ,
});
revokeApproval(toolId, options?)
Revoke an existing approval await this . approval . revokeApproval ( ' file_write ' , {
reason : ' User clicked Revoke ' ,
});
getApproval(toolId)
Promise<ApprovalRecord | undefined>
Get the current approval record for a tool const record = await this . approval . getApproval ( ' file_write ' );
if ( record ) {
console . log ( ' Approved at: ' , record . grantedAt );
console . log ( ' Granted by: ' , record . grantedBy );
}
Approval Audit Trail
Every approval records who granted it and how:
interface ApprovalRecord {
toolId : string ;
state : ' pending ' | ' approved ' | ' denied ' | ' expired ' ;
scope : ApprovalScope ;
grantedAt : number ;
grantedBy : {
source : ' user ' | ' policy ' | ' admin ' | ' system ' | ' agent ' | ' api ' | ' oauth ' ;
identifier ?: string ;
displayName ?: string ;
method ?: ' interactive ' | ' implicit ' | ' delegation ' | ' batch ' | ' api ' ;
delegatedFrom ?: DelegationContext ;
};
reason ?: string ;
expiresAt ?: number ;
revokedAt ?: number ;
revokedBy ?: ApprovalRevoker ;
}
Grantor Factory Functions
Create typed grantors for audit trails:
import { userGrantor , adminGrantor , policyGrantor , systemGrantor } from ' @frontmcp/plugin-approval ' ;
// User-initiated approval
await this . approval . grantSessionApproval ( ' tool ' , {
grantedBy : userGrantor ( ' user-123 ' , ' John Doe ' , ' interactive ' ),
});
// Admin approval
await this . approval . grantUserApproval ( ' tool ' , {
grantedBy : adminGrantor ( ' admin-456 ' , ' Admin User ' ),
});
// Policy-based auto-approval
await this . approval . grantSessionApproval ( ' tool ' , {
grantedBy : policyGrantor ( ' policy:read-only-safe ' ),
});
// System auto-approval
await this . approval . grantSessionApproval ( ' tool ' , {
grantedBy : systemGrantor ( ' initialization ' ),
});
PKCE Webhook Flow
For external approval systems, the plugin implements RFC 7636 PKCE:
1. Generate PKCE pair: code_verifier (64 chars) + code_challenge = SHA256(verifier)
2. Store challenge: challenge:{code_challenge} → {toolId, sessionId, scope, expiresAt}
3. Send to webhook: {code_challenge, toolId, requestInfo, callbackUrl} (NO sessionId!)
4. External system calls back: POST /approval/callback {code_verifier, approved}
5. Plugin validates: SHA256(code_verifier) === stored code_challenge
6. Grant approval if valid
Webhook Request
The plugin sends to your webhook URL:
{
" code_challenge " : " sha256-hash-of-verifier " ,
" toolId " : " file_write " ,
" requestInfo " : {
" toolName " : " file_write " ,
" category " : " write " ,
" riskLevel " : " medium " ,
" customMessage " : " Allow writing to file system? "
},
" callbackUrl " : " https://your-server.com/approval/callback "
}
Callback Response
Your approval system responds to the callback URL:
{
" code_verifier " : " original-verifier-string " ,
" approved " : true ,
" scope " : " session " ,
" ttlMs " : 3600000 ,
" grantedBy " : {
" source " : " user " ,
" identifier " : " user-123 " ,
" displayName " : " John Doe "
}
}
The sessionId is never sent to external webhooks. PKCE ensures only the original requester can complete the approval flow.
Storage Options
Auto-Detect (Default)
ApprovalPlugin . init ({
storage : { type : ' auto ' }, // Uses Redis if available, else memory
});
Memory Storage
ApprovalPlugin . init ({
storage : { type : ' memory ' },
});
Memory storage resets when the process restarts.
Redis Storage
ApprovalPlugin . init ({
storage : {
type : ' redis ' ,
config : {
host : ' localhost ' ,
port : 6379 ,
password : process . env . REDIS_PASSWORD ,
},
},
});
Use Existing Storage Instance
import { createStorage } from ' @frontmcp/utils ' ;
const storage = await createStorage ({ type : ' redis ' , ... });
ApprovalPlugin . init ({
storageInstance : storage ,
});
Best Practices
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
3. Use PKCE for External Approvals
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: await this . approval . grantSessionApproval ( ' tool ' , {
grantedBy : userGrantor ( userId , userName , ' interactive ' ),
reason : ' User approved via UI dialog ' ,
});
Complete Example
import { FrontMcp , App , Tool , ToolContext } from ' @frontmcp/sdk ' ;
import { ApprovalPlugin , userGrantor } from ' @frontmcp/plugin-approval ' ;
import { z } from ' zod ' ;
// Configure approval plugin with webhook mode
const approvalPlugin = ApprovalPlugin . init ({
mode : ' webhook ' ,
webhook : {
url : ' https://approval.example.com/webhook ' ,
challengeTtl : 300 ,
},
storage : {
type : ' redis ' ,
config : {
host : process . env . REDIS_HOST || ' localhost ' ,
port : parseInt ( process . env . REDIS_PORT || ' 6379 ' ),
},
},
});
// Tool requiring approval
@ Tool ({
name : ' delete-account ' ,
description : ' Permanently delete user account ' ,
inputSchema : { confirm : z . boolean () },
approval : {
required : true ,
riskLevel : ' critical ' ,
category : ' delete ' ,
approvalMessage : ' This will permanently delete your account. Are you sure? ' ,
alwaysPrompt : true ,
},
})
class DeleteAccountTool extends ToolContext {
async execute ( input : { confirm : boolean }) {
if (! input . confirm ) {
return { success : false , message : ' Deletion not confirmed ' };
}
// Perform account deletion
await this . deleteAccount ();
return { success : true , message : ' Account deleted ' };
}
}
// Tool for granting approvals (admin only)
@ Tool ({
name : ' grant-tool-access ' ,
description : ' Grant access to a tool for a user ' ,
inputSchema : {
toolId : z . string (),
scope : z . enum ([ ' session ' , ' user ' ]),
},
})
class GrantToolAccessTool extends ToolContext {
async execute ( input : { toolId : string ; scope : ' session ' | ' user ' }) {
const userId = this . context . authInfo ?. extra ?.[ ' userId ' ] as string ;
if ( input . scope === ' session ' ) {
await this . approval . grantSessionApproval ( input . toolId , {
grantedBy : userGrantor ( userId , ' Admin ' , ' interactive ' ),
reason : ' Admin granted access ' ,
});
} else {
await this . approval . grantUserApproval ( input . toolId , {
grantedBy : userGrantor ( userId , ' Admin ' , ' interactive ' ),
reason : ' Admin granted persistent access ' ,
});
}
return { success : true , message : ` Access granted for ${ input . toolId } ` };
}
}
@ App ({
id : ' secure-app ' ,
name : ' Secure App ' ,
plugins : [ approvalPlugin ],
tools : [ DeleteAccountTool , GrantToolAccessTool ],
})
class SecureApp {}
@ FrontMcp ({
info : { name : ' Secure Server ' , version : ' 1.0.0 ' },
apps : [ SecureApp ],
http : { port : 3000 },
})
export default class Server {}
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