MCP Server Security
Security context, authorization, and multi-tenant isolation documentation.
Overview
The MCP Server implements a comprehensive security model with:
- Multi-tenant isolation via
apiKey(organization ID) - User-level authorization via
userId - Role-based access control for organization members
- Resource ownership validation for sensitive operations
Security Context
Building Security Context
Every tool call requires a security context built from the user's authentication:
import { buildSecurityContext } from '@muzie/mcp-server';
// Build from authenticated user info
const securityContext = await buildSecurityContext({
email: 'user@example.com', // User's email address
userId: 'user_example_com', // Cleaned user ID
apiKey: 'org_123456', // Organization/tenant ID
isAdmin: false, // Admin privileges
timezone: 'America/New_York' // User's timezone
});
Security Context Structure
{
userEmail: 'user@example.com', // Original email
userId: 'user_example_com', // Normalized user ID
apiKey: 'org_123456', // Organization key
isOrgOwner: true, // Is organization owner
orgUserIds: [ // Accessible user IDs
'user_example_com',
'colleague_example_com'
],
isAdmin: false, // Admin status
timezone: 'America/New_York' // Timezone
}
User ID Normalization
User IDs are normalized from email addresses:
import { cleanUserId } from '@muzie/mcp-server';
const userId = cleanUserId('User@Example.com');
// Returns: 'user_example_com'
Rules:
- Lowercase the email
- Replace
@with_ - Replace
.with_ - Remove special characters
Multi-Tenant Isolation
How It Works
Every data query is automatically filtered by apiKey:
// Internal query building
const query = {
apiKey: securityContext.apiKey, // Tenant isolation
// ... other filters
};
Organization Structure
Organization (apiKey: 'org_123')
│
├── Owner (roles: ['org-owner'])
│ └── Can access ALL organization data
│
├── Members (roles: ['org-member'])
│ └── Can access own data + shared resources
│
└── Team Members (roles: ['team-member'])
└── Can access team data + assigned resources
Determining Organization Membership
// Check if user is organization owner
if (user.roles?.includes('org-owner')) {
// Full organization access
}
// Check if user is organization member
if (user.roles?.includes('org-member') || user.roles?.includes('team-member')) {
// Member-level access
// user.orgOwner points to org owner's userId
}
Authorization Checks
Room Access
import { authorization } from '@muzie/mcp-server';
const { validateRoomAccess, getAccessibleRoomIds } = authorization;
// Get all rooms user can access
const roomIds = await getAccessibleRoomIds(userId, apiKey);
// Validate specific room access
const hasAccess = await validateRoomAccess(roomId, userContext);
Schedule Access
const { validateScheduleAccess, validateScheduleModifyAccess } = authorization;
// Read access
const canView = await validateScheduleAccess(scheduleId, userContext);
// Modify access (owner or organizer only)
const canModify = await validateScheduleModifyAccess(scheduleId, userContext);
Applying Authorization Filters
const { applyAuthorizationFilters } = authorization;
// Apply filters to a MongoDB query
const filteredQuery = await applyAuthorizationFilters(baseQuery, securityContext);
// Adds: { apiKey: '...', $or: [{ owner: userId }, { participants: userId }] }
Authorization Error Handling
MCPAuthorizationError
Custom error for authorization failures:
import { MCPAuthorizationError } from '@muzie/mcp-server';
// Throwing an authorization error
if (!hasAccess) {
throw new MCPAuthorizationError('Access denied to room', {
userId: userContext.userId,
resource: 'room_123',
action: 'read',
reason: 'Not a participant'
});
}
// Catching authorization errors
try {
const result = await toolHandlers.get_transcript(params);
} catch (error) {
if (error instanceof MCPAuthorizationError) {
// Handle authorization failure
console.error('Access denied:', error.message);
return { success: false, error: 'Access denied' };
}
throw error; // Re-throw other errors
}
Error Response Format
{
success: false,
error: 'Access denied',
code: 'ACCESS_DENIED',
details: {
userId: 'user_example_com',
resource: 'room_123',
action: 'read'
}
}
Access Control Matrix
Data Query Tools
| Tool | Access Level | Filter Applied |
|---|---|---|
query_rooms |
Own + Participant | apiKey + (owner OR participant) |
query_schedules |
Own + Attendee | apiKey + (organizer OR attendee) |
query_stored_rooms |
Own | apiKey + owner |
query_files |
Room participant | apiKey + room access |
query_recordings |
Room participant | apiKey + room access |
get_transcript |
Recording access | apiKey + recording access |
Action Tools
| Tool | Required Access |
|---|---|
start_meeting |
Organization member |
create_room |
Organization member |
create_schedule |
Organization member |
cancel_schedule |
Schedule owner/organizer |
send_invites |
Schedule owner/organizer |
Graph Tools
| Tool | Access Level |
|---|---|
find_collaborators |
Own network |
get_person_meetings |
Shared meetings only |
get_graph_stats |
Own data |
Organization Owner Access
Organization owners have elevated access:
// In security context building
if (user.roles?.includes('org-owner')) {
securityContext.isOrgOwner = true;
// Get all organization user IDs
securityContext.orgUserIds = await getOrganizationUserIds(apiKey);
}
Owners can:
- Access all organization data
- View all members' meetings
- Manage rooms created by any member
- View organization-wide analytics
Implementation Patterns
Securing a New Tool
async function myNewTool(params) {
const { apiKey, userId, _userContext } = params;
// 1. Validate security context exists
if (!_userContext) {
return { success: false, error: 'Security context required' };
}
// 2. Build base query with tenant isolation
const query = { apiKey };
// 3. Apply user-level filters
if (!_userContext.isOrgOwner) {
query.owner = userId; // Only own resources
}
// 4. Execute query
const results = await Model.find(query);
return { success: true, data: results };
}
Checking Resource Ownership
async function updateResource(params) {
const { resourceId, updates, _userContext } = params;
// Fetch the resource
const resource = await Resource.findById(resourceId);
if (!resource) {
return { success: false, error: 'Not found' };
}
// Check ownership
const isOwner = resource.owner === _userContext.userId;
const isOrgOwner = _userContext.isOrgOwner;
if (!isOwner && !isOrgOwner) {
throw new MCPAuthorizationError('Only owner can update', {
userId: _userContext.userId,
resource: resourceId,
action: 'update'
});
}
// Proceed with update
await resource.updateOne(updates);
return { success: true };
}
Validating Participant Access
async function accessRoomData(roomId, userContext) {
const room = await Room.findById(roomId);
if (!room) {
return { success: false, error: 'Room not found' };
}
// Check tenant
if (room.apiKey !== userContext.apiKey) {
throw new MCPAuthorizationError('Wrong organization');
}
// Check participation
const isOwner = room.owner === userContext.userId;
const isParticipant = room.participants?.includes(userContext.userId);
const isOrgOwner = userContext.isOrgOwner;
if (!isOwner && !isParticipant && !isOrgOwner) {
throw new MCPAuthorizationError('Not a participant');
}
return { success: true, room };
}
Token-Based Authentication
MCP Tokens for External Access
For external API access (Claude Desktop, third-party integrations):
// Create MCP token
const token = await dbConnection.createMCPToken(
'creator@example.com',
'org_123',
{
name: 'Claude Desktop',
permissions: ['mcp:data:read', 'mcp:search:execute'],
expiresAt: new Date('2026-12-31')
}
);
// Token validation in middleware
const validated = await dbConnection.validateMCPToken(rawToken);
// Returns: { valid, tokenId, brandOrgId, permissions, ... }
Permission Mapping
| Permission | Allowed Tools |
|---|---|
mcp:data:read |
query_rooms, query_schedules, get_transcript, etc. |
mcp:search:execute |
search_content |
mcp:intelligence:read |
summarize_transcript, query_meeting_context |
mcp:analytics:read |
find_collaborators, get_graph_stats |
mcp:ai:query |
summarize_text, extract_info, answer_question |
Blocked Actions
These actions are blocked for external token access:
start_meetingcreate_roomcreate_schedulecancel_schedulesend_invites
Security Best Practices
1. Always Use Security Context
// CORRECT
const result = await toolHandlers.query_rooms({
...params,
_userContext: securityContext
});
// WRONG - bypasses security
const result = await toolHandlers.query_rooms(params);
2. Validate Before Acting
// Always validate access before modifying
const hasAccess = await validateScheduleModifyAccess(scheduleId, userContext);
if (!hasAccess) {
return { success: false, error: 'Access denied' };
}
3. Log Security Events
import { logger } from '@muzie/mcp-server';
logger.warn('Authorization failure', {
userId: userContext.userId,
resource: resourceId,
action: 'delete',
reason: 'Not owner'
});
4. Never Trust Client Input
// WRONG - trusts client-provided apiKey
const result = await query({ apiKey: params.apiKey });
// CORRECT - uses authenticated context
const result = await query({ apiKey: securityContext.apiKey });
Related Documentation
- Getting Started - Quick start guide
- API Reference - Complete API documentation
- Tools Reference - All primitive tools
- Architecture - System design
- Examples - Integration examples