Flowgenie — Excellence In Technology
MCPZohoAI AgentsSecurityIntegration

MCP + Zoho: Connecting Live Business Data to AI Agents Securely

Mahesh Ramala·8 min read·

How to build a production-ready MCP server that connects Claude AI agents to live Zoho CRM, Zoho Books, and Zoho Projects data — with field-level access control, audit logging, and secure credential management.

Building something like this?

I implement AI agents, Zoho automation & MCP integrations — end to end.

The combination of MCP (Model Context Protocol) and Zoho One is one of the most powerful AI integrations available to small and medium businesses. Your Zoho suite holds your live customer data, financial records, project status, and operational metrics. An MCP server gives Claude AI direct, controlled access to all of it — without requiring custom code for every new AI workflow.

This is how I build it for clients.

Why MCP Is Better Than Direct API Calls

The naïve approach is to write tool definitions that call the Zoho API directly inside the Claude agent. It works, but it has problems:

  • API credentials duplicated across multiple agents
  • No central logging of what data was accessed
  • Changes to the Zoho API require updates to every agent
  • No way to enforce access control between agents
  • Rate limiting is per-agent, not coordinated

An MCP server solves all of this. One server, one set of credentials, centralised logging, and every AI agent gets a consistent interface to Zoho data.

Zoho OAuth Setup

Zoho uses OAuth 2.0 for API access. For an MCP server (server-to-server, no user interaction), you need a self-client with a long-lived refresh token.

Step 1: Register a Self-Client

Go to api-console.zoho.com, select Self Client, and generate a code with the scopes you need:

ZohoCRM.modules.ALL
ZohoBooks.contacts.READ
ZohoBooks.invoices.READ
ZohoProjects.portals.READ
ZohoProjects.projects.READ

Only request the scopes your MCP server actually needs. The principle of least privilege applies here too.

Step 2: Exchange for Refresh Token

curl -X POST https://accounts.zoho.com.au/oauth/v2/token \
  -d "code=YOUR_AUTH_CODE" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "redirect_uri=https://api-console.zoho.com/v2/clientSecret" \
  -d "grant_type=authorization_code"

Save the refresh_token. This is your long-lived credential — treat it like a password.

Step 3: Store in AWS Secrets Manager

aws secretsmanager create-secret \
  --name "production/mcp-zoho/credentials" \
  --secret-string '{
    "client_id": "1000.xxxx",
    "client_secret": "xxxxxxxx",
    "refresh_token": "1000.xxxxxxxx",
    "datacenter": "au"
  }'

The MCP server retrieves these at startup and manages token refresh automatically.

Building the Zoho MCP Server

Here's the architecture of a production Zoho MCP server:

┌─────────────────────────────────────────────┐
│              Zoho MCP Server                 │
│                                              │
│  ┌──────────────┐    ┌───────────────────┐  │
│  │ MCP Protocol │    │  Zoho API Client  │  │
│  │  Handler     │───▶│  (with auto-      │  │
│  └──────────────┘    │   token refresh)  │  │
│                      └───────────────────┘  │
│  ┌──────────────┐              │            │
│  │ Access       │              ▼            │
│  │ Control      │    ┌───────────────────┐  │
│  │ Layer        │    │  Rate Limiter     │  │
│  └──────────────┘    └───────────────────┘  │
│                                │            │
│  ┌──────────────┐              ▼            │
│  │ Audit Logger │    ┌───────────────────┐  │
│  └──────────────┘    │  Zoho REST APIs   │  │
│                      │  CRM / Books /    │  │
│                      │  Projects         │  │
│                      └───────────────────┘  │
└─────────────────────────────────────────────┘

Token Management

The Zoho access token expires every hour. The MCP server must handle refresh automatically:

class ZohoTokenManager {
  private accessToken: string | null = null;
  private expiresAt: number = 0;

  async getAccessToken(): Promise<string> {
    if (this.accessToken && Date.now() < this.expiresAt - 60_000) {
      return this.accessToken; // Use cached token if valid for > 1 min
    }
    return this.refreshAccessToken();
  }

  private async refreshAccessToken(): Promise<string> {
    const creds = await getSecretsManagerCredentials();

    const response = await fetch(
      `https://accounts.zoho.${creds.datacenter}/oauth/v2/token`,
      {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          refresh_token: creds.refresh_token,
          client_id: creds.client_id,
          client_secret: creds.client_secret,
          grant_type: "refresh_token",
        }),
      }
    );

    const data = await response.json();
    this.accessToken = data.access_token;
    this.expiresAt = Date.now() + data.expires_in * 1000;

    return this.accessToken;
  }
}

Defining the Tools

Structure your tools around business questions, not API endpoints:

const ZOHO_TOOLS = [
  {
    name: "search_contacts",
    description: "Search for contacts in Zoho CRM by name, email, company, or phone number. Returns contact details and recent activity summary.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Search term (name, email, company, or phone)" },
        limit: { type: "number", description: "Maximum results to return (1-10, default 5)" },
      },
      required: ["query"],
    },
  },
  {
    name: "get_contact_deals",
    description: "Get all active deals associated with a specific contact, including deal stage, value, and expected close date.",
    inputSchema: {
      type: "object",
      properties: {
        contact_id: { type: "string", description: "Zoho CRM contact ID" },
      },
      required: ["contact_id"],
    },
  },
  {
    name: "get_overdue_invoices",
    description: "Retrieve all overdue invoices from Zoho Books, optionally filtered by customer. Returns invoice number, amount, due date, and days overdue.",
    inputSchema: {
      type: "object",
      properties: {
        customer_name: { type: "string", description: "Optional: filter by customer name" },
        max_results: { type: "number", description: "Maximum results (default 10)" },
      },
    },
  },
  {
    name: "get_project_status",
    description: "Get the current status of projects in Zoho Projects, including completion percentage, upcoming milestones, and overdue tasks.",
    inputSchema: {
      type: "object",
      properties: {
        project_name: { type: "string", description: "Optional: filter by project name" },
        status_filter: {
          type: "string",
          enum: ["active", "overdue", "all"],
          description: "Filter projects by status (default: active)",
        },
      },
    },
  },
  {
    name: "create_crm_note",
    description: "Create a note on a Zoho CRM contact or deal record to document an interaction or decision.",
    inputSchema: {
      type: "object",
      properties: {
        module: { type: "string", enum: ["Contacts", "Deals", "Leads"] },
        record_id: { type: "string", description: "Zoho CRM record ID" },
        note: { type: "string", description: "Note content (max 2000 characters)" },
      },
      required: ["module", "record_id", "note"],
    },
  },
];

Notice that write operations (like create_crm_note) are explicitly defined and limited in scope. The MCP server never exposes a "delete record" or "update any field" tool — write operations are pre-defined, narrow, and audited.

Access Control Layer

If your MCP server will be used by multiple AI agents with different permission levels, add an access control layer:

const AGENT_PERMISSIONS: Record<string, string[]> = {
  "customer-service-agent": [
    "search_contacts",
    "get_contact_deals",
    "create_crm_note",
  ],
  "operations-agent": [
    "get_overdue_invoices",
    "get_project_status",
    "search_contacts",
  ],
  "executive-assistant": [
    "search_contacts",
    "get_contact_deals",
    "get_overdue_invoices",
    "get_project_status",
  ],
};

function checkPermission(agentId: string, toolName: string): boolean {
  const allowed = AGENT_PERMISSIONS[agentId];
  if (!allowed) return false;
  return allowed.includes(toolName);
}

The customer service agent can look up contacts and log notes, but can't see financial data. The operations agent can see invoices and projects but isn't allowed to write CRM notes. This granularity is hard to achieve with direct API calls.

Audit Logging

Every tool call should be logged with full context:

async function logToolCall(params: {
  agentId: string;
  toolName: string;
  arguments: Record<string, unknown>;
  success: boolean;
  recordsAccessed?: string[];
  errorMessage?: string;
  durationMs: number;
}) {
  // Sanitise — remove any PII that shouldn't be in logs
  const sanitisedArgs = sanitiseForLogging(params.arguments);

  const logEntry = {
    timestamp: new Date().toISOString(),
    ...params,
    arguments: sanitisedArgs,
  };

  // Write to CloudWatch Logs (structured JSON)
  console.log(JSON.stringify(logEntry));

  // Optionally write to DynamoDB for queryable audit history
  await auditTable.put(logEntry);
}

This gives you a complete record of every piece of Zoho data an AI agent accessed, when, and why — essential for compliance and debugging.

Connecting Claude to Your Zoho MCP Server

Once the MCP server is running, connecting Claude is straightforward:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

// For Claude Desktop: configure in claude_desktop_config.json
// For custom agents: use the API with tool definitions

const response = await client.messages.create({
  model: "claude-3-5-sonnet-20241022",
  max_tokens: 2048,
  system: `You are an AI assistant with access to live Zoho business data.
           Use the available tools to answer questions accurately.
           When writing CRM notes, be professional and concise.
           Never share financial data in responses that will be visible to clients.`,
  tools: ZOHO_TOOLS,
  messages: [{
    role: "user",
    content: "What deals do we have with Acme Corp and are any of their invoices overdue?",
  }],
});

Claude will search for Acme Corp in CRM, retrieve their deals, check Zoho Books for overdue invoices, and compile a response — all in one turn, using live data.

Practical Query Examples

With a Zoho MCP server connected, your Claude agent can answer questions like:

  • "Who are our top 5 leads by deal value this quarter?"
  • "Which of our active projects are behind schedule?"
  • "What's the total amount outstanding from construction industry clients?"
  • "Summarise all interactions with TechStart Pty Ltd in the last 60 days"
  • "Which contacts haven't been contacted in more than 30 days?"

These are questions that currently require manual CRM searches, multiple tabs, and manual compilation. With MCP, they're answered in seconds.

Rate Limiting and Zoho API Quotas

Zoho CRM API has limits based on your subscription (typically 500–1000 calls per day for standard plans, higher for Enterprise). With AI agents that can run many tool calls, you can hit these limits unexpectedly.

Implement rate limiting in your MCP server:

import { RateLimiter } from "limiter";

// 100 requests per minute to Zoho APIs
const zohoLimiter = new RateLimiter({ tokensPerInterval: 100, interval: "minute" });

async function callZohoAPI(endpoint: string, params: Record<string, string>) {
  await zohoLimiter.removeTokens(1);
  // Proceed with API call
}

Also implement a circuit breaker — if Zoho returns repeated 429 errors, pause all calls for a cooldown period rather than hammering the API.


The Zoho + MCP combination is one of the quickest ways to turn your existing business data into an AI-powered decision-making tool. If you're already on Zoho One and want to know what's possible, let's talk.

Mahesh Ramala

Mahesh Ramala

AI Specialist · Zoho Authorized Partner · Upwork Top Rated Plus

I build custom AI agents, MCP server integrations, and Zoho automation for businesses across industries. If you found this article useful, let’s connect.

More from the Blog