#
tokens: 8711/50000 25/25 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .github
│   ├── actions
│   │   └── setup
│   │       └── action.yml
│   └── workflows
│       └── release.yml
├── .gitignore
├── .npmignore
├── cld-mcp-server.png
├── cli.js
├── glama.json
├── LICENSE.md
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│   ├── index.js
│   ├── tools
│   │   ├── deleteAssetTool.js
│   │   ├── deleteAssetTool.test.js
│   │   ├── findAssetsTool.js
│   │   ├── findAssetsTool.test.js
│   │   ├── getAssetTool.js
│   │   ├── getAssetTool.test.js
│   │   ├── getCloudinaryTool.js
│   │   ├── getUsageTool.js
│   │   ├── getUsageTool.test.js
│   │   ├── uploadTool.js
│   │   └── uploadTool.test.js
│   └── utils.js
├── test
│   ├── cloudinary.mock.js
│   └── vitest-setup.js
└── vitest.config.js
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
llm-docs/
.idea/
node_modules/
.env
dist/
coverage/
*.log
test/server.test.js
test/test.png
test/cld.js

```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
.idea/
.vscode/
.github/
src/
*.story.js
*.stories.js
stories/
*.test.js
tests/
test/
mocks/
*.log
jest*
.editorconfig
.eslintignore
.eslintrc.js
.circleci/
.sb-static/
.env
.jest-cache/
.jest-coverage/
scripts/
.storybook/
tsconfig.json
.eslintcache
cypress/
rollup.config.js
babel.config.js
cypress.json

.nyc_output/
coverage/
reports/
.reporters-config.json
.nycrc

test-setup.js
codecov
codecov.yml
.mocharc.js

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
<img src="https://github.com/yoavniran/cloudinary-mcp-server/blob/main/cld-mcp-server.png?raw=true" width="120" height="120" align="center" />

# Cloudinary MCP Server

<a href="https://glama.ai/mcp/servers/@yoavniran/cloudinary-mcp-server">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@yoavniran/cloudinary-mcp-server/badge" alt="cloudinary-mcp-server MCP server" />
</a>

<p align="center">
    <a href="https://badge.fury.io/js/cloudinary-mcp-server">
        <img src="https://badge.fury.io/js/cloudinary-mcp-server.svg" alt="npm version" height="20">
    </a>
</p>

A Model Context Protocol server that exposes Cloudinary Upload & Admin API methods as tools by AI assistants. 
This integration allows AI systems to trigger and interact with your Cloudinary cloud.

## How It Works

The MCP server:

-   Makes calls on your behalf to the Cloudinary API
-   Enables uploading of assets to Cloudinary
-   Enables management of assets in your Cloudinary cloud

It relies on the Cloudinary [API](https://cloudinary.com/documentation/admin_api) to perform these actions. Not all methods and parameters are supported. 
More will be added over time. 

Open an [issue](https://github.com/yoavniran/cloudinary-mcp-server/issues) with a request for specific method if you need it.

## Benefits

-   Turn your Cloudinary cloud actions into callable tools for AI assistants
-   Turn your Cloudinary assets into data for AI assistants

## Usage with Claude Desktop

### Prerequisites

-   NodeJS
-   MCP Client (like Claude Desktop App)
-   Create & Copy Cloudinary API Key/Secret at: [API KEYS](https://console.cloudinary.com/settings/api-keys)

### Installation

To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:

```json
{
    "mcpServers": {
        "cloudinary-mcp-server": {
            "command": "npx",
            "args": ["-y", "cloudinary-mcp-server"],
            "env": {
                "CLOUDINARY_CLOUD_NAME": "<cloud name>",
                "CLOUDINARY_API_KEY": "<api-key>",
                "CLOUDINARY_API_SECRET": "<api-secret>"
            }
        }
    }
}
```

-   `CLOUDINARY_CLOUD_NAME` - your cloud name
-   `CLOUDINARY_API_KEY` - The API Key for your cloud
-   `CLOUDINARY_API_SECRET` - The API Secret for your cloud


### Tools

The following tools are available:

1. **upload**
    - Description: Upload a file (asset) to Cloudinary
    - Parameters:
        - `source`: URL, file path, base64 content, or binary data to upload
        - `folder`: Optional folder path in Cloudinary
        - `publicId`: Optional public ID for the uploaded asset
        - `resourceType`: Type of resource to upload (image, video, raw, auto)
        - `tags`: Comma-separated list of tags to assign to the asset

2. **delete-asset**
    - Description: Delete a file (asset) from Cloudinary
    - Parameters:
        - `publicId`: The public ID of the asset to delete
        - `assetId`: The asset ID of the asset to delete

3. **get-asset**
    - Description: Get the details of a specific file (asset)
    - Parameters:
        - `assetId`: The Cloudinary asset ID
        - `publicId`: The public ID of the asset
        - `resourceType`: Type of asset (image, raw, video)
        - `type`: Delivery type (upload, private, authenticated, etc.)
        - `tags`: Whether to include the list of tag names
        - `context`: Whether to include contextual metadata
        - `metadata`: Whether to include structured metadata

4. **find-assets**
    - Description: Search for existing files (assets) in Cloudinary with a query expression
    - Parameters:
        - `expression`: Search expression (e.g. 'tags=cat' or 'public_id:folder/*')
        - `resourceType`: Resource type (image, video, raw)
        - `maxResults`: Maximum number of results (1-500)
        - `nextCursor`: Next cursor for pagination
        - `tags`: Include tags in the response
        - `context`: Include context in the response

5. **get-usage**
    - Description: Get a report on the status of your product environment usage, including storage, credits, bandwidth, requests, number of resources, and add-on usage
    - Parameters:
        - `date`: Optional. The date for the usage report in the format: yyyy-mm-dd. Must be within the last 3 months. Default: the current date
```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
MIT License

Copyright (c) 2025 Yoav Niran

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://glama.ai/mcp/schemas/server.json",
  "maintainers": [
    "yoavniran"
  ]
}

```

--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

import("./src/index.js").catch(err => {
	console.error("Failed to start server:", err);
	process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/tools/getCloudinaryTool.js:
--------------------------------------------------------------------------------

```javascript
const getCloudinaryTool = (tool) => {
	return (cloudinary) => (params) => tool(cloudinary, params);
};

export default getCloudinaryTool;

```

--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------

```javascript
import { defineConfig } from "vitest/config";

export default defineConfig({
	test: {
		globals: true,
		setupFiles: "./test/vitest-setup.js",
		include: ["src/**/*.test.js?(x)"],
	},
});

```

--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------

```javascript
export const getToolError = (msg, cloudinary) => {
	const conf = cloudinary.config()
	return {
		content: [{
			type: "text",
			text: `${msg} (cloud: ${conf.cloud_name}, key: ${conf.api_key.slice(0,4)}...)`,
		}],
		isError: true,
	};
};

```

--------------------------------------------------------------------------------
/test/vitest-setup.js:
--------------------------------------------------------------------------------

```javascript
import { afterEach } from "vitest";

// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
});

global.clearViMocks = (...mocks) => {
	mocks.forEach((mock) => {
		if (mock) {
			if (mock.mockClear) {
				mock.mockClear();
			} else {
				global.clearViMocks(...Object.values(mock));
			}
		}
	});
};

```

--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------

```yaml
name: Setup & install
description: install pnpm & deps

runs:
    using: composite
    steps:
        - uses: pnpm/action-setup@v4
          with:
              version: 9

        - uses: actions/setup-node@v4
          with:
              node-version: "20.15"
              cache: "pnpm"
              cache-dependency-path: "./pnpm-lock.yaml"

        - run: pnpm install --frozen-lockfile
          shell: bash

```

--------------------------------------------------------------------------------
/test/cloudinary.mock.js:
--------------------------------------------------------------------------------

```javascript
export const createCloudinaryMock = () => {
	return {
		config: () => ({
			cloud_name: "test-cloud",
			api_key: "test-key",
			api_secret: "test-secret"
		}),
		uploader: {
			upload: vi.fn(),
			upload_stream: vi.fn()
		},
		utils: {
			api_sign_request: vi.fn()
		},
		api: {
			resource: vi.fn(),
			resources_by_asset_ids: vi.fn(),
			delete_resources: vi.fn(),
			search: vi.fn(),
			usage: vi.fn(),  // Added usage method to the mock
		},
	};
};

```

--------------------------------------------------------------------------------
/src/tools/getUsageTool.js:
--------------------------------------------------------------------------------

```javascript
import { z } from "zod";
import { getToolError } from "../utils.js";
import getCloudinaryTool from "./getCloudinaryTool.js";

export const getUsageToolParams = {
	date: z.string().optional().describe("The date for the usage report. Must be within the last 3 months and specified in the format: yyyy-mm-dd. Default: the current date")
}

const getUsageTool = async (cloudinary, { date }) => {
	try {
		const usageOptions = {
			date
		};

		const usageResult = await cloudinary.api.usage(usageOptions);

		return {
			content: [
				{
					type: "text",
					text: JSON.stringify(usageResult, null, 2)
				}
			],
			isError: false,
		};
	} catch (error) {
		return getToolError(`Error getting usage report from Cloudinary: ${error.message}`, cloudinary);
	}
};

export default getCloudinaryTool(getUsageTool);

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "cloudinary-mcp-server",
  "version": "0.4.0",
  "description": "MCP server for Cloudinary's APIs",
  "main": "src/index.js",
  "type": "module",
  "scripts": {
    "test": "vitest --run",
    "inspect": "npx @modelcontextprotocol/inspector node src/index.js",
    "inspect-prod": "npx @modelcontextprotocol/inspector npx -y cloudinary-mcp-server"
  },
  "author": {
    "name": "Yoav Niran",
    "url": "https://linkedin.com/in/yoavniran/"
  },
  "bin": {
    "cloudinary-mcp-server": "./cli.js"
  },
  "files": [
    "LICENSE.md",
    "README.md",
    "src/",
    "package.json"
  ],
  "dependencies": {
    "@modelcontextprotocol/sdk": "latest",
    "cloudinary": "^1.41.3",
    "dotenv": "^16.4.7",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.6.3",
    "chai": "^5.2.0",
    "release-it": "^18.1.2",
    "vitest": "^3.0.9"
  },
  "private": false,
  "engines": {
    "node": ">=18.3.0"
  },
  "keywords": [
    "mcp",
    "mcp-server",
    "cloudinary"
  ]
}

```

--------------------------------------------------------------------------------
/src/tools/uploadTool.test.js:
--------------------------------------------------------------------------------

```javascript
import { createCloudinaryMock } from "../../test/cloudinary.mock";
import getUploadTool from "./uploadTool.js";

describe("uploadTool", () => {
	let cloudinaryMock;
	let uploadTool;

	beforeEach(() => {
		cloudinaryMock = createCloudinaryMock();
		uploadTool = getUploadTool(cloudinaryMock);
		vi.clearAllMocks();
	});

	it("should upload from URL successfully", async () => {
		const mockResult = { public_id: "test123", secure_url: "https://res.cloudinary.com/test" };
		cloudinaryMock.uploader.upload.mockResolvedValue(mockResult);

		const result = await uploadTool({
			source: "https://example.com/image.jpg",
			resourceType: "image",
		});

		expect(cloudinaryMock.uploader.upload).toHaveBeenCalledWith("https://example.com/image.jpg",
			expect.objectContaining({ resource_type: "image" }));
		expect(result.content[0].text).toContain("test123");
		expect(result.isError).toBe(false);
	});

	it("should handle errors during upload", async () => {
		const err = new Error("Upload failed")
		cloudinaryMock.uploader.upload.mockRejectedValue(err);

		const result = await uploadTool({
			source: "https://example.com/image.jpg",
			resourceType: "image",
		});

		expect(result.content[0].text).toContain("Error uploading to Cloudinary: Upload failed");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});
});

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Create NPM Release

on:
    workflow_dispatch:
        inputs:
            releaseType:
                type: choice
                description: 'Version Type'
                required: true
                default: 'patch'
                options:
                    - patch
                    - minor
                    - major
            dry:
                type: boolean
                description: 'Is Dry Run?'
                required: false
                default: false

permissions:
    contents: write

defaults:
    run:
        shell: bash

jobs:
    release:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
              with:
                  fetch-depth: 0
            - name: Define GIT author
              run: |
                  git config user.email "ci@cld-mcp-server"
                  git config user.name "CLD MCP-SERVER CI"

            - name: Set NPM Auth
              env:
                  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
              run: |
                  npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN

            - uses: ./.github/actions/setup

            - run: pnpm test

            - run: |
                git status

            - name: Release
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
              run: |
                  pnpm release-it ${{ inputs.releaseType }} ${{ inputs.dry && ' --dry-run' || '' }}

```

--------------------------------------------------------------------------------
/src/tools/findAssetsTool.js:
--------------------------------------------------------------------------------

```javascript
import { z } from "zod";
import { getToolError } from "../utils.js";
import getCloudinaryTool from "./getCloudinaryTool.js";

export const findAssetsToolParams = {
	expression: z.string().optional().describe("Search expression (e.g. 'tags=cat' or 'public_id:folder/*')"),
	resourceType: z.enum(["image", "video", "raw"]).default("image").describe("Resource type"),
	maxResults: z.number().min(1).max(500).default(10).describe("Maximum number of results"),
	nextCursor: z.string().optional().describe("Next cursor for pagination"),
	tags: z.boolean().optional().describe("Include tags in the response"),
	context: z.boolean().optional().describe("Include context in the response"),
};

const findAssetsTool = async (cloudinary, {
	expression,
	resourceType,
	maxResults,
	nextCursor,
	tags,
	context,
}) => {
	try {
		const options = {
			expression,
			resource_type: resourceType,
			max_results: maxResults,
			next_cursor: nextCursor,
			tags,
			context,
		};

		const result = await cloudinary.api.search(options);

		if (!result?.total_count) {
			return {
				content: [
					{
						type: "text",
						text: "No assets found matching the search criteria",
					},
				],
				isError: false,
			};
		}

		return {
			content: [{
				type: "text",
				text: JSON.stringify(result, null, 2),
			}],
			isError: false,
		};

	} catch (error) {
		return getToolError(`Error searching assets: ${error.message}`, cloudinary);
	}
};

export default getCloudinaryTool(findAssetsTool);

```

--------------------------------------------------------------------------------
/src/tools/getAssetTool.js:
--------------------------------------------------------------------------------

```javascript
import { z } from "zod";
import { getToolError } from "../utils.js";
import getCloudinaryTool from "./getCloudinaryTool.js";

export const getAssetToolParams = {
	assetId: z.string().optional().describe("The Cloudinary asset ID"),
	publicId: z.string().optional().describe("The public ID of the asset"),
	resourceType: z.enum(["image", "raw", "video"]).optional().describe("Type of asset. Default: image"),
	type: z.enum(["upload", "private", "authenticated", "fetch", "facebook", "twitter", "gravatar", "youtube", "hulu", "vimeo", "animoto", "worldstarhiphop", "dailymotion", "list"]).optional().describe("Delivery type. Default: upload"),
	tags: z.boolean().optional().describe("Whether to include the list of tag names. Default: false"),
	context: z.boolean().optional().describe("Whether to include contextual metadata. Default: false"),
	metadata: z.boolean().optional().describe("Whether to include structured metadata. Default: false"),
};

const getAssetTool = async (cloudinary, params) => {
	if (!params.assetId && !params.publicId) {
		return getToolError("Error: Either assetId or publicId must be provided", cloudinary);
	}

	try {
		const { assetId, publicId, tags, context, metadata, resourceType, type } = params;
		let resource;

		if (publicId) {
			resource = await cloudinary.api.resource(publicId, {
				resource_type: resourceType,
				type: type,
				tags,
				context,
				metadata,
			});
		} else {
			const result = await cloudinary.api
				.resources_by_asset_ids([assetId], {
					tags, context, metadata,
				});

			resource = result.resources[0] || null;
		}

		return {
			content: [{
				type: "text",
				text: JSON.stringify(resource, null, 2),
			}],
			isError: false,
		};

	} catch (error) {
		return getToolError(`Error retrieving asset: ${error.message || "unknown error"}`, cloudinary);
	}
};

export default getCloudinaryTool(getAssetTool);


```

--------------------------------------------------------------------------------
/src/tools/uploadTool.js:
--------------------------------------------------------------------------------

```javascript
import { z } from "zod";
import { getToolError } from "../utils.js";
import getCloudinaryTool from "./getCloudinaryTool.js";

export const uploadToolParams = {
	source: z.union([
		z.string().url().describe("URL of the image/video to upload"),
		z.string().describe("Base64 encoded file content or file path"),
		z.instanceof(Buffer).describe("Binary data to upload")
	]).describe("The source media to upload (URL, file path, base64 content, or binary data)"),
	folder: z.string().optional().describe("Optional folder path in Cloudinary"),
	publicId: z.string().optional().describe("Optional public ID for the uploaded asset"),
	resourceType: z.enum(["image", "video", "raw", "auto"]).default("auto").describe("Type of resource to upload"),
	tags: z.string().optional().describe("A string containing Comma-separated list of tags to assign to the asset"),
};

const uploadTool = async (cloudinary, { source, folder, publicId, resourceType, tags }) => {
	try {
		const uploadOptions = {
			resource_type: resourceType,
			folder,
			public_id: publicId,
			tags,
		};

		let uploadResult;

		// Handle different source types
		if (typeof source === 'string') {
			uploadResult = await cloudinary.uploader.upload(source, uploadOptions);
		} else if (Buffer.isBuffer(source)) {
			// Handle Buffer data
			uploadResult = await new Promise((resolve, reject) => {
				const uploadStream = cloudinary.uploader.upload_stream(
					uploadOptions,
					(error, result) => {
						if (error) {
							return reject(error);
						}
						resolve(result);
					}
				);
				uploadStream.end(source);
			});
		} else {
			throw new Error("unknown source type: " + typeof source);
		}

		return {
			content: [
				{
					type: "text",
					text: JSON.stringify(uploadResult, null, 2)
				}
			],
			isError: false,
		};
	} catch (error) {
		return getToolError(`Error uploading to Cloudinary: ${error.message}`, cloudinary);
	}
}

export default getCloudinaryTool(uploadTool);


```

--------------------------------------------------------------------------------
/src/tools/findAssetsTool.test.js:
--------------------------------------------------------------------------------

```javascript
import { createCloudinaryMock } from "../../test/cloudinary.mock";
import getFindAssetsTool from "./findAssetsTool.js";

describe("findAssetsTool", () => {
	let cloudinaryMock;
	let findAssetsTool;

	beforeEach(() => {
		cloudinaryMock = createCloudinaryMock();
		findAssetsTool = getFindAssetsTool(cloudinaryMock);
		vi.clearAllMocks();
	});

	it("should return assets when found", async () => {
		const mockResult = {
			total_count: 2,
			resources: [
				{ public_id: "test1", format: "jpg" },
				{ public_id: "test2", format: "png" },
			],
		};
		cloudinaryMock.api.search.mockResolvedValue(mockResult);

		const result = await findAssetsTool({
			expression: "tags=cat",
			resourceType: "image",
			maxResults: 10,
		});

		expect(cloudinaryMock.api.search).toHaveBeenCalledWith({
			expression: "tags=cat",
			resource_type: "image",
			max_results: 10,
			next_cursor: undefined,
			tags: undefined,
			context: undefined,
		});
		expect(result.content[0].text).toContain("test1");
		expect(result.content[0].text).toContain("test2");
		expect(result.isError).toBe(false);
	});

	it("should handle case when no assets are found", async () => {
		cloudinaryMock.api.search.mockResolvedValue({ total_count: 0, resources: [] });

		const result = await findAssetsTool({
			expression: "tags=nonexistent",
			resourceType: "image",
			maxResults: 10,
		});

		expect(result.content[0].text).toBe("No assets found matching the search criteria");
		expect(result.isError).toBeFalsy();
	});

	it("should handle API errors", async () => {
		cloudinaryMock.api.search.mockRejectedValue(new Error("Invalid search expression"));

		const result = await findAssetsTool({
			expression: "invalid:syntax",
			resourceType: "image",
			maxResults: 10,
		});

		expect(result.content[0].text).toContain("Error searching assets: Invalid search expression");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});
});

```

--------------------------------------------------------------------------------
/src/tools/deleteAssetTool.js:
--------------------------------------------------------------------------------

```javascript
import { z } from "zod";
import { getToolError } from "../utils.js";
import getCloudinaryTool from "./getCloudinaryTool.js";

export const deleteAssetToolParams = {
	publicId: z.string().optional().describe("The public ID of the asset to delete"),
	assetId: z.string().optional().describe("The asset ID of the asset to delete")
};

const deleteWithAssetId = (assetIds) => {
	const config = cloudinary.config();

	if (!assetIds || !Array.isArray(assetIds) || assetIds.length === 0) {
		return Promise.reject(new Error('You must provide an array of asset IDs'));
	}

	// Format asset_ids[] parameters according to the API requirements
	const formData = new URLSearchParams();
	assetIds.forEach(id => formData.append('asset_ids[]', id));

	// Build the request URL
	const apiUrl = `https://api.cloudinary.com/v1_1/${config.cloud_name}/resources`;

	return fetch(apiUrl, {
		method: 'DELETE',
		headers: {
			'Authorization': `Basic ${Buffer.from(`${config.api_key}:${config.api_secret}`).toString('base64')}`,
			'Content-Type': 'application/x-www-form-urlencoded'
		},
		body: formData.toString()
	});
}

const deleteAssetTool = async (cloudinary, { publicId, assetId }) => {
	try {
		let result;

		if (!publicId && !assetId) {
			throw new Error(`Must provide either publicId or assetId to delete`);
		}

		if (publicId) {
			// Delete by public ID using Cloudinary API
			result = await cloudinary.api.delete_resources(publicId);
			if (!result || result?.deleted[publicId] === "not_found") {
				return getToolError(`Failed to delete asset with publicId: '${publicId}' - not_found`, cloudinary);
			}
		} else {
			// Delete by asset ID using /resources endpoint
			result = await deleteWithAssetId([assetId]);

			if (!result.ok) {
				return getToolError(`Failed to delete asset with assetId: '${assetId}' - ${result.error.message}`, cloudinary);
			}
		}

		return {
			content: [{
				type: "text",
				text: `Successfully deleted asset: '${publicId || assetId}'`
			}],
			isError: false,
		};
	} catch (error) {
		return getToolError(`Error deleting asset: ${error.message}`, cloudinary);
	}
};

export default getCloudinaryTool(deleteAssetTool);

```

--------------------------------------------------------------------------------
/src/tools/getAssetTool.test.js:
--------------------------------------------------------------------------------

```javascript
import { createCloudinaryMock } from "../../test/cloudinary.mock";
import getGetAssetTool from "./getAssetTool.js";

describe("getAssetTool", () => {
	let cloudinaryMock;
	let getAssetTool;

	beforeEach(() => {
		cloudinaryMock = createCloudinaryMock();
		getAssetTool = getGetAssetTool(cloudinaryMock);
		vi.clearAllMocks();
	});

	it("should get asset by publicId", async () => {
		const mockAsset = { public_id: "test123", format: "jpg", url: "http://example.com/test.jpg" };
		cloudinaryMock.api.resource.mockResolvedValue(mockAsset);

		const result = await getAssetTool({
			publicId: "test123",
			resourceType: "image",
		});

		expect(cloudinaryMock.api.resource).toHaveBeenCalledWith("test123",
			expect.objectContaining({ resource_type: "image" }));
		expect(result.content[0].text).toContain("test123");
		expect(result.isError).toBe(false);
	});

	it("should get asset by assetId", async () => {
		const mockAsset = { public_id: "test123", asset_id: "asset123" };
		cloudinaryMock.api.resources_by_asset_ids.mockResolvedValue({
			resources: [mockAsset],
		});

		const result = await getAssetTool({
			assetId: "asset123",
		});

		expect(cloudinaryMock.api.resources_by_asset_ids).toHaveBeenCalledWith(["asset123"], expect.any(Object));
		expect(result.content[0].text).toContain("test123");
		expect(result.isError).toBe(false);
	});

	it("should return error if neither publicId nor assetId is provided", async () => {
		const result = await getAssetTool({});

		expect(result.content[0].text).toContain("Either assetId or publicId must be provided");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});

	it("should handle API errors", async () => {
		cloudinaryMock.api.resource.mockRejectedValue(new Error("Resource not found"));

		const result = await getAssetTool({
			publicId: "test123",
		});

		expect(result.content[0].text).toContain("Error retrieving asset: Resource not found");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});
});

```

--------------------------------------------------------------------------------
/src/tools/deleteAssetTool.test.js:
--------------------------------------------------------------------------------

```javascript
import { createCloudinaryMock } from "../../test/cloudinary.mock";
import getDeleteTool from "./deleteAssetTool.js";

describe("deleteAssetTool", () => {
	let cloudinaryMock;
	let deleteTool;

	global.fetch = vi.fn();

	beforeEach(() => {
		cloudinaryMock = createCloudinaryMock();
		deleteTool = getDeleteTool(cloudinaryMock);
		vi.clearAllMocks();
	});

	it("should delete asset by publicId", async () => {
		const mockResult = { deleted: { "test123": "deleted" } };
		cloudinaryMock.api.delete_resources.mockResolvedValue(mockResult);

		const result = await deleteTool({
			publicId: "test123",
		});

		expect(cloudinaryMock.api.delete_resources).toHaveBeenCalledWith("test123");
		expect(result.content[0].text).toContain("Successfully deleted asset: 'test123'");
		expect(result.isError).toBe(false);
	});

	it("should return error if deletion fails for nonexistent public ID", async () => {
		const mockResult = { deleted: { "test123": "not_found" } };
		cloudinaryMock.api.delete_resources.mockResolvedValue(mockResult);

		const result = await deleteTool({
			publicId: "test123",
		});

		expect(result.content[0].text).toContain("Failed to delete asset with publicId: 'test123'");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});

	it("should return error when neither publicId nor assetId is provided", async () => {
		const result = await deleteTool({});

		expect(result.content[0].text).toContain("Must provide either publicId or assetId to delete");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});

	it("should handle API errors", async () => {
		cloudinaryMock.api.delete_resources.mockRejectedValue(new Error("Permission denied"));

		const result = await deleteTool({
			publicId: "test123",
		});

		expect(result.content[0].text).toContain("Error deleting asset: Permission denied");
		expect(result.isError).toBe(true);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
	});
});

```

--------------------------------------------------------------------------------
/src/tools/getUsageTool.test.js:
--------------------------------------------------------------------------------

```javascript
import { createCloudinaryMock } from "../../test/cloudinary.mock";
import getUsageTool, { getUsageToolParams } from "./getUsageTool.js";
import { z } from "zod";

describe("getUsageTool", () => {
	let cloudinaryMock;
	let usageTool;

	beforeEach(() => {
		cloudinaryMock = createCloudinaryMock();
		usageTool = getUsageTool(cloudinaryMock);
		vi.clearAllMocks();
	});

	it("should have proper parameter schema", () => {
		expect(getUsageToolParams).toBeDefined();
		expect(getUsageToolParams.date).toBeInstanceOf(z.ZodOptional);
	});

	it("should successfully get usage data without date parameter", async () => {
		const mockUsageData = {
			plan: "free",
			last_updated: "2023-05-10",
			transformations: {
				usage: 10,
				limit: 500
			},
			objects: {
				usage: 5,
				limit: 100
			}
		};

		cloudinaryMock.api.usage.mockResolvedValue(mockUsageData);

		const result = await usageTool({});

		expect(cloudinaryMock.api.usage).toHaveBeenCalledWith({});
		expect(result.isError).toBe(false);
		expect(result.content).toHaveLength(1);
		expect(result.content[0].type).toBe("text");
		expect(JSON.parse(result.content[0].text)).toEqual(mockUsageData);
	});

	it("should successfully get usage data with date parameter", async () => {
		const mockUsageData = {
			plan: "free",
			last_updated: "2023-04-15",
			transformations: {
				usage: 8,
				limit: 500
			},
			objects: {
				usage: 3,
				limit: 100
			}
		};

		const date = "2023-04-15";
		cloudinaryMock.api.usage.mockResolvedValue(mockUsageData);

		const result = await usageTool({ date });

		expect(cloudinaryMock.api.usage).toHaveBeenCalledWith({ date });
		expect(result.isError).toBe(false);
		expect(result.content).toHaveLength(1);
		expect(result.content[0].type).toBe("text");
		expect(JSON.parse(result.content[0].text)).toEqual(mockUsageData);
	});

	it("should handle API errors", async () => {
		const errorMessage = "API Error: Invalid date format";
		cloudinaryMock.api.usage.mockRejectedValue(new Error(errorMessage));

		const result = await usageTool({ date: "invalid-date" });

		expect(result.isError).toBe(true);
		expect(result.content).toHaveLength(1);
		expect(result.content[0].type).toBe("text");
		expect(result.content[0].text).toContain(errorMessage);
		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`);
	});
});

```

--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------

```javascript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { v2 as cloudinary } from "cloudinary";
import dotenv from "dotenv";

import getUploadTool, { uploadToolParams } from "./tools/uploadTool.js";
import getDeleteTool, { deleteAssetToolParams } from "./tools/deleteAssetTool.js";
import getGetAssetTool, { getAssetToolParams } from "./tools/getAssetTool.js";
import getFindAssetsTool, { findAssetsToolParams } from "./tools/findAssetsTool.js";
import getUsageTool, { getUsageToolParams } from "./tools/getUsageTool.js";

dotenv.config();

["CLOUDINARY_CLOUD_NAME", "CLOUDINARY_API_KEY", "CLOUDINARY_API_SECRET"].forEach((envVar) => {
	if (!process.env[envVar]) {
		console.error(`Please provide ${envVar} environment variable.`);
		process.exit(1);
	}
});

// Configure Cloudinary with environment variables
cloudinary.config({
	cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
	api_key: process.env.CLOUDINARY_API_KEY,
	api_secret: process.env.CLOUDINARY_API_SECRET,
});

// Create server instance
const server = new McpServer({
	name: "cloudinary",
	version: "1.0.0",
});

/**
 * Tool for Uploading an asset to Cloudinary
 */
server.tool(
	"upload",
	"Upload a file (asset) to Cloudinary",
	uploadToolParams,
	getUploadTool(cloudinary),
);

/**
 * Tool for Deleting an asset from Cloudinary
 */
server.tool(
	"delete-asset",
	"Delete a file (asset) from Cloudinary",
	deleteAssetToolParams,
	getDeleteTool(cloudinary),
);

/**
 * Tool for Getting asset details from Cloudinary
 */
server.tool(
	"get-asset",
	"Get the details of a specific file (asset)",
	getAssetToolParams,
	getGetAssetTool(cloudinary),
);

/**
 * Tool for finding assets in Cloudinary
 */
server.tool(
	"find-assets",
	"Search for existing files (assets) in Cloudinary with a query expression",
	findAssetsToolParams,
	getFindAssetsTool(cloudinary),
);

/**
 * Tool for getting usage information from Cloudinary
 */
server.tool(
	"get-usage",
	"Get a report on the status of your product environment usage, including storage, credits, bandwidth, requests, number of resources, and add-on usage",
	getUsageToolParams,
	getUsageTool(cloudinary),
);


// Start the server with stdio transport
const main = async () => {
	const transport = new StdioServerTransport();
	await server.connect(transport);
	console.error("Cloudinary MCP Server running on stdio");
};

main()
	.catch((error) => {
		console.error("Fatal error in main():", error);
		process.exit(1);
	});

// Export Cloudinary for testing
export {
	cloudinary,
};

```