Skip to main content
Glama
florinel-chis

etoro-mcp

README.md
# etoro-mcp

MCP server for the [eToro public API](https://api-portal.etoro.com/): market data, balances,
portfolio, trade history, watchlists, alerts, investor rankings, user statistics, social feeds,
and — behind explicit opt-in gates — trading, watchlist/alert mutations, and social posting.
Coverage is organised into **toolsets**; only the toolsets you select are registered, and write
tools are not registered at all unless their gate is enabled — a client connected to the default
configuration cannot see or call them.

## Features

- **Read-first by design** — 62 read tools across eleven of the twelve toolsets (the `social`
  toolset is write-only); the 43 write tools exist only when their gate is set — 5 trading
  tools behind `ETORO_MCP_ENABLE_TRADING`, the other 38 (watchlist, alert, social, and
  notification mutations) behind `ETORO_MCP_ENABLE_WRITES`.
- **Demo environment by default** — trading tools call the demo endpoint group unless
  `ETORO_ENV=real` is set.
- **Toolset selection** — register only what you need via `ETORO_MCP_TOOLSETS`; unknown names
  fail startup rather than serving a partial tool set.
- **Trimmed responses** — tools return the fields an investor acts on (prices, units, P&L,
  timestamps), not raw API dumps; list tools are bounded by a `limit` parameter or by
  explicit range parameters (`min_date`/`max_date`/`count`) on the time-series tools.
- **Credentials stay in the environment** — keys travel only in the `x-api-key`/`x-user-key`
  headers, and error messages never echo header or credential values.
- **Rate-limit aware** — HTTP 429 responses are retried once, honouring `Retry-After`, re-sending
  the same `x-request-id` so a retried order cannot fill twice.

## Toolsets

Select toolsets with `ETORO_MCP_TOOLSETS` (comma-separated; `all` selects every toolset).
Without it, the trading core is registered:

| Toolset | Default | Covers |
|---|---|---|
| `market` | ✓ | Instrument search and details, candles, rates, closing prices, exchanges, instrument types, stock industries |
| `account` | ✓ | Profile, balances (aggregated, by account type, history), cash transactions |
| `trading` | ✓ | Portfolio, P&L, order status, eligibility and cost estimates; gated order/position writes |
| `history` | ✓ | Closed-trade history |
| `watchlists` | ✓ | Own/public/curated watchlists and recommendations; gated watchlist mutations |
| `alerts` | ✓ | Price alerts; gated alert mutations |
| `rankings` | | Investor rankings, presets, tags, sector/industry summaries |
| `users` | | User search, profiles, portfolios, performance and copier statistics |
| `feeds` | | News, following, for-you, instrument, and user feeds; posts and comments |
| `social` | | Gated posting, comments, replies, reactions, polls, and media upload |
| `clubs` | | eToro Club dashboard |
| `notifications` | | Notifications; gated mark-read |

## Tools

### Read tools (registered whenever their toolset is selected)

| Toolset | Tool | Description |
|---|---|---|
| `market` | `search_instruments` | Search instruments by ticker symbol and return candidate matches (contains-matching, so verify the symbol) |
| `market` | `get_instrument_details` | Display data for instruments by id: name, symbol, type, exchange |
| `market` | `get_candles` | OHLCV candles for one instrument (nine intervals, up to 1000 bars) |
| `market` | `get_closing_prices` | Latest official closing prices per instrument |
| `market` | `get_rates` | Real-time bid/ask quotes for instruments |
| `market` | `list_exchanges` | Exchanges (id and description) for resolving `exchangeId` values |
| `market` | `list_instrument_types` | Instrument types (stocks, crypto, ETFs, ...) by id |
| `market` | `list_stock_industries` | Stock industries (id and name) for resolving `stocksIndustryId` values |
| `account` | `get_profile` | The authenticated user's identity and the scopes their keys carry |
| `account` | `get_aggregated_balances` | Current balances aggregated across the user's accounts |
| `account` | `get_balances_by_type` | Current balances for every account of one account type |
| `account` | `get_balance` | Current balance of one specific account |
| `account` | `get_balance_history` | Daily balance snapshots over a date range |
| `account` | `list_cash_transactions` | Transactions of a cash account, newest pages first |
| `trading` | `get_portfolio` | Aggregated portfolio: account totals plus one aggregate per instrument |
| `trading` | `get_portfolio_breakdown` | Position-level snapshot: open positions and pending orders |
| `trading` | `get_pnl` | Portfolio snapshot with unrealized P&L per position |
| `trading` | `get_order` | Look up an order's status by order id or reference id |
| `trading` | `get_close_order_info` | Look up a close order's status by its order id |
| `trading` | `check_trading_eligibility` | Per-instrument trading rules: leverage, sizing mode, SL/TP bounds |
| `trading` | `get_trading_cost_estimate` | Estimate the costs of a hypothetical open order without placing it |
| `history` | `list_trading_history` | Closed trades since a given date |
| `watchlists` | `list_watchlists` | The authenticated user's watchlists with their items |
| `watchlists` | `get_watchlist` | A single watchlist owned by the authenticated user, by id |
| `watchlists` | `get_default_watchlist` | Items of the authenticated user's default watchlist |
| `watchlists` | `get_public_watchlists` | Another user's public watchlists by their numeric user id |
| `watchlists` | `get_curated_lists` | eToro's curated investment lists available to the user |
| `watchlists` | `get_market_recommendations` | Personalized market recommendations for the authenticated user |
| `alerts` | `list_price_alerts` | The active price alerts on the account |
| `rankings` | `list_ranking_presets` | Names of all publicly available ranking presets |
| `rankings` | `get_rankings_by_preset` | Investor ranking rows for a named preset |
| `rankings` | `get_investor_rankings` | A filtered, sorted page of investor ranking rows |
| `rankings` | `get_investor_ranking_row` | The full ranking row for a single investor |
| `rankings` | `bulk_ranking_rows` | Ranking rows for several investors in one call |
| `rankings` | `list_ranking_tags` | The catalog of ranking tags with resolved names |
| `rankings` | `get_ranking_universe_by_sector` | The ranking universe aggregated by sector or industry |
| `users` | `search_users` | Search public investors with performance and risk filters |
| `users` | `get_user_profile` | Public profile and account summary for one or more users |
| `users` | `get_user_portfolio` | A user's live public portfolio: open positions and copy trades |
| `users` | `get_user_performance` | A user's full monthly and yearly gain history |
| `users` | `get_user_daily_performance` | A user's day-by-day or aggregated gain over a date range |
| `users` | `get_user_trade_info` | A user's trading statistics for a period (copy-trading due diligence) |
| `users` | `get_gain_timeseries` | An investor's gain time-series at daily, monthly, or yearly resolution |
| `users` | `get_asset_allocation_history` | A portfolio's daily asset-allocation breakdown over time |
| `users` | `get_market_exposure_history` | A portfolio's daily directional market exposure per instrument |
| `users` | `get_copier_stats` | An investor's live copier count and assets-under-management tier |
| `users` | `get_copiers_info` | Anonymised info about the people copying your portfolio |
| `users` | `search_portfolios` | Search trader portfolios or Smart Portfolios by free text |
| `users` | `get_top_assets` | A user's top assets ranked by impressions-based score |
| `feeds` | `get_news_feed` | The eToro news feed (editorial and market-news posts) |
| `feeds` | `get_following_feed` | Posts from users the authenticated user follows |
| `feeds` | `get_for_you_feed` | The personalised For You feed for the authenticated user |
| `feeds` | `get_instrument_feed` | The discussion feed for one instrument (market) |
| `feeds` | `get_user_feed` | The posts published by one user |
| `feeds` | `get_saved_feed` | The posts the authenticated user has saved |
| `feeds` | `get_pinned_posts` | The posts a user has pinned to their profile |
| `feeds` | `get_post` | A single post by its UUID, with an inline comments preview |
| `feeds` | `list_post_comments` | The top-level comments on a post |
| `feeds` | `list_comment_replies` | The replies nested under one comment |
| `feeds` | `list_post_shares` | Who shared a post (plain reshares and quote shares) |
| `clubs` | `get_club_dashboard` | The eToro Club dashboard for the authenticated user |
| `notifications` | `list_notifications` | The account's in-app notifications, newest data first |

### Trading write tools (registered only when `ETORO_MCP_ENABLE_TRADING` is set)

| Toolset | Tool | Description |
|---|---|---|
| `trading` | `create_order` | Place an order to open a position — **not idempotent**: every call places a new order |
| `trading` | `cancel_order` | Cancel a pending (not yet executed) order |
| `trading` | `close_position` | Close an open position, fully or partially, at market |
| `trading` | `cancel_close_order` | Cancel a pending close order before it executes |
| `trading` | `modify_position_sltp` | Edit an open position's stop-loss and/or take-profit |

### Other write tools (registered only when `ETORO_MCP_ENABLE_WRITES` is set)

| Toolset | Tool | Description |
|---|---|---|
| `watchlists` | `create_watchlist` | Create a new watchlist |
| `watchlists` | `rename_watchlist` | Rename a watchlist |
| `watchlists` | `delete_watchlist` | Delete a watchlist |
| `watchlists` | `add_watchlist_items` | Add items to a watchlist |
| `watchlists` | `remove_watchlist_items` | Remove items from a watchlist |
| `watchlists` | `update_watchlist_items` | Reorder items within a watchlist |
| `watchlists` | `set_default_watchlist` | Set a watchlist as the selected default |
| `watchlists` | `change_watchlist_rank` | Change a watchlist's display position |
| `alerts` | `create_price_alert` | Create a price alert on an instrument |
| `alerts` | `update_price_alert` | Change the target price of an existing price alert |
| `alerts` | `delete_price_alert` | Permanently delete a price alert |
| `notifications` | `mark_notifications_read` | Mark all in-app notifications as read |
| `social` | `create_post` | Create a discussion post on the authenticated user's feed |
| `social` | `create_poll_post` | Create a poll post with 2 to 4 answer options |
| `social` | `update_post` | Edit the authenticated user's own post in-place |
| `social` | `delete_post` | Soft-delete the authenticated user's own post |
| `social` | `pin_post` | Pin one of the authenticated user's own posts to their profile |
| `social` | `unpin_post` | Remove the authenticated user's pin from a post |
| `social` | `create_comment` | Create a top-level comment on a post |
| `social` | `update_comment` | Edit the authenticated user's own comment in-place |
| `social` | `delete_comment` | Soft-delete the authenticated user's own comment |
| `social` | `create_reply` | Create a reply nested under an existing comment |
| `social` | `update_reply` | Edit the authenticated user's own reply in-place |
| `social` | `delete_reply` | Soft-delete the authenticated user's own reply |
| `social` | `share_post` | Share an existing post, as a plain reshare or a quote share |
| `social` | `upload_media` | Upload a JPEG or PNG image (max 8 MB) for use as an attachment; requires `ETORO_MCP_UPLOAD_DIR` and reads only inside it |
| `social` | `like_post` | Like a post |
| `social` | `unlike_post` | Remove a like from a post |
| `social` | `like_comment` | Like a comment on a post |
| `social` | `unlike_comment` | Remove a like from a comment |
| `social` | `like_reply` | Like a reply (a nested answer to a comment) |
| `social` | `unlike_reply` | Remove a like from a reply |
| `social` | `save_post` | Bookmark a post |
| `social` | `unsave_post` | Remove a post from the saved bookmarks |
| `social` | `follow_post` | Subscribe to notifications on a post |
| `social` | `unfollow_post` | Unsubscribe from notifications on a post |
| `social` | `vote_poll` | Cast a vote on a poll option |
| `social` | `remove_poll_vote` | Remove a vote from a poll option |

## Configuration

All configuration comes from environment variables — no config files, no flags carrying secrets.

| Variable | Default | Purpose |
|---|---|---|
| `ETORO_API_KEY` | — (required) | eToro API key, sent as the `x-api-key` header |
| `ETORO_USER_KEY` | — (required) | eToro user key, sent as the `x-user-key` header |
| `ETORO_ENV` | `demo` | `demo` or `real` — which trading endpoint group tools call. eToro keys are environment-bound, so a mismatched key fails at the API |
| `ETORO_MCP_TOOLSETS` | trading core (see table above) | Comma-separated toolset names; `all` selects every toolset. Unknown names fail startup with the list of valid names |
| `ETORO_MCP_ENABLE_TRADING` | off | `true`/`1`/`yes` registers the trading write tools: order placement/cancel, position close, stop-loss/take-profit edits |
| `ETORO_MCP_ENABLE_WRITES` | off | `true`/`1`/`yes` registers all other write tools: watchlist/alert mutations, notification mark-read, social posting and reactions |
| `ETORO_MCP_UPLOAD_DIR` | — (unset) | The **only** directory `upload_media` may read images from. Unset (the default) leaves `upload_media` unregistered, so the server can never read local files. Paths and symlinks resolving outside the directory are rejected |

## Getting started

Run straight from the repository with [uv](https://docs.astral.sh/uv/):

```sh
uvx --from git+https://github.com/florinel-chis/etoro-mcp etoro-mcp
```

The server speaks stdio by default; pass `--transport http --host 127.0.0.1 --port 8000` to serve
streamable HTTP at `/mcp` instead.

> **Warning:** the HTTP endpoint has **no authentication** — anyone who can reach the port can
> call every registered tool with your API keys' privileges. The server therefore
> **refuses to bind a non-loopback `--host` unless `--allow-remote` is also passed**; only use
> that flag behind an authenticated reverse proxy or an otherwise-restricted network (such as
> the Docker port-mapping below).

### MCP client configuration

Add the server to your MCP client's configuration (the exact file location depends on the client):

```json
{
  "mcpServers": {
    "etoro": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/florinel-chis/etoro-mcp", "etoro-mcp"],
      "env": {
        "ETORO_API_KEY": "your-api-key",
        "ETORO_USER_KEY": "your-user-key",
        "ETORO_ENV": "demo"
      }
    }
  }
}
```

Or run the Docker image (build it first, see below):

```json
{
  "mcpServers": {
    "etoro": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "-e", "ETORO_API_KEY", "-e", "ETORO_USER_KEY", "-e", "ETORO_ENV", "etoro-mcp"],
      "env": {
        "ETORO_API_KEY": "your-api-key",
        "ETORO_USER_KEY": "your-user-key",
        "ETORO_ENV": "demo"
      }
    }
  }
}
```

## Docker

```sh
docker build -t etoro-mcp .

# stdio (for MCP client configs)
docker run -i --rm -e ETORO_API_KEY -e ETORO_USER_KEY etoro-mcp

# streamable HTTP at http://127.0.0.1:8000/mcp
# (-p 127.0.0.1:8000:8000 keeps the unauthenticated endpoint off external interfaces;
#  --host 0.0.0.0 refers to interfaces inside the container and is required for the
#  port mapping to work, hence --allow-remote — the host-side bind stays loopback-only)
docker run --rm -p 127.0.0.1:8000:8000 -e ETORO_API_KEY -e ETORO_USER_KEY \
  etoro-mcp --transport http --host 0.0.0.0 --port 8000 --allow-remote
```

## Safety

- Trading tools call the **demo** endpoint group unless `ETORO_ENV=real` is set. Keys are
  environment-bound, so a demo key against `real` (or vice versa) fails at the API rather than
  trading the wrong account.
- Write tools are hidden behind **two independent gates** — `ETORO_MCP_ENABLE_TRADING` for
  order/position writes and `ETORO_MCP_ENABLE_WRITES` for everything else (watchlists, alerts,
  notifications, social). Gated tools are never registered, not merely rejected, so MCP clients
  cannot discover or call them.
- The HTTP transport is **unauthenticated**: anyone who can reach the port can call every
  registered tool — read balances and positions always, and place or close orders when trading
  is enabled — using your API keys' privileges. The server refuses non-loopback binds unless
  `--allow-remote` is passed; only use that flag behind an authenticated proxy or an
  otherwise-restricted network (the stdio transport has no such exposure).
- `upload_media` reads local JPEG/PNG files and returns a **public CDN URL** for them — an
  extra privilege class beyond the API keys themselves. It is therefore triple-gated: the
  `social` toolset, `ETORO_MCP_ENABLE_WRITES`, **and** `ETORO_MCP_UPLOAD_DIR` must all be set,
  and it reads only inside that directory (paths and symlinks resolving outside it are
  rejected, file content must actually be JPEG/PNG, and the 8 MB limit is checked before the
  file is read).
- `create_order` is **not idempotent across calls**: each call mints a fresh `x-request-id`,
  which is the order's idempotency key, so calling it twice places two orders. (The 429 retry
  re-sends the same id, so a rate-limited retry cannot double-fill.) Poll fill status with
  `get_order` before treating an order as filled — an accepted order can still be rejected.
- Candle history is capped at roughly **1000 bars per interval** with no way to page further
  back; use a coarser interval (e.g. `OneWeek`) for deeper history.
- The API enforces **per-group rate quotas** (order execution 20 requests/60 s; market data
  120/60 s; portfolio and most other groups 60/60 s; eligibility and cost estimates 20/60 s
  each). Each tool's description names its quota; 429s are retried once, honouring
  `Retry-After`.
- Trading leveraged products is risky. Use at your own risk, and test everything against a
  demo account first.

## Development

```sh
uv sync
uv run pytest
uv run ruff check .
```

Tests are hermetic: all HTTP is mocked with respx and the server is exercised through the
in-memory FastMCP client. No credentials or network access are needed to run them. A manual,
read-only smoke check against a real demo account lives at `scripts/smoke.py`.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4/5.0

Scored across 29 tools

Disambiguation5/5

Each tool targets a distinct resource or action, with clear descriptions. Even similar tools like get_balance, get_aggregated_balances, and get_balances_by_type are well-differentiated by scope. No overlapping purposes.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (e.g., get_balance, list_watchlists, search_instruments), using lowercase with underscores. No mixing of conventions.

Tool Count3/5

29 tools is on the high side for a single server, exceeding the 'heavy' threshold of 16-25. While each tool serves a specific endpoint, the count may overwhelm an agent. However, the domain is complex enough to justify most tools.

Completeness2/5

Several critical operations are missing, such as create_order, cancel_order, create/delete/update watchlists, and manage price alerts. The server provides many read operations but lacks corresponding write/delete actions, leading to dead ends for common trading workflows.

Maintenance

ActivityStale
ResponsivenessUnresponsive