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 authenticationroleandactor: Permission metadata for the current sessionsupabase: 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:
- Pre-execution: Arguments are sanitized and scope/role requirements are verified
- Execution: The tool handler receives the sanitized inputs and
McpContext - Auditing: The
auditMcpToolCallfunction logs the tenant ID, tool name, sanitized arguments, execution duration, and result summary - Result handling: Successful results are serialized to JSON within a uniform
contentarray, while errors are wrapped in anisErrorenvelope 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.tsbinds every request to a specificorganizationIdthrough JWT validation - Context isolation through
McpContextinlib/mcp/types.tsensures all database queries are automatically scoped to the tenant - Input hygiene via
higienizarUuidsDeAterroinlib/mcp/uuid-de-aterro.tsprevents brute-force attacks using UUID parameters - Audit logging in
lib/mcp/server.tstracks 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →