Skip to content
Conduit

mcp server

stashy

Self-hosted file storage for AI agents

stashysh/stashycommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 17, 2026

Stashy

CI Release License Go

Self-hosted file storage built for AI agents. Connect your agent over MCP or give it an API key — it uploads, manages, and shares files, and you stay in control.

  • Agent-native — built-in MCP server, plus REST, gRPC, gRPC-Web, and Connect on one endpoint
  • Any storage backend — local disk, Amazon S3, Cloudflare R2, MinIO, or Google Cloud Storage
  • Private, team, or public — choose who sees each file, with clean shareable URLs

Install

brew install stashysh/tap/stashy

Or download a binary from Releases.

Quick start

export SESSION_SECRET="your-secret-here"
export GOOGLE_CLIENT_ID="your-client-id"
export GOOGLE_CLIENT_SECRET="your-client-secret"

just run
# or: go run ./cmd/stashy serve --migrate

Visit http://localhost:8080 to sign in and generate API keys. Uses SQLite by default, with PostgreSQL available when you need a separate database.

Build

just build          # build binary
just generate       # regenerate proto code
just tidy           # go mod tidy
just clean          # remove build artifacts

Docker

cp .env.example .env  # fill in your secrets
docker compose up

Environment variables

VariableDescriptionDefault
PORTServer listen port8080
HOSTNAMEPublic base URLhttp://localhost:$PORT
DATABASE_URLDatabase connection string (see below)file:stashy.db
STORAGE_BACKENDStorage backend: memory, local, gcs, or s3memory
LOCAL_STORAGE_DIRDirectory for local file storage./storage
GCS_BUCKETGCS bucket name (required when STORAGE_BACKEND=gcs)—
S3_BUCKETS3 bucket name (required when STORAGE_BACKEND=s3)—
SESSION_SECRETHMAC key for signing session cookiesrequired
GOOGLE_CLIENT_IDGoogle OAuth 2.0 client IDrequired
GOOGLE_CLIENT_SECRETGoogle OAuth 2.0 client secretrequired
ALLOWED_DOMAINSComma-separated list of allowed email domains— (all allowed)

Database

Driver is auto-detected from the DSN:

DSNDatabase
file:stashy.dbSQLite (default)
postgres://user:pass@host/dbPostgreSQL

Migrations are managed by goose. Run them explicitly:

stashy migrate
# or: stashy serve --migrate

CLI

stashy serve [--migrate]   # start the server (default), optionally run migrations first
stashy migrate             # run database migrations and exit
stashy version             # print version
stashy help                # print usage

Authentication

Web UI (Google OAuth)

  1. Visit / — sign in with Google
  2. After login, the dashboard lets you generate and manage API keys

API (Bearer token)

All /v1/* API endpoints require a Bearer token:

curl -H "Authorization: Bearer <api-key>" \
  -X POST http://localhost:8080/v1/files \
  -H "Content-Type: image/png" \
  --data-binary @photo.png

Each file's visibility controls who can open its link (/{id}): private (only you), internal (any signed-in user; the default), or public (anyone).

Set ALLOWED_DOMAINS to restrict login to specific email domains.

Protocols

A single endpoint serves all protocols via vanguard-go transcoding:

ProtocolTransport
gRPCHTTP/2 (h2c)
gRPC-WebHTTP/1.1 or HTTP/2
ConnectHTTP/1.1 or HTTP/2
RESTHTTP/1.1 or HTTP/2

API

The API only works with your own files: every endpoint below acts on files created with your API keys.

Create a file

curl -H "Authorization: Bearer <api-key>" \
  -X POST "http://localhost:8080/v1/files?slug=my-photo&name=photo.png" \
  -H "Content-Type: image/png" \
  --data-binary @photo.png

All query parameters are optional:

  • slug: the human-readable end of the file's URL (/{id}/my-photo).
  • name: the original filename, shown in the file list and used as the filename when the file is downloaded.
  • visibility: who can open the file's link: private, internal (default), or public.

Creating and updating a file return its metadata:

{
  "id": "V1StGXR8_Z5jdHi6B-myT",
  "slug": "my-photo",
  "url": "http://localhost:8080/V1StGXR8_Z5jdHi6B-myT/my-photo",
  "name": "photo.png",
  "content_type": "image/png",
  "size": "48213",
  "checksum": "mnG7TA==",
  "visibility": "internal",
  "created_at": "2026-10-02T12:00:00Z",
  "updated_at": "2026-10-02T12:00:00Z"
}

size is a string, as protobuf JSON encodes 64-bit integers. checksum is the CRC32C (Castagnoli) of the content: the 4-byte big-endian value, base64-encoded, as GCS and S3 report it. It's empty for files uploaded before checksums were recorded.

List files

curl -H "Authorization: Bearer <api-key>" \
  "http://localhost:8080/v1/files?limit=50"

Returns a JSON array of your files, newest first. To get the next page, pass the last file's id as after; a page shorter than limit is the last.

Get a file

curl -H "Authorization: Bearer <api-key>" \
  http://localhost:8080/v1/files/{id}

Returns the file's metadata.

Get a file's content

curl -H "Authorization: Bearer <api-key>" \
  -o photo.png http://localhost:8080/v1/files/{id}/content

Returns the file's bytes. Supports Range requests. The ETag header is the quoted checksum; send it back as If-None-Match to get 304 Not Modified when the content hasn't changed.

Update a file

curl -H "Authorization: Bearer <api-key>" \
  -X PATCH http://localhost:8080/v1/files/{id} \
  -H "Content-Type: application/json" \
  -d '{"slug": "my-photo", "name": "My photo.png", "visibility": "public"}'

Updates a file's slug (the human-readable end of its URL), name (its original filename), and visibility (private, internal, or public). Omitted fields are left unchanged; send "" to clear a slug or name.

Update a file's content

curl -H "Authorization: Bearer <api-key>" \
  -X PUT http://localhost:8080/v1/files/{id}/content \
  -H "Content-Type: image/png" \
  --data-binary @photo-v2.png

Replaces the whole content; the file's ID and URL stay the same. To avoid overwriting someone else's change, send the ETag you last saw as If-Match: the update fails with 412 Precondition Failed if the content has changed since.

Delete a file

curl -H "Authorization: Bearer <api-key>" \
  -X DELETE http://localhost:8080/v1/files/{id}

MCP

Stashy serves a Model Context Protocol server at /mcp, so AI assistants can work with your files. It uses the same API keys as the REST API.

claude mcp add --transport http stashy http://localhost:8080/mcp \
  --header "Authorization: Bearer <api-key>"

Tools: list_files, get_file, update_file, delete_file. They work with file metadata; to create files or get and update their content, use the REST API.

File access

curl http://localhost:8080/{id}

Public files open for anyone. Internal files require signing in, and private files open only for their owner; anyone else signed in gets a 404. Ideal for CDN or subdomain mapping (e.g., cdn.example.com/{id}).

If a file has a slug, its canonical URL is /{id}/{slug} and /{id} redirects there. A stale or wrong slug also redirects to the current canonical URL, so links shared before a rename keep working.

Storage backends

  • memory — in-memory, ephemeral; good for development
  • local — files on disk at LOCAL_STORAGE_DIR
  • gcs — Google Cloud Storage
  • s3 — Amazon S3 or S3-compatible storage (MinIO, R2, etc.)
↑↓ navigate openesc close