assets-cdn-mcp
Allows uploading static assets to Cloudflare R2, an S3-compatible storage/CDN, with content-hashed immutable filenames, safe partial uploads, and deferred deletion.
Allows uploading static assets to MinIO, an S3-compatible object store, with content-hashed filenames, typed asset manifests, and race-safe file-map publishing.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@assets-cdn-mcpUpload changed files from ./assets to the imgs CDN and update assets.ts"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
assets-cdn
Content-hashed asset uploads for any S3-compatible CDN — with typed access, safe partial uploads, and deferred deletion.
assets-cdn uploads your static assets (images, video, fonts, …) to any
S3-compatible bucket that backs a CDN — AWS S3, Cloudflare R2, Tigris, MinIO —
and gives every file a content hash in its name:
img/logo.png → img/logo.ab12cd34ef56.pngBecause the URL changes whenever the bytes change, every asset can be served
with Cache-Control: public, max-age=31536000, immutable and cached forever —
no cache-busting query strings, no stale images after a deploy.
It also generates a typed assets.ts so your app references assets by their
logical name and always gets the current hashed URL:
import { asset } from "./assets";
asset("img/logo.png"); // → https://cdn.example.com/assets/img/logo.ab12cd34ef56.png
// ^^^^^^^^^^^^^^ autocompleted & type-checked; a typo won't compileWhy
Immutable caching, done right. Hashed names make
max-ageforever safe.Safe partial uploads. Uploading one source folder never deletes assets it doesn't own; only new/changed files are pushed.
Race-safe manifest. The
file-map.jsonis published with compare-and-swap (If-Match/If-None-Match), so two CI runners deploying at once merge instead of clobbering each other — a 412 just triggers a re-read and retry.Fast & resilient. Uploads run with bounded concurrency and retry transient errors with exponential backoff;
--verifyconfirms every object landed.Deferred deletion. Retired versions are tagged
pending-deletewith a grace window (default 7 days) so a rollback still has its old assets — then purged bycleanup-pending.Typed access.
assets.tsturns asset keys into a compile-time contract.Responsive images. Optionally transcode images to AVIF/WebP at multiple widths on upload and get typed
srcsethelpers — see below.Recoverable. Lost your local map?
rebuild-from-cdnreconstructs it from what's already published.
Related MCP server: Hostsmith MCP Server
Install
npm install -g assets-cdn
# or run without installing:
npx assets-cdn --helpConfiguration
Copy .env.example to .env and fill in your bucket credentials:
STATIC_BUCKET_NAME=my-bucket
STATIC_CDN_BASE=https://cdn.example.com
STATIC_AWS_ENDPOINT_URL_S3=https://s3.example.com # omit for AWS S3
STATIC_AWS_ACCESS_KEY_ID=your-access-key-id
STATIC_AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_REGION=auto # "auto" for R2/TigrisCommands that touch the CDN fail fast with a clear message if a required variable is missing.
Quick start
# 1. Preview what would change (no writes)
assets-cdn upload-partial --src ./assets --cdn imgs --plan
# 2. Upload new/changed files, publish the map, emit typed assets.ts
assets-cdn upload-partial \
--src ./assets \
--cdn imgs \
--out ./file-map.json \
--assets-ts ./src/assets.ts
# 3. Later, purge versions whose grace window has elapsed
assets-cdn cleanup-pending --cdn imgsResponsive images
With --images, source images (jpg/png/webp) are transcoded to modern
formats at multiple widths on upload, content-hashed, and recorded in the
manifest. The generated assets.ts then gives you typed srcset helpers:
assets-cdn upload-partial --src ./assets --cdn imgs \
--images --image-formats avif,webp --image-widths 640,1280,1920 \
--assets-ts ./src/assets.tsimport { imageSrcSet, imageSources, IMAGES } from "./assets";
// A single format as srcset:
<img
srcSet={imageSrcSet("hero.jpg", "avif")}
width={IMAGES["hero.jpg"].width} // intrinsic size → no layout shift
height={IMAGES["hero.jpg"].height}
/>;
// Or a full <picture> with avif → webp fallback:
<picture>
{imageSources("hero.jpg").map((s) => (
<source key={s.type} type={s.type} srcSet={s.srcset} />
))}
<img src={/* fallback */ IMAGES["hero.jpg"].fallback} />
</picture>;Image processing uses sharp, an optional
dependency — it's only needed when you pass --images. Variants never upscale
past the source width, output is deterministic (metadata stripped, EXIF
orientation baked in), and each variant gets an immutable cache header.
Scoped for v1: AVIF/WebP at configurable widths, original fallback, intrinsic dimensions. Not yet included:
<picture>art-direction crops, LQIP/blur placeholders, and animated GIF → video.
Vite plugin
Upload assets automatically as part of vite build — no separate CLI step:
// vite.config.ts
import { defineConfig } from "vite";
import assetsCdn from "assets-cdn/vite";
export default defineConfig({
plugins: [
assetsCdn({
src: "src/assets", // defaults to Vite's build.outDir
cdn: "imgs",
assetsTs: "src/assets.ts", // regenerate the typed manifest on each build
enabled: !!process.env.CI, // e.g. only upload in CI
}),
],
});The plugin runs on closeBundle (build only), reuses the same safe partial
upload — content hashing, race-safe manifest, deferred deletion — and reads the
bucket from the standard STATIC_* environment variables. All CLI options
(dryRun, concurrency, verify, pendingDays, …) are accepted.
Commands
Command | Description |
| Upload new/changed assets, publish the file-map, mark retired versions |
| Permanently delete objects whose |
| Rebuild |
| Repair |
| Delete CDN objects that no longer exist locally |
| Upload a local |
| Download |
| Generate |
Options
Option | Description | Default |
| Source directory (or JSON path for |
|
| Output path for |
|
| Key prefix on the CDN |
|
| Also emit a typed | — |
| Grace period before |
|
| Max concurrent S3 requests |
|
| After upload, confirm objects exist with expected size |
|
| Generate responsive image variants (needs |
|
| Comma-separated widths for variants |
|
| Comma-separated formats ( |
|
| Show the execution plan (diff only), make no changes |
|
| Simulate all actions without uploading/deleting |
|
| Emit a machine-readable JSON result (for agents/CI) |
|
| Show version | — |
| Show help | — |
For AI agents & CI
assets-cdn is built to be driven by machines, not just humans.
Machine-readable output
Add --json to any command and it prints a single JSON object to stdout (all
human logs are suppressed) with a stable exit code — 0 on success, 1 on
error (the error is emitted as {"error": "..."} on stderr):
assets-cdn upload-partial --src ./assets --cdn imgs --plan --json
# {"command":"upload-partial","dryRun":false,"uploaded":["imgs/logo.ab12.png"],
# "pendingDelete":[],"kept":4,"out":"file-map.json","planned":true}
assets-cdn upload-partial --src ./assets --json | jq '.uploaded | length'MCP server (Claude Code, Claude Desktop, Cursor…)
The package ships an MCP server so AI agents
can call every command as a tool (upload_partial, cleanup_pending,
rebuild_from_cdn, sync_fix_headers, delete_stale, upload_json,
download_json, generate_assets). Register it:
// claude_desktop_config.json (or .mcp.json / .cursor/mcp.json)
{
"mcpServers": {
"assets-cdn": {
"command": "npx",
"args": ["-y", "assets-cdn-mcp"],
"env": {
"STATIC_BUCKET_NAME": "my-bucket",
"STATIC_CDN_BASE": "https://cdn.example.com",
"STATIC_AWS_ENDPOINT_URL_S3": "https://s3.example.com",
"STATIC_AWS_ACCESS_KEY_ID": "…",
"STATIC_AWS_SECRET_ACCESS_KEY": "…",
"AWS_REGION": "auto"
}
}
}
}Then just ask: "upload the assets in ./dist to the imgs prefix and show me
what changed" — the agent calls upload_partial and reads the structured
result back.
How partial upload works
local files ──generate──▶ patch map ──┐
├─ diff vs. published file-map
CDN file-map ────────────────────────┘
│
┌─────────────────────────────────┴─────────────────────────────┐
│ new / changed → UPLOAD unchanged → KEEP │
│ superseded → mark pending-delete (delete-after = +N days) │
│ CDN-only → left untouched │
└───────────────────────────────────────────────────────────────┘
│
cleanup-pending ───┘ (deletes once delete-after passes)The upload never removes an object just because it is absent locally — only the previous version of a file you re-uploaded is retired, and even then only after the grace window. That makes deploys safe to roll back.
The manifest itself is published with compare-and-swap: assets-cdn re-reads
the currently published file-map.json, merges this run's entries into it, and
writes conditionally on the object's ETag. If another deploy wrote in between, the
conditional write fails with 412 and the merge is retried — so parallel deploys
never lose each other's assets. (Requires an S3 provider with conditional writes:
AWS S3, Cloudflare R2, Tigris and MinIO all support them.)
Development
pnpm install
pnpm test # fast unit/mock suites (vitest, no Docker)
pnpm test:integration # end-to-end against a real MinIO container (needs Docker)
pnpm typecheck # tsc --noEmit
pnpm build # bundle to dist/ via tsup
pnpm dev <cmd> # run the CLI from source (tsx)src/core.ts— pure logic (hashing, name ↔ hash conversion, diffing, header rules), no I/O.src/index.ts— S3/IO orchestration; each command returns a structuredCommandResult.src/cli.ts— CLI entry.src/mcp.ts— MCP server exposing the commands as agent tools.
Tests cover the pure logic (core.test.ts), the S3 command
paths against a mocked S3 (commands.test.ts), the
--json contract (json.test.ts), the MCP server over an
in-memory transport (mcp.test.ts), and full end-to-end runs
against a real MinIO container
(test/integration/minio.test.ts) — including
the compare-and-swap manifest under genuine concurrent deploys.
License
MIT © Igor Chepelev
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceProvides AI agents with seamless access to Filecoin's decentralized storage network for persistent file storage and dataset management. It features automatic payment handling, CDN support, and cost estimation tools for efficient storage operations.2113MIT
- AlicenseAqualityCmaintenanceEnables AI agents to deploy static files and get live HTTPS URLs instantly, with support for custom domains and data residency.9142MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to deploy static websites to StaticX, including creating sites, uploading builds, publishing releases, and managing domains.19MIT
- FlicenseNot gradedqualityCmaintenanceEnables AI agents to edit and serve a static website via natural language, providing file management tools over MCP and HTTP hosting.
Related MCP Connectors
AI-manageable audio CDN: upload, transcode, normalize, stream & deliver audio, plus grounded docs.
Sync Lightroom, Figma, Dropbox & Canva assets to WordPress and Shopify via natural language.
Sync Lightroom, Figma, Dropbox & Canva assets to WordPress and Shopify via natural language.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChepelevIgor/cdn-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server