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 "Deploy 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: StaticX 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 deployed
Maintenance
Related MCP Connectors
AI-native digital asset management: semantic search, generative image edits, and CDN delivery.
AI-manageable audio CDN: upload, transcode, normalize, stream & deliver audio, plus grounded docs.
Artifact store for AI agents — read, write, and search files by path; share by rendered URL.
Deploy and manage your apps, databases, storage, and scheduled jobs from your AI agent
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI agents to deploy static files and get live HTTPS URLs instantly, with support for custom domains and data residency.954 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to deploy static websites to StaticX, including creating sites, uploading builds, publishing releases, and managing domains.31 npmMIT
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to edit and serve a static website via natural language, providing file management tools over MCP and HTTP hosting.-
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to deploy generated files (HTML, PDF, images, etc.) directly to cloud71 hosting, returning a public URL, and manage cloud71 sites via natural language.MIT