#
tokens: 11674/50000 25/25 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | llm-docs/
 2 | .idea/
 3 | node_modules/
 4 | .env
 5 | dist/
 6 | coverage/
 7 | *.log
 8 | test/server.test.js
 9 | test/test.png
10 | test/cld.js
11 | 
```

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

```
 1 | .idea/
 2 | .vscode/
 3 | .github/
 4 | src/
 5 | *.story.js
 6 | *.stories.js
 7 | stories/
 8 | *.test.js
 9 | tests/
10 | test/
11 | mocks/
12 | *.log
13 | jest*
14 | .editorconfig
15 | .eslintignore
16 | .eslintrc.js
17 | .circleci/
18 | .sb-static/
19 | .env
20 | .jest-cache/
21 | .jest-coverage/
22 | scripts/
23 | .storybook/
24 | tsconfig.json
25 | .eslintcache
26 | cypress/
27 | rollup.config.js
28 | babel.config.js
29 | cypress.json
30 | 
31 | .nyc_output/
32 | coverage/
33 | reports/
34 | .reporters-config.json
35 | .nycrc
36 | 
37 | test-setup.js
38 | codecov
39 | codecov.yml
40 | .mocharc.js
41 | 
```

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

```markdown
  1 | <img src="https://github.com/yoavniran/cloudinary-mcp-server/blob/main/cld-mcp-server.png?raw=true" width="120" height="120" align="center" />
  2 | 
  3 | # Cloudinary MCP Server
  4 | 
  5 | <a href="https://glama.ai/mcp/servers/@yoavniran/cloudinary-mcp-server">
  6 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@yoavniran/cloudinary-mcp-server/badge" alt="cloudinary-mcp-server MCP server" />
  7 | </a>
  8 | 
  9 | <p align="center">
 10 |     <a href="https://badge.fury.io/js/cloudinary-mcp-server">
 11 |         <img src="https://badge.fury.io/js/cloudinary-mcp-server.svg" alt="npm version" height="20">
 12 |     </a>
 13 | </p>
 14 | 
 15 | A Model Context Protocol server that exposes Cloudinary Upload & Admin API methods as tools by AI assistants. 
 16 | This integration allows AI systems to trigger and interact with your Cloudinary cloud.
 17 | 
 18 | ## How It Works
 19 | 
 20 | The MCP server:
 21 | 
 22 | -   Makes calls on your behalf to the Cloudinary API
 23 | -   Enables uploading of assets to Cloudinary
 24 | -   Enables management of assets in your Cloudinary cloud
 25 | 
 26 | It relies on the Cloudinary [API](https://cloudinary.com/documentation/admin_api) to perform these actions. Not all methods and parameters are supported. 
 27 | More will be added over time. 
 28 | 
 29 | Open an [issue](https://github.com/yoavniran/cloudinary-mcp-server/issues) with a request for specific method if you need it.
 30 | 
 31 | ## Benefits
 32 | 
 33 | -   Turn your Cloudinary cloud actions into callable tools for AI assistants
 34 | -   Turn your Cloudinary assets into data for AI assistants
 35 | 
 36 | ## Usage with Claude Desktop
 37 | 
 38 | ### Prerequisites
 39 | 
 40 | -   NodeJS
 41 | -   MCP Client (like Claude Desktop App)
 42 | -   Create & Copy Cloudinary API Key/Secret at: [API KEYS](https://console.cloudinary.com/settings/api-keys)
 43 | 
 44 | ### Installation
 45 | 
 46 | To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
 47 | 
 48 | ```json
 49 | {
 50 |     "mcpServers": {
 51 |         "cloudinary-mcp-server": {
 52 |             "command": "npx",
 53 |             "args": ["-y", "cloudinary-mcp-server"],
 54 |             "env": {
 55 |                 "CLOUDINARY_CLOUD_NAME": "<cloud name>",
 56 |                 "CLOUDINARY_API_KEY": "<api-key>",
 57 |                 "CLOUDINARY_API_SECRET": "<api-secret>"
 58 |             }
 59 |         }
 60 |     }
 61 | }
 62 | ```
 63 | 
 64 | -   `CLOUDINARY_CLOUD_NAME` - your cloud name
 65 | -   `CLOUDINARY_API_KEY` - The API Key for your cloud
 66 | -   `CLOUDINARY_API_SECRET` - The API Secret for your cloud
 67 | 
 68 | 
 69 | ### Tools
 70 | 
 71 | The following tools are available:
 72 | 
 73 | 1. **upload**
 74 |     - Description: Upload a file (asset) to Cloudinary
 75 |     - Parameters:
 76 |         - `source`: URL, file path, base64 content, or binary data to upload
 77 |         - `folder`: Optional folder path in Cloudinary
 78 |         - `publicId`: Optional public ID for the uploaded asset
 79 |         - `resourceType`: Type of resource to upload (image, video, raw, auto)
 80 |         - `tags`: Comma-separated list of tags to assign to the asset
 81 | 
 82 | 2. **delete-asset**
 83 |     - Description: Delete a file (asset) from Cloudinary
 84 |     - Parameters:
 85 |         - `publicId`: The public ID of the asset to delete
 86 |         - `assetId`: The asset ID of the asset to delete
 87 | 
 88 | 3. **get-asset**
 89 |     - Description: Get the details of a specific file (asset)
 90 |     - Parameters:
 91 |         - `assetId`: The Cloudinary asset ID
 92 |         - `publicId`: The public ID of the asset
 93 |         - `resourceType`: Type of asset (image, raw, video)
 94 |         - `type`: Delivery type (upload, private, authenticated, etc.)
 95 |         - `tags`: Whether to include the list of tag names
 96 |         - `context`: Whether to include contextual metadata
 97 |         - `metadata`: Whether to include structured metadata
 98 | 
 99 | 4. **find-assets**
100 |     - Description: Search for existing files (assets) in Cloudinary with a query expression
101 |     - Parameters:
102 |         - `expression`: Search expression (e.g. 'tags=cat' or 'public_id:folder/*')
103 |         - `resourceType`: Resource type (image, video, raw)
104 |         - `maxResults`: Maximum number of results (1-500)
105 |         - `nextCursor`: Next cursor for pagination
106 |         - `tags`: Include tags in the response
107 |         - `context`: Include context in the response
108 | 
109 | 5. **get-usage**
110 |     - Description: Get a report on the status of your product environment usage, including storage, credits, bandwidth, requests, number of resources, and add-on usage
111 |     - Parameters:
112 |         - `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
 1 | MIT License
 2 | 
 3 | Copyright (c) 2025 Yoav Niran
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
```

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

```json
1 | {
2 |   "$schema": "https://glama.ai/mcp/schemas/server.json",
3 |   "maintainers": [
4 |     "yoavniran"
5 |   ]
6 | }
7 | 
```

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

```javascript
1 | #!/usr/bin/env node
2 | 
3 | import("./src/index.js").catch(err => {
4 | 	console.error("Failed to start server:", err);
5 | 	process.exit(1);
6 | });
7 | 
```

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

```javascript
1 | const getCloudinaryTool = (tool) => {
2 | 	return (cloudinary) => (params) => tool(cloudinary, params);
3 | };
4 | 
5 | export default getCloudinaryTool;
6 | 
```

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

```javascript
 1 | import { defineConfig } from "vitest/config";
 2 | 
 3 | export default defineConfig({
 4 | 	test: {
 5 | 		globals: true,
 6 | 		setupFiles: "./test/vitest-setup.js",
 7 | 		include: ["src/**/*.test.js?(x)"],
 8 | 	},
 9 | });
10 | 
```

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

```javascript
 1 | export const getToolError = (msg, cloudinary) => {
 2 | 	const conf = cloudinary.config()
 3 | 	return {
 4 | 		content: [{
 5 | 			type: "text",
 6 | 			text: `${msg} (cloud: ${conf.cloud_name}, key: ${conf.api_key.slice(0,4)}...)`,
 7 | 		}],
 8 | 		isError: true,
 9 | 	};
10 | };
11 | 
```

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

```javascript
 1 | import { afterEach } from "vitest";
 2 | 
 3 | // runs a cleanup after each test case (e.g. clearing jsdom)
 4 | afterEach(() => {
 5 | });
 6 | 
 7 | global.clearViMocks = (...mocks) => {
 8 | 	mocks.forEach((mock) => {
 9 | 		if (mock) {
10 | 			if (mock.mockClear) {
11 | 				mock.mockClear();
12 | 			} else {
13 | 				global.clearViMocks(...Object.values(mock));
14 | 			}
15 | 		}
16 | 	});
17 | };
18 | 
```

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

```yaml
 1 | name: Setup & install
 2 | description: install pnpm & deps
 3 | 
 4 | runs:
 5 |     using: composite
 6 |     steps:
 7 |         - uses: pnpm/action-setup@v4
 8 |           with:
 9 |               version: 9
10 | 
11 |         - uses: actions/setup-node@v4
12 |           with:
13 |               node-version: "20.15"
14 |               cache: "pnpm"
15 |               cache-dependency-path: "./pnpm-lock.yaml"
16 | 
17 |         - run: pnpm install --frozen-lockfile
18 |           shell: bash
19 | 
```

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

```javascript
 1 | export const createCloudinaryMock = () => {
 2 | 	return {
 3 | 		config: () => ({
 4 | 			cloud_name: "test-cloud",
 5 | 			api_key: "test-key",
 6 | 			api_secret: "test-secret"
 7 | 		}),
 8 | 		uploader: {
 9 | 			upload: vi.fn(),
10 | 			upload_stream: vi.fn()
11 | 		},
12 | 		utils: {
13 | 			api_sign_request: vi.fn()
14 | 		},
15 | 		api: {
16 | 			resource: vi.fn(),
17 | 			resources_by_asset_ids: vi.fn(),
18 | 			delete_resources: vi.fn(),
19 | 			search: vi.fn(),
20 | 			usage: vi.fn(),  // Added usage method to the mock
21 | 		},
22 | 	};
23 | };
24 | 
```

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

```javascript
 1 | import { z } from "zod";
 2 | import { getToolError } from "../utils.js";
 3 | import getCloudinaryTool from "./getCloudinaryTool.js";
 4 | 
 5 | export const getUsageToolParams = {
 6 | 	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")
 7 | }
 8 | 
 9 | const getUsageTool = async (cloudinary, { date }) => {
10 | 	try {
11 | 		const usageOptions = {
12 | 			date
13 | 		};
14 | 
15 | 		const usageResult = await cloudinary.api.usage(usageOptions);
16 | 
17 | 		return {
18 | 			content: [
19 | 				{
20 | 					type: "text",
21 | 					text: JSON.stringify(usageResult, null, 2)
22 | 				}
23 | 			],
24 | 			isError: false,
25 | 		};
26 | 	} catch (error) {
27 | 		return getToolError(`Error getting usage report from Cloudinary: ${error.message}`, cloudinary);
28 | 	}
29 | };
30 | 
31 | export default getCloudinaryTool(getUsageTool);
32 | 
```

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

```json
 1 | {
 2 |   "name": "cloudinary-mcp-server",
 3 |   "version": "0.4.0",
 4 |   "description": "MCP server for Cloudinary's APIs",
 5 |   "main": "src/index.js",
 6 |   "type": "module",
 7 |   "scripts": {
 8 |     "test": "vitest --run",
 9 |     "inspect": "npx @modelcontextprotocol/inspector node src/index.js",
10 |     "inspect-prod": "npx @modelcontextprotocol/inspector npx -y cloudinary-mcp-server"
11 |   },
12 |   "author": {
13 |     "name": "Yoav Niran",
14 |     "url": "https://linkedin.com/in/yoavniran/"
15 |   },
16 |   "bin": {
17 |     "cloudinary-mcp-server": "./cli.js"
18 |   },
19 |   "files": [
20 |     "LICENSE.md",
21 |     "README.md",
22 |     "src/",
23 |     "package.json"
24 |   ],
25 |   "dependencies": {
26 |     "@modelcontextprotocol/sdk": "latest",
27 |     "cloudinary": "^1.41.3",
28 |     "dotenv": "^16.4.7",
29 |     "zod": "^3.24.2"
30 |   },
31 |   "devDependencies": {
32 |     "@testing-library/jest-dom": "^6.6.3",
33 |     "chai": "^5.2.0",
34 |     "release-it": "^18.1.2",
35 |     "vitest": "^3.0.9"
36 |   },
37 |   "private": false,
38 |   "engines": {
39 |     "node": ">=18.3.0"
40 |   },
41 |   "keywords": [
42 |     "mcp",
43 |     "mcp-server",
44 |     "cloudinary"
45 |   ]
46 | }
47 | 
```

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

```javascript
 1 | import { createCloudinaryMock } from "../../test/cloudinary.mock";
 2 | import getUploadTool from "./uploadTool.js";
 3 | 
 4 | describe("uploadTool", () => {
 5 | 	let cloudinaryMock;
 6 | 	let uploadTool;
 7 | 
 8 | 	beforeEach(() => {
 9 | 		cloudinaryMock = createCloudinaryMock();
10 | 		uploadTool = getUploadTool(cloudinaryMock);
11 | 		vi.clearAllMocks();
12 | 	});
13 | 
14 | 	it("should upload from URL successfully", async () => {
15 | 		const mockResult = { public_id: "test123", secure_url: "https://res.cloudinary.com/test" };
16 | 		cloudinaryMock.uploader.upload.mockResolvedValue(mockResult);
17 | 
18 | 		const result = await uploadTool({
19 | 			source: "https://example.com/image.jpg",
20 | 			resourceType: "image",
21 | 		});
22 | 
23 | 		expect(cloudinaryMock.uploader.upload).toHaveBeenCalledWith("https://example.com/image.jpg",
24 | 			expect.objectContaining({ resource_type: "image" }));
25 | 		expect(result.content[0].text).toContain("test123");
26 | 		expect(result.isError).toBe(false);
27 | 	});
28 | 
29 | 	it("should handle errors during upload", async () => {
30 | 		const err = new Error("Upload failed")
31 | 		cloudinaryMock.uploader.upload.mockRejectedValue(err);
32 | 
33 | 		const result = await uploadTool({
34 | 			source: "https://example.com/image.jpg",
35 | 			resourceType: "image",
36 | 		});
37 | 
38 | 		expect(result.content[0].text).toContain("Error uploading to Cloudinary: Upload failed");
39 | 		expect(result.isError).toBe(true);
40 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
41 | 	});
42 | });
43 | 
```

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

```yaml
 1 | name: Create NPM Release
 2 | 
 3 | on:
 4 |     workflow_dispatch:
 5 |         inputs:
 6 |             releaseType:
 7 |                 type: choice
 8 |                 description: 'Version Type'
 9 |                 required: true
10 |                 default: 'patch'
11 |                 options:
12 |                     - patch
13 |                     - minor
14 |                     - major
15 |             dry:
16 |                 type: boolean
17 |                 description: 'Is Dry Run?'
18 |                 required: false
19 |                 default: false
20 | 
21 | permissions:
22 |     contents: write
23 | 
24 | defaults:
25 |     run:
26 |         shell: bash
27 | 
28 | jobs:
29 |     release:
30 |         runs-on: ubuntu-latest
31 |         steps:
32 |             - uses: actions/checkout@v4
33 |               with:
34 |                   fetch-depth: 0
35 |             - name: Define GIT author
36 |               run: |
37 |                   git config user.email "ci@cld-mcp-server"
38 |                   git config user.name "CLD MCP-SERVER CI"
39 | 
40 |             - name: Set NPM Auth
41 |               env:
42 |                   NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |               run: |
44 |                   npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
45 | 
46 |             - uses: ./.github/actions/setup
47 | 
48 |             - run: pnpm test
49 | 
50 |             - run: |
51 |                 git status
52 | 
53 |             - name: Release
54 |               env:
55 |                   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 |               run: |
57 |                   pnpm release-it ${{ inputs.releaseType }} ${{ inputs.dry && ' --dry-run' || '' }}
58 | 
```

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

```javascript
 1 | import { z } from "zod";
 2 | import { getToolError } from "../utils.js";
 3 | import getCloudinaryTool from "./getCloudinaryTool.js";
 4 | 
 5 | export const findAssetsToolParams = {
 6 | 	expression: z.string().optional().describe("Search expression (e.g. 'tags=cat' or 'public_id:folder/*')"),
 7 | 	resourceType: z.enum(["image", "video", "raw"]).default("image").describe("Resource type"),
 8 | 	maxResults: z.number().min(1).max(500).default(10).describe("Maximum number of results"),
 9 | 	nextCursor: z.string().optional().describe("Next cursor for pagination"),
10 | 	tags: z.boolean().optional().describe("Include tags in the response"),
11 | 	context: z.boolean().optional().describe("Include context in the response"),
12 | };
13 | 
14 | const findAssetsTool = async (cloudinary, {
15 | 	expression,
16 | 	resourceType,
17 | 	maxResults,
18 | 	nextCursor,
19 | 	tags,
20 | 	context,
21 | }) => {
22 | 	try {
23 | 		const options = {
24 | 			expression,
25 | 			resource_type: resourceType,
26 | 			max_results: maxResults,
27 | 			next_cursor: nextCursor,
28 | 			tags,
29 | 			context,
30 | 		};
31 | 
32 | 		const result = await cloudinary.api.search(options);
33 | 
34 | 		if (!result?.total_count) {
35 | 			return {
36 | 				content: [
37 | 					{
38 | 						type: "text",
39 | 						text: "No assets found matching the search criteria",
40 | 					},
41 | 				],
42 | 				isError: false,
43 | 			};
44 | 		}
45 | 
46 | 		return {
47 | 			content: [{
48 | 				type: "text",
49 | 				text: JSON.stringify(result, null, 2),
50 | 			}],
51 | 			isError: false,
52 | 		};
53 | 
54 | 	} catch (error) {
55 | 		return getToolError(`Error searching assets: ${error.message}`, cloudinary);
56 | 	}
57 | };
58 | 
59 | export default getCloudinaryTool(findAssetsTool);
60 | 
```

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

```javascript
 1 | import { z } from "zod";
 2 | import { getToolError } from "../utils.js";
 3 | import getCloudinaryTool from "./getCloudinaryTool.js";
 4 | 
 5 | export const getAssetToolParams = {
 6 | 	assetId: z.string().optional().describe("The Cloudinary asset ID"),
 7 | 	publicId: z.string().optional().describe("The public ID of the asset"),
 8 | 	resourceType: z.enum(["image", "raw", "video"]).optional().describe("Type of asset. Default: image"),
 9 | 	type: z.enum(["upload", "private", "authenticated", "fetch", "facebook", "twitter", "gravatar", "youtube", "hulu", "vimeo", "animoto", "worldstarhiphop", "dailymotion", "list"]).optional().describe("Delivery type. Default: upload"),
10 | 	tags: z.boolean().optional().describe("Whether to include the list of tag names. Default: false"),
11 | 	context: z.boolean().optional().describe("Whether to include contextual metadata. Default: false"),
12 | 	metadata: z.boolean().optional().describe("Whether to include structured metadata. Default: false"),
13 | };
14 | 
15 | const getAssetTool = async (cloudinary, params) => {
16 | 	if (!params.assetId && !params.publicId) {
17 | 		return getToolError("Error: Either assetId or publicId must be provided", cloudinary);
18 | 	}
19 | 
20 | 	try {
21 | 		const { assetId, publicId, tags, context, metadata, resourceType, type } = params;
22 | 		let resource;
23 | 
24 | 		if (publicId) {
25 | 			resource = await cloudinary.api.resource(publicId, {
26 | 				resource_type: resourceType,
27 | 				type: type,
28 | 				tags,
29 | 				context,
30 | 				metadata,
31 | 			});
32 | 		} else {
33 | 			const result = await cloudinary.api
34 | 				.resources_by_asset_ids([assetId], {
35 | 					tags, context, metadata,
36 | 				});
37 | 
38 | 			resource = result.resources[0] || null;
39 | 		}
40 | 
41 | 		return {
42 | 			content: [{
43 | 				type: "text",
44 | 				text: JSON.stringify(resource, null, 2),
45 | 			}],
46 | 			isError: false,
47 | 		};
48 | 
49 | 	} catch (error) {
50 | 		return getToolError(`Error retrieving asset: ${error.message || "unknown error"}`, cloudinary);
51 | 	}
52 | };
53 | 
54 | export default getCloudinaryTool(getAssetTool);
55 | 
56 | 
```

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

```javascript
 1 | import { z } from "zod";
 2 | import { getToolError } from "../utils.js";
 3 | import getCloudinaryTool from "./getCloudinaryTool.js";
 4 | 
 5 | export const uploadToolParams = {
 6 | 	source: z.union([
 7 | 		z.string().url().describe("URL of the image/video to upload"),
 8 | 		z.string().describe("Base64 encoded file content or file path"),
 9 | 		z.instanceof(Buffer).describe("Binary data to upload")
10 | 	]).describe("The source media to upload (URL, file path, base64 content, or binary data)"),
11 | 	folder: z.string().optional().describe("Optional folder path in Cloudinary"),
12 | 	publicId: z.string().optional().describe("Optional public ID for the uploaded asset"),
13 | 	resourceType: z.enum(["image", "video", "raw", "auto"]).default("auto").describe("Type of resource to upload"),
14 | 	tags: z.string().optional().describe("A string containing Comma-separated list of tags to assign to the asset"),
15 | };
16 | 
17 | const uploadTool = async (cloudinary, { source, folder, publicId, resourceType, tags }) => {
18 | 	try {
19 | 		const uploadOptions = {
20 | 			resource_type: resourceType,
21 | 			folder,
22 | 			public_id: publicId,
23 | 			tags,
24 | 		};
25 | 
26 | 		let uploadResult;
27 | 
28 | 		// Handle different source types
29 | 		if (typeof source === 'string') {
30 | 			uploadResult = await cloudinary.uploader.upload(source, uploadOptions);
31 | 		} else if (Buffer.isBuffer(source)) {
32 | 			// Handle Buffer data
33 | 			uploadResult = await new Promise((resolve, reject) => {
34 | 				const uploadStream = cloudinary.uploader.upload_stream(
35 | 					uploadOptions,
36 | 					(error, result) => {
37 | 						if (error) {
38 | 							return reject(error);
39 | 						}
40 | 						resolve(result);
41 | 					}
42 | 				);
43 | 				uploadStream.end(source);
44 | 			});
45 | 		} else {
46 | 			throw new Error("unknown source type: " + typeof source);
47 | 		}
48 | 
49 | 		return {
50 | 			content: [
51 | 				{
52 | 					type: "text",
53 | 					text: JSON.stringify(uploadResult, null, 2)
54 | 				}
55 | 			],
56 | 			isError: false,
57 | 		};
58 | 	} catch (error) {
59 | 		return getToolError(`Error uploading to Cloudinary: ${error.message}`, cloudinary);
60 | 	}
61 | }
62 | 
63 | export default getCloudinaryTool(uploadTool);
64 | 
65 | 
```

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

```javascript
 1 | import { createCloudinaryMock } from "../../test/cloudinary.mock";
 2 | import getFindAssetsTool from "./findAssetsTool.js";
 3 | 
 4 | describe("findAssetsTool", () => {
 5 | 	let cloudinaryMock;
 6 | 	let findAssetsTool;
 7 | 
 8 | 	beforeEach(() => {
 9 | 		cloudinaryMock = createCloudinaryMock();
10 | 		findAssetsTool = getFindAssetsTool(cloudinaryMock);
11 | 		vi.clearAllMocks();
12 | 	});
13 | 
14 | 	it("should return assets when found", async () => {
15 | 		const mockResult = {
16 | 			total_count: 2,
17 | 			resources: [
18 | 				{ public_id: "test1", format: "jpg" },
19 | 				{ public_id: "test2", format: "png" },
20 | 			],
21 | 		};
22 | 		cloudinaryMock.api.search.mockResolvedValue(mockResult);
23 | 
24 | 		const result = await findAssetsTool({
25 | 			expression: "tags=cat",
26 | 			resourceType: "image",
27 | 			maxResults: 10,
28 | 		});
29 | 
30 | 		expect(cloudinaryMock.api.search).toHaveBeenCalledWith({
31 | 			expression: "tags=cat",
32 | 			resource_type: "image",
33 | 			max_results: 10,
34 | 			next_cursor: undefined,
35 | 			tags: undefined,
36 | 			context: undefined,
37 | 		});
38 | 		expect(result.content[0].text).toContain("test1");
39 | 		expect(result.content[0].text).toContain("test2");
40 | 		expect(result.isError).toBe(false);
41 | 	});
42 | 
43 | 	it("should handle case when no assets are found", async () => {
44 | 		cloudinaryMock.api.search.mockResolvedValue({ total_count: 0, resources: [] });
45 | 
46 | 		const result = await findAssetsTool({
47 | 			expression: "tags=nonexistent",
48 | 			resourceType: "image",
49 | 			maxResults: 10,
50 | 		});
51 | 
52 | 		expect(result.content[0].text).toBe("No assets found matching the search criteria");
53 | 		expect(result.isError).toBeFalsy();
54 | 	});
55 | 
56 | 	it("should handle API errors", async () => {
57 | 		cloudinaryMock.api.search.mockRejectedValue(new Error("Invalid search expression"));
58 | 
59 | 		const result = await findAssetsTool({
60 | 			expression: "invalid:syntax",
61 | 			resourceType: "image",
62 | 			maxResults: 10,
63 | 		});
64 | 
65 | 		expect(result.content[0].text).toContain("Error searching assets: Invalid search expression");
66 | 		expect(result.isError).toBe(true);
67 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
68 | 	});
69 | });
70 | 
```

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

```javascript
 1 | import { z } from "zod";
 2 | import { getToolError } from "../utils.js";
 3 | import getCloudinaryTool from "./getCloudinaryTool.js";
 4 | 
 5 | export const deleteAssetToolParams = {
 6 | 	publicId: z.string().optional().describe("The public ID of the asset to delete"),
 7 | 	assetId: z.string().optional().describe("The asset ID of the asset to delete")
 8 | };
 9 | 
10 | const deleteWithAssetId = (assetIds) => {
11 | 	const config = cloudinary.config();
12 | 
13 | 	if (!assetIds || !Array.isArray(assetIds) || assetIds.length === 0) {
14 | 		return Promise.reject(new Error('You must provide an array of asset IDs'));
15 | 	}
16 | 
17 | 	// Format asset_ids[] parameters according to the API requirements
18 | 	const formData = new URLSearchParams();
19 | 	assetIds.forEach(id => formData.append('asset_ids[]', id));
20 | 
21 | 	// Build the request URL
22 | 	const apiUrl = `https://api.cloudinary.com/v1_1/${config.cloud_name}/resources`;
23 | 
24 | 	return fetch(apiUrl, {
25 | 		method: 'DELETE',
26 | 		headers: {
27 | 			'Authorization': `Basic ${Buffer.from(`${config.api_key}:${config.api_secret}`).toString('base64')}`,
28 | 			'Content-Type': 'application/x-www-form-urlencoded'
29 | 		},
30 | 		body: formData.toString()
31 | 	});
32 | }
33 | 
34 | const deleteAssetTool = async (cloudinary, { publicId, assetId }) => {
35 | 	try {
36 | 		let result;
37 | 
38 | 		if (!publicId && !assetId) {
39 | 			throw new Error(`Must provide either publicId or assetId to delete`);
40 | 		}
41 | 
42 | 		if (publicId) {
43 | 			// Delete by public ID using Cloudinary API
44 | 			result = await cloudinary.api.delete_resources(publicId);
45 | 			if (!result || result?.deleted[publicId] === "not_found") {
46 | 				return getToolError(`Failed to delete asset with publicId: '${publicId}' - not_found`, cloudinary);
47 | 			}
48 | 		} else {
49 | 			// Delete by asset ID using /resources endpoint
50 | 			result = await deleteWithAssetId([assetId]);
51 | 
52 | 			if (!result.ok) {
53 | 				return getToolError(`Failed to delete asset with assetId: '${assetId}' - ${result.error.message}`, cloudinary);
54 | 			}
55 | 		}
56 | 
57 | 		return {
58 | 			content: [{
59 | 				type: "text",
60 | 				text: `Successfully deleted asset: '${publicId || assetId}'`
61 | 			}],
62 | 			isError: false,
63 | 		};
64 | 	} catch (error) {
65 | 		return getToolError(`Error deleting asset: ${error.message}`, cloudinary);
66 | 	}
67 | };
68 | 
69 | export default getCloudinaryTool(deleteAssetTool);
70 | 
```

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

```javascript
 1 | import { createCloudinaryMock } from "../../test/cloudinary.mock";
 2 | import getGetAssetTool from "./getAssetTool.js";
 3 | 
 4 | describe("getAssetTool", () => {
 5 | 	let cloudinaryMock;
 6 | 	let getAssetTool;
 7 | 
 8 | 	beforeEach(() => {
 9 | 		cloudinaryMock = createCloudinaryMock();
10 | 		getAssetTool = getGetAssetTool(cloudinaryMock);
11 | 		vi.clearAllMocks();
12 | 	});
13 | 
14 | 	it("should get asset by publicId", async () => {
15 | 		const mockAsset = { public_id: "test123", format: "jpg", url: "http://example.com/test.jpg" };
16 | 		cloudinaryMock.api.resource.mockResolvedValue(mockAsset);
17 | 
18 | 		const result = await getAssetTool({
19 | 			publicId: "test123",
20 | 			resourceType: "image",
21 | 		});
22 | 
23 | 		expect(cloudinaryMock.api.resource).toHaveBeenCalledWith("test123",
24 | 			expect.objectContaining({ resource_type: "image" }));
25 | 		expect(result.content[0].text).toContain("test123");
26 | 		expect(result.isError).toBe(false);
27 | 	});
28 | 
29 | 	it("should get asset by assetId", async () => {
30 | 		const mockAsset = { public_id: "test123", asset_id: "asset123" };
31 | 		cloudinaryMock.api.resources_by_asset_ids.mockResolvedValue({
32 | 			resources: [mockAsset],
33 | 		});
34 | 
35 | 		const result = await getAssetTool({
36 | 			assetId: "asset123",
37 | 		});
38 | 
39 | 		expect(cloudinaryMock.api.resources_by_asset_ids).toHaveBeenCalledWith(["asset123"], expect.any(Object));
40 | 		expect(result.content[0].text).toContain("test123");
41 | 		expect(result.isError).toBe(false);
42 | 	});
43 | 
44 | 	it("should return error if neither publicId nor assetId is provided", async () => {
45 | 		const result = await getAssetTool({});
46 | 
47 | 		expect(result.content[0].text).toContain("Either assetId or publicId must be provided");
48 | 		expect(result.isError).toBe(true);
49 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
50 | 	});
51 | 
52 | 	it("should handle API errors", async () => {
53 | 		cloudinaryMock.api.resource.mockRejectedValue(new Error("Resource not found"));
54 | 
55 | 		const result = await getAssetTool({
56 | 			publicId: "test123",
57 | 		});
58 | 
59 | 		expect(result.content[0].text).toContain("Error retrieving asset: Resource not found");
60 | 		expect(result.isError).toBe(true);
61 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
62 | 	});
63 | });
64 | 
```

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

```javascript
 1 | import { createCloudinaryMock } from "../../test/cloudinary.mock";
 2 | import getDeleteTool from "./deleteAssetTool.js";
 3 | 
 4 | describe("deleteAssetTool", () => {
 5 | 	let cloudinaryMock;
 6 | 	let deleteTool;
 7 | 
 8 | 	global.fetch = vi.fn();
 9 | 
10 | 	beforeEach(() => {
11 | 		cloudinaryMock = createCloudinaryMock();
12 | 		deleteTool = getDeleteTool(cloudinaryMock);
13 | 		vi.clearAllMocks();
14 | 	});
15 | 
16 | 	it("should delete asset by publicId", async () => {
17 | 		const mockResult = { deleted: { "test123": "deleted" } };
18 | 		cloudinaryMock.api.delete_resources.mockResolvedValue(mockResult);
19 | 
20 | 		const result = await deleteTool({
21 | 			publicId: "test123",
22 | 		});
23 | 
24 | 		expect(cloudinaryMock.api.delete_resources).toHaveBeenCalledWith("test123");
25 | 		expect(result.content[0].text).toContain("Successfully deleted asset: 'test123'");
26 | 		expect(result.isError).toBe(false);
27 | 	});
28 | 
29 | 	it("should return error if deletion fails for nonexistent public ID", async () => {
30 | 		const mockResult = { deleted: { "test123": "not_found" } };
31 | 		cloudinaryMock.api.delete_resources.mockResolvedValue(mockResult);
32 | 
33 | 		const result = await deleteTool({
34 | 			publicId: "test123",
35 | 		});
36 | 
37 | 		expect(result.content[0].text).toContain("Failed to delete asset with publicId: 'test123'");
38 | 		expect(result.isError).toBe(true);
39 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
40 | 	});
41 | 
42 | 	it("should return error when neither publicId nor assetId is provided", async () => {
43 | 		const result = await deleteTool({});
44 | 
45 | 		expect(result.content[0].text).toContain("Must provide either publicId or assetId to delete");
46 | 		expect(result.isError).toBe(true);
47 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
48 | 	});
49 | 
50 | 	it("should handle API errors", async () => {
51 | 		cloudinaryMock.api.delete_resources.mockRejectedValue(new Error("Permission denied"));
52 | 
53 | 		const result = await deleteTool({
54 | 			publicId: "test123",
55 | 		});
56 | 
57 | 		expect(result.content[0].text).toContain("Error deleting asset: Permission denied");
58 | 		expect(result.isError).toBe(true);
59 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`)
60 | 	});
61 | });
62 | 
```

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

```javascript
 1 | import { createCloudinaryMock } from "../../test/cloudinary.mock";
 2 | import getUsageTool, { getUsageToolParams } from "./getUsageTool.js";
 3 | import { z } from "zod";
 4 | 
 5 | describe("getUsageTool", () => {
 6 | 	let cloudinaryMock;
 7 | 	let usageTool;
 8 | 
 9 | 	beforeEach(() => {
10 | 		cloudinaryMock = createCloudinaryMock();
11 | 		usageTool = getUsageTool(cloudinaryMock);
12 | 		vi.clearAllMocks();
13 | 	});
14 | 
15 | 	it("should have proper parameter schema", () => {
16 | 		expect(getUsageToolParams).toBeDefined();
17 | 		expect(getUsageToolParams.date).toBeInstanceOf(z.ZodOptional);
18 | 	});
19 | 
20 | 	it("should successfully get usage data without date parameter", async () => {
21 | 		const mockUsageData = {
22 | 			plan: "free",
23 | 			last_updated: "2023-05-10",
24 | 			transformations: {
25 | 				usage: 10,
26 | 				limit: 500
27 | 			},
28 | 			objects: {
29 | 				usage: 5,
30 | 				limit: 100
31 | 			}
32 | 		};
33 | 
34 | 		cloudinaryMock.api.usage.mockResolvedValue(mockUsageData);
35 | 
36 | 		const result = await usageTool({});
37 | 
38 | 		expect(cloudinaryMock.api.usage).toHaveBeenCalledWith({});
39 | 		expect(result.isError).toBe(false);
40 | 		expect(result.content).toHaveLength(1);
41 | 		expect(result.content[0].type).toBe("text");
42 | 		expect(JSON.parse(result.content[0].text)).toEqual(mockUsageData);
43 | 	});
44 | 
45 | 	it("should successfully get usage data with date parameter", async () => {
46 | 		const mockUsageData = {
47 | 			plan: "free",
48 | 			last_updated: "2023-04-15",
49 | 			transformations: {
50 | 				usage: 8,
51 | 				limit: 500
52 | 			},
53 | 			objects: {
54 | 				usage: 3,
55 | 				limit: 100
56 | 			}
57 | 		};
58 | 
59 | 		const date = "2023-04-15";
60 | 		cloudinaryMock.api.usage.mockResolvedValue(mockUsageData);
61 | 
62 | 		const result = await usageTool({ date });
63 | 
64 | 		expect(cloudinaryMock.api.usage).toHaveBeenCalledWith({ date });
65 | 		expect(result.isError).toBe(false);
66 | 		expect(result.content).toHaveLength(1);
67 | 		expect(result.content[0].type).toBe("text");
68 | 		expect(JSON.parse(result.content[0].text)).toEqual(mockUsageData);
69 | 	});
70 | 
71 | 	it("should handle API errors", async () => {
72 | 		const errorMessage = "API Error: Invalid date format";
73 | 		cloudinaryMock.api.usage.mockRejectedValue(new Error(errorMessage));
74 | 
75 | 		const result = await usageTool({ date: "invalid-date" });
76 | 
77 | 		expect(result.isError).toBe(true);
78 | 		expect(result.content).toHaveLength(1);
79 | 		expect(result.content[0].type).toBe("text");
80 | 		expect(result.content[0].text).toContain(errorMessage);
81 | 		expect(result.content[0].text).toContain(`(cloud: ${cloudinaryMock.config().cloud_name}, key: ${cloudinaryMock.config().api_key.slice(0, 4)}...)`);
82 | 	});
83 | });
84 | 
```

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

```javascript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import { v2 as cloudinary } from "cloudinary";
  4 | import dotenv from "dotenv";
  5 | 
  6 | import getUploadTool, { uploadToolParams } from "./tools/uploadTool.js";
  7 | import getDeleteTool, { deleteAssetToolParams } from "./tools/deleteAssetTool.js";
  8 | import getGetAssetTool, { getAssetToolParams } from "./tools/getAssetTool.js";
  9 | import getFindAssetsTool, { findAssetsToolParams } from "./tools/findAssetsTool.js";
 10 | import getUsageTool, { getUsageToolParams } from "./tools/getUsageTool.js";
 11 | 
 12 | dotenv.config();
 13 | 
 14 | ["CLOUDINARY_CLOUD_NAME", "CLOUDINARY_API_KEY", "CLOUDINARY_API_SECRET"].forEach((envVar) => {
 15 | 	if (!process.env[envVar]) {
 16 | 		console.error(`Please provide ${envVar} environment variable.`);
 17 | 		process.exit(1);
 18 | 	}
 19 | });
 20 | 
 21 | // Configure Cloudinary with environment variables
 22 | cloudinary.config({
 23 | 	cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
 24 | 	api_key: process.env.CLOUDINARY_API_KEY,
 25 | 	api_secret: process.env.CLOUDINARY_API_SECRET,
 26 | });
 27 | 
 28 | // Create server instance
 29 | const server = new McpServer({
 30 | 	name: "cloudinary",
 31 | 	version: "1.0.0",
 32 | });
 33 | 
 34 | /**
 35 |  * Tool for Uploading an asset to Cloudinary
 36 |  */
 37 | server.tool(
 38 | 	"upload",
 39 | 	"Upload a file (asset) to Cloudinary",
 40 | 	uploadToolParams,
 41 | 	getUploadTool(cloudinary),
 42 | );
 43 | 
 44 | /**
 45 |  * Tool for Deleting an asset from Cloudinary
 46 |  */
 47 | server.tool(
 48 | 	"delete-asset",
 49 | 	"Delete a file (asset) from Cloudinary",
 50 | 	deleteAssetToolParams,
 51 | 	getDeleteTool(cloudinary),
 52 | );
 53 | 
 54 | /**
 55 |  * Tool for Getting asset details from Cloudinary
 56 |  */
 57 | server.tool(
 58 | 	"get-asset",
 59 | 	"Get the details of a specific file (asset)",
 60 | 	getAssetToolParams,
 61 | 	getGetAssetTool(cloudinary),
 62 | );
 63 | 
 64 | /**
 65 |  * Tool for finding assets in Cloudinary
 66 |  */
 67 | server.tool(
 68 | 	"find-assets",
 69 | 	"Search for existing files (assets) in Cloudinary with a query expression",
 70 | 	findAssetsToolParams,
 71 | 	getFindAssetsTool(cloudinary),
 72 | );
 73 | 
 74 | /**
 75 |  * Tool for getting usage information from Cloudinary
 76 |  */
 77 | server.tool(
 78 | 	"get-usage",
 79 | 	"Get a report on the status of your product environment usage, including storage, credits, bandwidth, requests, number of resources, and add-on usage",
 80 | 	getUsageToolParams,
 81 | 	getUsageTool(cloudinary),
 82 | );
 83 | 
 84 | 
 85 | // Start the server with stdio transport
 86 | const main = async () => {
 87 | 	const transport = new StdioServerTransport();
 88 | 	await server.connect(transport);
 89 | 	console.error("Cloudinary MCP Server running on stdio");
 90 | };
 91 | 
 92 | main()
 93 | 	.catch((error) => {
 94 | 		console.error("Fatal error in main():", error);
 95 | 		process.exit(1);
 96 | 	});
 97 | 
 98 | // Export Cloudinary for testing
 99 | export {
100 | 	cloudinary,
101 | };
102 | 
```