Role-based authorization lets you control which tools users can see and execute based on their roles (admin, manager, user, etc.). This guide shows you how to build an authorization plugin that filters tools based on user claims.
Prerequisites :
Understanding of FrontMCP plugins (see Create a Plugin )
A FrontMCP project with authentication configured
What You’ll Build
An authorization plugin that:
Extends tool metadata to include required roles
Filters the tool list based on user roles
Works with any authentication provider
First, extend the tool metadata to include authorization requirements:
// plugins/authorization.types.ts
// Extend global tool metadata to add authorization fields
declare global {
interface ExtendFrontMcpToolMetadata {
authorization ?: AuthorizationToolOptions ;
}
}
export interface AuthorizationPluginOptions {
/**
* Whether to enable authorization checks.
* @ default true
*/
enabled ?: boolean ;
}
export interface AuthorizationToolOptions {
/**
* Roles required to access this tool.
* User must have ALL specified roles.
*/
requiredRoles : string [];
}
By extending ExtendFrontMcpToolMetadata, TypeScript will recognize the authorization field on all tool metadata.
Step 2: Create the Authorization Plugin
// plugins/authorization.plugin.ts
import { DynamicPlugin , Plugin , FlowCtxOf , FlowHooksOf } from ' @frontmcp/sdk ' ;
import { AuthorizationPluginOptions , AuthorizationToolOptions } from ' ./authorization.types ' ;
// Get the hook decorator for list-tools flow
const ListToolsHook = FlowHooksOf ( ' tools:list-tools ' );
@ Plugin ({
name : ' authorization ' ,
description : ' Role-based access control for tools ' ,
})
export default class AuthorizationPlugin extends DynamicPlugin < AuthorizationPluginOptions > {
constructor ( protected options ?: AuthorizationPluginOptions ) {
super ();
}
/**
* Hook that runs AFTER finding tools, filtering out unauthorized ones.
* Using 'findTools' stage ensures we filter the list before it's returned.
*/
@ ListToolsHook . Did ( ' findTools ' )
async filterToolsByRole ( flowCtx : FlowCtxOf < ' tools:list-tools ' >) {
const { tools } = flowCtx . state . required ;
const { ctx : { authInfo } } = flowCtx . rawInput ;
const authorizedTools = tools . filter (({ tool }) => {
const metadata = tool . metadata ;
// If no authorization required, allow access
if (! metadata . authorization ) return true ;
const { requiredRoles } = metadata . authorization ;
const userRoles = this . extractRoles ( authInfo );
// If user has no roles, deny access to tools requiring roles
if (! userRoles || userRoles . length === 0 ) return false ;
// Check if user has ALL required roles
return requiredRoles . every (( role ) => userRoles . includes ( role ));
});
// Update the tools list with filtered results
flowCtx . state . set ( ' tools ' , authorizedTools );
}
/**
* Safely extract roles array from authInfo
*/
private extractRoles ( authInfo : unknown ): string [] {
if (! authInfo || typeof authInfo !== ' object ' ) return [];
const auth = authInfo as Record < string , unknown >;
const user = auth . user as Record < string , unknown > | undefined ;
if (! user ) return [];
const roles = user . roles ;
if ( Array . isArray ( roles )) {
return roles . map ( String );
}
return [];
}
}
Step 3: Apply to Your App
Register the plugin with your app:
import { App } from ' @frontmcp/sdk ' ;
import AuthorizationPlugin from ' ./plugins/authorization.plugin ' ;
import CreateExpenseTool from ' ./tools/create-expense.tool ' ;
import ApproveExpenseTool from ' ./tools/approve-expense.tool ' ;
import DeleteExpenseTool from ' ./tools/delete-expense.tool ' ;
@ App ({
id : ' expense ' ,
name : ' Expense App ' ,
plugins : [ AuthorizationPlugin ],
tools : [
CreateExpenseTool , // Available to all
ApproveExpenseTool , // Manager only
DeleteExpenseTool , // Admin only
],
})
export default class ExpenseApp {}
Mark tools with their required roles:
// tools/approve-expense.tool.ts
import { Tool , ToolContext } from ' @frontmcp/sdk ' ;
import { z } from ' zod ' ;
@ Tool ({
name : ' approve-expense ' ,
description : ' Approve a pending expense (managers only) ' ,
inputSchema : {
expenseId : z . string (),
comment : z . string (). optional (),
},
// Add authorization metadata
authorization : {
requiredRoles : [ ' manager ' ], // Requires manager role
},
})
export default class ApproveExpenseTool extends ToolContext {
async execute ( input : { expenseId : string ; comment ?: string }) {
// Approval logic here
return { approved : true , expenseId : input . expenseId };
}
}
// tools/delete-expense.tool.ts
import { Tool , ToolContext } from ' @frontmcp/sdk ' ;
import { z } from ' zod ' ;
@ Tool ({
name : ' delete-expense ' ,
description : ' Delete an expense (admin only) ' ,
inputSchema : {
expenseId : z . string (),
reason : z . string (),
},
authorization : {
requiredRoles : [ ' admin ' ], // Requires admin role
},
})
export default class DeleteExpenseTool extends ToolContext {
async execute ( input : { expenseId : string ; reason : string }) {
// Deletion logic here
return { deleted : true , expenseId : input . expenseId };
}
}
// tools/create-expense.tool.ts
import { Tool , ToolContext } from ' @frontmcp/sdk ' ;
import { z } from ' zod ' ;
@ Tool ({
name : ' create-expense ' ,
description : ' Create a new expense ' ,
inputSchema : {
amount : z . number (). positive (),
category : z . string (),
description : z . string (),
},
// No authorization field = available to all authenticated users
})
export default class CreateExpenseTool extends ToolContext {
async execute ( input : { amount : number ; category : string ; description : string }) {
// Creation logic here
return { created : true , id : ' exp-123 ' };
}
}
How It Works
User requests tool list
Client calls tools/list to get available tools.
Plugin hook intercepts
The ListToolsHook.Did('findTools') hook runs after tools are found but before the response is sent.
Filter by roles
The plugin checks each tool’s authorization.requiredRoles against the user’s roles from authInfo.
Return filtered list
Only tools the user is authorized to see are returned.
Advanced: Multiple Roles
Require multiple roles for sensitive operations:
@ Tool ({
name : ' bulk-delete ' ,
description : ' Bulk delete expenses ' ,
inputSchema : { expenseIds : z . array ( z . string ()) },
authorization : {
requiredRoles : [ ' admin ' , ' finance ' ], // Must have BOTH roles
},
})
export default class BulkDeleteTool extends ToolContext {
// ...
}
Advanced: OR Logic for Roles
Modify the plugin to support OR logic:
// Update types
export interface AuthorizationToolOptions {
requiredRoles : string [];
mode ?: ' all ' | ' any ' ; // 'all' = AND, 'any' = OR
}
// Update plugin
@ ListToolsHook . Did ( ' findTools ' )
async filterToolsByRole ( flowCtx : FlowCtxOf < ' tools:list-tools ' >) {
const { tools } = flowCtx . state . required ;
const { ctx : { authInfo } } = flowCtx . rawInput ;
const authorizedTools = tools . filter (({ tool }) => {
const auth = tool . metadata . authorization ;
if (! auth ) return true ;
const userRoles = this . extractRoles ( authInfo );
if (! userRoles . length ) return false ;
const { requiredRoles , mode = ' all ' } = auth ;
if ( mode === ' any ' ) {
// User needs at least ONE of the required roles
return requiredRoles . some (( role ) => userRoles . includes ( role ));
}
// Default: User needs ALL required roles
return requiredRoles . every (( role ) => userRoles . includes ( role ));
});
flowCtx . state . set ( ' tools ' , authorizedTools );
}
Usage:
@ Tool ({
name : ' view-reports ' ,
authorization : {
requiredRoles : [ ' manager ' , ' finance ' , ' executive ' ],
mode : ' any ' , // Any of these roles can access
},
})
Testing Authorization
import { test , expect } from ' @frontmcp/testing ' ;
test . use ({ server : ' ./src/main.ts ' });
test ( ' should hide admin tools from regular users ' , async ({ mcp , auth }) => {
// Authenticate as a regular user before listing tools
const token = await auth . createToken ({ sub : ' user-1 ' , claims : { roles : [ ' user ' ] } });
await mcp . authenticate ( token );
const tools = await mcp . tools . list ();
expect ( tools ). not . toContainTool ( ' delete-expense ' );
expect ( tools ). toContainTool ( ' create-expense ' );
});
test ( ' should show admin tools to admins ' , async ({ mcp , auth }) => {
// Authenticate as an admin before listing tools
const token = await auth . createToken ({ sub : ' admin-1 ' , claims : { roles : [ ' admin ' ] } });
await mcp . authenticate ( token );
const tools = await mcp . tools . list ();
expect ( tools ). toContainTool ( ' delete-expense ' );
expect ( tools ). toContainTool ( ' create-expense ' );
});
Best Practices
This plugin hides unauthorized tools from the list. Users won’t see tools they can’t use, providing a cleaner UX. For additional security, also validate roles during tool execution.
Use clear, consistent role names: // Good
requiredRoles : [ ' admin ' ]
requiredRoles : [ ' manager ' , ' finance ' ]
// Avoid
requiredRoles : [ ' ADMIN ' ] // Inconsistent casing
requiredRoles : [ ' admin-level-2 ' ] // Too specific
For sensitive applications, flip the default: // In plugin: deny if no authorization specified
if (! metadata . authorization ) {
return hasRole ( userRoles , ' authenticated ' );
}
Combine with Execution Hooks
For defense in depth, also check roles during execution: import { ToolHook , FlowCtxOf } from ' @frontmcp/sdk ' ;
@ ToolHook . Will ( ' execute ' , { priority : 900 })
async validateExecution ( flowCtx : FlowCtxOf < ' tools:call-tool ' >) {
const { tool , toolContext } = flowCtx . state ;
// Re-validate roles here
}
Next Steps
Site-Scoped Authorization Multi-tenant authorization patterns
Authentication Modes Configure authentication for your server
Plugin Development Build more custom plugins
Testing Guide Test authorization scenarios