This folder is a small HTTP API for AI agents to upload, list, and delete public files on Vercel Blob. There is no database and nothing is stored on the PHP server’s filesystem — only in your Blob store under the prefix (blob root).
Humans open the returned url in a browser. Images and PDFs use the Vercel Blob URL directly; HTML uses view.php; audio and video use play.php with a mobile player. Agents use mcp.php for all operations. Operators manage files in admin.php (SSO login).
Use this when an AI agent needs to publish a file (chart, screenshot, PDF report, simple HTML page) and get back a stable HTTPS link to share with a human or embed elsewhere.
Other extensions are rejected. The filename must include the correct extension so the MIME type is set correctly (inline display for images/PDF/HTML/media; download for archives).
secrets.php (not in git — copy from secrets.php.example):
CDN_AUTH_TOKEN — secret shared with your agent; required on every API call.CDN_PODCAST_RSS_TOKEN — secret for the private Pocket Casts Feed only (rss.php?token=…). Keep separate from the agent token.VERCEL_BLOB_TOKEN — Vercel Blob read-write token from your Vercel project.settings.php (safe to commit):
VERCEL_BLOB_API_BASE — usually https://blob.vercel-storage.com.CDN_BLOB_PREFIX — folder prefix inside Blob (empty = root).CDN_PUBLIC_BASE_URL — optional full URL to this folder if auto-detect fails behind a proxy.CDN_MAX_UPLOAD_BYTES — max file size per upload (currently 300 MB). For multipart uploads, PHP upload_max_filesize and post_max_size must be ≥ this value.Every request to mcp.php must be authenticated. Send Authorization: Bearer <CDN_AUTH_TOKEN> or include "token" / form field token (or ?token= for read-only GET list).
Uploaded files are public on Vercel Blob. Anyone with the URL can open them. Do not upload secrets.
mcp.phpUse POST with JSON or multipart/form-data. Every request must include action and authentication.
GET ?action=list&token=… is supported for read-only listing.
Prefer multipart for files ≳ a few MB (no base64 bloat). JSON + content_base64 remains fine for small/simple agent calls.
{"action":"list","token":"YOUR_TOKEN"}
Returns all files under the configured Blob prefix with pathname, url, size, and uploaded_at.
Look up one file by pathname or public url (play.php, stream.php, view.php, or Vercel Blob).
{"action":"get","token":"YOUR_TOKEN","url":"https://example.com/cdn/play.php?pathname=uuid-track.mp3"}
Send file bytes as base64 or raw UTF-8/binary in content for small HTML. Max 300 MB per request (see CDN_MAX_UPLOAD_BYTES).
{
"action": "upload",
"token": "YOUR_TOKEN",
"filename": "chart.png",
"content_base64": "iVBORw0KGgoAAAANSUhEUg..."
}
Raw file bytes via multipart/form-data. Fields: action=upload, file (or content), optional filename (defaults to the upload name), optional token if not using Bearer.
curl -fsS -X POST "https://example.com/cdn/mcp.php" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "action=upload" \ -F "filename=big.mp3" \ -F "file=@/path/to/big.mp3;type=audio/mpeg"
On success (both paths) the response includes url — give this link to the human.
url (also in blob_url).url points to view.php?pathname=…. Raw blob URL is in blob_url.url points to play.php?pathname=… (mobile player with resume). Stream URL is stream.php; raw blob in blob_url.Delete one file by pathname or public url (play.php, stream.php, view.php, or Vercel Blob).
{"action":"delete","token":"YOUR_TOKEN","url":"https://example.com/cdn/play.php?pathname=uuid-track.mp3"}
Deletes every file under the configured CDN prefix. Use with care.
{"action":"delete_all","token":"YOUR_TOKEN"}
view.php (HTML proxy)Public, no auth. Fetches HTML from Vercel Blob and serves it with Content-Type: text/html and Content-Disposition: inline.
GET view.php?pathname=uuid-page.html
Only .html / .htm files under your CDN prefix are allowed.
play.php (audio / video player)Public, no auth. Mobile-friendly player with play/pause toggle, ±15 s skip, seek bar, sleep timer (15 / 30 / 60 min), download, and playback position saved in localStorage for resume.
GET play.php?pathname=uuid-track.mp3
Supported: mp3, m4a, webm. Media bytes are served via stream.php (Range-aware proxy).
stream.php (media proxy)Public stream proxy with byte-range support for seeking. Append &download=1 to force download.
GET stream.php?pathname=uuid-track.mp3 GET stream.php?pathname=uuid-track.mp3&download=1
podcast.php (Episode API for agents)Dedicated API for uploading listen-able Episodes (audio + metadata + optional cover). Auth is the same as mcp.php (Authorization: Bearer, JSON/form token, or ?token= on GET). Each Episode is stored under podcasts/episodes/{id}/ on Vercel Blob with an episode.json Episode Record as the source of truth. Manage Episodes and copy the private Feed URL in admin.php.
GET ?action=list_episodes&token=… is supported for read-only listing.
Prefer multipart for large audio (e.g. tens of MB). JSON + base64 remains for small/simple calls.
{
"action": "add_episode",
"token": "YOUR_TOKEN",
"title": "Episode title",
"description": "Optional description",
"pub_date": "2026-08-11T12:00:00Z",
"duration_seconds": 3600,
"source_url": "https://example.com/original",
"audio_filename": "talk.mp3",
"audio_base64": "…",
"image_filename": "cover.jpg",
"image_base64": "…"
}
title, audio_filename, audio_base64.mp3 or m4a only.image_filename and image_base64 are required. Cover bytes are sniffed (magic) and stored with a matching extension/Content-Type even if image_filename is wrong (e.g. JPEG named cover.png).CDN_MAX_UPLOAD_BYTES per binary part as mcp.php.episode.audio.stream_url is the Range-aware playback URL (stream.php).Raw audio (and optional cover) bytes. Fields: action=add_episode, title, audio (file), optional audio_filename, optional image + image_filename, plus optional description, pub_date, duration_seconds, source_url, token.
curl -fsS -X POST "https://example.com/cdn/podcast.php" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "action=add_episode" \ -F "title=Test large episode" \ -F "audio=@/path/to/big.mp3;type=audio/mpeg" \ -F "image=@/path/to/cover.png;type=image/png" \ -F "duration_seconds=3600"
{"action":"list_episodes","token":"YOUR_TOKEN"}
Returns Episode Records newest pub_date first.
{"action":"get_episode","token":"YOUR_TOKEN","id":"EPISODE_UUID"}
{"action":"delete_episode","token":"YOUR_TOKEN","id":"EPISODE_UUID"}
Deletes the Episode Record plus audio and cover blobs for that id.
rss.php (private Feed)RSS 2.0 Feed of all Episodes for podcast apps (e.g. Pocket Casts). Private via CDN_PODCAST_RSS_TOKEN (not the agent CDN_AUTH_TOKEN). Includes <itunes:block>Yes</itunes:block>.
GET rss.php?token=CDN_PODCAST_RSS_TOKEN
Enclosure URLs point at stream.php (Range-aware). Paste the full URL into Pocket Casts Discover/search.
filename.url from the upload response (not ?download=1).url from the upload response (the view.php link).url from the upload response (the play.php link).JSON includes "ok": true on success. Errors return HTTP 4xx/5xx with "ok": false and an error field.
PHP 7.2+, no database, no local file storage, Vercel Blob for public CDN URLs.