silo-mcp
MCP server that moves files between your filesystem and cloud storage and across cloud storages
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
`/photos` file 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.pdf` to
Dropbox", "download `notes.txt` from 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_file` here lands a local
path you can hand straight to another server (e.g. a social-posting
server's `media_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
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_link` behave
the same regardless of which platform you point them at — object
stores (bucket+key) and document stores (path-based) share one
`FileStore` contract.
- Confined by design, not by convention. `local_path` for 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`, and
`create_share_link` all mutate remote state or create a
bearer-credential link and require deliberate confirmation.
`download_file`/`list_files`/`search_files` only write locally, so they
don't gate.
- Multi-account from the start. Every tool takes an optional
`account` label — 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(stdio)"]
OL["Ollama bridge(streamable-http / SSE)"]
end
subgraph Silo["Silo MCP Server (server.py)"]
direction TB
Tools["Tool surfaceupload_file · download_file · list_filessearch_files · delete_file · create_share_link"]
Paths["paths.pyupload/download root containment"]
Registry["stores/registry.pyresolve(platform, account)"]
Accounts["accounts.pycredential storage + OAuth refresh"]
History[("db.pySQLite transfer log")]
end
subgraph Backends["FileStore implementations"]
direction LR
Doc["Document storesDropbox · Drive · OneDriveBox · Photos · Yandex Disk"]
Obj["Object storesS3-compatible · GCS · Azure Blob"]
end
FS[("Local filesystemupload/download roots")]
Cred[("OS credential storeWindows / macOS / Linux keyring")]
CD --> Tools
OL --> Tools
Tools --> Paths --> FS
Tools --> Registry
Registry --> Doc
Registry --> Obj
Registry --> Accounts --> Cred
Tools --> HistoryEvery 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 `keyring` can 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 threeA 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.0` or 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-mcp` isn't on the launching shell's `PATH` (common with
> virtualenvs), set `command` to the venv's executable directly, e.g.
> `C:\\path\\to\\silo-mcp\\.venv\\Scripts\\silo-mcp.exe` on Windows or
> `/path/to/.venv/bin/silo-mcp` elsewhere.
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:
1. Start the server in HTTP mode: `SILO_MCP_TRANSPORT=streamable-http silo-mcp`.
2. Point your Ollama-side MCP client at `http://127.0.0.1:8000/mcp`.
3. Use a tool-calling-capable model (e.g. `llama3.1`, `qwen2.5`) — Ollama
only routes tool calls for models that support the `tools` API 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 listFirst-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 personalThis 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](https://console.cloud.google.com) → 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:`
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/callback`
exactly (Azure requires an exact match). API permissions → Microsoft
Graph → add `Files.ReadWrite` and `offline_access` (delegated).
Certificates & secrets → new client secret.
→ Create new app → Custom App → User Authentication (OAuth 2.0)
→ under Configuration, set Redirect URI to `http://localhost:8765/callback`
exactly (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.
1. oauth.yandex.com → Create app (or
reuse an existing one).
2. 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.
3. Under Permissions, grant Disk access: `cloud_api:disk.read` and
`cloud_api:disk.write` (or `cloud_api:disk.app_folder` instead of
`disk.write` if you want to scope it to an app-specific folder rather
than the whole disk).
4. Save the app and note its ID.
5. Visit `https://oauth.yandex.com/authorize?response_type=token&client_id=`
in a browser, approve access — the token is shown directly on the
redirected page.
6. `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.yamlJSON 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__` before prompting:
SILO_MCP_CRED_DROPBOX_ACCESS_TOKEN=sl.xxx silo-mcp-accounts add dropbox --label personalEnv 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.pdf` from my uploads folder to Dropbox."
- "What's in the root of my Dropbox? Find anything named `invoice`."
- "Download `notes.txt` from Dropbox and give me a shareable link to it."
- "Delete `old-draft.txt` from my Dropbox."
Google Drive / Box (id-addressed — search first, then act on the id):
- "Find `budget.xlsx` in 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.zip` to my S3 bucket `my-backups`."
- "List everything under `logs/` in bucket `my-backups`."
- "Give me a 1-hour presigned link to `backup.zip` in `my-backups`."
Cross-cloud copy (one platform straight to another):
- "Copy `report.pdf` from my Dropbox to my Google Drive."
- "Migrate everything I just found in Drive over to my S3 `archive` bucket."
Cross-server:
- "Grab my headshot from Google Drive so I can attach it to a post." —
`download_file` lands 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(Claude)
participant Silo as Silo MCP
participant Src as Source cloud(Dropbox)
participant Dst as Destination cloud(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 filethe 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,log 'copy' to the audit trail
Silo-->>Client: copied ✓
Client-->>User: Done — report.pdf is now in your Google DriveThe 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_file` is).
- `from_remote_path` is 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_path`
defaults to the source's basename; pass it explicitly when the source is
id-addressed.
- `from_target`/`to_target` are 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 a `copy`, 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
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_scope` 401 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_files` all 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`'s `remote_path` is used as a filename (Drive
root / Box root folder only, no folder targeting in v1); to
download/delete you need the id from `list_files`/`search_files` first.
OneDrive, Dropbox, and Yandex Disk use real paths throughout, no
asymmetry.
- Object stores don't support search — only prefix listing via
`list_files`. `search_files` returns a clear "not supported" error.
Yandex Disk has no dedicated search endpoint either and behaves the
same way (path listing via `list_files` only).
- 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_ROOT`
and downloads can only write to `SILO_MCP_DOWNLOAD_ROOT`. A crafted
request for an SSH key or `.env` outside the root is rejected before
any network call. Keep the upload root a dedicated folder, not your
home directory.
`create_share_link` require `confirm=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.
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.
(see the permissions table);
a read-only token can't be talked into deleting anything.
- Audit trail — `list_transfers` and 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]"
pytestTests 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:
1. `pywin32-ctypes`'s `CredWrite` shim. `keyring` prefers this shim
over real `pywin32` even when both are installed (see `keyring`'s
`backends/Windows.py`, which tries `pywin32-ctypes` first). Fix:
pip uninstall pywin32-ctypes -yRequires `pywin32` already installed (already pulled in transitively
here via `mcp`). Reinstalling/upgrading `keyring` later can pull
`pywin32-ctypes` back in as *its* dependency — re-run the uninstall if
this resurfaces after an upgrade.
2. **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 `pywin32` and a plain
oversized string. Some Dropbox token formats and GCS's
`service_account_json` (routinely 2000+ characters) both exceed this
easily on entirely legitimate credentials, not just paste mistakes.
Handled transparently now — `add_account` splits 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`).
License
Frequently asked questions
What is silo-mcp?
silo-mcp is MCP server that moves files between your filesystem and cloud storage and across cloud storages
How do I install silo-mcp?
Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.
Is silo-mcp open source?
Yes — it is hosted on GitHub at https://github.com/gouthamkallempudi/silo-mcp and has 2 stars.
Related MCP tools
Cognee is the open-source AI memory platform for agents. Give your AI agents persistent long-term memory across sessions with a self-hosted knowledge graph engine.
Automate browser based workflows with AI
Hindsight: Agent Memory That Learns
A privacy-first app that strips AI watermarks from content you own.
Agent framework and applications built upon Qwen>=3.0, featuring Function Calling, MCP, Code Interpreter, RAG, Chrome extension, etc.
The power of Claude Code / GeminiCLI / CodexCLI + [Gemini / OpenAI / OpenRouter / Azure / Grok / Ollama / Custom Model / All Of The Above] working as one.
Run your own MCP server? See who uses it and what to fix.
Measure it with TrackMCP