Memo
Tool-Level Security for Remote MCP Servers
The Model Context Protocol (MCP) has emerged as a powerful standardized framework for AI-to-tool communication, enabling more sophisticated interactions between LLMs and external systems. As organizations deploy MCP servers in production environments, implementing robust access control becomes essential to protect sensitive data and operations while enabling the right level of access for different user groups.
This guide explores how to implement Role-Based Access Control (RBAC) for MCP servers, allowing you to grant precisely the right level of access to each user or system while maintaining strong security boundaries around your tools and data.
TL;DR: Implementing RBAC for MCP servers
Role-Based Access Control for MCP servers enhances OAuth authentication by associating tools with permissions and applying data access policies during execution. The server filters available tools based on user roles and applies data access constraints, ensuring users can only access authorized tools and data. This approach secures both connection establishment and each individual tool invocation.
// Tool registry with permission requirements
const toolRegistry = {
"slack_post_message": {
tool: slackPostMessageTool,
requiredPermissions: ["slack:write"],
dataAccessPolicy: { channelVisibility: "authorized_only" }
}
};
// Filter tools during ListToolsRequest
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
const userPermissions = await getPermissionsForUser(request.transport.session.userId);
return {
tools: Object.values(toolRegistry)
.filter(t => t.requiredPermissions.every(p => userPermissions.includes(p)))
.map(t => t.tool)
};
});
// Enforce permissions during CallToolRequest
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const userId = request.transport.session.userId;
const toolEntry = toolRegistry[request.params.name];
if (!hasRequiredPermissions(userId, toolEntry.requiredPermissions)) {
return errorResponse("Insufficient permissions");
}
const filteredData = await applyDataAccessPolicy(
toolEntry.dataAccessPolicy, request.params.arguments, userId
);
return await executeTool(request.params.name, filteredData);
});
The need for tool-level access control
While our previous guide covered securing the MCP connection itself through OAuth 2.1 and Bearer token authentication, production systems require deeper security controls that operate at the tool invocation level. This multi-layered security approach addresses several critical requirements for modern AI systems integrating with powerful backend capabilities.
Production MCP servers require granular permission management that allows different users or applications to access specific subsets of available tools based on their responsibilities and authorization level. These servers must also implement data privacy protection since tools often expose sensitive data that should only be accessible to properly authorized users. Proper regulatory compliance becomes essential as many organizations operate under strict data protection regulations like GDPR, HIPAA, or CCPA that mandate precise controls over data access. Finally, the principle of least privilege embodied in operational security dictates that users should only have access to the minimum set of tools needed to perform their tasks.
MCP servers often serve as gateways to powerful capabilities—from querying databases and accessing internal knowledge bases to modifying production systems or sending authenticated messages. Without proper access controls, an authenticated but malicious user could potentially access sensitive information or perform unauthorized actions that extend far beyond their intended privileges.
Security architecture for tool-level access control
Building upon the OAuth authentication framework described in our previous guide, we need to implement a comprehensive RBAC system that operates across multiple dimensions of security. The foundation begins with role definitions – named collections of permissions such as “Admin,” “Developer,” or “Analyst” that map to organizational responsibilities. These roles contain permissions that represent fine-grained access controls mapped to specific tool operations and data access patterns.
flowchart TB
subgraph "Security Perimeter"
direction TB
subgraph "Network Security"
FW[Firewall] --> VPN[VPN Gateway]
VPN --> LB[Load Balancer]
end
subgraph "MCP Server"
LB --> OA[OAuth Authentication]
OA --> SA[Session Authorization]
SA --> TR[Tool Registry]
end
subgraph "Tool Access Control"
TR --> TE{Tools Endpoint}
TE --> |List Request| PF[Permission Filter]
TE --> |Call Request| PC[Permission Checker]
PC --> |Authorized| DAP[Data Access Policy]
PC --> |Unauthorized| RJ[Reject Request]
end
subgraph "Backend Resources"
DAP --> |Filtered Request| BE[Backend Services]
BE --> |Raw Response| DF[Data Filter]
DF --> |Filtered Response| RES[Response Handler]
end
end
Client[Client AI System] <--> FW
RES --> Client
style OA fill:#f96,stroke:#333,stroke-width:2px,color:black
style SA fill:#f96,stroke:#333,stroke-width:2px,color:black
style PF fill:#f9f,stroke:#333,stroke-width:2px,color:black
style PC fill:#f9f,stroke:#333,stroke-width:2px,color:black
style DAP fill:#f9f,stroke:#333,stroke-width:2px,color:black
style DF fill:#f9f,stroke:#333,stroke-width:2px,color:black
At the heart of this system sits the tool registry, a central configuration that maps each MCP tool to its required permissions and data access policies. This registry serves as the single source of truth for all permission checks throughout the system. When tools are requested or executed, permission enforcement applies runtime checks to ensure the requesting user has sufficient authorization for the attempted operation. Beyond simply allowing or denying access, data access policies implement row-level security and field-level filtering to ensure users only see data elements they’re authorized to access, even within results from allowed tools.
This architecture creates a defense-in-depth approach where multiple security layers work in concert. Initially, OAuth authentication establishes the user’s identity with confidence. Once authenticated, role assignments determine which permissions the user holds within the system. During operation, permission checks filter which tools are exposed to the user through the ListTools endpoint. Finally, when tools are executed, data access policies restrict which specific data elements are visible within the tool results.
Building the role-based security system
Implementing effective role-based security for MCP requires careful design of both the data structures and runtime enforcement mechanisms. The security model must balance flexibility, performance, and maintainability while providing robust protection across diverse deployment environments.
The data foundation of RBAC
The foundation of our security model lies in a carefully designed data structure that captures the relationships between users, roles, and permissions. These relationships establish who can access what within the MCP environment. We’ll implement a standard relational model that follows established RBAC patterns, making it easy to integrate with existing identity systems.
In this model, we create separate tables for users, roles, and permissions, with junction tables mapping the many-to-many relationships between them. The users table captures identity information for authenticated users, while the roles table defines named responsibility sets like “Admin” or “Analyst.” The permissions table defines granular access rights such as “slack:read” or “analytics:execute” that can be combined into roles. The user_roles table establishes which users have which roles, while role_permissions maps which permissions are included in each role.
erDiagram
USERS {
uuid id PK
string email
string name
timestamp created_at
timestamp last_login
}
ROLES {
uuid id PK
string name
string description
timestamp created_at
}
PERMISSIONS {
uuid id PK
string name
string description
string resource_type
string action
timestamp created_at
}
USER_ROLES {
uuid user_id FK
uuid role_id FK
uuid granted_by FK
timestamp granted_at
}
ROLE_PERMISSIONS {
uuid role_id FK
uuid permission_id FK
}
RESOURCE_ACCESS_RULES {
uuid id PK
string resource_type
string resource_id
uuid role_id FK
string access_level
}
SECURITY_AUDIT_LOG {
uuid id PK
string action_type
uuid actor_id FK
string target_type
uuid target_id
json details
timestamp performed_at
}
TOOLS {
uuid id PK
string name
string description
json input_schema
json security_metadata
}
TOOL_PERMISSIONS {
uuid tool_id FK
uuid permission_id FK
}
USERS ||--o{ USER_ROLES : "has"
ROLES ||--o{ USER_ROLES : "assigned to"
ROLES ||--o{ ROLE_PERMISSIONS : "includes"
PERMISSIONS ||--o{ ROLE_PERMISSIONS : "granted to"
ROLES ||--o{ RESOURCE_ACCESS_RULES : "controls access to"
USERS ||--o{ SECURITY_AUDIT_LOG : "performs"
TOOLS ||--o{ TOOL_PERMISSIONS : "requires"
PERMISSIONS ||--o{ TOOL_PERMISSIONS : "enables"
Beyond basic role-permission mapping, the resource_access_rules table implements fine-grained control over specific resources. This table allows us to define which roles can access particular data elements, implementing row-level security across the system. For example, we can specify that the “Sales” role can only view Slack channels in the sales department, while the “Engineering” role can access engineering channels.
-- Core security model for RBAC in MCP
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMPTZ
);
CREATE TABLE roles (
id UUID PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE permissions (
id UUID PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT,
resource_type TEXT NOT NULL,
action TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(resource_type, action)
);
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
granted_by UUID REFERENCES users(id),
granted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE resource_access_rules (
id UUID PRIMARY KEY,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
access_level TEXT NOT NULL,
UNIQUE(resource_type, resource_id, role_id)
);
This schema provides a solid foundation for implementing RBAC while allowing for flexible extensions to meet specific organizational needs. For audit purposes, we can also implement a change history table that tracks modifications to permissions and roles over time:
CREATE TABLE security_audit_log (
id UUID PRIMARY KEY,
action_type TEXT NOT NULL, -- 'grant_role', 'revoke_role', 'create_permission', etc.
actor_id UUID REFERENCES users(id),
target_type TEXT NOT NULL, -- 'user', 'role', 'permission'
target_id UUID NOT NULL,
details JSONB,
performed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
The tool registry: Mapping capabilities to permissions
With our data structures in place, we now need to establish the connection between MCP tools and the permissions required to access them. The tool registry serves as the central configuration that maps each tool to its required permissions and data access policies. This registry becomes the single source of truth for all permission checks throughout the system.
The tool registry extends beyond the standard MCP tool definitions to include security metadata for each tool. For each tool entry, we maintain the standard tool definition including name, description, and input schema, but we augment this with two critical security properties: requiredPermissions and dataAccessPolicy.
The requiredPermissions property defines an array of permission identifiers that a user must possess to access the tool. For example, the Slack message posting tool requires the “slack:write” permission, while a knowledge base search tool might require “knowledge:read” permission. The system enforces an “AND” relationship for these permissions – users must have all the listed permissions to access the tool.
The dataAccessPolicy property defines more granular constraints on the data the tool can access. These policies vary by tool type but often include visibility rules for specific resources. For instance, a Slack channel listing tool might include a “channelVisibility” policy that restricts which channels a user can see based on their role assignments. Similarly, an analytics tool might include dataset and column visibility rules that filter results based on user permissions.
// Tool registry with security metadata (simplified example)
const toolRegistry = {
"slack_list_channels": {
tool: {
name: "slack_list_channels",
description: "List public channels in the workspace with pagination",
inputSchema: { /* schema definition */ }
},
requiredPermissions: ["slack:read"],
dataAccessPolicy: {
channelVisibility: "authorized_only"
}
},
"slack_post_message": {
tool: {
name: "slack_post_message",
description: "Post a new message to a Slack channel",
inputSchema: { /* schema definition */ }
},
requiredPermissions: ["slack:write"],
dataAccessPolicy: {
channelVisibility: "authorized_only"
}
},
"knowledge_search": {
tool: {
name: "knowledge_search",
description: "Search the organization's knowledge base",
inputSchema: { /* schema definition */ }
},
requiredPermissions: ["knowledge:read"],
dataAccessPolicy: {
documentVisibility: "role_based"
}
},
"data_analytics": {
tool: {
name: "data_analytics",
description: "Run analytics queries on organizational data",
inputSchema: { /* schema definition */ }
},
requiredPermissions: ["analytics:read"],
dataAccessPolicy: {
datasetVisibility: "role_based",
columnVisibility: "role_based"
}
}
}
Permission enforcement in server implementation
The most critical aspect of our RBAC implementation lies in the server-side enforcement of permissions. We need to modify the standard MCP server implementation to integrate permission checks at two key points: when listing available tools and when executing tool requests.
When handling a ListTools request, the server needs to filter the available tools based on the user’s permissions. This ensures that users only see tools they’re authorized to access. This filtering happens transparently to the client, creating a seamless experience where unauthorized tools simply don’t exist from the user’s perspective.
// Permission enforcement during tool listing
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// Extract user identity from the validated token
const userId = request.transport.session.userId;
// Determine user's permissions based on their roles
const userRoles = await getUserRoles(userId);
const userPermissions = await getAllPermissionsForRoles(userRoles);
// Filter tools based on user permissions
const authorizedTools = Object.values(toolRegistry)
.filter(toolEntry => {
// User must have ALL required permissions for this tool
return toolEntry.requiredPermissions.every(
permission => userPermissions.includes(permission)
);
})
.map(toolEntry => toolEntry.tool);
return { tools: authorizedTools };
});
When handling a CallTool request, the server performs an additional permission check even if the tool was previously exposed in the listing. This defense-in-depth approach prevents unauthorized access even if a client attempts to directly call tools they shouldn’t access. Beyond the basic permission check, the system also applies data access policies that filter both the arguments provided to the tool and the results returned to the user.
// Permission enforcement during tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const toolEntry = toolRegistry[toolName];
if (!toolEntry) {
return createErrorResponse(`Tool not found: ${toolName}`);
}
// Get user identity and permissions
const userId = request.transport.session.userId;
const userRoles = await getUserRoles(userId);
const userPermissions = await getAllPermissionsForRoles(userRoles);
// Verify permissions for the requested tool
const hasPermission = toolEntry.requiredPermissions.every(
permission => userPermissions.includes(permission)
);
if (!hasPermission) {
// Audit the access attempt
await logSecurityEvent(userId, "unauthorized_tool_access", toolName);
return createErrorResponse("Insufficient permissions");
}
// Apply data access policies to filter tool arguments
const filteredArguments = await applyDataAccessPolicy(
toolEntry.dataAccessPolicy,
request.params.arguments,
userId,
userRoles
);
// Execute the tool with filtered arguments
const result = await executeTool(toolName, filteredArguments);
// Apply data access policies to filter the results
const filteredResult = await filterToolResults(
result,
toolEntry.dataAccessPolicy,
userId,
userRoles
);
return {
content: [{ type: "text", text: JSON.stringify(filteredResult) }]
};
});
Data filtering and access policies
The most sophisticated part of our RBAC implementation is the data filtering system that enforces fine-grained access control on the data processed by tools. This system applies filtering at two points: when processing tool arguments and when returning tool results.
For argument filtering, the system examines resource identifiers in the request to ensure the user has access to the referenced resources. For example, if a user attempts to post a message to a Slack channel they don’t have access to, the system will reject the request before it reaches the underlying Slack client.
// Filter tool arguments based on data access policies
async function applyDataAccessPolicy(policy, args, userId, userRoles) {
if (!policy) return args;
// Create a copy of arguments to avoid modifying the original
const filteredArgs = { ...args };
// Check Slack channel access if relevant
if (policy.channelVisibility === "authorized_only" && filteredArgs.channel_id) {
const hasAccess = await checkChannelAccess(userId, filteredArgs.channel_id);
if (!hasAccess) {
throw new Error(`Access denied to channel: ${filteredArgs.channel_id}`);
}
}
// Additional policy rules would be applied here based on tool type
return filteredArgs;
}
For result filtering, the system applies similar checks but operates on the data returned from the tool. This filtering can be quite sophisticated, removing specific documents from knowledge search results or filtering columns and rows from analytics query results based on user permissions.
// Filter knowledge base documents by access permissions
async function filterDocumentsByAccess(documents, userId, userRoles) {
if (!documents || documents.length === 0) return documents;
// Query accessible documents based on user roles
const accessibleDocumentIds = await getAccessibleDocumentIds(userRoles);
// Filter documents to include only those the user can access
return documents.filter(doc => accessibleDocumentIds.has(doc.id));
}
// Filter analytics results by column permissions
async function filterColumnsByAccess(results, userId, userRoles) {
if (!results.columns || !results.rows) return results;
// Determine which columns the user has permission to see
const accessibleColumns = await getAccessibleColumns(results.dataset, userRoles);
// Create a filtered view of the results
const columnIndexes = results.columns
.map((col, index) => accessibleColumns.has(col) ? index : -1)
.filter(idx => idx !== -1);
return {
columns: results.columns.filter((_, idx) => columnIndexes.includes(idx)),
rows: results.rows.map(row => columnIndexes.map(idx => row[idx]))
};
}
Integrating with existing identity systems
Most organizations deploying MCP servers already have established identity systems, whether traditional Active Directory, cloud-based identity providers like Auth0 or Okta, or custom OAuth servers. Our RBAC implementation needs to integrate with these systems rather than creating a completely independent security infrastructure.
The core integration approach involves using the existing identity system for authentication while maintaining an MCP-specific permission model for authorization. When a user connects to the MCP server, the OAuth flow confirms their identity using the established identity provider. Once authenticated, the server maps the external identity to internal roles and permissions that control MCP tool access.
This mapping can occur through various mechanisms, depending on the identity provider’s capabilities. For providers that support scopes or custom claims in tokens, we can extract role information directly from the authentication token. For simpler providers, we may need to maintain a mapping table that associates external user identities with our internal role assignments.
// Extract roles from an external identity token
async function getRolesFromExternalToken(token) {
try {
// Decode and verify the token
const decodedToken = await verifyToken(token);
// Extract roles from token claims
// This varies by identity provider - some use custom claims
if (decodedToken.roles) {
return decodedToken.roles;
}
if (decodedToken.scope) {
// Parse space-separated scopes
const scopes = decodedToken.scope.split(' ');
return scopes.filter(s => s.startsWith('role:'))
.map(s => s.substring(5));
}
// If no roles in token, fall back to database mapping
return await getRolesFromDatabase(decodedToken.sub);
} catch (error) {
console.error('Error extracting roles from token:', error);
return [];
}
}
This federated approach allows organizations to maintain a single source of truth for identity while still implementing fine-grained control over MCP tool access. Changes to user responsibilities in the primary identity system can automatically flow through to MCP access permissions, ensuring consistency across the organization’s security infrastructure.
Practical implementation strategies
Implementing RBAC for MCP involves more than just coding the technical components. Successful deployments require careful planning and strategy to ensure the security model aligns with organizational needs while remaining maintainable over time.
Start with a comprehensive inventory
The first step in implementing RBAC is creating a comprehensive inventory of tools, data resources, and access patterns. This inventory should identify the sensitivity level of each tool and the data it accesses, providing the foundation for designing appropriate permission boundaries. Engage with stakeholders across the organization to understand who needs access to which capabilities and under what circumstances.
For each MCP tool, document its purpose, the data it accesses or modifies, and the operational risk associated with its use. Group tools with similar risk profiles and access patterns to begin defining your permission model. This inventory becomes the reference for designing your role structure and permission assignments.
Design role hierarchies with inheritance
Rather than creating a flat list of roles, design hierarchical role structures that leverage inheritance to simplify permission management. Create base roles that provide fundamental access needed by most users, then extend these with specialized roles that grant additional permissions for specific functions.
For example, a “StandardUser” role might provide access to basic knowledge search capabilities, while a “DataAnalyst” role inherits those permissions and adds access to analytics tools. This approach reduces redundancy in permission assignments and makes it easier to maintain consistency as your permission model evolves.
Implement progressive access controls
Security should operate as a progressive series of checks that become more specific as operations proceed. The initial OAuth authentication confirms basic identity and authorization. The ListTools handler filters available tools based on user roles. The CallTool handler verifies specific permissions for the requested tool. The data access policies apply fine-grained filtering to the specific data elements being accessed.
This progressive approach ensures that security failures occur as early as possible in the request lifecycle, improving both security and performance. It also creates multiple layers of defense, ensuring that a single vulnerability won’t compromise your entire security model.
Establish comprehensive audit trails
Robust security requires visibility into how your system is being accessed and used. Implement comprehensive audit logging that captures key security events like authentication attempts, permission checks, and sensitive data access. These logs should include sufficient context to understand who performed what action and whether it succeeded or failed.
// Log a security event
async function logSecurityEvent(userId, eventType, details, success = true) {
await db.query(
`INSERT INTO security_events (user_id, event_type, details, success, timestamp)
VALUES ($1, $2, $3, $4, NOW())`,
[userId, eventType, JSON.stringify(details), success]
);
}
These audit trails serve multiple purposes: they help detect security incidents, support compliance requirements, and provide data for refining your security model over time. Store security logs securely and develop processes for regular review and analysis.
Security considerations and best practices
Implementing RBAC for MCP servers requires attention to several critical security considerations beyond the basic role and permission model.
Defense in depth
While RBAC provides powerful access controls, it should be part of a comprehensive security strategy that includes multiple defensive layers. Ensure your MCP servers implement network security through firewalls and VPNs, transport security through proper TLS configuration, and operational security through monitoring and alerting.
Never rely on a single security mechanism to protect sensitive systems. Even with perfect RBAC implementation, additional controls like network isolation, request rate limiting, and anomaly detection remain essential to a robust security posture.
Principle of least privilege
The principle of least privilege dictates that users should have only the minimum access needed to perform their responsibilities. When designing your permission model, start with minimal access and add specific permissions as needed rather than starting with broad access and attempting to restrict it.
Regularly review permission assignments to identify and remove unnecessary access rights. Implement time-bound permissions for temporary access needs rather than granting permanent permissions that must be manually revoked later.
Regular security reviews
Security is not a one-time implementation but an ongoing process. Schedule regular reviews of your RBAC model to ensure it remains aligned with organizational needs and security best practices. These reviews should examine role definitions, permission assignments, and actual usage patterns.
Look for common issues like permission creep (accumulation of unnecessary permissions), orphaned permissions (access rights no longer used by any role), and role explosion (proliferation of overly specific roles that complicate management).
Data minimization and field-level security
Beyond controlling which tools users can access, implement data minimization practices that limit the exposure of sensitive information. Apply field-level security to filter out sensitive data elements that users don’t need to see, even if they have access to the related tool.
For example, a user might have permission to search the knowledge base, but certain document fields like “internal notes” might be hidden from their view. Similarly, analytics results might mask specific columns containing sensitive business metrics based on the user’s role.
Wrap-up
Implementing Role-Based Access Control for MCP servers creates a secure foundation for AI-to-tool communication in production environments. By controlling not just which users can connect to your MCP server but also which tools they can access and what data they can see, you establish precise security boundaries that protect sensitive resources while enabling powerful capabilities for authorized users.
The multi-layered approach described in this guide—combining authentication, permission-based tool filtering, and data access policies—provides comprehensive protection aligned with security best practices. By integrating with existing identity systems and implementing proper audit trails, you can maintain security while leveraging your organization’s established infrastructure.
As MCP adoption continues to grow, robust security controls become increasingly essential to realizing its full potential in enterprise environments. By implementing these patterns early, you establish a foundation that can evolve with your organization’s needs while maintaining appropriate security boundaries.
Mentioned in
Subscribe to Dwarves Memo
Receive the latest updates directly to your inbox.
