Documentation
The content engine

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

MethodPathWhoWhat
POST/mediaan Author token, or an upload tokenupload a file
GET/media/<file>anyonethe stored file
GET/mediaan Editor tokenevery entry, optionally ?type=image, video, audio, document or other
DELETE/media/<id>an Editor tokendelete 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

KindExtensions
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

ToolWhat
create_fileupload: filename, data (the file in base64) and description
list_filesevery entry
get_fileone entry, by its ID
delete_filedelete 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>.