SILO-MCP

MCP server that moves files between your filesystem and cloud storage.

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

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 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 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.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 Desktopclaude_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:

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/callback exactly (Azure requires an exact match). API permissions → Microsoft Graph → add Files.ReadWrite and offline_access (delegated). Certificates & secrets → new client secret.
  • Box: Box Developer Console → 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=<your-app-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.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.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.

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_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
Dropboxaccess_tokenStatic tokenApp Console → generate token. No refresh dance. See scopes below — a missing_scope error means regenerate, not reconfigure.
Yandex Diskaccess_tokenStatic token, implicit grant (no code-exchange step)See Getting a Yandex Disk token above.
Azure Blobconnection_stringStaticStorage account → Access keys. Simplest of the object stores.
S3access_key_id, secret_access_key, optional region/endpoint_urlStaticAWS IAM user, or R2/MinIO credentials + endpoint_url.
GCSservice_account_jsonJWT (service account)Google Cloud Console → create a service account key, paste the whole JSON.
Google Driveclient_id, client_secret, refresh_tokenOAuth2, 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.
OneDriveclient_id, client_secret, refresh_tokenOAuth2, refreshed every callAzure AD app registration.
Boxclient_id, client_secret, refresh_tokenOAuth2, 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
Dropboxfiles.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 Drivehttps://www.googleapis.com/auth/driveRequested automatically by silo-mcp-accounts oauth google_drive — nothing to configure by hand beyond enabling the Drive API on the project.
Google Photoshttps://www.googleapis.com/auth/photoslibrary.appendonlySame as above, via silo-mcp-accounts oauth google_photos; enable the Photos Library API on the project.
OneDriveFiles.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.
S3s3: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.
    • Confirmation gatesupload_file, delete_file, and 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.
    • 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.
  • Audit traillist_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]"
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:

    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.

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

MIT