Skip to content

MCP server (AI access) ​

TinyS3 can expose itself to AI clients over the Model Context Protocol so an assistant can explore and act on buckets on a user's behalf. It is delivered as an optional plugin (tiny-s3-mcp) that serves its endpoints on the main server port under /mcp — the same path-routing the web console uses — so it inherits the server's address binding and TLS. There is no second port to open or firewall.

The key property: the MCP server does not reach into storage directly. Every tool call is issued as a signed S3 request under the user's own credentials, so TinyS3's normal authentication and authorization apply — the AI can never do anything the bound user could not do through the S3 API.

Enabling ​

The MCP server is opt-in and activates only with an explicit flag. All mutating capabilities are off by default; you turn each class on deliberately.

bash
# Read-only MCP endpoints at /mcp on the server port
tinys3 server /data --mcp

# Allow specific mutating tool classes
tinys3 server /data --mcp --mcp-allow-write            # create bucket, put object, tags
tinys3 server /data --mcp --mcp-allow-write --mcp-allow-delete
tinys3 server /data --mcp --mcp-allow-admin            # data-usage / cluster info tools
FlagEffect
--mcpServe MCP endpoints at /mcp on the server port (read tools only).
--mcp-allow-writeExpose write tools (create bucket, put object, set tags).
--mcp-allow-deleteExpose delete tools (delete object, delete bucket).
--mcp-allow-adminExpose admin tools (data usage, cluster info).
--mcp-require-tlsRequire TLS for the credential endpoints (behind a plaintext-origin proxy).

Via the YAML config ​

The same options can go in a plugins: section of the config file instead of on the command line — enabled: true activates the plugin and each camelCase key maps to its flag:

yaml
plugins:
  mcp:
    enabled: true       # == --mcp
    allowWrite: true    # == --mcp-allow-write
    allowDelete: true   # == --mcp-allow-delete
    requireTls: false

CLI flags and config are additive; a flag present on the command line always wins.

SAME PORT AS S3

Because MCP shares the main port, it is reachable wherever the server is — including through a Cloudflare Tunnel if one is enabled. It is not loopback-restricted. Access is gated only by the OAuth device-flow bearer token, so run the server with TLS before exposing it publicly, and treat the /mcp/device/approve page (which takes a secret key) accordingly.

The credential endpoints (/mcp/device/approve, /mcp/authorize, /mcp/token/issue) are served only over TLS or to a loopback client. If you place the server behind a plaintext-origin proxy or tunnel (e.g. cloudflared --url http://localhost:8000), every request appears to arrive from loopback, so that check no longer proves the channel is local — add --mcp-require-tls to drop the loopback exception and require a real TLS connection to those endpoints.

On startup the server prints a banner line, e.g.:

MCP    http://127.0.0.1:8000/mcp

Getting a token from the console (easiest) ​

If you use the web console, the simplest path is Settings → MCP access → Generate MCP token. Because you are already signed in, the console exchanges your credentials for a bearer token in one click (no device-code steps) and shows a ready-to-paste claude mcp add … command. The token is displayed once and expires in 8 hours. This mints via POST /mcp/token/issue, which — like the approval page — is served only over TLS or to a loopback client.

The programmatic device flow below is for headless clients that can't use the console.

Connecting a client (OAuth 2.0 device flow) ​

A client authenticates with the OAuth 2.0 Device Authorization Grant. The user proves they hold a TinyS3 access key/secret once, in a browser, and the client receives a bearer token bound to that identity.

  1. Request a device code:

    bash
    curl -sX POST http://127.0.0.1:8000/mcp/device
    json
    {
      "device_code": "…",
      "user_code": "ABCD-2345",
      "verification_uri": "http://127.0.0.1:8000/mcp/device/approve",
      "verification_uri_complete": "http://127.0.0.1:8000/mcp/device/approve?user_code=ABCD-2345",
      "expires_in": 600,
      "interval": 5
    }
  2. Approve in a browser. Open verification_uri_complete, enter your access key and secret key, tick "I generated this code myself", and submit. The credentials are verified with a live S3 call; on success the session is bound to that identity.

    The approval page is served only over TLS or to a loopback client — remote approval fails closed unless the server runs with TLS. Never approve a code someone else sent you: approving binds the session to your identity, so a third party who holds the matching device_code would then act as you.

  3. Poll for the token (the client repeats this every interval seconds until approved):

    bash
    curl -sX POST http://127.0.0.1:8000/mcp/token \
         -d device_code=<device_code>

    While pending you get {"error":"authorization_pending"} (or slow_down); once approved:

    json
    {
      "access_token": "…",
      "token_type": "Bearer",
      "expires_in": 28800,
      "refresh_token": "…",
      "refresh_expires_in": 2591999
    }
  4. Call the MCP endpoint with the token. The MCP JSON-RPC endpoint is POST /mcp; send the bearer token on every request:

    Authorization: Bearer <access_token>

Tokens are session-scoped and expire (the expires_in seconds returned with the token; 8 hours by default). The device code is single-use.

Refreshing an access token ​

The device-flow and console grants also return a refresh_token (30-day lifetime). Exchange it for a fresh access token — and a rotated refresh token — without re-approving:

bash
curl -sX POST http://127.0.0.1:8000/mcp/token/refresh \
     -d refresh_token=<refresh_token>

The response has the same shape as the grant above (new access_token + new refresh_token). Refresh is rotating and single-use: each refresh returns a new refresh token and invalidates the one you presented. Presenting an already-used refresh token is treated as theft — it returns {"error":"invalid_grant"} and kills the entire lineage (every refresh token descended from that grant plus the identity's live access tokens), so a leaked-and-replayed token cannot outlive detection. Rotation does not extend the 30-day deadline; it is anchored to the original grant. Unknown, expired, and reused tokens all return the same opaque invalid_grant.

Because reuse is treated as theft, a client must serialize its refreshes — fire one at a time and wait for the response. Two concurrent refreshes of the same token, or a retry after a dropped response where the server already rotated, will look like a replay and drop the session. Under --mcp-require-tls the refresh endpoint additionally requires a real TLS connection (the 30-day refresh token is never returned over the untrusted plaintext hop).

Connecting Claude Code (browser sign-in, auto-refresh) ​

TinyS3 implements the MCP Authorization spec — OAuth 2.1 authorization-code with PKCE, served alongside the MCP endpoint on the same port:

EndpointPurpose
GET /.well-known/oauth-protected-resourceRFC 9728 — names the authorization server for /mcp
GET /.well-known/oauth-authorization-serverRFC 8414 — advertises the endpoints + capabilities
POST /mcp/registerRFC 7591 Dynamic Client Registration (public client, PKCE)
GET/POST /mcp/authorizeauthorization endpoint; the credential form is the consent screen
POST /mcp/tokentoken endpoint (authorization_code, refresh_token, device grant)

A request to /mcp without a valid bearer returns 401 with a WWW-Authenticate: Bearer resource_metadata="…" header pointing at the discovery document — the trigger that starts an MCP-auth-spec client's browser sign-in. Because the client obtains a refresh token this way, it renews the 8-hour access token on its own; you sign in once.

Register it with just the URL — no --header:

bash
claude mcp add --transport http tinys3 http://localhost:8000/mcp

claude mcp list          # Claude Code opens the browser to /mcp/authorize on first use
claude mcp get tinys3

On first use Claude Code registers itself, opens /mcp/authorize in your browser, and you enter your TinyS3 access key + secret on the consent page (only over HTTPS or loopback). It then holds the tokens and refreshes automatically — no manual token juggling.

BEHIND A PROXY OR CACHE

The discovery documents advertise the authorization/token endpoints. By default they are built from each request's Host header and served uncached (Cache-Control: no-store, Vary: Host) so a shared cache can't serve one client's host to another. If you front TinyS3 with a proxy or CDN, set the canonical external URL with -Dtinys3.mcp.public-url=https://mcp.example.com — the metadata then uses that fixed issuer and becomes cacheable. Also note that enabling MCP claims the whole /.well-known/ path prefix on the server; don't rely on TinyS3 to serve other well-known resources (e.g. ACME http-01) while MCP is on.

Pointing any other MCP client at it ​

Any client that speaks streamable HTTP with a bearer token can connect without the auth spec: point it at http://127.0.0.1:8000/mcp and supply an access token as the bearer credential. Mint one from the console's Generate MCP token button, the device flow above, or POST /mcp/token/issue. A static bearer (e.g. claude mcp add … --header "Authorization: Bearer <token>") does not auto-refresh — re-issue when it expires, or use the auth-spec flow above. (Clients that only speak stdio need a stdio-to-HTTP bridge.)

Tools ​

Tools are grouped by the capability they need; a tool is only advertised when its flag is set, and every call is still authorized by the S3 server under the bound identity.

Read (always on) — list_buckets, list_bucket_contents, search_objects, get_object_metadata, read_object.

Write (--mcp-allow-write) — create_bucket, put_text_object, set_object_tags.

Delete (--mcp-allow-delete) — delete_object.

Admin (--mcp-allow-admin) — get_data_usage.

read_object is range-limited (default 64 KiB) so a model never pulls a whole large object.

TIP

While --mcp is enabled, the /mcp path is reserved on the server, so a path-style bucket literally named mcp is not addressable. Virtual-host addressing (mcp.<domain>) is unaffected.

Security model ​

  • No privilege bypass. Tools are ordinary S3 calls signed with the session's credentials, so TinyS3's SigV4 auth and bucket/IAM policy decide what is allowed. The MCP layer adds no access of its own.
  • Explicit, layered opt-in. The server is off unless --mcp is set, and each mutating class is a separate flag. A tool that is not enabled is never advertised to the client.
  • Device approval + bearer token. A session cannot start until a human approves it with real credentials in the browser; every call carries a high-entropy, expiring bearer token, and the device code is single-use. Because MCP shares the main port, enable TLS before exposing it beyond the host.
  • Secrets are session-scoped. The approved credentials are held in memory only for the session's lifetime to sign requests, and are never logged.
  • Revocable. From Settings → MCP access → Revoke tokens (or POST /mcp/token/revoke with your credentials) you can invalidate every MCP token bound to your identity immediately, rather than waiting for the TTL.
  • Inherited timing note. The credential endpoints authenticate by a live S3 request, so — like the S3 API itself — they can exhibit a small timing difference between an unknown access key and a wrong secret for a known key. This is a property of the shared SigV4 auth (not specific to MCP) and is bounded by the per-IP rate limit on these endpoints.

WARNING

Enabling --mcp-allow-write/--mcp-allow-delete lets a connected AI mutate or delete data — within the bound user's permissions. Grant the MCP session an access key scoped by IAM policy to exactly what the assistant should be able to touch.

TinyS3 is AGPLv3 licensed. Cloudflare and Cloudflare Workers are trademarks of Cloudflare, Inc. Amazon S3 and AWS are trademarks of Amazon.com, Inc. or its affiliates. MinIO is a trademark of MinIO, Inc. They are named only to describe compatibility; totis.ai is not affiliated with, endorsed by, or sponsored by any of them.