Skip to content

About

The CleverTap REST API as MCP tools. Stdio MCP server, no runtime dependencies.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

CleverTap MCP server

The CleverTap REST API as MCP tools. Runs as a stdio MCP server behind a gateway that owns login, per-user authorization and auditing; deployment lives with our infrastructure manifests, not here.

34 tools — 18 reads (profiles, events, counts, trends, campaign/message reports, wallet/voucher/coupon lookups, remote config, category groups) and 16 writes (upload profiles/events/device tokens, create/stop campaign, send message, external trigger, delete/demerge profile, bulletins, remote config, custom-list segments, catalog upload, wallet debit/credit, coupon redeem/revert).

Extracted from an earlier HTTP service that carried the same tools. The tool logic moved across unchanged; transport, authentication and auditing were dropped on purpose — see What this server does not do.

⚠️ The 16 write tools have never run against a live CleverTap project. Production is deployed read-only until they are validated against integration — see the warning below.

Layout

File Purpose
server.py stdio entry point: newline-delimited JSON-RPC 2.0 on stdin/stdout. No dependencies.
clevertap_mcp/config.py Region, timeouts, credentials from the environment, CleverTap path maps.
clevertap_mcp/client.py Authenticated HTTP against one CleverTap project, plus the async-poll and error-shape quirks. Read the comments before "simplifying" anything here — each one records a live-API surprise.
clevertap_mcp/errors.py MCPError.
clevertap_mcp/profiles.py Profiles, events, device tokens — 7 tools.
clevertap_mcp/analytics.py Counts, trends, req_id polling — 6 tools.
clevertap_mcp/campaigns.py Campaigns and outbound messaging — 8 tools. The blast radius lives here.
clevertap_mcp/segments.py Remote config, custom lists, catalog — 6 tools.
clevertap_mcp/loyalty.py Wallet, vouchers, coupons — 7 tools.
clevertap_mcp/reference.py The clevertap://reference resource.
clevertap_mcp/app.py Assembles the 34 tools from the five domain modules, and CleverTapMCPServer — tool listing, resource listing, dispatch.
test_server.py python3 test_server.py — self-check, no framework, no network.

Each domain module owns three exports, and that co-location is the point of the split: a tool's JSON Schema, its handler, and whether it is a write all live in one file, so adding a tool cannot silently skip the write classification.

TOOLS     # list of tool-definition dicts, sent verbatim on tools/list
HANDLERS  # {tool name: (env, args) -> (text, is_error)}
WRITES    # the subset of tool names that mutate CleverTap state

Adding a tool: add the schema, the handler and (if it writes) the name to WRITES, in the domain module. Adding a domain: write the module, add it to _DOMAINS in app.py. See CONTRIBUTING.md — it opens with the one rule that matters here.

Configuration

All configuration is environment variables. No credentials are committed to this repo, and none should be — in production they are injected from a secret store.

Variable Required Meaning
CLEVERTAP_ACCOUNT_ID yes X-CleverTap-Account-Id
CLEVERTAP_PASSCODE yes X-CleverTap-Passcode
CLEVERTAP_TOKEN no X-CleverTap-Token — only needed by clevertap_trigger_external_campaign when triggering more than one campaign at once. Single-campaign triggers work without it.
CLEVERTAP_ENV no integration | production (default integration). Selects the label stamped onto every tool description so the model can see which project it is talking to. It does not select credentials.
CLEVERTAP_TOOLS no all | read | write (default all). Which slice of the 34 tools this process exposes.
CLEVERTAP_IDENTIFIER_TYPES no Comma-separated subset of identity, email, objectId (default: all three). Restricts which user-lookup fields get_user_profiles/get_wallet_details/messaging tools advertise, to match this account's Settings → User Identity config — CleverTap returns an opaque 503 for a disabled identifier type rather than a clear error. An unrecognised value falls back to all three.

One process serves exactly one CleverTap project. Which project is reached is decided by the credentials in the environment, not by CLEVERTAP_ENV — run one container per project.

Region is pinned to eu1 in config.py. CleverTap's own docs are inconsistent about whether EU needs that prefix; it was confirmed against a live account that it does. Do not change it to api.clevertap.com without re-testing.

Missing credentials are not fatal: the process still starts and reports Ready, and every tool returns a "not configured" error instead. That is so a server can be deployed before its credentials are provisioned.

Tool modes

CLEVERTAP_TOOLS selects which slice of the 34 tools a process serves. Out-of-scope tools are absent from tools/list and refused by tools/call — checked before the credential check, so a refusal never depends on configuration having landed.

Mode Tools Use for
all 34 Integration
read 18 Production
write 16 Production, later — see the warning below

read and write partition the 34 exactly, with no overlap (asserted in test_server.py). That is what lets a read instance and a write instance sit behind one gateway without duplicate tool names: a reader sees 18 tools at that URL, someone granted both sees all 34, and there is only one endpoint to maintain.

Each write tool also enforces its own confirm=true argument, but confirm is set by the model, so it guards against the model being careless — not against prompt injection in data the model reads (an Instagram comment, a CleverTap profile custom field). The mode is the boundary that holds, because there is no confirm left for injected text to flip.

Same idea as a server-level READ_ONLY_MODE, which beats an external allow-list for a tool surface this size — 16 names to deny here rather than 18 to enumerate there. Layer an allow-list on top anyway if you want default-deny.

Write tools are annotated destructiveHint: true, which is what lets a gateway policy recognise them — such a policy is a silent no-op without the annotation.

⚠️ The write tools have never been run against real data

None of the 16 write tools has been executed against a live CleverTap project. They were written from CleverTap's API docs and moved across from the earlier service unchanged, but that service's production credentials were never configured, so no write has ever reached CleverTap from this code. The reads are exercised; the writes are not.

Consequences, and they are not theoretical — several of these are irreversible: clevertap_send_message and clevertap_trigger_external_campaign put real push/SMS/email on real devices, clevertap_wallet_debit_credit moves value, clevertap_delete_user_profile is processed ~24h after the call and cannot be recovered.

Gate: production write access (CLEVERTAP_TOOLS=write) does not get deployed until someone has run the write tools against the integration project and confirmed they behave. Until then production runs CLEVERTAP_TOOLS=read only. When validating on integration, work through the confirm=false dry-run path first — the upload tools run CleverTap's native dryRun=1 and clevertap_create_campaign forces estimate_only=true, so the payload can be checked before anything is written.

Record what was verified here when it happens, so the next person knows which of the 16 are actually trusted rather than merely deployed.

Running locally

set -x CLEVERTAP_ACCOUNT_ID <account-id>
set -x CLEVERTAP_PASSCODE <passcode>
set -x CLEVERTAP_TOOLS read

claude mcp add clevertap -- python3 (pwd)/server.py

Or drive the protocol by hand:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 server.py

Self-check (never touches the network — it asserts loudly if a code path tries):

python3 test_server.py

Dependencies — and when to revisit this

There are none. No requirements.txt, no mcp SDK: server.py speaks newline-delimited JSON-RPC 2.0 over stdio using only the standard library, ported from the earlier dispatcher. That means nothing to fetch at pod start, nothing to CVE-patch, and no third-party package in a process holding a CleverTap passcode.

It is a deliberate trade, not an oversight. What the official SDK would give us and this does not: streamable-http transport, tools/list pagination cursors, request cancellation, progress notifications, _meta propagation, params validation, OTel hooks, and someone else owning MCP spec drift. None are needed today, and stdio bridged to HTTP is the well-trodden path for the servers we run, not a workaround.

📌 Monitor this and reconsider

Assessed 2026-07-28. The Python SDK had just shipped a breaking major:

mcp 2.0.0 released 2026-07-28 — zero patch releases; 2.0.0rc1 was the day before
mcp 1.29.0 released the same day — the 1.x line is maintained, not abandoned
Breaking changes in 2.0 decorator API removed from Server; low-level moved to constructor callbacks (on_list_tools=, on_call_tool=); high-level FastMCP renamed MCPServer; dependency moved httpx → httpx2

Adopting a major version on release day, for a server that reaches production CleverTap, was not a good trade. The trigger to revisit: once 2.0.x patch releases exist (realistically 2.0.2 or later, a few weeks of real-world use). Check with:

pip index versions mcp

When that happens, reconsider migrating — the destination was never in doubt, only the timing. What to know going in:

  • The migration is ~55 lines in one file. Only server.py changes. Nothing in clevertap_mcp/ moves: the 34 tool dicts pass through mcp.types.Tool.model_validate() unmodified, annotations.destructiveHint included (verified against 2.0.0). The transport is already isolated behind tools() / call_tool() / read_resource() — that is what the package split bought.
  • Use the low-level Server, not the high-level MCPServer. The high-level API infers schemas from Python type hints, which would mean rewriting all 34 hand-written JSON Schemas. The same applies to the TypeScript SDK: its low-level Server takes raw JSON Schema, its McpServer wants Zod.
  • Pin exactly — mcp==2.0.0, never a >= range. An unpinned mcp>=1.0.0 is exactly how the community CleverTap MCP servers broke: their code no longer imports against 2.0.0. Pin so the break happens when you bump it and run the tests, not on a 3am pod restart.
  • Pin only bites in a built image. With the pip install-at-startup pattern a re-tagged or yanked release can still break a restart. Another reason to prefer the Dockerfile path.
  • Don't adopt 1.x. Deliberately writing against the superseded major is the worst option: obsolete API now and a forced migration later.
  • The reason to go is streamable-http (app.streamable_http_app()) — it drops the stdio↔HTTP bridge and gives the operator real readiness probes. If that is not what you need, staying dependency-free is still the right answer.

Rerun python3 test_server.py after any migration; the stdio subprocess test will need rewriting to drive the SDK client instead.

What this server does not do

An earlier version of this layer ended with HTTP routes plus its own OAuth 2.1 authorization server, a per-user allow-list in a database table, and an access-log insert per call. None of that is here, because the gateway in front of the process does all three better: login, per-user authorization scoped per person, short-lived tokens it never stores in plaintext, and an append-only audit trail that captures request payloads.

This process sees no network socket, no bearer token and no user identity. Keep it that way — if it ever needs to know who is calling, that belongs in the gateway.

Deployment

Deployment manifests, credential provisioning and access control live with our infrastructure configuration, not here. This repo is just the server.

Also worth doing

CleverTap supports up to 100 account-level passcodes with expiries, and User Passcodes granted per dashboard user (Settings > Passcodes / Settings > Users > Grant). The shared forever-passcode this server inherits is not the only option — a dedicated expiring passcode per deployment is strictly better, and per-user passcodes would give CleverTap-side attribution to match the gateway's audit trail. Open question for the CSM: do User Passcodes inherit the dashboard user's role permissions (i.e. can one be genuinely read-only)?

About

The CleverTap REST API as MCP tools. Stdio MCP server, no runtime dependencies.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages