
HybridS3
io.github.psyb0t由 GitHub 帳號 psyb0t 發布v0.3.8更新於 Oct 24, 2026
Self-hosted S3-compatible object storage server with an MCP interface for AI agents.
概覽
自架的 S3 相容物件儲存,提供 MCP 介面,讓 AI 助理可以上傳、下載、列出、檢視與刪除存放的物件。
- 可以做什麼
- 以文字或 base64 二進位上傳物件,未指定時自動偵測內容類型;下載物件內容並以文字或 base64 回傳(超過 50 MB 的物件需改用 HTTP API)。依可選前綴篩選列出桶內物件,並列出桶:主金鑰可見全部桶,桶金鑰僅見自己的桶。檢視物件中繼資料(大小、內容類型、ETag、到期時間)而不必下載內容。刪除物件,並產生可分享的 GET 或 PUT 預簽章連結,可指定有效期限。
- 適用情境
- 當你想要一個小型自架檔案儲存,讓一般 S3 用戶端和 AI 助理都能讀寫時使用,例如作為代理產生檔案、上傳或報告的暫存空間。適合需要桶層級 TTL 到期或預簽章分享連結、又不想架設完整雲端儲存堆疊的情境。
- 執行需求
- 需要本機 Docker 執行環境;服務以 psyb0t/hybrids3 容器在 8080 埠執行,需掛載 YAML 設定檔到 /config/config.yaml,並掛載資料磁碟區到 /data。設定中定義桶及其金鑰,包括 master_key 與各桶的 key。MCP 用戶端連線到 /mcp/ 的 Streamable HTTP 端點,可選用 bearer 權杖或 auth 查詢參數。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 HybridS3,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
HybridS3
[CI] [version] [license] [Docker Pulls]
Lightweight object storage that speaks S3 (boto3/AWS SDK compatible), plain HTTP, and MCP. SQLite for metadata, flat files on disk.
Table of Contents
- Why
- Quick Start
- Running
- Configuration
- Keys and Access
- HTTP API
- S3 Interface
- MCP Interface
- Agent integrations
- Presigned URLs
- MIME Type Detection
- TTL and Expiry
- Locking
- Logging
- Behind a Reverse Proxy
- Security
- Testing
- Development
- Architecture
- Limitations
- License
Why
Most self-hosted S3-compatible storage is designed for large-scale deployments. Distributed erasure coding, IAM policies, WORM compliance, full web consoles — useful if you're running a cloud, overkill if you just want a place to put files that various services and AI agents can read and write.
AWS Sig V4 breaks behind reverse proxy path prefixes. Most implementations verify signatures using the full original upstream path. Put them behind nginx at /storage/, nginx strips the prefix, the server sees /bucket/key instead of /storage/bucket/key, the signature check fails. HybridS3 has a path_prefix config option — set it to /storage and all routes move under that prefix. No path stripping, no special proxy headers. boto3's signed path matches what the server sees.
Three interfaces, one service. boto3 works out of the box. Plain HTTP with curl works. AI agents connect via MCP and get structured tool definitions. No separate services for different clients.
Buckets are configuration, not state. There is no API to create or delete buckets. They live in the YAML config file. You always know exactly what exists, it's version-controlled, and there are no surprise buckets accumulating garbage.
TTL expiry is built in. Set ttl: 24h on a bucket and objects expire automatically after their last write. No lifecycle policies, no cron jobs, no separate process.
Readable and modifiable. Small enough to understand in an afternoon.
Quick Start
Running
The container expects:
- Config file at
/config/config.yaml - Data directory at
/data - Port
8080
Runs as UID 1000.
docker run:
docker-compose:
Configuration
Keys and Access
Key pair model
Each bucket has two keys defined in config:
The split is what makes presigned URLs work safely. A presigned URL must embed an identifier in the Credential= field so the server knows which key to verify against — that identifier is the public_key. Since it is non-secret, having it in the URL is fine. The private key signs the URL on the server and never appears in it.
Master key
The master_key is a cross-bucket credential that works on every bucket for every operation, without needing individual bucket keys. Two situations call for it:
- You need to operate across multiple buckets from a single client
- You need to list all configured buckets — bucket keys only see their own bucket in
list_buckets
The master_public_key is the non-secret identifier that pairs with master_key in S3 auth (used as aws_access_key_id).
Do not embed the master key in client-facing code. Use per-bucket keys for that — they limit access to exactly one bucket.
Bucket visibility
HTTP API
Authentication
HTTP requests authenticate using a Bearer token in the Authorization header. Pass the bucket's private key, or the master key for cross-bucket operations.
- Public bucket reads (GET, HEAD, LIST) — no authentication required.
- All writes (PUT, DELETE) and reads on private buckets — require the bucket key or master key.
- Listing buckets (
GET /) — master key lists all buckets; bucket key lists only its own bucket. - Presign endpoint (
POST /presign/...) — requires the bucket key or master key.
Endpoints
PUT /{bucket} exists purely for S3 client compatibility. boto3 sends a create_bucket call before any operation, which maps to this endpoint. HybridS3 treats it as a no-op — no buckets are created or modified.
Listing objects accepts prefix and max-keys query parameters:
Upload returns an ETag header (MD5 of the file content). GET and HEAD also return ETag, Last-Modified, and Content-Length.
Object keys
Keys support nested paths using /. Parent directories are created automatically on write and pruned when empty on delete.
Response format
Requests that include an AWS Sig V4 Authorization header receive S3-compatible XML responses. All other requests receive JSON. Error responses include an "error" field and a "request_id" field.
Every response includes X-Request-Id for log correlation and X-Content-Type-Options: nosniff.
S3 Interface
Authentication
S3 clients authenticate using AWS Signature V4. The client signs each request using the bucket's public_key as aws_access_key_id and the bucket's private key as aws_secret_access_key. The resulting Authorization header contains the access key ID in plaintext in the Credential= field, and the computed HMAC in Signature=. The private key is never transmitted — it is only used locally to compute the signature.
HybridS3 reads the access key ID from Credential=, finds the bucket with that public_key, then re-derives the expected signature using that bucket's private key and compares it to the one in the header. The access key ID alone grants nothing — the signature must match.
Operations
MCP Interface
An MCP server runs at /mcp/ using the Streamable HTTP transport. AI agents connect via any MCP-compatible client and receive structured tool definitions with typed inputs and outputs.
Connecting
Authentication
Endpoint-level auth
The /mcp/ endpoint accepts an optional token to authenticate the connection before any tool is invoked. Use the master key for full access, or a bucket key to limit the connection to that bucket's scope. The token is validated against the master key and all bucket keys.
Two methods are accepted:
Authorization header — for clients that support custom headers (e.g. Claude Code):
Query parameter — for clients that cannot set custom headers (e.g. ChatGPT):
If a token is provided and does not match any known key, the request is rejected with 401. If no token is provided at all, the request passes through and per-tool auth applies.
Per-tool auth
Each tool that operates on a bucket accepts an auth_key parameter — the bucket's private key or the master key. This is checked independently of endpoint-level auth and controls what each individual tool call is allowed to do.
Tools
All tools return structured output (structuredContent) for clients that support it, with a plain text fallback.
Agent integrations
The skill works in any agent that reads .agents/skills/ (such as peen), and installs natively in the clients below.
Claude Code
Claude Code prompts for the HybridS3 URL and, if the endpoint requires connection-level auth, the bucket/master key — the key is stored in your OS keychain.
Codex
Installed via the marketplace, the skill invokes as $hybrids3:hybrids3. Codex also picks the skill up automatically, no install required, in any repo containing .agents/skills/ — there it invokes as plain $hybrids3.
OpenClaw
The skill is published to ClawHub on every release:
For MCP clients that speak local stdio, the @psyb0t/hybrids3 plugin bridges to the service's /mcp/ endpoint:
Then set HYBRIDS3_URL (and HYBRIDS3_KEY if your endpoint requires connection-level auth).
Presigned URLs
Presigned URLs allow anyone with the link to read or write a specific object for a limited time, without sending an Authorization header. The /presign/{bucket}/{key} endpoint supports two methods via the method query parameter:
method=GET(default) — recipient can download the object.method=PUT— recipient can upload (overwrite) the object.
A presigned URL is bound to its HTTP verb. A GET URL cannot be used to PUT and vice versa — the signature includes the method in its canonical request.
Expiry range: 1 second to 604800 seconds (7 days). Default: 3600. Expired or tampered URLs return 403.
Presigned GET
Private bucket — generates an AWS Sig V4 presigned URL. The server signs the URL using the bucket's private key, which never appears in the URL. The URL contains the public_key in the Credential= field and the HMAC in X-Amz-Signature.
Public bucket — returns a plain URL with no signature and no expiry, since GET on public buckets requires no auth anyway.
Presigned PUT
Use method=PUT to hand someone a URL that lets them upload a specific key without seeing your bucket key. Public buckets are not a shortcut here — anonymous reads are allowed, anonymous writes never are — so a presigned PUT URL is always signed, even for public buckets.
The bucket's max_file_size is enforced server-side during the upload; oversized bodies are rejected with 413 regardless of how the request was authenticated.
With boto3:
Generating a presigned URL requires the bucket's private key or the master key. The resulting URL grants exactly one action (GET or PUT) on exactly one key.
Both the /presign/ HTTP endpoint and the presign_url MCP tool follow this same logic — pass method="PUT" to the MCP tool to get an upload URL.
MIME Type Detection
Content type is detected automatically on every upload. No Content-Type header is required. Detection uses libmagic to inspect the first 8 KB of the file content, with a filename extension fallback when libmagic returns a generic type. The detected type is stored in metadata and returned in Content-Type on GET and HEAD responses.
To override auto-detection, set Content-Type explicitly on the upload request.
TTL and Expiry
Set ttl on a bucket and objects expire automatically after that duration from the last write. Overwriting an object resets its expiry clock. Setting ttl: 0 means objects never expire.
A background cleanup loop runs every cleanup_interval seconds and performs two tasks:
- TTL expiry — finds all objects whose
expires_athas passed and deletes them from disk and metadata. - Orphan cleanup — scans each bucket's directory on disk and deletes files that have no corresponding metadata record. This handles files left behind by interrupted uploads.
Empty parent directories are removed up to the bucket root on every deletion.
Locking
Every object key gets its own async read-write lock. Multiple readers hold the lock simultaneously. A writer gets exclusive access and blocks all concurrent readers and writers on that key. Locks are acquired before any operation begins and released after the response is fully sent.
This prevents torn reads, partial write visibility, and data corruption under concurrent access.
Three conditions cause 503 Service Unavailable:
The hold timeout protects against a slow or stalled upload holding a write lock indefinitely. When it fires, the lock is released and the request receives 503.
The defaults (30s acquire, 300s hold, 100 max waiters) suit large file uploads. For high-throughput small-object workloads, tighten these to shed load faster.
Logging
Structured JSON logs to stdout, one entry per line:
The request ID appears in every log line produced during a request, making it easy to trace a single request end-to-end. Exceptions are logged with full tracebacks.
Behind a Reverse Proxy
Set path_prefix in config to match the proxy location. HybridS3 serves all routes under that prefix natively — nginx forwards the path as-is, no stripping required. SigV4 signatures work because the client's signed path matches what the server sees.
The key: proxy_pass http://backend:8080; (no trailing /) forwards the full path. proxy_pass http://backend:8080/; (trailing /) tells nginx to replace the location prefix — that strips /storage/ and breaks SigV4.
With this config:
When path_prefix is set, all endpoints move under it — including health (/storage/health).
Security
- AWS Sig V4 verification — full signature verification for header auth and presigned URLs; expired or tampered presigned URLs return 403
- Constant-time key comparison — all credential checks use
hmac.compare_digestto prevent timing attacks - Private key isolation — the private
keyis never returned in any response, never embedded in presigned URLs, and never logged - Path traversal protection — all object key paths are validated with
Path.relative_tobefore any disk access; upload attempts with traversal keys return 400 - Bucket enumeration prevention —
GET /requires a valid key; master key lists all buckets, bucket key lists only its own; unauthorized or non-existent bucket access always returns 404 — callers cannot distinguish "does not exist" from "you don't have access" - SQLite prefix injection prevention — user-supplied
prefixvalues are escaped before use inLIKEqueries - Atomic writes — objects are written to a temporary file and renamed into place; readers never see partial writes
- Streaming uploads — request bodies are streamed directly to disk without buffering the full body in memory
- Orphan file cleanup — files on disk with no metadata record are deleted by the background sweep
- Per-key RW locking — overloaded or timed-out requests get 503 rather than queuing indefinitely
- MCP error masking — internal error details are not exposed to MCP tool callers
- MCP download cap — objects over 50 MB cannot be downloaded via MCP; use the HTTP API directly
- Non-root container — runs as UID 1000
X-Content-Type-Options: nosniff— on every response
Testing
All test output is teed to test.log automatically.
The integration tests cover: HTTP API, S3/boto3 compatibility, MCP tools, Bearer auth, AWS Sig V4 auth, presigned URLs (HTTP endpoint and S3 SDK, GET and PUT), master key, cross-bucket key isolation, TTL expiry, orphan cleanup, MIME detection, size limits, path traversal attempts, binary files, concurrent reads and writes, RW lock behavior at unit and HTTP level, request IDs, and security headers.
Development
All development tooling lives inside a sandboxed dev container so the host stays clean and supply-chain blast radius is contained. The host only needs docker, make, git, and a shell — no Python interpreter, no pip, no uv, no project deps installed globally.
Dependency management
The project uses uv with a hash-locked uv.lock (committed) and a supply-chain age gate ([tool.uv] exclude-newer in pyproject.toml) that refuses to install any package version published after a fixed date. Every dep mutation bumps that date to 3 days ago in the same commit, then re-locks.
Never run uv directly — every supported mutation has a Make target so the gate stays anchored.
Production image
make build produces the production image. The Dockerfile is multi-stage, pins both the Python base and uv by @sha256 digest, installs with uv sync --frozen --no-dev (lockfile-verified, no dev deps), and copies only the resulting venv + app source into the runtime stage. There is no pip invocation anywhere in this project.
Architecture
Limitations
- No multipart upload (S3's chunked upload protocol)
- No object versioning
- No ACLs beyond bucket-level public/private
- No bucket creation or deletion via API — buckets are defined in config
- No CORS headers — add these at the reverse proxy layer if needed
- No replication
- No encryption at rest
License
來源:README.md,提交 a7b6c5c
工具
0版本歷史
1- v0.3.8最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分