trackmcp
Back to directory

MCP server that moves files between your filesystem and cloud storage and across cloud storages

2 stars PythonOthers Updated Jul 22, 2026

Documentation

Silo MCP

License: MIT
tests
Python 3.11+
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

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

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

mermaid
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 --> 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 `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

bash
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:

bash
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`):

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:

bash
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:

ClientConfigNotes
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` blockstdio.
Cursor / VS Code (MCP)Their `mcp.json` — same `mcpServers` blockstdio.
Ollama (via a bridge)Point the bridge at the HTTP transportSee 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:

bash
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:

bash
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](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.

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 AppUser 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.comCreate 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:

bash
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__` before prompting:

bash
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.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 sayWhat 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

ToolConfirm gateDescription
`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.

`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.

mermaid
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 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_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.

PlatformKindUploadDownloadListSearchDeleteShare linkAuth
DropboxdocumentStatic token
Google DrivedocumentOAuth2 refresh
OneDrivedocumentOAuth2 refresh
Boxdocument✅ (with expiry)OAuth2 refresh
Google PhotosdocumentOAuth2 refresh
Yandex DiskdocumentStatic token
S3-compatibleobject (needs `target`)✅ (presigned)Static key/secret
Google Cloud Storageobject (needs `target`)✅ (signed)Service-account JSON
Azure Blobobject (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

PlatformCredentialAuth modelSetup
Dropbox`access_token`Static tokenApp 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`StaticStorage account → Access keys. Simplest of the object stores.
S3`access_key_id`, `secret_access_key`, optional `region`/`endpoint_url`StaticAWS 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 callGoogle Cloud OAuth consent screen + a one-time authorization to get the initial refresh token.
Google Photossame as DriveOAuth2, `photoslibrary.appendonly` scopeSame Google Cloud app; upload-only, see below.
OneDrive`client_id`, `client_secret`, `refresh_token`OAuth2, refreshed every callAzure AD app registration.
Box`client_id`, `client_secret`, `refresh_token`OAuth2, refreshed every call, token rotatesBox 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:

PlatformRequired 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 DiskFull 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.
GCSStorage Object Admin (or Object Viewer + Object Creator for a narrower grant) on the bucket/projectIAM & Admin → grant the role to the service account before generating its key.
Azure BlobFull account access via the connection string's account key — no separate scope conceptN/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.

PlatformStatus
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

        bash
        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:

        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:

        powershell
        pip uninstall pywin32-ctypes -y

        Requires `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.

        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

        MIT

        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

        Run your own MCP server? See who uses it and what to fix.

        Measure it with TrackMCP