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| Flag | Effect |
|---|---|
--mcp | Serve MCP endpoints at /mcp on the server port (read tools only). |
--mcp-allow-write | Expose write tools (create bucket, put object, set tags). |
--mcp-allow-delete | Expose delete tools (delete object, delete bucket). |
--mcp-allow-admin | Expose admin tools (data usage, cluster info). |
--mcp-require-tls | Require 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: falseCLI 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/mcpGetting 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.
Request a device code:
bashcurl -sX POST http://127.0.0.1:8000/mcp/devicejson{ "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 }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_codewould then act as you.Poll for the token (the client repeats this every
intervalseconds until approved):bashcurl -sX POST http://127.0.0.1:8000/mcp/token \ -d device_code=<device_code>While pending you get
{"error":"authorization_pending"}(orslow_down); once approved:json{ "access_token": "…", "token_type": "Bearer", "expires_in": 28800, "refresh_token": "…", "refresh_expires_in": 2591999 }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:
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-protected-resource | RFC 9728 — names the authorization server for /mcp |
GET /.well-known/oauth-authorization-server | RFC 8414 — advertises the endpoints + capabilities |
POST /mcp/register | RFC 7591 Dynamic Client Registration (public client, PKCE) |
GET/POST /mcp/authorize | authorization endpoint; the credential form is the consent screen |
POST /mcp/token | token 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 tinys3On 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
--mcpis 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/revokewith 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.