The VuIO Media Server exposes a variety of endpoints for Web UI rendering, media streaming, UPnP/DLNA service discovery, Prometheus metrics, and Model Context Protocol (MCP) clients.
These APIs are used by the Web UI to interact with media rendering devices.
Discovers DLNA, Chromecast, and compatible AirPlay renderers on the local network.
- Endpoint:
GET /api/renderers - Response:
200 OK[ { "id": "chromecast:01234567", "friendly_name": "Living Room TV", "model_name": "Chromecast", "protocol": "chromecast", "capabilities": { "video": true, "audio": true, "image": true, "playlists": true, "controls": ["play", "pause", "stop"] } } ]
Creates a temporary playlist and starts casting it to the selected playback device.
- Endpoint:
POST /api/cast/playlist - Content-Type:
application/json - Request Payload:
{ "renderer_id": "chromecast:01234567", "folder_name": "Season 5", "file_ids": [343, 330, 331] } - Response:
200 OK{ "status": "playing", "playlist_id": 12, "tracks_count": 3, "current_index": 0, "current_file": "Kuhnya.s05.e01.tahiy.mkv", "queued_next_file": "Kuhnya.s05.e02.tahiy.mkv", "renderer": "Living Room TV", "renderer_id": "chromecast:01234567", "protocol": "chromecast", "media_url": "http://192.168.1.170:8080/media/343" }
Returns the full settings schema, the values currently in force, and which keys the configuration file actually writes. Backs the dashboard's Admin tab.
- Endpoint:
GET /api/admin/config - Response:
200 OK
Field type is one of bool, int (with min/max), text, path, enum (with
options and free_form), or string_list. impact is live, next_start — the setting
only describes startup, so there is nothing to apply now — or restart, meaning the running
server is still using the old value. Only database.path and database.cache_mb are
restart.
removable: false marks a key that must always carry a value, because AppConfig
declares no default for it. Some fields carry a note describing a caveat in what the
setting actually does.
Applies a set of changes to config.toml. The file is edited in place, so comments and
unrecognised keys survive. The result is parsed and validated before anything is written,
and the write is atomic, so a rejected change leaves the file untouched. The file watcher
then reloads it — the same path a hand edit takes.
- Endpoint:
POST /api/admin/config - Request Payload:
{ // Dotted key to value. `null` removes the key, restoring its default. "values": { "media.autoplay_enabled": false, "server.ip": null }, // Optional. Replaces the whole [[media.directories]] array. "directories": [{ "path": "/media", "recursive": true }] } - Response:
200 OK—{"saved": true, "impact": "live"}, whereimpactisno_change,live,next_start, orrestart_required. - A save that changes
server.portorserver.interfacealso carriesmoved, reporting what the listener actually did rather than predicting it:On{ "state": "moved", "serving": "0.0.0.0:9090", "port": 9090 } { "state": "failed", "serving": "0.0.0.0:8080", "desired": "0.0.0.0:80", "error": "Failed to bind to 0.0.0.0:80: Permission denied (os error 13)" } { "state": "pending", "serving": "0.0.0.0:8080" }failedthe server keeps serving on the old address and every advertised URL keeps naming it;runtime.bound_addr,runtime.desired_addrandruntime.bind_errorreport the same disagreement on subsequentGETs, so it survives a page reload. 400 Bad Request—{"error": "..."}for an unknown key, a value of the wrong type, a failed validation, or an attempt to unset a key that has no default.409 Conflict— the configuration is not editable. This means a container configured by environment variables, whose config is a scratch file that a restart discards.
Command-line overrides (--port, --name, -m) do not make the configuration
read-only. They are layered over the file on every load, so they hold for the run while
the file stays editable. overrides in the GET response reports what they force, keyed
by config key, so a value saved for one of those keys can be shown as taking effect at the
next start rather than looking as though it failed. values deliberately reports the
file's value, not the running one, so a save cannot write an override back into the file.
Runs the normal graceful shutdown and exits. The process only returns if something supervises it — Docker, systemd or launchd.
- Endpoint:
POST /api/admin/restart - Response:
202 Accepted—{"stopping": true, "supervised": false}
Fetches titles, synopses, ratings and artwork from public metadata services. This is the
only part of VuIO that contacts anything outside the local network, and it is only reached
by an explicit request to /run. Requires the mediainfo cargo feature (on by default)
and mediainfo.enabled in the configuration.
Five providers answer without an account — tvmaze, musicbrainz (with Cover Art Archive
for artwork), jikan, anilist and kitsu. Five more work once a credential is saved:
tmdb, omdb, discogs, lastfm and genius. Requests are paced per provider to the
rate limits each publisher documents; MusicBrainz's one-per-second ceiling makes a large
music library slow by design.
- Endpoint:
GET /api/admin/mediainfo - Response:
200 OKStored credentials are never returned by this or any other endpoint;{ "enabled": true, "min_confidence": 60, "providers": [ { "id": "tmdb", "label": "TheMovieDB", "group": "Movies & TV", "provides": "Movies, TV, posters, trailers and ratings.", "credential_label": "API key", "signup_url": "https://developer.themoviedb.org", "needs_credential": true, "has_credential": true, // whether one is stored — never the value "enabled": true // whether it is in mediainfo.providers } ], "job": { "running": true, "total": 1240, "processed": 318, "matched": 290, "low_confidence": 22, "failed": 6, "cancelled": false, "current": "Arrival.2016.1080p.mkv", "started_at": 1765000000 }, "stats": { "total": 318, "confident": 290, "low_confidence": 28, "with_artwork": 271 }, "flagged": [ { "media_file_id": 91, "confidence": 35, "provider": "tvmaze", "matched_title": "Some Show", "filename": "unknown.s01e02.mkv" } ] }has_credentialis the only thing reported about them.
- Endpoint:
POST /api/admin/mediainfo/credentials - Body:
{"provider": "tmdb", "token": "…"}— an emptytokenclears the stored one. - Response:
200 OK—{"saved": true, "has_credential": true} - Errors:
400 Bad Requestfor an unknown provider, or for one that needs no account.
Credentials are kept in the database's secrets table rather than config.toml. Under
Docker the configuration is built from environment variables and PUT /api/admin/config
returns 409, so a credential in the file would be unsettable in exactly the deployment
most likely to need one.
Walks every file with no usable record — never looked up, looked up by an older reader version, or matched too weakly to trust — and returns as soon as the run is scheduled.
- Endpoint:
POST /api/admin/mediainfo/run - Response:
200 OK—{"started": true, "total": 1240} - Errors:
409 Conflictif a run is already going, if the feature is off, or if no provider is enabled.
Stops after the item currently in flight; whatever was already matched stays.
- Endpoint:
POST /api/admin/mediainfo/cancel - Response:
200 OK—{"cancelled": true} - Errors:
409 Conflictwhen nothing is running.
When a run finishes it publishes a ContentDirectory revision, so DLNA clients and the dashboard both pick up the new titles, synopses and artwork without further prompting.
Endpoints for playing back video/audio and retrieving subtitles.
Streams the requested media file. Supports HTTP range requests (essential for scrubbing/seeking in video players).
- Endpoint:
GET /media/{id} - Response Headers:
Content-Type: Matching media file mime type (e.g.video/x-matroska,audio/mpeg)Accept-Ranges:bytesTransferMode.dlna.org:Streaming
Returns artwork for an item, trying three sources in order: an image file sitting beside
the media (cover.jpg, folder.png, <basename>.webp, …), artwork embedded in the file's
own tags, and finally a poster cached by the media info fetch. The local sources apply to
audio only; video reaches this endpoint through the cache, which is what gives a movie or
an episode a poster at all.
- Endpoint:
GET /media/{id}/cover - Response:
200 OKwith the image, or404 Not Foundwhen no source has one. - Also advertised to DLNA clients as
upnp:albumArtURI.
Serves the sidecar subtitle track (<media basename>.srt) if one exists, in either of two
formats. Both return 404 Not Found when there is no sidecar file.
-
Endpoint:
GET /media/{id}/subtitle -
Response:
200 OK,Content-Type: text/srt— the file verbatim. This is what DLNA renderers consume (Samsung via theCaptionInfo.secresponse header, LG and Panasonic viapv:subtitleFileUri). -
Endpoint:
GET /media/{id}/subtitle.vtt -
Response:
200 OK,Content-Type: text/vtt; charset=utf-8— the same file converted to WebVTT on the fly. Browsers'<track>element accepts WebVTT only, so this is the endpoint the dashboard player uses. Non-UTF-8 sidecars are decoded lossily.
Endpoints for health monitoring, log scraping, and metrics.
- Endpoint:
GET /metrics - Response: Prometheus exposition text format.
- Endpoint:
GET /metrics/json - Response:
200 OK(JSON statistics)
- Endpoint:
GET /healthz - Response:
200 OK"OK"
- Endpoint:
GET /readyz - Response:
200 OK"OK"
- Endpoint:
GET /logs?limit={num_lines} - Response:
200 OK(plain text log lines)
One endpoint, on the main port, letting an AI assistant browse, search and cast
the library. See the MCP Integration Guide
for how to connect a client, and mcp/reference.json for the tool schemas.
-
Endpoint:
POST /mcp -
Content-Type:
application/json -
Protocol: MCP
2026-07-28. Clients that open with aninitializehandshake are answered too, for2025-11-25,2025-06-18and2025-03-26. -
Methods:
server/discover,tools/list,tools/call. Plusinitializeandpingfor the handshake-based revisions. -
Required headers:
MCP-Protocol-Version— must equalparams._meta'sio.modelcontextprotocol/protocolVersionMcp-Method— must equal the body'smethodMcp-Name— ontools/call, must equalparams.name
A mismatch returns
400with JSON-RPC error-32020; an unsupported version returns400with-32022and the list of versions the server does speak. -
Origin: validated when present, to prevent DNS rebinding. Clients that send no
Origin— which is most of them — are unaffected. -
Auth: behind the management middleware, plus
[mcp].require_authfor a bearer token even when management authentication is off. -
GET/DELETE:405. Both belonged to the session-based revisions of the transport; this one has no sessions. -
Body limit: 256 KiB, shared with the other JSON endpoints.
Notifications (a message with no id) are answered 202 Accepted with no body.
Everything else returns 200 with the JSON-RPC response in the body.
For clients that launch a local process instead of calling an endpoint:
vuio mcp --url http://nas.local:8080 --token-file ~/.vuio/admin.tokenIt reads JSON-RPC on stdin and writes answers to stdout, forwarding to the
server's /mcp. It serves nothing itself.
These endpoints implement the UPnP MediaServer:1 and ContentDirectory:1 protocols for TV/Receiver client discovery.
GET /description.xml- Device XML definition.GET /ContentDirectory.xml- ContentDirectory SCPD.POST /control/ContentDirectory- ContentDirectory control endpoint (SOAP actions).GET /ConnectionManager.xml- ConnectionManager SCPD.POST /control/ConnectionManager- ConnectionManager control endpoint.GET /X_MS_MediaReceiverRegistrar.xml- MediaReceiverRegistrar SCPD.POST /control/X_MS_MediaReceiverRegistrar- MediaReceiverRegistrar control endpoint.
{ "sections": [ { "id": "network", "title": "Network", "blurb": "Discovery and advertisement on the local network.", "fields": [ { "key": "network.mdns_enabled", "label": "Advertise over mDNS", "type": "bool", "impact": "live", "removable": true, "help": "Also announce the server over Bonjour/DNS-SD, alongside SSDP." } ] } ], // The file's value where the file sets one; otherwise the default in force. "values": { "network.mdns_enabled": true }, // False means the key is absent from config.toml and `values` is showing a default. "present": { "network.mdns_enabled": false }, // Settings the command line forces for this run, which the file cannot change until restart. "overrides": { "server.port": "9090" }, // Libraries as the file writes them; absent optional keys stay absent. "directories": [{ "path": "/media", "recursive": true }], // The same libraries with defaults filled in, for display only. "effective_directories": [{ "path": "/media", "recursive": true, "validation_mode": "Warn" }], "runtime": { "config_path": "/opt/vuio/config/config.toml", "writable": true, "read_only_reason": null, "auth_enabled": false, "is_docker": false, "version": "0.0.50", // Where the server is actually accepting, which is what every advertised URL uses. "bound_addr": "0.0.0.0:8080", "desired_addr": null, "bind_error": null } }