AI CDN

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).

What it is for

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.

Supported file types

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).

Configuration

secrets.php (not in git — copy from secrets.php.example):

settings.php (safe to commit):

Security

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.

Endpoint: mcp.php

Use 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.

list

{"action":"list","token":"YOUR_TOKEN"}

Returns all files under the configured Blob prefix with pathname, url, size, and uploaded_at.

get

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"}

upload (JSON)

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..."
}

upload (multipart)

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.

delete

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"}

delete_all

Deletes every file under the configured CDN prefix. Use with care.

{"action":"delete_all","token":"YOUR_TOKEN"}

Endpoint: 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.

Endpoint: 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).

Endpoint: 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

Endpoint: 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.

add_episode (JSON)

{
  "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": "…"
}

add_episode (multipart)

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"

list_episodes

{"action":"list_episodes","token":"YOUR_TOKEN"}

Returns Episode Records newest pub_date first.

get_episode

{"action":"get_episode","token":"YOUR_TOKEN","id":"EPISODE_UUID"}

delete_episode

{"action":"delete_episode","token":"YOUR_TOKEN","id":"EPISODE_UUID"}

Deletes the Episode Record plus audio and cover blobs for that id.

Endpoint: 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.

Display in browser (not download)

Successful responses

JSON includes "ok": true on success. Errors return HTTP 4xx/5xx with "ok": false and an error field.

Stack

PHP 7.2+, no database, no local file storage, Vercel Blob for public CDN URLs.