Architecture
hybrids3 is one Python process: a FastAPI application served by uvicorn on port 8080, with a FastMCP server mounted inside it and a background cleanup task. HTTP, S3 and MCP requests all end up in the same storage functions, the same SQLite connection and the same lock table, so an object written through one interface is visible through the others at once.
Modules
All code is in app/, as flat modules that import each other by name.
| Module | Job |
|---|---|
main.py | Builds the app, loads the configuration at startup, opens the database, wires the other modules, mounts MCP at /mcp, adds the middlewares and starts the cleanup task |
config.py | Parses and validates config.yaml into AppConfig and BucketConfig |
routes.py | HTTP and S3 routes, the presign endpoint, auth checks and the JSON or XML error format |
auth.py | Bearer token checks and SigV4 header or presigned URL checks |
sigv4.py | AWS Signature V4 parsing, verification and presigned URL generation |
mcp_server.py | The seven MCP tools |
lock.py | Per-object async read-write locks with timeouts and a queue limit |
storage.py | Safe path resolution, atomic writes, reads, deletes and the on-disk listing |
db.py | The SQLite objects table |
scheduler.py | The cleanup loop for expired objects and orphan files |
mimetype.py | libmagic detection with an extension fallback |
s3xml.py | S3 XML response documents |
logger.py | JSON log lines with the request ID |
Middlewares
Every request passes three middlewares before it reaches a route, in this order:
RequestIdMiddlewarecreates the request ID, stores it for the logger and addsX-Request-IdandX-Content-Type-Options: nosniffto the response.McpAuthMiddlewarechecks the optional MCP connection token on paths that start with/mcp.PathPrefixMiddlewarehandlespath_prefix. It refuses paths outside the prefix with404, strips the prefix so the routes match, and keeps the original path for the signature check.
An upload, step by step
client
-> RequestIdMiddleware request ID
-> McpAuthMiddleware (only /mcp paths)
-> PathPrefixMiddleware strip path_prefix, keep the signed path
-> routes.put_object
_get_bucket 404 if the bucket is not configured
_check_write_auth Bearer or SigV4, 404 if neither fits
storage.write_path resolve the key, 400 if it escapes the bucket
LockManager.write per-object lock, 503 on queue or timeout
stream body to <name>.tmp.<hex>, 413 past max_file_size
rename onto the final path
MD5 ETag, MIME detection
db.upsert_object metadata row with expiry time
<- 200 with ETag
An MCP upload_object call skips the routes and runs the same steps from mcp_server.py, after checking its auth_key.
How the three interfaces differ
| HTTP | S3 | MCP | |
|---|---|---|---|
| Entry point | routes.py | routes.py | mcp_server.py |
| Credential | Bearer token | SigV4 signature | auth_key argument |
| Response | JSON | XML | JSON-RPC result with a JSON object |
| Unauthorized | 404 | 404 | "Bucket '<name>' does not exist" |
routes.py decides between JSON and XML per request: an AWS4-HMAC-SHA256 Authorization header or any x-amz-* header means S3.
State
All state is in /data: object files under /data/<bucket>/ and the objects table in /data/hybrids3.db. Configuration is read once at startup and kept in memory. Locks live in memory too and disappear with the process, which is fine because they only cover requests in flight. Storage, expiry and locking shows the table.
Concurrency
uvicorn runs one worker process with one event loop. SQLite calls and file hashing are synchronous and run on that loop, so they serialize with each other. Body streaming and lock waits are asynchronous, so many requests can be in flight at once. Running more than one container on the same /data is not supported: the locks are per process.