# Silo MCP [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![tests](https://github.com/gouthamkallempudi/silo-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/gouthamkallempudi/silo-mcp/actions/workflows/test.yml) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml) [![MCP](https://img.shields.io/badge/MCP-server-6f42c1.svg)](https://modelcontextprotocol.io) 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](#talking-to-it-in-natural-language). **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](#installation) and [Add accounts](#add-accounts) below. ## Contents - [What can you do with Silo MCP?](#what-can-you-do-with-silo-mcp) - [Why](#why) - [Architecture](#architecture) - [Requirements](#requirements) - [Installation](#installation) - [Configure your MCP client](#configure-your-mcp-client) - [Supported clients](#supported-clients) - [Using with Ollama](#using-with-ollama) - [Add accounts](#add-accounts) - [Example workflows](#talking-to-it-in-natural-language) - [Tools](#tools) - [Supported platforms & tools](#supported-platforms--tools) - [Setup friction and credential shape per platform](#setup-friction-and-credential-shape-per-platform) - [Path safety](#path-safety) - [Platform-specific notes](#platform-specific-notes) - [Safety](#safety) - [Security](#security) - [Testing](#testing) - [Troubleshooting](#troubleshooting) - [Related](#related) - [License](#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 ```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 surface
upload_file · download_file · list_files
search_files · delete_file · create_share_link"] Paths["paths.py
upload/download root containment"] Registry["stores/registry.py
resolve(platform, account)"] Accounts["accounts.py
credential storage + OAuth refresh"] History[("db.py
SQLite transfer log")] end subgraph Backends["FileStore implementations"] direction LR Doc["Document stores
Dropbox · Drive · OneDrive
Box · Photos · Yandex Disk"] Obj["Object stores
S3-compatible · GCS · Azure Blob"] end FS[("Local filesystem
upload/download roots")] Cred[("OS credential store
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 ```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](#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: | 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](#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. - **OneDrive**: [Azure Portal](https://portal.azure.com) → 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](https://app.box.com/developers/console) → 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](https://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`](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 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](#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](#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. ```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 file
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,
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. | Platform | Kind | Upload | Download | List | Search | Delete | Share link | Auth | |---|---|:--:|:--:|:--:|:--:|:--:|:--:|---| | Dropbox | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Static token | | Google Drive | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | OAuth2 refresh | | OneDrive | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | OAuth2 refresh | | Box | document | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (with expiry) | OAuth2 refresh | | Google Photos | document | ✅ | — | — | — | — | — | OAuth2 refresh | | Yandex Disk | document | ✅ | ✅ | ✅ | — | ✅ | ✅ | Static token | | S3-compatible | object (needs `target`) | ✅ | ✅ | ✅ | — | ✅ | ✅ (presigned) | Static key/secret | | Google Cloud Storage | object (needs `target`) | ✅ | ✅ | ✅ | — | ✅ | ✅ (signed) | Service-account JSON | | Azure Blob | object (needs `target`) | ✅ | ✅ | ✅ | — | ✅ | ✅ (SAS) | Connection string | > [!NOTE] > Google Photos is **upload-only** — Google removed read-library API > access in March 2025. Object stores and Yandex Disk have no content > search, only prefix/path listing. Details in > [Platform-specific notes](#platform-specific-notes). ## Setup friction and credential shape per platform
Credential shape table | Platform | Credential | Auth model | Setup | |---|---|---|---| | Dropbox | `access_token` | Static token | App Console → generate token. No refresh dance. See [scopes](#required-permissionsscopes-per-platform) 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](#add-accounts) 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](https://modelcontextprotocol.io/docs/concepts/security). - **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 gates** — `upload_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](#required-permissionsscopes-per-platform)); 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.
## 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](LICENSE)