Thumbdrive MCP
by hkrlogic
README.md
# Thumbdrive MCP
A USB stick you load for your agents: you put files on it, they read exactly what you put there, with a checksum to prove it, and nothing they do can change it.
Thumbdrive is a small, self-hosted [MCP](https://modelcontextprotocol.io) server. You manage files; any MCP agent (Claude Desktop, Cursor, VS Code and others) reads them byte-for-byte from only the folders you share with it.
- **Read-only for agents.** Four tools: `list`, `read`, `search`, `history`. No agent can write, rename or delete.
- **Exact.** Every read returns the file's SHA-256 so the agent can verify it got the right bytes.
- **Versioned.** Every change you make is kept; nothing is silently overwritten.
- **Plain files.** Current files live as normal files on a mounted folder, plus a small SQLite database. Backup means copying the folder.
- **Folder keys.** Each agent gets its own key that opens only the folders you choose. Everything else is invisible to it.
- **A web UI for you.** Upload, organize and restore files, hand out keys with ready-to-paste client configs, share a folder with a person by link, and see what your agents read. Agents never get write access; every change goes through the UI or the CLI.
> Status: early development (milestones M1 and M3).
## Web UI
Manage the drive from your browser. Agents never get write access; every change goes through the UI or the CLI.

| Files, versions and restore | Agent keys with ready-to-paste client configs |
| --- | --- |
|  |  |
**Try it in two minutes** with Docker or Portainer (image tag `:edge`, which follows main; `:m3` also works):
1. Deploy [`docker-compose.yml`](docker-compose.yml): run `docker compose up -d`, or paste it into Portainer under **Stacks → Add stack** (see [docs/portainer.md](docs/portainer.md)). It uses `ghcr.io/hkrlogic/thumbdrive-mcp:edge`.
2. Open **http://HOST:8484/**. `HOST` is `127.0.0.1` on the Docker host, or its LAN IP if you publish `8484:8484`.
3. Choose the admin password on the first screen. Or set `THUMBDRIVE_ADMIN_PASSWORD` in the compose file before the first start, so no one else can set it first.
4. Drop files on **Files**, create a key on **Agent keys**, and paste the snippet into Cursor, Claude Desktop or Claude Code.
**Docs:**
- [Web UI guide](docs/web-ui.md): every screen, with screenshots
- [Configuration and CLI reference](docs/configuration.md): every environment variable and admin command
- [Security](docs/security.md): sign-in, sessions, CSRF, HTTPS, read-only agents, hashed keys and links
- [Portainer setup](docs/portainer.md)
- [Changelog](CHANGELOG.md)
## Quick start
Requires Node 22.13 or newer, or Docker.
```sh
git clone https://github.com/hkrlogic/thumbdrive-mcp.git
cd thumbdrive-mcp
npm ci
npm start # web UI on http://127.0.0.1:8484/, MCP on /mcp
```
Open http://127.0.0.1:8484/ and set the admin password (first run only). Then drop files on **Files**, create a key on **Agent keys** and paste the snippet it shows into your client.
Everything also works from the CLI:
```sh
npm run admin -- import ~/Documents/handbook /team
npm run admin -- key create my-agent --folder /team # prints td_... once
```
With Docker (images for amd64 and arm64 are published to `ghcr.io/hkrlogic/thumbdrive-mcp`):
```sh
docker compose up -d # uses docker-compose.yml, then open http://127.0.0.1:8484/
```
Or build it yourself:
```sh
docker build -t thumbdrive-mcp .
docker run -d --init --name thumbdrive -e THUMBDRIVE_HOST=0.0.0.0 \
-p 127.0.0.1:8484:8484 -v thumbdrive-data:/data thumbdrive-mcp
```
Portainer: see [docs/portainer.md](docs/portainer.md).
The server listens on localhost unless `THUMBDRIVE_HOST` is set, and that includes inside a container. The compose file sets `THUMBDRIVE_HOST=0.0.0.0` and publishes the port on `127.0.0.1` only. Change the port to `8484:8484` to reach it from your home network. Plain HTTP is accepted from localhost and private networks (10/8, 172.16/12, 192.168/16, 100.64/10 and IPv6 ULA). It is refused from public addresses, so for anything beyond the LAN put HTTPS in front (Caddy or Cloudflare Tunnel).
## Web UI pages
The UI is served at `/` on the same port as `/mcp`. See the [Web UI guide](docs/web-ui.md) for details.
| Page | What you can do |
| --- | --- |
| Dashboard | Copy the MCP URL, see counts and recent agent reads |
| Files | Browse; create, rename, move and delete folders; drag and drop files or whole folders to upload; download, rename, move and delete files; view a file's versions and restore any of them; restore deleted files |
| Agent keys | Create a key for one or more folders. The key is shown once, with a copy button and snippets for Cursor, Claude Desktop (via `mcp-remote`), Claude Code and other clients that use the address you opened the UI on. Change a key's folders or revoke it |
| Share links | Give a person read-only browser access to one folder: 1 hour, 1 day (default), 7 days or 30 days. Links are random 256-bit tokens stored as hashes. Revoke any time |
| Activity | Every agent read (key, tool, path, time), filterable by key, action and path; optionally your own changes and share-link downloads |
| Settings | Change the admin password (signs out every other session) |
**Admin account.** There is one admin password. On first run the UI asks you to set it. You can instead set `THUMBDRIVE_ADMIN_PASSWORD` (applied at start; changing it signs out all sessions) or run `admin set-password`. Passwords are stored as scrypt hashes.
**Security.** Sign-in uses a single scrypt-hashed password and a 7-day `HttpOnly`, `SameSite=Strict` session cookie. Every change is CSRF-checked, pages carry a strict CSP, and downloads are sandboxed. The UI follows the same Host/Origin allow-list and HTTPS rules as `/mcp`, so a hostname such as `nas.local` must be in `ALLOWED_HOSTS` (or be `THUMBDRIVE_PUBLIC_URL`). Until a password is set, the first person to open the UI can set one. Set `THUMBDRIVE_ADMIN_PASSWORD`, or keep the port on localhost or your LAN until you have opened it once. Details: [docs/security.md](docs/security.md).
## Connect an agent
The **Agent keys** page writes these for you. By hand, for Streamable HTTP (most clients):
```json
{
"mcpServers": {
"thumbdrive": {
"url": "http://127.0.0.1:8484/mcp",
"headers": { "Authorization": "Bearer td_..." }
}
}
}
```
stdio (the client starts the server itself):
```json
{
"mcpServers": {
"thumbdrive": {
"command": "npx",
"args": ["tsx", "/path/to/thumbdrive-mcp/src/index.ts", "--stdio"],
"env": { "THUMBDRIVE_KEY": "td_...", "DATA_DIR": "/path/to/thumbdrive-mcp/data" }
}
}
}
```
Check a running server end to end: `THUMBDRIVE_KEY=td_... npm run smoke -- http://127.0.0.1:8484/mcp`.
## Tools
All four are annotated `readOnlyHint: true`. Anything outside the key's folders answers `Not found`, exactly like a missing file.
| Tool | What it does |
| --- | --- |
| `list(path)` | Files and folders with size, SHA-256 and modified time |
| `read(path, format, page, version, base64)` | Paged read. `raw` gives UTF-8 text, or base64 for binaries (or when `base64` is true). `text` gives CSV/TSV as JSON rows, and text files as text. Returns metadata JSON (sha256, version, page, pages, has_more...) then the content |
| `search(query, path)` | Case-insensitive match on names and text content, up to 50 hits |
| `history(path)` | Stored versions, newest first; read one with `read(path, version)` |
Text views for Excel, PDF and Word files are not built yet: `format: "text"` says so, and `format: "raw"` returns them as base64.
## Admin CLI
Every command is described in [docs/configuration.md](docs/configuration.md#admin-cli).
```text
npm run admin -- put <local-file> <drive-path>
npm run admin -- import <local-folder> <drive-folder>
npm run admin -- mkdir | rm | ls | history <drive-path>
npm run admin -- mv <drive-path> <new-drive-path>
npm run admin -- restore <drive-path> <version>
npm run admin -- key create <name> --folder <folder> [--folder ...]
npm run admin -- key list | key revoke <name>
npm run admin -- activity [count]
npm run admin -- set-password # prompts, or reads the first line of stdin
```
## Storage
```text
data/
files/ current files, as plain files
versions/ every stored version, named by SHA-256
thumbdrive.db versions, hashed keys and share links, admin password hash,
sessions, activity log (SQLite)
```
Every admin change adds a version; deletes keep history. Files you copy into `data/files` by hand are served too, and get their first version on the next admin change. Symlinks inside the drive are ignored.
## Configuration
The main settings are below. [docs/configuration.md](docs/configuration.md) lists every variable, with examples.
| Variable | Default | |
| --- | --- | --- |
| `DATA_DIR` | `./data` | Mounted data folder |
| `PORT` | `8484` | |
| `THUMBDRIVE_HOST` | `127.0.0.1` | Address to listen on. Set `0.0.0.0` in containers (the compose file does). `BIND_HOST` also works |
| `ALLOWED_HOSTS` | | Host names accepted in `Host`/`Origin` besides localhost and private IPs, comma separated (e.g. `nas.local`) |
| `ALLOW_INSECURE_HTTP` | | `1` accepts plain HTTP from public addresses. Not recommended |
| `MAX_FILE_MB` | `50` | Largest file the admin can store or upload |
| `READ_PAGE_BYTES` | `65536` | Bytes per read page |
| `READ_PAGE_ROWS` | `200` | Rows per CSV page |
| `RATE_LIMIT_PER_MINUTE` | `120` | Requests per key per minute |
| `THUMBDRIVE_ADMIN_PASSWORD` | | Sets the admin password at start (at least 8 characters). Without it, the UI asks on first run |
| `THUMBDRIVE_PUBLIC_URL` | | The address people and agents use, e.g. `https://drive.example.com`. Used for the MCP URL, key snippets and share links, and its host is allowed. By default the UI uses the address you opened it on |
| `THUMBDRIVE_KEY` | | Key for `--stdio` mode |
## Development
```sh
npm run typecheck
npm run lint
npm test
npm run build # compiles to dist/, which is what the image runs
```
## License
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). The license does not grant rights to the Thumbdrive name.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues