SILO-MCP
MCP server that moves files between your filesystem and cloud storage.
Documentation
Silo MCP
A self-hosted MCP server that moves files between your filesystem and cloud storage — nine platforms, one tool surface. Upload, download, list, search, delete, generate share links, and copy files directly from one cloud to another, without giving an LLM raw filesystem access or a bag of platform-specific credentials to juggle.
It's not "local-first" — the files live in the cloud and every operation talks to a remote provider API. What stays local is the part that matters for trust: the server runs as a subprocess on your machine, your credentials sit in the OS keyring (never in the cloud, a config file, or a tool-call argument), and the transfer audit log is a local SQLite file.
Document stores: Dropbox · Google Drive · OneDrive · Box · Google Photos (upload-only) · Yandex Disk Object stores: S3-compatible (AWS S3, Cloudflare R2, MinIO) · Google Cloud Storage · Azure Blob Storage
What can you do with Silo MCP?
- Copy a file directly from one cloud to another — "copy my Dropbox
/photosfile to my S3 bucket", "migrate this Drive file to OneDrive" — in a single tool call, no manual download-then-upload. The one thing a single-vendor storage integration structurally can't do. - Move files in and out of cloud storage — "upload
report.pdfto Dropbox", "downloadnotes.txtfrom Google Drive so I can use it" — across all nine platforms with one set of tools. - Browse and search your document stores by folder or query, and list object-store buckets by key prefix.
- Generate shareable links to a file — presigned/signed URLs on the object stores and Box (with expiry), platform share links on the rest.
- Bridge to other MCP servers — a
download_filehere lands a local path you can hand straight to another server (e.g. a social-posting server'smedia_paths). - Keep a local audit trail — every upload, download, delete, and share
link is recorded in a local SQLite log you can query with
list_transfers.
All of it runs through your MCP client in natural language — see Example workflows.
Quick start: pip install -e . → add silo-mcp to your MCP client
config → silo-mcp-accounts add dropbox --label personal → ask your
client to list your Dropbox files. Full steps in
Installation and Add accounts below.
Contents
- What can you do with Silo MCP?
- Why
- Architecture
- Requirements
- Installation
- Configure your MCP client
- Supported clients
- Using with Ollama
- Add accounts
- Example workflows
- Tools
- Supported platforms & tools
- Setup friction and credential shape per platform
- Path safety
- Platform-specific notes
- Safety
- Security
- Testing
- Troubleshooting
- Related
- License
Why
Most "give the LLM your cloud storage" setups fall into one of two traps: a single-platform integration that's dead the moment you switch providers, or unrestricted local file access that lets a manipulated conversation read or upload anything on disk. Silo MCP is built against both:
- One tool surface, nine platforms.
upload_file,download_file,list_files,search_files,delete_file,create_share_linkbehave the same regardless of which platform you point them at — object stores (bucket+key) and document stores (path-based) share oneFileStorecontract. - Confined by design, not by convention.
local_pathfor uploads and the destination for downloads are both LLM-supplied tool-call arguments, so both are hard-confined to configured root directories — a manipulated conversation can't ask this server to read an SSH key or write a download somewhere it shouldn't. - Mutations require
confirm=true.upload_file,delete_file, andcreate_share_linkall mutate remote state or create a bearer-credential link and require deliberate confirmation.download_file/list_files/search_filesonly write locally, so they don't gate. - Multi-account from the start. Every tool takes an optional
accountlabel — run a work Dropbox and a personal Dropbox side by side without reconfiguring anything.
Architecture
flowchart TB
subgraph Client["MCP Client"]
direction LR
CD["Claude Desktop / Code<br/>(stdio)"]
OL["Ollama bridge<br/>(streamable-http / SSE)"]
end
subgraph Silo["Silo MCP Server (server.py)"]
direction TB
Tools["Tool surface<br/>upload_file · download_file · list_files<br/>search_files · delete_file · create_share_link"]
Paths["paths.py<br/>upload/download root containment"]
Registry["stores/registry.py<br/>resolve(platform, account)"]
Accounts["accounts.py<br/>credential storage + OAuth refresh"]
History[("db.py<br/>SQLite transfer log")]
end
subgraph Backends["FileStore implementations"]
direction LR
Doc["Document stores<br/>Dropbox · Drive · OneDrive<br/>Box · Photos · Yandex Disk"]
Obj["Object stores<br/>S3-compatible · GCS · Azure Blob"]
end
FS[("Local filesystem<br/>upload/download roots")]
Cred[("OS credential store<br/>Windows / macOS / Linux keyring")]
CD --> Tools
OL --> Tools
Tools --> Paths --> FS
Tools --> Registry
Registry --> Doc
Registry --> Obj
Registry --> Accounts --> Cred
Tools --> History
Every tool call resolves a (platform, account) pair to a FileStore
implementation and a credential from accounts.py, checks any local
path against the upload/download roots, executes against the real
platform API, and logs the result — the same shape regardless of which
of the nine backends is on the other end.
Requirements
- Python 3.11+
- An OS credential store
keyringcan use (Windows Credential Manager, macOS Keychain, or a Secret Service provider on Linux) — credentials are never written to disk in plaintext or passed through an MCP tool call.
Installation
git clone https://github.com/gouthamkallempudi/silo-mcp.git
cd silo-mcp
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
pip install -e .
Object-store SDKs are optional extras — only install what you need:
pip install -e ".[s3]" # AWS S3 / Cloudflare R2 / MinIO
pip install -e ".[gcs]" # Google Cloud Storage
pip install -e ".[azure]" # Azure Blob Storage
pip install -e ".[all]" # all three
A core install (no extras) covers Dropbox/Drive/OneDrive/Box/Google
Photos/Yandex Disk with no dependency beyond httpx — the server
degrades gracefully if an object-store SDK isn't installed rather than
failing to start.
Configure your MCP client
Add to your client's MCP config (e.g. Claude Desktop's
claude_desktop_config.json, or Claude Code's .mcp.json):
{
"mcpServers": {
"silo-mcp": {
"command": "silo-mcp"
}
}
}
silo-mcp must resolve on PATH inside the environment your client
launches the server from — if you installed into a virtualenv, point
command at that venv's silo-mcp executable directly (e.g.
/path/to/silo-mcp/.venv/bin/silo-mcp) instead of relying on shell
activation.
Transport options (stdio default, HTTP for network clients)
Defaults to stdio, what every desktop MCP client (Claude Desktop,
Claude Code, Cursor, etc.) spawns as a subprocess — no network port opens.
For a client that can't spawn a local subprocess and needs to reach the
server over HTTP instead (see Using with Ollama
below), set:
SILO_MCP_TRANSPORT=streamable-http SILO_MCP_HOST=127.0.0.1 SILO_MCP_PORT=8000 silo-mcp
SILO_MCP_TRANSPORT also accepts sse (the older HTTP transport, kept
for clients that haven't moved to streamable-http yet). SILO_MCP_HOST/
SILO_MCP_PORT are only read for the two HTTP transports and default to
127.0.0.1:8000.
[!WARNING] This server has no built-in auth for HTTP mode — don't bind it to
0.0.0.0or expose it past localhost without putting a reverse proxy with auth in front of it, since every tool call reaches your cloud storage accounts.
Supported clients
Any MCP client that can launch a local stdio server works — the server uses only standard MCP tool calls, no client-specific features. Verified and expected-to-work clients:
| Client | Config | Notes |
|---|---|---|
| Claude Code | .mcp.json in the project ({"mcpServers":{"silo-mcp":{"command":"silo-mcp"}}}) | Verified end-to-end over the MCP protocol. |
| Claude Desktop | claude_desktop_config.json — same mcpServers block | stdio. |
| Cursor / VS Code (MCP) | Their mcp.json — same mcpServers block | stdio. |
| Ollama (via a bridge) | Point the bridge at the HTTP transport | See Using with Ollama. |
[!TIP] If
silo-mcpisn't on the launching shell'sPATH(common with virtualenvs), setcommandto the venv's executable directly, e.g.C:\\path\\to\\silo-mcp\\.venv\\Scripts\\silo-mcp.exeon Windows or/path/to/.venv/bin/silo-mcpelsewhere.
Using with Ollama
Ollama doesn't speak MCP natively — it needs an MCP client in the loop that turns Ollama's tool-calling into MCP tool calls, the same role Claude Desktop/Code play for Claude. This server doesn't ship that bridge (kept out of scope to stay a plain server package), but any MCP-aware Ollama client works once you point it at streamable-http instead of stdio:
- Start the server in HTTP mode:
SILO_MCP_TRANSPORT=streamable-http silo-mcp. - Point your Ollama-side MCP client at
http://127.0.0.1:8000/mcp. - Use a tool-calling-capable model (e.g.
llama3.1,qwen2.5) — Ollama only routes tool calls for models that support thetoolsAPI field.
Add accounts
Credentials are added via a CLI with hidden input, never through an MCP tool call, and stored in your OS credential store:
silo-mcp-accounts add dropbox --label personal
silo-mcp-accounts add google_drive --label personal
silo-mcp-accounts add onedrive --label personal
silo-mcp-accounts add box --label personal
silo-mcp-accounts add google_photos --label personal
silo-mcp-accounts add yandex_disk --label personal
silo-mcp-accounts add s3 --label personal
silo-mcp-accounts add gcs --label personal
silo-mcp-accounts add azure_blob --label personal
silo-mcp-accounts list
First-time OAuth setup (Drive, Photos, OneDrive, Box)
These four platforms need a refresh_token before silo-mcp-accounts add
will accept them — and getting the first one requires a one-time
browser authorization, not just a client_id/secret pair. Register an app
on each platform's developer console, then run:
silo-mcp-accounts oauth google_drive --label personal
silo-mcp-accounts oauth google_photos --label personal
silo-mcp-accounts oauth onedrive --label personal
silo-mcp-accounts oauth box --label personal
This opens your browser to the platform's consent screen, listens on
http://localhost:8765/callback for the redirect, exchanges the code for
a refresh_token, and stores the account — no need to run add afterward.
App registration, per platform:
- Google (Drive + Photos share one app): Google Cloud
Console → new project → APIs &
Services → enable the Google Drive API and Photos Library API →
OAuth consent screen (External is fine for personal testing, add
yourself as a test user) → Credentials → Create OAuth client ID → type
Desktop app. Desktop-app clients accept any
http://localhost:<port>redirect without pre-registering it, so no redirect URI setup needed. - OneDrive: Azure Portal → App
registrations → New registration → platform Mobile and desktop
applications → add redirect URI
http://localhost:8765/callbackexactly (Azure requires an exact match). API permissions → Microsoft Graph → addFiles.ReadWriteandoffline_access(delegated). Certificates & secrets → new client secret. - Box: Box Developer Console
→ Create new app → Custom App → User Authentication (OAuth 2.0)
→ under Configuration, set Redirect URI to
http://localhost:8765/callbackexactly (Box also requires an exact match) and check the scopes you need (Read/write files).
If you use --port to pick a different local port, use that same port
in the redirect URI you register.
Getting a Yandex Disk token
Yandex Disk is a static token like Dropbox — no refresh_token, so it's
not part of the silo-mcp-accounts oauth bootstrap flow above. Its OAuth
app config uses an implicit grant: the token comes back directly in
the browser, no code-exchange step to script.
- oauth.yandex.com → Create app (or reuse an existing one).
- Under Platforms, check Web services and set the redirect URI
to
https://oauth.yandex.com/verification_code— Yandex's own built-in page that just displays the token, no local listener needed for this one. - Under Permissions, grant Disk access:
cloud_api:disk.readandcloud_api:disk.write(orcloud_api:disk.app_folderinstead ofdisk.writeif you want to scope it to an app-specific folder rather than the whole disk). - Save the app and note its ID.
- Visit
https://oauth.yandex.com/authorize?response_type=token&client_id=<your-app-id>in a browser, approve access — the token is shown directly on the redirected page. silo-mcp-accounts add yandex_disk --label personal, paste the token.
Faster bulk setup: import file or env vars
Running add once per platform gets old fast when you're configuring
several at once. Two faster paths — both still end up in the OS keyring,
never in a file or env var this project persists itself:
Import file — copy the template, fill in real values, import in one
shot. silo-accounts.example.yaml at the
repo root has an entry for all nine platforms with the exact field names
each one needs and a comment on where to get each value:
cp silo-accounts.example.yaml silo-accounts.yaml
# edit silo-accounts.yaml with real values, delete platforms you don't use
silo-mcp-accounts import silo-accounts.yaml
JSON works too (same platform -> label -> fields shape), dispatched by
file extension — anything not ending in .json is parsed as YAML.
silo-accounts.yaml/.json, credentials.yaml/.json, and any
*.local.yaml/.json are already in .gitignore, but treat that as a
backstop, not the plan — delete the file right after importing it.
It's plaintext secrets on disk for as long as it exists, gitignored or
not.
Env vars — for scripted or CI setup where a file isn't practical,
add reads SILO_MCP_CRED_<PLATFORM>_<FIELD> before prompting:
SILO_MCP_CRED_DROPBOX_ACCESS_TOKEN=sl.xxx silo-mcp-accounts add dropbox --label personal
Env vars are more exposure-prone than a file you delete (process listings, shell history, crash dumps) — prefer the import file for anything beyond quick automated test setup.
Example workflows
Once the server is connected, you don't call the tools directly — you ask your MCP client (Claude Desktop, Claude Code, etc.) in plain language and it picks the right tool and arguments. These examples were all exercised end-to-end over the real MCP protocol against live Dropbox, Google Drive, and Google Photos accounts.
Dropbox / OneDrive / Yandex Disk (real paths):
- "Upload
report.pdffrom my uploads folder to Dropbox." - "What's in the root of my Dropbox? Find anything named
invoice." - "Download
notes.txtfrom Dropbox and give me a shareable link to it." - "Delete
old-draft.txtfrom my Dropbox."
Google Drive / Box (id-addressed — search first, then act on the id):
- "Find
budget.xlsxin my Google Drive." → then "Download that one." - "Share that file with a public link."
Google Photos (upload-only):
- "Upload this screenshot to my Google Photos."
Object stores — S3 / GCS / Azure Blob (need a bucket/container):
- "Upload
backup.zipto my S3 bucketmy-backups." - "List everything under
logs/in bucketmy-backups." - "Give me a 1-hour presigned link to
backup.zipinmy-backups."
Cross-cloud copy (one platform straight to another):
- "Copy
report.pdffrom my Dropbox to my Google Drive." - "Migrate everything I just found in Drive over to my S3
archivebucket."
Cross-server:
- "Grab my headshot from Google Drive so I can attach it to a post." —
download_filelands a local path another MCP server can use.
The full mapping of phrasings to tool calls:
| You say | What runs |
|---|---|
| "What cloud storage can I use here, and which accounts are set up?" | list_supported_stores |
"Upload report.pdf from my uploads folder to Dropbox." | upload_file(dropbox, …, confirm=true) — the client sets confirm after you agree |
| "What's in the root of my Dropbox?" | list_files(dropbox) |
"Find files named invoice in my Dropbox." | search_files(dropbox, "invoice") |
"Download notes.txt from Dropbox so I can use it." | download_file(dropbox, "/notes.txt") |
| "Get me a shareable link to that file." | create_share_link(dropbox, …, confirm=true) |
"Delete old-draft.txt from Dropbox." | delete_file(dropbox, "/old-draft.txt", confirm=true) |
| "Put this image into my Google Photos." | upload_file(google_photos, …, confirm=true) |
| "Copy my resume from Dropbox into a tweet draft." | download_file here → hand the local path to another MCP server's media_paths |
| "Show me what I've moved recently." | list_transfers |
Because Google Drive and Box address files by id, a natural flow there is
two steps — "find budget.xlsx in my Drive" (search_files, returns the
id), then "download that one" / "share that one" using the id the client
just saw. The client handles that chaining for you.
Mutating actions (upload_file, delete_file, create_share_link)
require confirm=true, so a good client will show you exactly what it's
about to do and only proceed once you approve — a delete or a public link
never happens silently from a vague request.
Tools
| Tool | Confirm gate | Description |
|---|---|---|
list_supported_stores() | — | Every known platform, its kind (document vs. object store), capabilities, and configured accounts. |
list_accounts(platform) | — | Configured accounts, optionally filtered. |
upload_file(platform, local_path, remote_path, account, target, confirm) | ✅ | local_path must be inside the upload root. |
download_file(platform, remote_path, local_filename, account, target) | — | Writes into the download root and returns the local path. |
list_files(platform, folder_or_prefix, account, target) | — | Folder listing (document stores) or prefix listing (object stores). |
search_files(platform, query, account, target) | — | Errors clearly where the platform doesn't support search. |
delete_file(platform, remote_path, account, target, confirm) | ✅ | |
create_share_link(platform, remote_path, account, target, expires_in_seconds, confirm) | ✅ | A link anyone can use to read the file without an account, where supported. See Share links. |
copy_file(from_platform, from_remote_path, to_platform, to_remote_path, from_account, to_account, from_target, to_target, confirm) | ✅ | Copy a file straight from one cloud to another. See Cross-cloud copy. |
list_transfers(platform, limit) | — | Local audit log of uploads, downloads, deletes, share links, and cross-cloud copies. |
get_client_capabilities(platform) | — | Check what a platform supports before calling it. |
target is the bucket/container name for object stores — ignored for
document stores.
Share links
expires_in_seconds is honored natively on S3, GCS, Azure Blob, and Box
(presigned/signed URLs, or Box's unshared_at). Dropbox, Google Drive,
OneDrive, and Yandex Disk create a link too, but none of their APIs
support expiry on a personal/non-Business account, so the parameter is
accepted for interface consistency but not enforced there. Not available
on Google Photos (upload-only).
Gated behind confirm=true even though it doesn't move or delete data —
the returned URL is itself a bearer credential, the same exfiltration
concern upload_file has.
Cross-cloud copy
copy_file moves a file directly from one platform to another —
"copy my Dropbox /photos/id.png to my S3 backups bucket", "migrate
this Drive file to OneDrive" — in a single tool call. This is the one
thing a single-vendor storage integration structurally can't do; it's the
payoff of putting every backend behind one interface.
sequenceDiagram
actor User
participant Client as MCP Client<br/>(Claude)
participant Silo as Silo MCP
participant Src as Source cloud<br/>(Dropbox)
participant Dst as Destination cloud<br/>(Google Drive)
User->>Client: "Copy report.pdf from Dropbox to my Google Drive"
Client->>Silo: copy_file(from=dropbox, to=google_drive, confirm=false)
Silo-->>Client: dry run — will copy /report.pdf → google_drive:report.pdf
Client-->>User: About to copy Dropbox → Google Drive. Approve?
User->>Client: yes
Client->>Silo: copy_file(…, confirm=true)
Note over Silo: stream through a temp file<br/>the server controls
Silo->>Src: download /report.pdf
Src-->>Silo: bytes → temp file on disk
Silo->>Dst: upload from temp file
Dst-->>Silo: new file id + metadata
Note over Silo: delete temp file,<br/>log 'copy' to the audit trail
Silo-->>Client: copied ✓
Client-->>User: Done — report.pdf is now in your Google Drive
The LLM never touches the file bytes or an intermediate path — it just
issues one copy_file call and the server handles the download → temp →
upload → cleanup, gated on your approval.
- It streams through a temporary local file the server creates and deletes
— you never handle an intermediate download/upload, and the temp path is
never a caller-supplied argument (so it isn't subject to the upload-root
containment check the way
upload_fileis). from_remote_pathis addressed however the source platform expects for a download (a path for Dropbox/OneDrive/Yandex/object stores; a file id for Google Drive/Box — search first to get it).to_remote_pathdefaults to the source's basename; pass it explicitly when the source is id-addressed.from_target/to_targetare the bucket/container names when either end is an object store.- Requires
confirm=true— it writes to the destination. Recorded in the audit log as acopy, with both ends captured.
[!NOTE] The copy is bounded by the same 5 GiB download/upload caps, and the source download is streamed to disk, so a large cross-cloud copy won't buffer the whole file in memory.
Supported platforms & tools
Which operations each platform supports, and its authentication model. ✅
supported · — not supported. target = the bucket/container name object
stores require.
| Platform | Kind | Upload | Download | List | Search | Delete | Share link | Auth |
|---|---|---|---|---|---|---|---|---|
| Dropbox | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Static token |
| Google Drive | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | OAuth2 refresh |
| OneDrive | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | OAuth2 refresh |
| Box | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (with expiry) | OAuth2 refresh |
| Google Photos | document | ✅ | — | — | — | — | — | OAuth2 refresh |
| Yandex Disk | document | ✅ | ✅ | ✅ | — | ✅ | ✅ | Static token |
| S3-compatible | object (needs target) | ✅ | ✅ | ✅ | — | ✅ | ✅ (presigned) | Static key/secret |
| Google Cloud Storage | object (needs target) | ✅ | ✅ | ✅ | — | ✅ | ✅ (signed) | Service-account JSON |
| Azure Blob | object (needs target) | ✅ | ✅ | ✅ | — | ✅ | ✅ (SAS) | Connection string |
[!NOTE] Google Photos is upload-only — Google removed read-library API access in March 2025. Object stores and Yandex Disk have no content search, only prefix/path listing. Details in Platform-specific notes.
Setup friction and credential shape per platform
Credential shape table
| Platform | Credential | Auth model | Setup |
|---|---|---|---|
| Dropbox | access_token | Static token | App Console → generate token. No refresh dance. See scopes below — a missing_scope error means regenerate, not reconfigure. |
| Yandex Disk | access_token | Static token, implicit grant (no code-exchange step) | See Getting a Yandex Disk token above. |
| Azure Blob | connection_string | Static | Storage account → Access keys. Simplest of the object stores. |
| S3 | access_key_id, secret_access_key, optional region/endpoint_url | Static | AWS IAM user, or R2/MinIO credentials + endpoint_url. |
| GCS | service_account_json | JWT (service account) | Google Cloud Console → create a service account key, paste the whole JSON. |
| Google Drive | client_id, client_secret, refresh_token | OAuth2, refreshed every call | Google Cloud OAuth consent screen + a one-time authorization to get the initial refresh token. |
| Google Photos | same as Drive | OAuth2, photoslibrary.appendonly scope | Same Google Cloud app; upload-only, see below. |
| OneDrive | client_id, client_secret, refresh_token | OAuth2, refreshed every call | Azure AD app registration. |
| Box | client_id, client_secret, refresh_token | OAuth2, refreshed every call, token rotates | Box developer app. Each refresh issues a new refresh token automatically and this server persists it back to keyring — don't reuse an old one manually. |
Dropbox, Yandex Disk, S3, GCS, and Azure Blob use static credentials read once. Google Drive, OneDrive, Box, and Google Photos are all Big-Tech OAuth2 with short-lived (~1h) access tokens — this server calls each platform's token endpoint fresh before every API request rather than caching, keeping the correctness story simple at the cost of one extra fast (~200ms) request per call.
Required permissions/scopes per platform
The credential fields above only get you a token — the token also needs the right scope, or every call fails with a permissions error regardless of how correctly it's stored. Configured on the platform's side, not here:
| Platform | Required scope(s) | Where to set them |
|---|---|---|
| Dropbox | files.content.write, files.content.read, files.metadata.read, sharing.write (for create_share_link) | App Console → Permissions tab → check the scopes → click Submit. Scopes are fixed at token-issue time — you must generate a new token after changing scopes; an already-issued token doesn't retroactively gain them. |
| Yandex Disk | Full disk access (read/write) | oauth.yandex.com app config → check the Disk read/write permission when creating the app, before issuing the token. |
| Google Drive | https://www.googleapis.com/auth/drive | Requested automatically by silo-mcp-accounts oauth google_drive — nothing to configure by hand beyond enabling the Drive API on the project. |
| Google Photos | https://www.googleapis.com/auth/photoslibrary.appendonly | Same as above, via silo-mcp-accounts oauth google_photos; enable the Photos Library API on the project. |
| OneDrive | Files.ReadWrite, offline_access (delegated) | Azure Portal → app registration → API permissions → Microsoft Graph → add both, then Grant admin consent if your tenant requires it. Also requested automatically by silo-mcp-accounts oauth onedrive. |
| Box | "Read and write all files and folders stored in Box" (or narrower, matching what you need) | Developer Console → app → Configuration tab → Application Scopes. |
| S3 | s3:GetObject, s3:PutObject, s3:ListBucket, s3:DeleteObject on the target bucket (plus presigned-URL generation needs no extra permission — it's a local signing operation) | IAM policy attached to the user/role whose access_key_id/secret_access_key you're using. |
| GCS | Storage Object Admin (or Object Viewer + Object Creator for a narrower grant) on the bucket/project | IAM & Admin → grant the role to the service account before generating its key. |
| Azure Blob | Full account access via the connection string's account key — no separate scope concept | N/A — the connection string itself is the permission boundary; use a SAS-restricted connection string instead if you want to scope it down. |
[!TIP] Dropbox is the one most likely to bite you: a
missing_scope401 with an otherwise-correct token almost always means the scopes were changed after the token was generated. Regenerate the token, don't just re-save it.
Path safety
local_path for uploads and the destination filename for downloads are
both LLM-supplied tool-call arguments, so both are confined to one
configured root each, in both directions:
SILO_MCP_UPLOAD_ROOT(default~/silo-mcp/uploads) — files outside this directory can't be uploaded. This matters a lot here: without it, a manipulated conversation could ask the server to upload an arbitrary local file (SSH keys,.env, browser credential stores) to a cloud account — persistent, shareable, no visible platform artifact warning anyone something left the machine.SILO_MCP_DOWNLOAD_ROOT(default~/silo-mcp/downloads) — downloads can't be written outside this directory via a crafted filename.
Uploads are capped at 5 GiB (MAX_UPLOAD_BYTES) and downloads at 5 GiB
(SILO_MCP_MAX_DOWNLOAD_BYTES, overridable). Downloads are streamed to
disk and aborted mid-flight if they run over the cap, so an unexpectedly
huge file can't exhaust memory. Credentials are stored in the OS keyring,
never in the SQLite DB; the one-time OAuth authorization flow uses PKCE
and a random state value (RFC 8252) so the loopback redirect can't be
forged.
Platform-specific notes
Known asymmetries and limitations
- Google Photos is upload-only. Google removed the Photos Library
API's read-library scopes in March 2025 — an app can now only manage
media items it created itself. Browsing or downloading a user's
existing library needs the interactive "Picker API" (a web UI session),
which a headless MCP tool call can't drive.
list_files/download_file/search_filesall return a clear error rather than pretending to work. There is also no delete capability at all for Google Photos, in this project or Google's API — anything uploaded is permanent until removed manually via photos.google.com. - Google Drive and Box address files by id, not path, for
download/delete —
upload'sremote_pathis used as a filename (Drive root / Box root folder only, no folder targeting in v1); to download/delete you need the id fromlist_files/search_filesfirst. OneDrive, Dropbox, and Yandex Disk use real paths throughout, no asymmetry. - Object stores don't support search — only prefix listing via
list_files.search_filesreturns a clear "not supported" error. Yandex Disk has no dedicated search endpoint either and behaves the same way (path listing vialist_filesonly). - Upload endpoints here are all single-shot (Dropbox ≤150MB, OneDrive ≤4MB); large-file chunked/resumable upload isn't implemented for any platform yet.
Safety
upload_file, delete_file, and create_share_link all require
confirm=true — they either mutate remote state or hand out a
bearer-credential link. download_file/list_files/search_files don't
gate, since they only write locally.
Real-account testing status
Dropbox, Google Drive, and Google Photos have been verified end-to-end over the actual MCP protocol — an MCP client spawns the server, does tool discovery, and calls the tools by name, exactly as Claude Desktop/ Code would — not just via direct Python calls.
| Platform | Status |
|---|---|
| Dropbox | ✅ Verified over the MCP protocol against a real account: upload_file (dry-run + confirmed), list_files, search_files, download_file (streamed to disk), create_share_link, delete_file (dry-run + confirmed), list_transfers. Confirm gates on upload_file/delete_file behaved correctly (blocked without confirm=true). |
| Google Drive | ✅ Verified over the MCP protocol against a real account, same tool coverage as Dropbox above, addressed by file id per the id-vs-path note above. OAuth refresh fired independently before every call as designed, no caching bugs observed. |
| Google Photos | ✅ Verified over the MCP protocol against a real account: upload_file (dry-run + confirmed, real image content), and confirmed list_files/search_files/download_file/delete_file all fail with a clear, correctly-worded error rather than a crash — expected given the upload-only design, not a bug. |
Cross-cloud copy_file | ✅ Verified against real accounts both directions — Dropbox → Google Drive and Google Drive → Dropbox — with the copied bytes checksummed against the source each way. Confirm gate blocked the dry run; both copies were recorded in the audit log. |
Issues and PRs reporting real-account testing results for the remaining platforms are very welcome.
Security
This server hands an LLM the ability to read local files (from one directory), move them to cloud accounts, and generate public links — so it's worth being clear about the risks and what's done about them.
- Prompt injection is the core risk. Content the model reads (a file
name, a document's text, an earlier message) can try to steer it into
calling a tool you didn't intend — e.g. "also upload
~/.ssh/id_rsa" or "share this file publicly". This is a general property of agentic tool use, not specific to this server. - Mitigations built in:
- Path containment — uploads can only read from
SILO_MCP_UPLOAD_ROOTand downloads can only write toSILO_MCP_DOWNLOAD_ROOT. A crafted request for an SSH key or.envoutside the root is rejected before any network call. Keep the upload root a dedicated folder, not your home directory. - Confirmation gates —
upload_file,delete_file, andcreate_share_linkrequireconfirm=true, so a well-behaved client surfaces exactly what's about to happen and a human approves it. A public link or a delete never fires from a vague request. - Credentials never reach the model — they live in the OS keyring and are read server-side; no tool call can enumerate or exfiltrate them, and they're never in a config file or env dump the model sees.
- Least privilege — scope each platform token to only what you need (see the permissions table); a read-only token can't be talked into deleting anything.
- Path containment — uploads can only read from
- Audit trail —
list_transfersand the local SQLite log record every upload, download, delete, and share link, so you can review after the fact what actually moved.
[!WARNING] Treat a generated share link as a public bearer credential — anyone with the URL can read the file, no account required. Revoke by deleting the file (or unsharing it on the platform) when you're done.
Testing
pip install -e ".[all,dev]"
pytest
Tests are pure-logic and respx-mocked HTTP — no real network calls, no
real credentials required.
Troubleshooting
Windows: [WinError 1783] The stub received bad data from silo-mcp-accounts add
Two distinct causes surface as this exact error:
-
pywin32-ctypes'sCredWriteshim.keyringprefers this shim over realpywin32even when both are installed (seekeyring'sbackends/Windows.py, which triespywin32-ctypesfirst). Fix:pip uninstall pywin32-ctypes -yRequires
pywin32already installed (already pulled in transitively here viamcp). Reinstalling/upgradingkeyringlater can pullpywin32-ctypesback in as its dependency — re-run the uninstall if this resurfaces after an upgrade. -
Windows Credential Manager's hard ~2560-byte (~1280-character) cap per secret — a genuine OS limit unrelated to (1), confirmed by reproducing the identical error with real
pywin32and a plain oversized string. Some Dropbox token formats and GCS'sservice_account_json(routinely 2000+ characters) both exceed this easily on entirely legitimate credentials, not just paste mistakes. Handled transparently now —add_accountsplits any secret over the limit across multiple keyring entries and reassembles it on read, on every platform (not just Windows, so the behavior doesn't silently differ by OS). Nothing to do here; if you still hit an error, it's likely genuinely too large (200,000+ characters) rather than this limit.
Related
Sibling project in the same self-hosted, confirm-gated MCP style: a
social-posting MCP server (broadcast posts instead of moving files) —
different capability shape and trust boundary, composable at the
MCP-client level rather than a shared codebase (download_file here can
hand a local path straight to that server's media_paths).