plementation that abstracts the FavCRM MCP tools into a reusable provisioner.
Architecture Decisions & Rationale
- Two-Step Handshake: Registration is split into
register_organisation_request and register_organisation_verify. This separation enforces ownership proof before workspace creation, preventing automated abuse while keeping the initial call unauthenticated.
- No-Auth Initial Endpoint: The first tool requires no bearer token because the agent does not yet possess credentials. This is a deliberate design choice to bootstrap the trust chain.
- Masked Email Feedback: The response returns a masked email (
yo*@example.com) instead of the raw address. This prevents credential leakage in logs while giving the user enough context to locate the verification email.
- Explicit Expiry Windows: Verification codes are time-bound. The provisioner must track
expiresAt and handle expiration gracefully rather than retrying indefinitely.
- CLI Fallback Wrapper: The
favcrm CLI mirrors the MCP flow for terminal-driven automation or fallback scenarios where MCP clients are unavailable.
Implementation
import { z } from 'zod';
// Schema definitions for type safety
const InitRegistrationSchema = z.object({
email: z.string().email(),
organisationName: z.string().min(2),
industry: z.string(),
country: z.string().length(2),
timezone: z.string()
});
const VerificationSchema = z.object({
requestId: z.string().startsWith('signup_req_'),
code: z.string().length(6)
});
const ProvisionResultSchema = z.object({
organisationId: z.string(),
companyId: z.string(),
userId: z.string(),
apiKey: z.string().startsWith('fav_mcp_'),
loginUrl: z.string().url(),
nextSteps: z.string()
});
export class FavCRMProvisioner {
private mcpClient: any; // Replace with actual MCP client implementation
constructor(client: any) {
this.mcpClient = client;
}
/**
* Phase 1: Request verification code
* Sends workspace metadata to FavCRM and triggers email delivery
*/
async initiateRegistration(meta: z.infer<typeof InitRegistrationSchema>) {
const validated = InitRegistrationSchema.parse(meta);
const response = await this.mcpClient.callTool('register_organisation_request', {
email: validated.email,
organisationName: validated.organisationName,
industry: validated.industry,
country: validated.country,
timezone: validated.timezone
});
const masked = response.maskedEmail;
const expiry = new Date(response.expiresAt);
const ttlMs = expiry.getTime() - Date.now();
if (ttlMs <= 0) {
throw new Error('Registration window expired before verification could begin');
}
return {
requestId: response.requestId,
maskedEmail: masked,
expiresAt: expiry,
ttlSeconds: Math.floor(ttlMs / 1000),
instructions: response.instructions
};
}
/**
* Phase 2: Verify code and provision workspace
* Consumes the 6-digit code and returns operational credentials
*/
async confirmRegistration(requestId: string, code: string) {
const payload = VerificationSchema.parse({ requestId, code });
const response = await this.mcpClient.callTool('register_organisation_verify', payload);
const result = ProvisionResultSchema.parse(response);
// Security guard: never log the raw key
console.info(`Workspace provisioned. Key prefix: ${result.apiKey.slice(0, 8)}...`);
return {
orgId: result.organisationId,
companyId: result.companyId,
userId: result.userId,
apiKey: result.apiKey,
portalUrl: result.loginUrl,
authHeader: `Bearer ${result.apiKey}`
};
}
/**
* Phase 3: Post-provision diagnostic validation
* Verifies endpoint reachability, auth configuration, and tool availability
*/
async runDiagnostics() {
const diagnostics = await this.mcpClient.callTool('favcrm_doctor', {});
return {
endpointReachable: diagnostics.mcpEndpointStatus === 'active',
authConfigured: diagnostics.authStatus === 'configured',
availableTools: diagnostics.toolCount,
orgContext: diagnostics.organisationContext,
planStatus: diagnostics.planStatus,
whatsappConnected: diagnostics.whatsappStatus || 'unavailable'
};
}
}
Why This Structure Works
The provisioner enforces strict schema validation at each phase, preventing malformed payloads from reaching the MCP layer. The initiateRegistration method calculates remaining TTL and throws early if the window has already closed, avoiding wasted verification attempts. The confirmRegistration method strips the API key before logging, adhering to secret-handling best practices. Finally, runDiagnostics abstracts the favcrm doctor command into a structured health check, ensuring the workspace is fully operational before the agent begins CRM operations.
Pitfall Guide
Agent-native registration introduces unique failure modes that differ from traditional API integration. Below are the most common production mistakes and their resolutions.
1. Hardcoding Provisioned Keys
Explanation: Developers sometimes write the returned fav_mcp_* key directly into source files or commit it to version control.
Fix: Route the key through a secret manager (AWS Secrets Manager, HashiCorp Vault, or environment variables). The provisioner should return the key to the agent's memory layer, not to disk.
2. Ignoring Verification Window Expiry
Explanation: The email code expires at a specific timestamp. Retrying verification after expiry wastes API calls and confuses the agent's state machine.
Fix: Parse expiresAt immediately after initialization. If Date.now() >= expiresAt, automatically trigger a fresh register_organisation_request instead of retrying verification.
3. Mishandling Pre-existing Identities
Explanation: The registration flow assumes a greenfield workspace. If the email already belongs to an existing account, the verify step fails or returns a conflict.
Fix: Catch identity conflicts and route the agent to the existing merchant flow. Instruct the user to generate an MCP key from the portal Settings, or switch to a login-based authentication path.
4. Logging Masked Identifiers as Plain Text
Explanation: The maskedEmail field is designed for user reference, not for programmatic matching. Some agents attempt to use it for deduplication or logging.
Fix: Treat masked emails as display-only strings. Use the requestId for state tracking and correlation. Never store masked emails in audit logs.
5. Skipping Post-Provision Diagnostics
Explanation: Agents often assume successful verification means the workspace is fully operational. Network partitions, plan restrictions, or tool schema mismatches can still block operations.
Fix: Always run favcrm doctor (or the equivalent diagnostic tool) immediately after provisioning. Verify toolCount > 0, authStatus === 'configured', and planStatus before executing CRM operations.
Explanation: A freshly provisioned workspace may return zero services, contacts, or records. Agents sometimes interpret this as an authentication failure.
Fix: Treat empty collections as a valid initial state. The diagnostic check confirms auth success. Proceed to seed data or wait for user input.
7. Bypassing Secret Managers for Agent Context
Explanation: Some agent frameworks store credentials in plaintext memory or conversation history. This exposes keys in transcripts, screenshots, or crash dumps.
Fix: Configure the MCP client to inject the Authorization: Bearer header at runtime without persisting it in conversation logs. Use ephemeral credential injection patterns.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Fully autonomous agent workflow | MCP client registration | Programmatic, stateful, no UI dependency | Low (reduces manual intervention) |
| Terminal-driven automation or CI/CD | favcrm CLI wrapper | Scriptable, idempotent, built-in secret masking | Low (no additional infrastructure) |
| Human-assisted provisioning or troubleshooting | Portal dashboard | Visual feedback, manual key generation, plan management | Medium (requires human time) |
| High-security compliance environment | CLI + external secret manager | Audit trails, rotation policies, zero-plaintext storage | High (infrastructure overhead) |
| Rapid prototyping or local testing | MCP client with ephemeral keys | Fast iteration, disposable workspaces | Low (no long-term storage) |
Configuration Template
# .env or secret manager
FAVCRM_API_KEY=fav_mcp_XXXXXXXXXXXXXXXXXXXXXXXX
FAVCRM_ORG_ID=org_XXXXXXXXXXXX
FAVCRM_MCP_ENDPOINT=https://api.favcrm.io/mcp/v1
FAVCRM_TIMEOUT_MS=5000
FAVCRM_DIAG_INTERVAL=30000
// mcp-config.json
{
"mcpServers": {
"favcrm": {
"command": "favcrm",
"args": ["mcp", "serve"],
"env": {
"FAVCRM_API_KEY": "${FAVCRM_API_KEY}",
"FAVCRM_ORG_ID": "${FAVCRM_ORG_ID}"
},
"transport": "stdio",
"autoRestart": true,
"healthCheck": {
"enabled": true,
"interval": 30000,
"tool": "favcrm_doctor"
}
}
}
}
Quick Start Guide
- Install the CLI: Run
cargo install --path . from the cloned repository, or use your package manager to fetch the latest favcrm binary.
- Request Verification: Execute
favcrm signup request --email you@example.com --organisation-name "Your Org" --industry tech --country US --timezone America/New_York. Note the requestId and masked email.
- Verify & Provision: Run
favcrm signup verify --request-id <request-id> --code <6-digit-code>. The CLI automatically masks the key and saves it to the local config.
- Validate Setup: Run
favcrm doctor to confirm endpoint reachability, auth configuration, and tool availability. Once diagnostics pass, your agent can begin calling CRM operations with the provisioned workspace.