Tutorial

SECURITY

MCP Server Security

Security context, authorization, and multi-tenant isolation documentation.

Overview

The MCP Server implements a comprehensive security model with:

  1. Multi-tenant isolation via apiKey (organization ID)
  2. User-level authorization via userId
  3. Role-based access control for organization members
  4. 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_meeting
  • create_room
  • create_schedule
  • cancel_schedule
  • send_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