Media
The media module stores uploaded files on the server's disk, keeps an entry for each file in the database, and serves them at /media/<file>. It is the Go module smeldr.dev/media, and the release binary includes it.
Turn it on
In the release binary, set ENABLE_MEDIA. Files go to the directory that media_path names in smeldr.config, and to ./media without it. An upload may be at most 5 MB, unless media_max_size in smeldr.config sets another limit in bytes. See Configure a Smeldr server.
In your own Go program, register the module and give it to the MCP server:
import (
"smeldr.dev/mcp"
"smeldr.dev/media"
)
store := media.NewLocalMediaStore(app)
mediaSrv := media.Register(app, store)
mcpSrv := mcp.New(app, mcp.WithModule(mediaSrv))
Register needs the app's database (Config.DB) and creates the smeldr_media table if it is missing.
HTTP routes
| Method | Path | Who | What |
|---|---|---|---|
POST | /media | an Author token, or an upload token | upload a file |
GET | /media/<file> | anyone | the stored file |
GET | /media | an Editor token | every entry, optionally ?type=image, video, audio, document or other |
DELETE | /media/<id> | an Editor token | delete the entry and the file |
An upload is a multipart form with the field file, and description for the alt text, which an image must have:
curl -H "Authorization: Bearer $SMELDR_TOKEN" \
-F file=@dot.png -F "description=A single dot" \
http://localhost:8080/media
{"id":"KxsV5dF2ytFMWLH_fMUbKQ","media_type":"image","mime_type":"image/png","url":"http://localhost:8080/media/3696e22fc06ec6380858310126fc2b20-dot.png"}
The stored name is a random prefix and the original name, so two uploads of dot.png do not collide.
Accepted files
| Kind | Extensions |
|---|---|
| image | .jpg, .jpeg, .png, .gif, .webp, .avif, .svg |
| video | .mp4, .webm |
| audio | .mp3, .wav, .ogg |
| document | .pdf |
Any other extension is refused with a validation error that lists the accepted ones. The file's first bytes must match its extension, so a PNG named dot.jpg is refused:
{"error":{"code":"validation_failed","message":"Validation failed","request_id":"","fields":[{"field":"file","message":"expected JPEG (from .jpg extension), got PNG content"}]}}
An image without a description is refused with description: required for image uploads. A file over the size limit gets 413.
Upload tokens
A browser form or another program can upload without a full token. The MCP tool create_upload_token (Author role) returns a token that is valid for 15 minutes:
{"expires_in":900,"token":"...","upload_url":"http://localhost:8080/media"}
Send it as Authorization: UploadToken <token>. An upload token accepts images only: JPEG, PNG, WebP, GIF and AVIF.
MCP tools
| Tool | What |
|---|---|
create_file | upload: filename, data (the file in base64) and description |
list_files | every entry |
get_file | one entry, by its ID |
delete_file | delete the entry and the file |
smeldr-cli
./smeldr-cli media upload dot.png --description "A single dot"
./smeldr-cli media list --type image
./smeldr-cli media delete <id>
Serving and safety
A stored file is public: anyone with its URL can fetch it, without a token. Do not upload what must stay private.
Every file is served with X-Content-Type-Options: nosniff. An SVG is also served with a Content-Security-Policy that sandboxes it, so script inside an SVG does not run when its URL is opened, and the SVG still shows in an <img>.