AI CDN

This folder is a small HTTP API for AI agents to upload, list, and delete public files stored on the PHP server’s filesystem under the prefix (storage root). There is no database.

Humans open the returned url in a browser. Images and PDFs use file.php; 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

All settings come from the environment (stack.env in Docker):

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. 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 storage 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 file.php).

{"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 file.php).

{"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. Serves HTML from local storage 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: file.php (direct file)

Public, no auth. Serves images, PDFs, archives, and other non-HTML/non-media files with the correct Content-Type. Archives download; images and PDFs display inline.

GET file.php?pathname=uuid-chart.png

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}/ 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 files 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 8.3, no database. Files live on the server filesystem under CDN_STORAGE_DIR.