Skip to main content
Glama
Zoho Inventory Connector

Zoho Inventory Connector

by Achindra2003

Zoho Inventory connector for Razorpay Agent Studio

A private, read-only connector that gives Agent Studio agents a merchant's stock, orders and proof of delivery from Zoho Inventory. It is an MCP server with OAuth 2.0, list/get/search primitives, three-layer rate limiting and a published tool spec. It runs fully offline in demo mode, so you can try it in two minutes without a Zoho account.

The merchant problem

Saffron Threads is a D2C apparel and home brand. It takes payments on Razorpay and runs stock and fulfilment in Zoho Inventory, with warehouses in Bengaluru and Delhi NCR. It has switched on Agent Studio.

  • The cart recovery agent keeps calling shoppers about an olive tee that has been out of stock for a week.

  • The dispute responder can't answer a "never received" chargeback. The carrier, AWB and delivery date live in Zoho, and someone on the ops team copies them out by hand.

The ask was "let the agent read inventory and orders." The real need is "let agents check fulfilment facts before they talk to a customer or answer a dispute."

So on top of the list/get/search primitives, the connector has two tools built for those jobs:

  • check_availability: up to 25 SKUs in one call, optionally for one warehouse. Answers sellable: yes/no with a reason.

  • get_fulfilment_evidence: looks up an order by its Razorpay order ID. Returns carrier, AWB, ship and delivery dates, a verdict, a sentence the agent can quote, and the gaps that weaken the evidence.

Related MCP server: Zoho Inventory MCP Connector

Try it in 2 minutes (no Zoho account)

Needs Python 3.11+ and uv.

git clone <this repo> && cd zoho-inventory-connector
uv sync
uv run python scripts/demo.py        # narrated walkthrough through a real MCP client session
uv run pytest -q                     # 66 tests, ~10 s, no network

scripts/demo.py connects through the OAuth code path, then runs the cart and dispute scenarios. It also shows PII masking, a prompt-injection record, recovery from Zoho 429s, and the structured error an agent gets when the budget runs out.

Use it from an MCP host (Claude Desktop, Claude Code, MCP Inspector) with fictional demo data:

uv run zoho-connector serve --demo                                              # stdio
npx -y @modelcontextprotocol/inspector uv run zoho-connector serve --demo       # click through every tool

Claude Code picks up the bundled .mcp.json automatically. For Claude Desktop, add this to claude_desktop_config.json:

{
  "mcpServers": {
    "zoho-inventory": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/zoho-inventory-connector", "run", "zoho-connector", "serve", "--demo"]
    }
  }
}

Then ask, for example: "A shopper abandoned ST-TEE-OLV-M and ST-TEE-BLK-M. What can we still pitch?" or "Customer disputes Razorpay order order_DemoQ7Hk2pXa01 as not received. What's our evidence?"

Connect a real Zoho Inventory org

Everything below is free: Zoho Inventory's free plan includes API access at 1,000 calls/day.

  1. Org. Sign up at zoho.com/in/inventory (free plan).

  2. OAuth client. At api-console.zoho.in, choose Add Client → Server-based Applications and set:

    • Homepage URL: http://localhost

    • Authorized Redirect URI: http://localhost:8765/callback

  3. Configure. cp .env.example .env and set ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET and ZOHO_DC (in for zoho.in accounts).

  4. Connect.

    uv run zoho-connector connect      # opens Zoho's consent screen (5 read-only scopes) → pick org → tokens go to the OS keyring
    uv run zoho-connector status       # verifies with one API call, shows budget

    On a server without a browser, create a Self Client in the API console, put its client ID/secret in .env, generate a code with the same read scopes, and run zoho-connector connect --code 1000.xxxx.

  5. Demo data (optional). Seed the same fictional records into your org with scripts/seed_zoho.py. It uses a separate, short-lived write grant and revokes it at the end, because the connector itself never gets write access.

    uv run python scripts/seed_zoho.py --dry-run         # see the ~55 requests first
    uv run python scripts/seed_zoho.py --code 1000.xxxx --client-id <self client id> --client-secret <self client secret>
  6. Serve. uv run zoho-connector serve, or use the Claude Desktop config above without --demo. Add --transport http --port 8000 to serve over streamable HTTP.

  7. Disconnect. uv run zoho-connector disconnect revokes the grant at Zoho and deletes the local tokens.

Command

What it does

zoho-connector connect [--code X] [--org-id Y] [--no-browser]

OAuth consent, bind to one organization

zoho-connector status

Connection, scopes, token expiry, per-minute and daily budget

zoho-connector serve [--demo] [--transport stdio|http]

Run the MCP server

zoho-connector disconnect

Revoke and forget

zoho-connector mock-server

Mock Zoho on :8900, to try the browser OAuth flow without an account

zoho-connector spec

Regenerate docs/tools.json

--connection <name> (or ZOHO_CONNECTION) keeps one named connection per merchant org.

Tools

The full JSON Schemas, including output schemas, are in docs/tools.json. A test keeps that file in sync with the code.

Tool

Use it when

Zoho calls

check_availability

Before pitching or promising a product. ≤25 SKUs, optional warehouse

1 per SKU, or 1 catalogue page for ≥6 SKUs

get_fulfilment_evidence

Disputes, "where is my order". By Razorpay ref, SO number or id

2 + 1 per package

list_items / search_items / get_item

Catalogue, low stock, per-warehouse stock

1–2

list_sales_orders / search_sales_orders / get_sales_order

Orders by status, date, ref, customer

1–2

connector_status

Which org, scopes, budget left

0

All tools are annotated readOnlyHint: true, destructiveHint: false. Pagination uses opaque next_cursor tokens bound to the original query. Errors are JSON with a stable code, an optional retry_after_s and a hint telling the agent what to do next.

docs/CAPABILITIES.md is the one-page summary of what an agent can and cannot do.

How it handles the hard parts

  • OAuth. Authorization-code flow with offline access, a loopback redirect, a state check, and validation of Zoho's multi-DC accounts-server. Token refresh is single-flight, and a 401 triggers one refresh and retry. Tokens are stored in the OS keyring and revoked on disconnect.

  • Rate limits. Zoho enforces 100 requests/min, 5–10 concurrent and 1,000–10,000/day per org. The connector:

    • keeps a sliding 90/min window and a concurrency semaphore;

    • persists a daily quota counter;

    • honours Retry-After and otherwise backs off with jitter, within a bounded total wait;

    • fails fast with RATE_LIMITED or DAILY_QUOTA_EXHAUSTED instead of hanging an agent;

    • caches reads (items 60s, orders 30s) to save quota.

  • Safety.

    • Read-only, enforced by OAuth scope, by the tool surface and by MCP annotations.

    • Customer email, phone and street are masked unless the operator sets ZOHO_PII_MODE=full.

    • Merchant free text is returned as UntrustedText.

    • Every call writes a JSON audit line with arguments (names redacted), status, latency, Zoho calls spent and quota left.

The reasoning behind these choices is in docs/DESIGN.md. How to tell whether the connector is worth deploying is in docs/IMPACT.md.

Tests and evals

uv run pytest -q                    # 66 tests
uv run python evals/run_eval.py     # LLM eval; needs EVAL_API_KEY (free Groq key works; Anthropic/Ollama via env)

The tests cover:

  • the full browser OAuth flow on real sockets;

  • state mismatch and untrusted accounts-server;

  • expired-token refresh, including concurrent callers sharing one refresh;

  • revoked grants;

  • 429 with Retry-After, the 1070 concurrency error and 5xx retries;

  • a 150-call burst throttled client-side with zero upstream 429s;

  • the daily quota persisting across restarts and rolling over at IST midnight;

  • every fulfilment verdict;

  • cursor misuse, PII masking and the prompt-injection record;

  • the MCP schema, plus a real stdio subprocess launched the way Claude Desktop launches it.

Eval result (gpt-oss-120b on Groq, demo data): 12/12 scenarios, with tool choice 12/12 and grounded answers 12/12. Groq rejected one malformed tool call (tool_use_failed); the harness resampled it once and counts that in the report. The scenarios cover cart recovery, disputes, order tracking, inventory, prompt injection and write refusal.

The first run scored 10/12, and the misses were useful:

  • A real product gap. For "is the mug available in Bengaluru?" the tool returned only "out of stock", so the model couldn't offer Delhi. check_availability now returns other_locations, and the model answers "not from Bengaluru; Delhi NCR WH has 26".

  • Two harness bugs. The model wrote SO‑00104 with a non-breaking hyphen and can’t with a curly apostrophe, so the scorer now normalises punctuation. A provider error used to crash the whole run.

On the injection record, the model described the candle and noted that "the description field contains internal notes; no action is required". It listed no customer data and attempted no refund.

Re-run with make eval (free Groq key) after changing any tool description. A scripted oracle in the tests shows every scenario is answerable from the data.

Assumptions

  • The Razorpay order or payment ID is stored in the Zoho sales order's reference_number. This is common for storefront syncs. If a merchant uses a custom field, that becomes a per-connection setting (see DESIGN "next steps").

  • One connection = one Zoho organization. Multi-org merchants create one connection each.

  • Delivery facts are what the merchant recorded in Zoho. The connector does not query courier APIs.

  • The daily quota resets at midnight IST. Zoho resets per org day, and ZOHO_QUOTA_UTC_OFFSET_MINUTES adjusts this.

Limitations

  • Verified against a live Zoho Inventory org (free plan, India DC) seeded with scripts/seed_zoho.py. Run scripts/verify_live.py to check yours. That run turned up four differences from Zoho's docs, now handled and covered by tests:

    • contact details live on contact_person_details;

    • the ship date is shipping_date on package and shipment records;

    • customer_name is exact-match, so search uses customer_name_contains;

    • the best stock figure is actual_available_for_sale_stock.

  • Delivery dates can be missing. When a shipment is marked delivered through the API (or without a date), Zoho leaves shipment_delivered_date empty. The connector reports this as an evidence gap rather than guessing a date.

  • Single-warehouse orgs (Zoho's free plan) return no per-warehouse stock, so location_name checks explain that and point to total stock.

  • Stock can be stale by up to 60s because of the read cache. Order data can be up to 30s old.

  • Pull only. No webhooks.

  • Search is limited to Zoho's exact, contains and starts-with matching.

  • The daily quota counter is per process host. Two servers on different machines for the same org would each count separately. A shared store such as Redis would fix that in production.

  • The demo mode is a test double, not a Zoho emulator. It implements only what this connector uses.

Project layout

src/zoho_connector/
  config.py      settings, data centres, read-only scopes, plan quotas
  auth.py        OAuth: consent URL, loopback callback, code exchange, single-flight refresh, revoke
  store.py       token storage: OS keyring / file / memory
  ratelimit.py   sliding window, daily quota, backoff
  client.py      Zoho HTTP client: auth, retries, error mapping, cache
  models.py      agent-facing output models, PII masking, untrusted text
  tools.py       the 9 capabilities (transport-agnostic)
  server.py      MCP adapter, tool descriptions, audit log
  cli.py         connect / status / serve / disconnect / spec / mock-server
  mock/          mock Zoho accounts + Inventory API, fictional dataset
docs/            CAPABILITIES.md · DESIGN.md · IMPACT.md · tools.json
evals/           scenarios.json + run_eval.py
scripts/         demo.py · seed_zoho.py
tests/           66 tests

No real customer data, passwords or keys are included. All demo records are fictional, and emails use the reserved example.com domain.

Available Tools

9 tools
check_availabilityA
Read-onlyIdempotent

Can these SKUs be sold right now? Returns sellable yes/no with a reason per SKU (out of stock, insufficient quantity, inactive, unknown SKU). Call this before an agent pitches, recovers or promises any product to a customer.

Inputs: lines (required), location_name. Returns: note, as_of, results, all_sellable

connector_statusA
Read-onlyIdempotent

Which Zoho organization is connected, granted scopes, and how much of the per-minute and daily API budget is left. Costs no Zoho API calls.

Inputs: no named parameters. Returns: object

get_fulfilment_evidenceA
Read-onlyIdempotent

Proof-of-fulfilment pack for one order: every shipment with carrier, tracking (AWB), ship and delivery dates and items, a verdict (DELIVERED, PARTIALLY_DELIVERED, SHIPPED_NOT_DELIVERED, NOT_SHIPPED, CANCELLED), a one-line factual summary and the gaps that weaken it. Use for chargebacks/disputes and 'where is my order' questions.

Inputs: salesorder_id, reference_number, salesorder_number. Returns: source, summary, verdict, shipments, +11 more

get_itemA
Read-onlyIdempotent

Full detail for one item, including stock per warehouse/location. Give exactly one of item_id or sku.

Inputs: sku, item_id. Returns: sku, name, rate, unit, +9 more

get_sales_orderA
Read-onlyIdempotent

One sales order with line items, shipped quantities, packages and (masked) customer contact. Give exactly one identifier.

Inputs: salesorder_id, reference_number, salesorder_number. Returns: date, notes, total, status, +13 more

list_itemsA
Read-onlyIdempotent

Browse the merchant's catalogue with stock levels (all warehouses combined). Use for overviews such as 'what is running low?'. For specific SKUs use check_availability.

Inputs: cursor, status, sort_by, page_size, +1 more. Returns: results, has_more, returned, next_cursor

list_sales_ordersA
Read-onlyIdempotent

Browse sales orders, newest first, optionally by status and order-date range.

Inputs: cursor, status, date_to, date_from, +1 more. Returns: results, has_more, returned, next_cursor

search_itemsA
Read-onlyIdempotent

Find items by partial name or SKU when you do not know the exact SKU.

Inputs: query (required), field, match, cursor, +1 more. Returns: results, has_more, returned, next_cursor

search_sales_ordersA
Read-onlyIdempotent

Find sales orders by reference number, order number, customer or text. Give at least one criterion.

Inputs: text, cursor, date_to, date_from, +4 more. Returns: results, has_more, returned, next_cursor

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 9 tools

    Disambiguation5/5

    Each tool targets a distinct resource+action: availability check, catalogue browsing, single-item detail, item search, order listing, order search, single-order fetch, fulfilment evidence, and connector status. The list/search and get/list pairs are cleanly separated by explicit guidance in the descriptions (e.g. 'For specific SKUs use check_availability').

    Naming Consistency4/5

    Nearly all tools follow a consistent verb_noun snake_case pattern (check_availability, list_items, get_item, search_sales_orders). The only deviation is the noun-only connector_status, which is a minor and understandable exception for a meta/status tool.

    Tool Count5/5

    Nine tools is well-scoped for a merchant inventory/sales-order connector, with each tool earning its place and no redundant padding.

    Completeness3/5

    Read/lookup coverage is strong (items, availability, orders, fulfilment evidence, status), but the surface is entirely read-only: there are no tools to create or update items, adjust stock, create sales orders, or record shipments. These are notable gaps for a connector whose agents may need to act on inventory or orders, though some may be intentional scoping.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables an AI agent to query a warehouse management system for orders, stock, stalled orders, and audit logs, and to make narrowly scoped writes by changing order status with a required reason or appending notes, with all writes recorded in a full audit trail.
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to answer merchant questions about stock levels, reorder alerts, sales orders, and customers through a read-only MCP interface, with OAuth 2.0, rate-limit handling, and a mock backend for testing.
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Enables AI agents to securely access and reason over WooCommerce orders, products, and inventory through read-only MCP tools, with PII redaction, rate-limit handling, and bounded pagination.
      5
      MIT
    • A
      license
      B
      quality
      C
      maintenance
      A read-only MCP server that lets an Agent Studio agent query a WooCommerce store's orders, products, inventory and customers, with pagination, filtering, typed errors and automatic rate-limit retry. It never mutates store data, keeping the blast radius of an agent's actions bounded to reads.
      7
      MIT