How the MCP Server Exposes CRM Capabilities Without Leaking Tenant Data

The DeskcommCRM MCP server prevents cross-tenant data leakage by enforcing JWT-based authentication, organization-scoped queries via McpContext, brute-force UUID sanitization, and comprehensive audit logging that returns only sanitized results.

The MCP (Model‑Context‑Protocol) server in the DeskcommCRM repository acts as the secure gateway for exposing CRM functionality to AI agents and external callers. According to the source code, the implementation relies on a layered security model that strictly isolates tenant data while maintaining rich API capabilities. This architecture ensures that tools for managing leads, contacts, and conversations remain accessible only within the boundaries of the authenticated tenant’s organization.

Authentication and Authorization Layer

Incoming requests first pass through strict validation in lib/mcp/auth.ts. The verifyMcpToken function validates JWT tokens and extracts critical security metadata.

Two core guard functions enforce access control:

  • ensureScope: Verifies that the JWT-derived scopes contain the tool’s required scope (e.g., mcp:read)
  • ensureRole: Confirms the caller’s role matches the required permission (e.g., actor:ai_agent)

The authentication result carries the tenant’s organizationId, which becomes the foundation for all subsequent data filtering. This ensures that only authenticated tenants can invoke tools, and every call is explicitly tied to a single organization.

Tenant Isolation via McpContext

Once authenticated, the server constructs a secure execution environment defined in lib/mcp/types.ts. The McpContext object encapsulates the tenant’s identity and database access:

  • organizationId: The unique tenant identifier extracted during authentication
  • role and actor: Permission metadata for the current session
  • supabase: A Supabase admin client pre-configured for the tenant

All downstream queries in the tool handlers must filter on organization_id, guaranteeing that data retrieval is physically scoped to the caller’s tenant. The context is immutable and passed as the second argument to every tool handler, preventing accidental scope escalation.

Input Sanitization and Hygiene

Before any tool executes, raw arguments undergo sanitization in lib/mcp/uuid-de-aterro.ts. The higienizarUuidsDeAterro function removes "dump" UUIDs from input parameters.

This prevents attackers from using optional UUID filters to brute-force records belonging to other tenants. By stripping potentially malicious UUID values from query parameters, the system ensures that optional filters never become vectors for cross-tenant data leakage.

Tool Registration and Audit Trails

The createMcpServer function in lib/mcp/server.ts registers every tool with a strict contract including name, description, input schema, required scopes, and roles. Each invocation follows a secure lifecycle:

  1. Pre-execution: Arguments are sanitized and scope/role requirements are verified
  2. Execution: The tool handler receives the sanitized inputs and McpContext
  3. Auditing: The auditMcpToolCall function logs the tenant ID, tool name, sanitized arguments, execution duration, and result summary
  4. Result handling: Successful results are serialized to JSON within a uniform content array, while errors are wrapped in an isError envelope to prevent stack trace exposure

The audit records contain tenant identification and execution metadata but never include raw database rows, ensuring that logs themselves do not become leakage channels.

Implementation Example: Secure CRM Tool Creation

The following pattern demonstrates how the MCP server instantiates a secure environment per request in app/api/v1/mcp/route.ts:

import { createMcpServer } from '@/lib/mcp/server';
import { verifyMcpToken } from '@/lib/mcp/auth';

export async function GET(request: Request) {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  const authResult = await verifyMcpToken(token!);
  const server = createMcpServer(authResult, request.headers.get('x-request-id') ?? '');
  return server.handle(request);
}

Inside individual tools, such as the lead management functionality, the organizationId from McpContext enforces strict data boundaries:

export const crmListLeads = {
  name: 'crmListLeads',
  description: 'List leads belonging to the tenant',
  inputSchema: z.object({
    status: z.string().optional(),
    limit: z.number().min(1).max(100).default(20),
  }),
  requiresScope: 'mcp:read',
  requiresRole: 'actor:ai_agent',
  async handler(args, ctx) {
    const { organizationId, supabase } = ctx;
    const q = supabase.from('leads')
      .select('*')
      .eq('organization_id', organizationId)
      .limit(args.limit);
    if (args.status) q.eq('status', args.status);
    const { data } = await q;
    return data;
  },
};

The sanitization and auditing occur transparently through the server wrapper:

await auditMcpToolCall({
  ctx,
  toolName: tool.name,
  args,
  durationMs,
  success: true,
  resultSummary: summarizeResult(result),
});

Summary

  • Authentication via lib/mcp/auth.ts binds every request to a specific organizationId through JWT validation
  • Context isolation through McpContext in lib/mcp/types.ts ensures all database queries are automatically scoped to the tenant
  • Input hygiene via higienizarUuidsDeAterro in lib/mcp/uuid-de-aterro.ts prevents brute-force attacks using UUID parameters
  • Audit logging in lib/mcp/server.ts tracks all tool invocations without exposing raw database content
  • Result sanitization prevents error messages and stack traces from leaking internal system details

Frequently Asked Questions

How does the MCP server authenticate incoming requests?

The server validates JWT tokens using verifyMcpToken in lib/mcp/auth.ts. This function extracts the tenant’s organizationId, assigned scopes, and role from the token, then verifies these against the tool’s requirements using ensureScope and ensureRole before allowing execution.

What prevents a tenant from accessing another tenant's CRM data?

Every tool handler receives a McpContext object containing the authenticated organizationId. All database queries in lib/mcp/tools/* must include .eq('organization_id', organizationId), ensuring the Supabase client returns only records belonging to the requesting tenant.

How does the system protect against malicious UUID inputs?

Before processing, all tool arguments pass through higienizarUuidsDeAterro in lib/mcp/uuid-de-aterro.ts. This function sanitizes UUID parameters to prevent attackers from using optional filters to scan for records across tenant boundaries.

Where are MCP tool calls logged for security auditing?

The auditMcpToolCall function in lib/mcp/server.ts creates audit records containing the tenant ID, tool name, sanitized arguments, execution time, and result summaries. This logging occurs within the server's try/catch wrapper, ensuring every call is tracked without exposing sensitive data in the logs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →