Announcing CIMD support for MCP Client registration
Learn more

MCP vs API for Integrating Cloudinary to AI Agents

Nishant Choudhary
Tech Evangelist

Cloudinary publishes five MCP servers, with asset management on one and presets, metadata rules and analysis on others, and keeps the Upload and Admin APIs its SDKs have used for years. The MCP servers default to OAuth and apply the user's roles and folder roles. The REST APIs take only a product environment's key and secret. This guide compares what each path lets a media agent do, how an hourly request budget shapes both, and how to run per-editor grants across many customers.

TL;DR

  • "Cloudinary MCP" is five servers. Asset Management (https://asset-management.mcp.cloudinary.com/mcp), Environment Config, Structured Metadata, and Analysis are hosted remotely and also ship as local npm packages; MediaFlows is a fifth, documented separately. The Asset Management server, version v0.11.6 at the time of writing, lists 43 tools for uploads, search, visual search, folders, tags, metadata, transformations, image generation, signed uploads, and collections. Cloudinary does not label the servers beta or GA.
  • Remote servers default to OAuth: the flow starts when a client connects without auth headers. They also accept a cloudinary-url header or separate cloud name, API key, and API secret headers, except Analysis, which does not support API keys. The REST APIs take only the API key and secret: HTTP Basic Auth on the Admin API, Basic Auth or SHA-1/SHA-256 signatures on the Upload API. There is no OAuth on the REST path.
  • The Admin API is rate limited by the hour: 500 requests on the Free plan, starting at 2,000 on paid plans, reported in X-FeatureRateLimit-* headers, with HTTP 420 when you run out. The Upload API is described as rate-unlimited. Configuration objects (upload presets, named transformations, webhook triggers, metadata rules) live on separate MCP servers, not the asset server.
  • MCP is the faster path for a marketer or developer asking Claude or Cursor to find every product shot without alt text and tag it. The API is the right path for upload pipelines, signed browser uploads at volume, webhook-driven processing, and anything that must survive an hourly request budget.
  • Scalekit's cloudinarymcp connector proxies Cloudinary's Asset Management MCP server with 30 tools over OAuth 2.1, with Scalekit-provided OAuth credentials, stores each user's Cloudinary tokens in the token vault, and refreshes them, so the MCP vs API decision doesn't change your auth infrastructure.

The decision your Cloudinary agent forces first

Your agent sits between your users and their media library. It needs to find every hero image uploaded last quarter that has no alt context, crop a batch of product shots to 4:5 for a new storefront layout, tag uploads by what is in them, or hand a browser a signed upload so a user's file never passes through your servers. Cloudinary ships hosted MCP servers that do exactly this for an assistant in Claude or Cursor, and a set of REST APIs that every Cloudinary SDK and upload widget has used for years. They reach the same product environment. They diverge on which credentials they take, whose permissions apply, and how much of Cloudinary's configuration surface an agent can touch. Here's how to pick.

What Cloudinary MCP and Cloudinary API actually are

Cloudinary MCP

Cloudinary publishes its Model Context Protocol (MCP) servers in cloudinary/mcp-servers and documents them in its LLM and MCP guide. There are five:

  • Asset Management at https://asset-management.mcp.cloudinary.com/mcp: upload and manage images, videos, and raw files, including transformations and image generation through the Image Generation add-on.
  • Environment Config at https://environment-config.mcp.cloudinary.com/mcp: upload presets, upload mappings, named transformations, webhook notifications, and streaming profiles.
  • Structured Metadata at https://structured-metadata.mcp.cloudinary.com/mcp: metadata fields, values, and conditional rules.
  • Analysis at https://analysis.mcp.cloudinary.com/sse: AI tagging, moderation, safety checks, object detection, and recognition.
  • MediaFlows at https://mediaflows.mcp.cloudinary.com/v2/mcp: media processing and delivery automations, with its own cld-cloud-name, cld-api-key, and cld-secret headers instead of OAuth.

Remote servers serve Streamable HTTP on /mcp; /sse is deprecated for every server except Analysis. Each of the first four also ships as a local npm package that runs over stdio with CLOUDINARY_URL or CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET in its environment, on Node.js 18 or later. Cloudinary's guide connects Claude Code with claude mcp add --transport sse <name> <url> followed by /mcp to authenticate, and lists Asset Management in Claude's connector directory. Cloudinary attaches no beta or GA label to the servers and notes that some features may require a paid plan.

The Asset Management server's own page lists 43 tools: uploads (upload-asset, sign-upload), discovery (list-images, list-videos, list-files, get-asset-details, search-assets, visual-search-assets, search-folders), editing (asset-rename, asset-update, manage-asset-tags, manage-asset-context, manage-asset-metadata), relations, folders, deletes, get-tx-reference and transform-asset, generate-image and generate-image-from-images, get-usage-details, and 13 collection and public-page tools. A cloudinary-tools header narrows a session to named tools, applied at session creation.

Cloudinary API

Cloudinary's REST surface is several APIs on the same base, https://api.cloudinary.com/v1_1/:cloud_name/:action. The Upload API uploads, renames, destroys, and regenerates assets (upload, explicit, rename, destroy, download_backup), edits tags, context, and metadata, and generates archives, text images, and sprites; Cloudinary calls it rate-unlimited. The Admin API covers resources and search, folders, tags, metadata fields and rules, upload presets and mappings, named transformations, streaming profiles, triggers, and usage.

Auth on both is the product environment's API key and API secret. The Admin API takes them as HTTP Basic Auth (Admin API overview). The Upload API takes Basic Auth or a signature: sort the parameters, append the API secret, hash with SHA-1 (the SDK default) or SHA-256, and the signature is valid for one hour from its timestamp. That signature is how a browser uploads directly without ever seeing the secret. Delivery is a separate surface again: transformation parameters in a res.cloudinary.com URL, no API call at all.

Comparing them where it matters for agents

What your agent can actually do

Capability
Cloudinary MCP (Asset Management)
Cloudinary API
Upload from a URL or bucket
Yes: From an HTTP(S), S3, Google Storage or FTP URL
Yes: Upload API, also accepts local file bytes
Signed client-side uploads
Yes: Signs one or more parameter sets
Yes: Signature with API secret, valid one hour
List and inspect assets
Yes
Yes: Admin API resources
Search by expression
Yes: Lucene-like expressions, up to 500 results
Yes: Admin API search
Visual search by image or text
Yes
Yes: Admin API visual search
Tags, context, structured metadata values
Yes: Tags on up to 1,000 assets, plus context and metadata
Yes
Apply a transformation to a stored asset
Yes
Yes: Upload API eager transformations, or a delivery URL
Generate images
Yes: Image Generation add-on
Limited: Not in the Upload or Admin API method lists
Upload presets, named transformations, webhook triggers
Limited: Separate Environment Config server; not on the asset server
Yes
Metadata field definitions and rules
Limited: Separate Structured Metadata server
Yes
AI tagging and moderation
Limited: Separate Analysis server, OAuth only
Yes: Add-ons through the Upload API
Event notifications
No
Yes: Notification URL per call and global triggers

The asset server is not a thin wrapper. get-tx-reference returns Cloudinary's transformation rules so the model builds a valid string before transform-asset runs it, and sign-upload gives an agent a way to hand out upload credentials without the secret entering the conversation. The gap is everything Cloudinary split into the other servers. An agent connected to Asset Management alone cannot create an upload preset, define a metadata field, or register a webhook.

What the MCP path makes easier

"Crop every product shot in catalog/spring to 4:5 and tag it" on the raw API means paging GET /resources/image/upload?prefix=catalog/spring with next_cursor, knowing that c_fill,ar_4:5,g_auto is the right chain, calling explicit per asset, then tags in batches, while watching X-FeatureRateLimit-Remaining on every Admin API call.

On MCP, the model calls get-tx-reference once per session, as the tool description requires, lists the folder with list-images, applies the chain with transform-asset, and tags the set with one manage-asset-tags call that skips assets the caller lacks permission to update instead of failing the batch. Writing the schema is the hard part. Not the API call.

The tradeoff is that Cloudinary owns the surface. The server is at v0.11.6, its tool list already differs from what Scalekit exposes, and the docs advise disabling tools you don't need to reduce context usage. An agent tuned against today's tool names can change behavior without a release on your side.

The auth path each one puts you on

On the MCP path, OAuth is the default for the remote servers, and the flow "is initiated automatically when the client connects with no auth headers." The user signs in to Cloudinary, and the grant acts as that user. Cloudinary's permissions system applies roles to users and to API keys, including folder roles, so an editor limited to catalog/ gets an agent limited to catalog/. What the user can't do, the agent can't do.

The same remote servers accept static credentials instead: a cloudinary-url header carrying cloudinary://api_key:api_secret@cloud_name, or the three values as separate headers. That is the headless option, and it is the same secret the REST API uses, sent to Cloudinary's MCP host on every request. Analysis is the exception: it does not support API keys, so a headless agent cannot use it.

On the direct API path, every credential is a product environment API key and secret. Keys are principals in the permissions system: they can hold product environment roles and, through the Admin API, folder roles, on every plan. Custom roles and the Permissions API are Enterprise only. The product environment's root API key has full administrative permissions, and roles don't apply to it. That is the key most sample code copies.

Signed uploads soften this for browsers. Your server signs timestamp, folder, and other parameters; the client uploads with the signature and the API key, never the secret, and the signature expires in an hour. The secret still lives on your server.

Both paths still need per-user isolation

In a multi-tenant B2B agent, MCP's OAuth flow gives you a token per user and the direct API gives you a key and secret per product environment per customer. In neither case does the path itself solve storage, rotation, or revocation; those are infrastructure problems regardless of which path you choose.

Cloudinary sharpens it. An API key belongs to a product environment, not a person, so five editors at one customer sharing a key act as one identity, and a root key acts as an administrator. A leaked secret signs uploads and deletes assets until someone rotates it.

Recommended reading: OAuth vs API keys for AI agents

What you own in production

On the MCP path, Cloudinary hosts the servers, owns the tool schemas, and enforces the user's roles. You own each user's OAuth session if your product is the MCP client, or a key and secret per customer if you send them as headers. You also own the request budget: the asset tools call the same APIs, the debug preset of cloudinary-embed-headers returns rate-limit data with each result, and a model that pages list-images 10 at a time on the Free plan can spend a meaningful share of 500 hourly Admin API requests on one question.

Some tools are asynchronous. generate-image returns a task that you poll with get-generation-task. Your loop needs to treat a pending task as normal, not as a failure.

On the API path, you own pagination, signatures, retries, and the keys. The Admin API counts each request as one, except versions=true, which costs one more request per version returned. A 420 means the hour is spent, and X-FeatureRateLimit-Reset tells you when it comes back. EU and AP endpoints (api-eu, api-ap) are Enterprise options chosen at account creation, so a multi-region customer base means routing per customer.

For events, Cloudinary webhook notifications post JSON with a notification_type such as upload, delete, rename, resource_tags_changed, or resource_metadata_changed. Set notification_url per call or register global triggers through the Admin API; by default a per-call URL suppresses the global trigger for that event unless the trigger is additive. Each POST carries X-Cld-Timestamp and X-Cld-Signature (HMAC-SHA1) and X-Cld-Signature_v2 (EdDSA). Cloudinary retries three times, at 3, 6, and 9 minutes, after a non-200 or a 20-second timeout. A webhook is how a headless agent learns an upload finished, on either path; the MCP server has no subscription.

When to use MCP, when to use the API

Use Cloudinary MCP when:

  • A content editor in Claude or Cursor asks "find every image in campaigns/q4 without alt text and tag the ones with people in them," and search-assets, manage-asset-context, and manage-asset-tags do it with a human reading along.
  • A developer wants the agent to produce a correct transformation string, and get-tx-reference plus transform-asset validate it against Cloudinary's own rules instead of the model's memory.
  • Your product lets each user work in their own Cloudinary account, and you want their folder roles, not a shared key, to bound what the agent can touch.
  • A marketer needs a new hero variant, and generate-image-from-images creates it from existing brand assets.

Use the Cloudinary API when:

  • Your app accepts user uploads at volume: your server signs parameters and browsers post straight to the Upload API, which Cloudinary calls rate-unlimited, as a deterministic pipeline.
  • An ingest job reacts to upload notifications, runs moderation, and writes structured metadata, verifying X-Cld-Signature on every webhook.
  • The agent provisions a product environment per customer: upload presets, named transformations, metadata fields, and webhook triggers, which the asset MCP server does not expose.
  • A nightly job pages the whole library and must stay inside the Admin API's hourly budget, with explicit 420 handling and backoff.

The credential problem that exists on both paths

Both paths hand you a Cloudinary credential. Neither hands you a vault. A multi-tenant media agent serving 50 customer organizations with 6 editors each holds 300 OAuth grants on the MCP path. On the API path it holds at least 50 API key and secret pairs, more if customers split production and staging into separate product environments. Those secrets don't expire on their own, each one acts as no particular person, and each one has to be stored encrypted, kept out of logs and LLM context, and deleted when a customer churns.

The tempting shortcut is the root API key per customer, pasted into the agent's config as a CLOUDINARY_URL. Fewer secrets, larger blast radius per secret: roles don't apply to the root key, so every editor's request runs with administrator reach, including delete-asset on the whole library.

Scalekit's cloudinarymcp connector handles the Cloudinary OAuth flow, stores each user's tokens in the token vault, and refreshes them, so the MCP vs API decision doesn't change your auth infrastructure. Your agent passes an identifier; credentials never touch the agent runtime or the LLM context.

Recommended reading: why a token vault matters for AI agent workflows

How to connect a Cloudinary agent with Scalekit

Scalekit's cloudinarymcp connector proxies Cloudinary's Asset Management MCP server. It exposes 30 tools: 12 read-only, 11 write, and 7 destructive. They match the server's asset tools one for one; the 13 collection and public-page tools are not included. The core set:

Tool
What it does
cloudinarymcp_get_tx_reference
Returns Cloudinary's transformation rules; call it before any transformation work
cloudinarymcp_list_images
Lists image assets, filtered by prefix, tags, or public_ids
cloudinarymcp_search_assets
Searches assets with a Lucene-like expression
cloudinarymcp_get_asset_details
Returns one asset by its 32-character asset_id
cloudinarymcp_transform_asset
Applies a validated transformation to a stored asset
cloudinarymcp_manage_asset_tags
Adds, removes, or replaces tags on up to 1,000 assets
cloudinarymcp_get_usage_details
Returns storage, credits, bandwidth, and request usage

The connector authenticates with OAuth 2.1. Scalekit provides OAuth credentials for the Cloudinary MCP server, so you do not register your own OAuth app, though you can bring one.

Prerequisites and the connection name

In the Scalekit dashboard, go to AgentKit > Connections and create a Cloudinary MCP connection. The name you give it is the connection_name your code passes, and the string must match the dashboard exactly. This is the most common integration error. Copy SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET from Developers > API Credentials into .env. Full tool schemas are on the connector docs page.

pip install scalekit-sdk-python python-dotenv langchain-openai

Authorize each user once

get_or_create_connected_account returns the user's connected account. If it is not ACTIVE, get_authorization_link returns the Cloudinary sign-in link the user opens once. Then cloudinarymcp_get_usage_details confirms the grant reaches the user's product environment and shows how much of the plan is already used.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions CONNECTION_NAME = "cloudinarymcp" # must match the connection name in AgentKit > Connections identifier = "user_123" # your app's stable ID for this editor account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier, ) print("Authorize Cloudinary:", link.link) input("Press Enter after signing in to Cloudinary...") account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": raise RuntimeError(f"Cloudinary MCP is {account.connected_account.status}, not ACTIVE") usage = actions.execute_tool( tool_name="cloudinarymcp_get_usage_details", tool_input={}, connection_name=CONNECTION_NAME, identifier=identifier, ) print(usage.execution_id, usage.data)

Retrieve the Cloudinary tools this user can call

This is not a catalog load. The agent asks for the tools this user's connected account is authorized to call, then narrows them to the seven a catalog-cleanup agent needs. cloudinarymcp_delete_asset, cloudinarymcp_delete_folder, and cloudinarymcp_move_folder never reach the model, so it cannot remove or reorganize a library while it fixes tags.

tools = actions.langchain.get_tools( identifier=identifier, connection_names=[CONNECTION_NAME], tool_names=[ "cloudinarymcp_get_tx_reference", "cloudinarymcp_list_images", "cloudinarymcp_search_assets", "cloudinarymcp_get_asset_details", "cloudinarymcp_transform_asset", "cloudinarymcp_manage_asset_tags", "cloudinarymcp_manage_asset_context", ], page_size=100, ) tool_map = {t.name: t for t in tools} print(sorted(tool_map))

Run the agent loop

The loop runs until the model stops requesting tools. Every call executes with this editor's Cloudinary grant and sees only the folders their role can see.

from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ SystemMessage( "You maintain a Cloudinary media library. Call cloudinarymcp_get_tx_reference " "once before any transformation and use only parameters it documents. Use " "cloudinarymcp_list_images with a prefix, or cloudinarymcp_search_assets, to find " "assets, and page with next_cursor instead of raising max_results above 100. " "Change tags, context, and transformations only on assets the user named. " "Never delete, rename, or move assets." ), HumanMessage( "In the catalog/spring folder, find product images that have no alt text in " "their context. Tag them needs-alt, then apply a 4:5 fill crop with automatic " "gravity to the first five and tell me their public IDs." ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: tool = tool_map.get(tc["name"]) if tool: result = tool.invoke(tc["args"]) else: result = f"Unknown tool: {tc['name']}" messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Deterministic calls with execute_tool

For pipelines, skip the model. execute_tool runs one tool with a fixed input and returns data plus an execution_id you can log. This job lists the newest images in a folder and applies one fixed, pre-validated transformation to a named asset. The connected account belongs to the editor who scheduled the job, so every change is attributed to them.

folder_prefix = os.environ.get("CLOUDINARY_PREFIX", "catalog/spring/") recent = actions.execute_tool( tool_name="cloudinarymcp_list_images", tool_input={ "prefix": folder_prefix, "direction": "desc", "max_results": 50, }, connection_name=CONNECTION_NAME, identifier=identifier, ) print(recent.execution_id, recent.data) transformed = actions.execute_tool( tool_name="cloudinarymcp_transform_asset", tool_input={ "publicId": os.environ["CLOUDINARY_PUBLIC_ID"], "transformations": "c_fill,ar_4:5,g_auto", "resourceType": "image", "invalidate": True, # purge cached versions on the CDN }, connection_name=CONNECTION_NAME, identifier=identifier, ) print(transformed.execution_id, transformed.data)

The list_images response carries a next_cursor when more assets exist; pass it back to read the next page. Keep the transformation string in version control rather than letting a model write it for an unattended job.

Reaching the Cloudinary REST API

Scalekit does not ship a prebuilt connector for the Cloudinary REST APIs. Its custom connectors support BASIC auth, which fits the Admin API's key and secret, but that pair belongs to a product environment, not to a user, and signing uploads still needs the secret in your code. A server-side job that pages a folder through the Admin API and signs a browser upload looks like this:

import hashlib import os import time import requests CLOUD = os.environ["CLOUDINARY_CLOUD_NAME"] API_KEY = os.environ["CLOUDINARY_API_KEY"] API_SECRET = os.environ["CLOUDINARY_API_SECRET"] BASE = f"https://api.cloudinary.com/v1_1/{CLOUD}" params = {"prefix": "catalog/spring/", "max_results": 100} assets = [] while True: resp = requests.get( f"{BASE}/resources/image/upload", params=params, auth=(API_KEY, API_SECRET), timeout=30, ) if resp.status_code == 420: reset = resp.headers.get("X-FeatureRateLimit-Reset") raise RuntimeError(f"Admin API hourly limit reached; resets at {reset}") resp.raise_for_status() print("remaining this hour:", resp.headers.get("X-FeatureRateLimit-Remaining")) body = resp.json() assets.extend(r["public_id"] for r in body.get("resources", [])) if "next_cursor" not in body: break params["next_cursor"] = body["next_cursor"] print(len(assets), "assets") def sign_upload(upload_params: dict) -> dict: """Sign Upload API parameters so a browser can upload without the secret.""" signed = {**upload_params, "timestamp": int(time.time())} to_sign = "&".join(f"{k}={signed[k]}" for k in sorted(signed)) signature = hashlib.sha1((to_sign + API_SECRET).encode()).hexdigest() return {**signed, "signature": signature, "api_key": API_KEY} # Valid for one hour from timestamp; the browser posts the file with these # fields to https://api.cloudinary.com/v1_1/<cloud_name>/image/upload print(sign_upload({"folder": "uploads/user_123", "tags": "pending-review"}))

Create a dedicated API key per customer product environment with the narrowest role and folder roles that cover the job, never the root key, and rotate it on a schedule you own.

Multi-tool, multi-tenant Cloudinary agents on a virtual MCP server

A media agent rarely stops at one system. It cleans up the library in Cloudinary, then tells the content team what changed. Load all 30 cloudinarymcp tools and the model holds cloudinarymcp_delete_asset, cloudinarymcp_delete_folder, and cloudinarymcp_delete_derived_assets while answering a tagging question. At roughly 200 tokens each, 30 tools is about 6,000 tokens before the agent does any work. The fix is not better prompting. It is surface reduction. A Scalekit virtual MCP server declares exactly which tools an agent role can see across connections, and session tokens keep each run bound to one user's connected accounts. Docs: set up a virtual MCP server.

Create the server once per agent role

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = actions.mcp.create_config( name="cloudinary-catalog-curator", description="Finds untagged or unlabeled assets in Cloudinary, fixes tags, and posts a summary to Slack", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="cloudinarymcp", tools=[ "cloudinarymcp_search_assets", "cloudinarymcp_list_images", "cloudinarymcp_get_asset_details", "cloudinarymcp_manage_asset_tags", "cloudinarymcp_manage_asset_context", ], ), McpConfigConnectionToolMapping( connection_name="slack", tools=["slack_send_message"], ), ], ) print(vmcp.config.id, vmcp.config.mcp_server_url)

Mint a session token per user, per run

One server definition serves every editor at every customer. Before each run, check that the user's accounts are active and mint a short-lived session token scoped to them. Any MCP client that speaks Streamable HTTP can connect; this example uses langchain-mcp-adapters (pip install langchain-mcp-adapters) and passes the token as a Bearer header.

import asyncio from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url user_id = "user_123" accounts = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=user_id, include_auth_link=True, ) not_ready = [ a for a in accounts.connected_accounts if str(a.connected_account_status).upper() != "ACTIVE" ] for a in not_ready: print(f"{a.connection_name} is {a.connected_account_status}: {a.authentication_link}") if not_ready: raise RuntimeError("Connect the accounts above before starting this run") session_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=user_id, expiry=timedelta(minutes=30), ).token async def load_tools(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_server_url, "headers": {"Authorization": f"Bearer {session_token}"}, } } ) return await client.get_tools() mcp_tools = asyncio.run(load_tools()) print([t.name for t in mcp_tools])

The agent sees six tools instead of 30 plus everything Slack exposes. The endpoint is static; the identity is per-user. Cloudinary's cloudinary-tools header narrows tools too, but it is set per client session against Cloudinary directly; the virtual server applies one tool list per agent role across every user and every connection.

Observability: every Cloudinary action tied to a user

Every execute_tool call returns an execution_id, and every execution is attributed to a connected account, which maps to one of your users and one customer. When a customer asks which agent run re-cropped 400 product shots or tagged their legal-hold folder, the answer is a lookup, not an investigation. Cloudinary's delete, rename, and resource_tags_changed notifications give you the Cloudinary side of the same record.

The API path is weaker by construction. An API key belongs to a product environment, so every upload, tag change, and delete your pipeline makes is recorded against that key, not against a person. If one key drives every editor's requests, your side has to reconstruct which agent run, which prompt, and which user led there. Shared credentials are a single-user solution. They do not survive a second user.

Recommended reading: audit trails for agent auth

Which one to build against

If your agent is an editor's or developer's assistant in Claude, Cursor, or ChatGPT, searching the library, fixing tags and context, building transformations, and generating variants with a human reading along, use Cloudinary's hosted Asset Management MCP server. If you are building that agent into your own product for many customers, Scalekit's cloudinarymcp connector gives each user a per-user OAuth grant without your code holding the token or registering an OAuth app. If your agent ingests uploads at volume, signs browser uploads, reacts to webhooks, or provisions presets, named transformations, and metadata fields per customer, build against the Cloudinary API. Either way, you hold a Cloudinary credential per user or per product environment that can delete a customer's media library, and that's what needs production-grade infrastructure.

FAQs

Does Cloudinary have an official MCP server?

Cloudinary publishes five: Asset Management, Environment Config, Structured Metadata, Analysis and MediaFlows. The first four are hosted remotely and also ship as local npm packages. The Asset Management server lists 43 tools for uploads, search, visual search, folders, tags, metadata, transformations, image generation, signed uploads and collections.

How does Cloudinary MCP authentication work?

Remote servers start an OAuth flow when a client connects without auth headers, and the grant acts as the signed-in user with their roles and folder roles. They also accept the product environment's key and secret as headers, except the Analysis server, which supports OAuth only.

How does the Cloudinary REST API authenticate?

With the product environment's API key and secret only, as HTTP Basic auth on the Admin API and as Basic auth or a signature on the Upload API. There is no OAuth on the REST path. The root key has full administrative permissions and roles do not apply to it, so dedicated keys with narrow roles are safer.

What are Cloudinary's API rate limits?

The Admin API is limited per hour: 500 requests on the Free plan and from 2,000 on paid plans, reported in rate-limit headers, with HTTP 420 once the hour is spent. Cloudinary describes the Upload API as rate-unlimited. MCP asset tools call the same APIs, so a chatty model spends the same budget.

What can the Cloudinary API do that the Asset Management MCP server cannot?

Create upload presets and named transformations, define structured metadata fields and rules, register webhook triggers and run AI analysis add-ons through uploads. On MCP those live on separate servers, and none of them receives event notifications, so ingest pipelines that react to uploads belong on the API.

How does Scalekit connect an AI agent to Cloudinary?

Scalekit's Cloudinary MCP connector proxies the Asset Management server with 30 tools over OAuth 2.1, using Scalekit-provided credentials, and stores each user's tokens in a vault. It has 12 read, 11 write and 7 destructive tools, so you narrow them per agent role and serve them through a virtual MCP server.

Build your Cloudinary agent with Scalekit

Browse the Scalekit Cloudinary MCP connector and the Cloudinary MCP connector docs with the full tool list. See Scalekit pricing, or talk to us.

No items found.
On this page
Share this article
Scalekit
Find us on ProductHunt

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.