small-share-app

Hand a file from an agent
to a human. And back.

Your AI agent produces something worth looking at. It uploads, gets one URL, and hands it over. You open it and read the thing properly. Fifteen days later it is gone.

  agent ──POST /api/v1/files  (or MCP get_upload_url)──►┐
                                                         │   small-share-app
  agent ◄──── https://share.your-net/f/<32 random chars> ─┤
    │                                                    │   sqlite + files on disk
    │ "here you go"                                      │
    ▼                                                    │
  human ──opens the URL in a browser────────────────────►┘
          header: name, size, uploaded, expires in 14d, [Download] [Delete]
          below:  the file, rendered, in its own frame

What it does

One Perl process, one SQLite file, one directory of blobs. About 1,500 lines.

📖

Renders, does not just store

Markdown gets real typography and drawn mermaid diagrams. Images are shown. PDFs open in the browser's own viewer. Reading it is the point.

🔌

MCP built in

A stateless Streamable HTTP server at /mcp. One line to register, and it explains itself to the agent on connect.

🪶

No bytes through the model

MCP tools hand out URLs; the agent moves files with curl. A 3 MB PDF has no business passing through a context window.

↩️

Works both ways

A drop zone on the home page, so you can hand a screenshot or a spec to an agent and paste back the URL.

Expires by itself

Files are deleted after fifteen days and the URL dies with them. This is a hand-off, not storage — nothing to clean up later.

🛡️

Treats uploads as hostile

An agent can be talked into writing anything. A sanitiser allowlist, a sandboxed iframe with no same-origin, and a strict CSP. Three layers, none trusted alone.

📦

One directory is the state

No database server, no object store, no queue. Back up one folder and you have backed up the service.

🔒

Private by default

Ships with a Tailscale sidecar, so it lives on your tailnet with a real certificate and no ports open to anyone else.

🧪

Tested where it counts

The image build runs the suite, so an image whose tests fail cannot be published. 90% coverage floor, plus a browser suite that checks mermaid really draws.

Install

Docker and a Tailscale auth key. That is the whole list.

  1. Get it and configure it

    git clone https://github.com/melo/small-share-app
    cd small-share-app
    cp .env.example .env      # put a Tailscale auth key in it
  2. Start it

    docker compose up -d

    The app publishes no host ports. A tailscale serve sidecar joins your tailnet as its own node, gets a Let's Encrypt certificate automatically, and proxies to the app. You end up with https://share.<your-tailnet>.ts.net.

  3. Point your agent at it

    claude mcp add --transport http share https://share.<your-tailnet>.ts.net/mcp

No local Tailscale? docker compose -f docker-compose.local.yml up -d --build runs it on 127.0.0.1:8080 for you to put behind your own reverse proxy.

Configuration

Environment variables, all optional except the auth key.

VariableDefaultWhat it does
TS_AUTHKEYTailscale auth key. Required for the tailnet stack.
TS_HOSTNAMEshareThe node name, and so the hostname.
SHARE_TTL_DAYS15Retention, and the ceiling any upload may ask for.
SHARE_MAX_BYTES33554432Per-file limit. 32 MB.
SHARE_NOTICEemptyOne line of deployment truth — who can reach this, who to ask. Shown to humans and to agents.
SHARE_BASE_URLfrom the requestThe base of every URL handed out.

Before you deploy it

One thing to understand, stated plainly.

There is no authentication. Anything that can reach the port may upload, list and delete. That is a deliberate trade: it removes the token-distribution problem entirely, so an agent container needs no credential, no mount and no rotation. It is the right trade on a private network and the wrong one on the open internet. The share URL itself is 32 characters from /dev/urandom — about 190 bits — and every response carrying it is marked noindex, no-referrer and no-store.