Skip to main content
Glama
slemos

eToro MCP Server

by slemos
README.md
# eToro MCP Server (unofficial)

**Talk to your eToro account from Claude.** Ask about your portfolio in plain language, check prices and costs, and — only if you choose to turn it on — place orders that you approve one by one.

[![tests](https://img.shields.io/github/actions/workflow/status/slemos/etoro-mcp-server/ci.yml?branch=main&label=tests%20%C2%B7%20build%20%C2%B7%20security%20checks)](https://github.com/slemos/etoro-mcp-server/actions/workflows/ci.yml)
[![security](https://img.shields.io/github/actions/workflow/status/slemos/etoro-mcp-server/security.yml?branch=main&label=SAST%20%C2%B7%20dependencies%20%C2%B7%20secrets)](https://github.com/slemos/etoro-mcp-server/actions/workflows/security.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/slemos/etoro-mcp-server/badge)](https://scorecard.dev/viewer/?uri=github.com/slemos/etoro-mcp-server)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![node](https://img.shields.io/badge/node-%E2%89%A520-339933)](package.json)
[![MCP](https://img.shields.io/badge/MCP-server-8A2BE2)](https://modelcontextprotocol.io)

> **Disclaimer.** This project is not affiliated with, endorsed by or supported by eToro. "eToro" is a trademark of its owner. Nothing here is financial advice. Trading — especially with leverage or CFDs — can lose money, and software that lets an AI act on a brokerage account can lose it faster. Read the [safety model](#safety-model), start on the **demo** environment, and check eToro's API terms before automating anything.

## What it does

[MCP](https://modelcontextprotocol.io) is the standard way to give Claude tools. This server gives Claude a set of tools that talk to the [eToro Public API](https://api-portal.etoro.com) with **your own keys**, so instead of opening the app and clicking around you can just ask:

| You say | What happens |
|---|---|
| *"How is my portfolio doing? What are my biggest positions?"* | Claude reads your positions, balances and profit and loss, and summarises them. |
| *"How are the traders I copy performing?"* | One compact summary per copied trader, with their positions available on request. |
| *"What would it cost to buy 50 dollars of AAPL, and can my account even do that?"* | Live price, the settlement types and leverage your account is offered, and an estimate of the fees. |
| *"What if I had bought 1,000 dollars of AAPL in January with a stop at 170?"* / *"How would investing 100 dollars every two weeks have done over two years?"* | A simulation on eToro's past price candles (`etoro_simulate_position`, `etoro_backtest`): entry and exit, why it ended, the result, the worst moment. It places nothing and prepares nothing, and it is not a forecast or advice. |
| *"Find popular investors with moderate risk who trade mostly tech, and show me one's stats."* | Searches investors by performance, risk and what they hold (`etoro_search_investors`), then reads one's public statistics, copiers and history (`etoro_get_investor`). Public data only, past performance is not a forecast. |
| *"How did my balance change over the last three months? What went through my cash account?"* | `etoro_get_balance_history` and `etoro_get_cash_transactions`. |
| *"Show my closed trades since January."* | Your trade history, filtered by date. |
| *"Find the Apple instrument and tell me how it did over the last year."* | Searches by name, then reads the price candles and summarises them (first open, last close, high, low, change). |
| *"Prepare a purchase of 20 dollars of AAPL on my demo account."* | Claude **prepares** the order (instrument, size, costs, environment) and your browser opens an approval page with the exact action. **You** press Execute there; Claude cannot. Then it follows the order. |
| *"Prepare closing that position."* / *"Add a stop loss at 780 to it."* / *"Prepare cancelling that pending order."* | Same: Claude proposes, you execute on the page. |
| *"Tell me when AAPL reaches 350."* / *"Move that alert to 340."* / *"Delete the alert."* | Price alerts: Claude lists them and **prepares** creating, changing or deleting one; you execute on the page. An alert only notifies you: it places no order and moves no money. |
| *"Add these instruments to my Tech watchlist."* | Creates and edits watchlists (no money involved). |

24 read tools, 12 prepare-only write tools and one gated transfer tool; see [Tools](#tools).

**Safe by default.** It starts **read-only and on eToro's demo environment**. The tools that can move money are not even registered until you switch them on, real money needs a second switch, and Claude can only *prepare* an action: **you execute it yourself on a local approval page**, never Claude. Size caps, a rate limit and an audit log apply on top. Your keys stay on your machine (OS keychain, password manager or a protected file) and are never shown to Claude. Details in the [safety model](#safety-model).

## TL;DR: install in two minutes

You need an eToro **API key pair**; a **Read** key on the **Demo** environment is enough to start ([how to get one](#getting-etoro-api-keys)).

**Claude Desktop**

1. Download `etoro-mcp-server-<version>.mcpb` from the [latest release](https://github.com/slemos/etoro-mcp-server/releases/latest) (or build it: `npm ci && npm run mcpb:pack`).
2. Double-click it, or drag it into **Settings → Extensions**. Claude Desktop shows a red warning that the extension gets access to your computer and that Anthropic has not verified the developer: that is expected for an independent project, and you can [check where the file came from](#verifying-a-release).
3. Paste your API key and user key, leave **Use the REAL environment** and **Enable write tools** off. The keys go to your OS keychain.
4. Start a chat and ask: *"Check my eToro connection."* Then try *"How is my portfolio doing?"*

**Claude Code**

```bash
git clone https://github.com/slemos/etoro-mcp-server.git && cd etoro-mcp-server && npm ci && npm run build
claude mcp add etoro -- node "$(pwd)/dist/index.js"   # reads ETORO_API_KEY / ETORO_USER_KEY from your environment
```

Other MCP clients and safer ways to hand over the keys (keychain, password manager, protected file) are in [Install](#install) and [Securing your setup](#securing-your-setup).

## Why you can trust it

An AI that can touch a brokerage account deserves more scrutiny than most code, so the project treats security as a feature, and checks it automatically on every change:

| Check | What it proves | Runs |
|---|---|---|
| **Tests** | Logic, the prepare → you-execute flow, caps and blocked paths, against a mocked eToro API and an in-memory MCP client | every push and pull request |
| **SAST** (CodeQL, `security-extended`) | No known vulnerability patterns in the TypeScript source | every push and pull request, weekly |
| **Dependency audit** (`npm audit`, registry signatures, Dependabot) | The few production dependencies have no known high-severity advisories | same, plus weekly |
| **Secret scan** (Gitleaks) | No keys or tokens in the repository or its history | same |
| **Dynamic security checks** (`npm run security:check`) | The *built* server, run as a real process with the network cut off, exposes only the tools each permission switch allows, refuses a non-eToro base URL, rejects hostile arguments before any request, cannot be made to call another API path, and never leaks the keys into results, logs or the audit trail | every CI run and release |
| **OpenSSF Scorecard** | An independent score of the repository's security practices (protected `main`, pinned actions, least-privilege tokens, static analysis, signed releases, ...), published at scorecard.dev | on every push to `main` and weekly |
| **Protected `main`** | Nobody, the owner included, can push to `main` directly or rewrite its history: every change goes through a pull request whose CI and security checks must pass | always |
| **Build provenance + checksums + SBOM** | A release file was built by this repository's workflow from the tagged commit, after everything above passed | every release |

Details and the threat model are in [SECURITY.md](SECURITY.md).

**Where it stands (v0.8.0).** Early software, tried against a live eToro **demo** account from Claude Desktop: the connection check, portfolio, positions, PnL, balances, trade history, instrument lookup and text search, rates, candles, eligibility, cost estimates and watchlist listing, the prepare → you-execute flow for opening an order and changing a stop loss, and the action history page with the daily counter. Closing and cancelling from Claude Desktop, watchlist changes and transfers are covered by tests but not yet exercised live (the demo script has closed positions), and nothing has been run with real money. Start on demo.

## This server and eToro's official MCP server

eToro publishes its own MCP server (`https://mcp.public-api.etoro.com`, described at <https://mcp.public-api.etoro.com/skill>). This project is independent of it and not affiliated with eToro. The two solve overlapping problems differently, and you can install both: their tool names do not collide.

| | eToro's official server | This server |
|---|---|---|
| Publisher and support | eToro | An independent open-source project (MIT) |
| Where it runs | Remote, hosted by eToro; your credentials go in the connection headers | Local, on your machine; credentials from your keychain, a password manager or a protected file |
| Authentication | API key pair or OAuth | API key pair |
| Coverage | Read and write tools plus a generic route catalogue (`execute-read`, `execute-write`) covering the whole Public API, and trader profiles | A fixed, reviewed route allowlist; no trader profiles yet |
| Who executes a trade | Claude: `prepare-trade` returns a signed token to the model, and `place-trade` sends the order with it | **You**: Claude prepares, and only the Execute button on a local page sends anything |
| What enforces the approval step | Its documentation describes it as an instruction to the model (show the confirmation, get an explicit yes); it does not describe a server-side human confirmation | The server: no tool Claude can call executes anything, and the HTTP client refuses a write without the permission that button issues |
| Size limits, history | Not described in its documentation | Per-order, per-session and per-day caps; a searchable local history |
| Extras | Instrument overview, trader profiles | Candles with summaries, what-if simulations and backtests, a pre-close estimate |

As of October 2026, from its public documentation and our own tests on a demo account: in one Claude Code session the official server's `prepare-trade` and `place-trade` flow placed an order after a plain "yes". In an earlier test of this project, a session declined to call the tool that executed. We did not isolate why; whether a model calls an execute tool is its own decision, and a gate that depends on it is only as strong as that decision.

**Which to use.** Use the official server for the widest coverage and for OAuth, if you accept that the model can place orders once you approve in chat. Use this one if you want the approval to be enforced outside the model, local control of your keys, spending limits and a record of what you did. If you only read data, either works. Whichever you choose, start on demo, give the key only the permissions it needs, and keep real-money access off until you have decided you want it.

## Safety model

| Layer | Default | What it does |
|---|---|---|
| Environment | `demo` | Keys and routes target eToro's demo environment unless you set `ETORO_ENV=real`. |
| Write tools | **off** | `ETORO_ENABLE_WRITE=true` registers the order/close/cancel/watchlist tools. A client cannot call a tool that does not exist. |
| Real-money writes | **off** | On `real`, write tools also need `ETORO_ALLOW_REAL_WRITE=true`. |
| Transfers | **off** | The internal-transfer tool needs `real` + both switches + `ETORO_ALLOW_TRANSFERS=true`. |
| Claude proposes, you execute | always | Every change (orders, closes, cancels, transfers, watchlists) is an `etoro_prepare_*` call that only registers a proposal. The server opens a local approval page in your browser; **only pressing Execute there sends anything to eToro**. No tool Claude can call executes an action, and the HTTP client refuses every write route without the permission that button issues. |
| The approval page | always | Served on `127.0.0.1` only, with a one-time secret in its address that is not given to Claude by default, a `Host` check (DNS rebinding), `Origin` and anti-CSRF checks, no JavaScript and every external string escaped. |
| Size caps | 100 USD / order, 500 USD / session, 1000 USD / day | `ETORO_MAX_ORDER_USD` (exposure = amount × leverage), `ETORO_MAX_SESSION_USD` and `ETORO_MAX_DAILY_USD`. |
| Daily limits | 1000 USD and 25 writes per day, per environment | `ETORO_MAX_DAILY_USD` and `ETORO_MAX_DAILY_WRITES`, counted in the history database, so they hold across restarts and across server processes (Claude Desktop and Claude Code each start their own). A day starts at midnight in `ETORO_TIMEZONE`. Only you can change them, in the server's configuration: no tool can. |
| Rate brake | 5 writes / minute | `ETORO_MAX_WRITES_PER_MINUTE`. |
| Route allowlist | fixed | The HTTP client can only call the routes in [`src/endpoints.ts`](src/endpoints.ts), only on the eToro host, and refuses write routes when writes are off. |
| Environment guard | always | Before any trading preview the server reads the key's scopes (`GET /api/v1/me`) and checks that the account answering for `ETORO_ENV` is that environment's account (`demoCid`/`realCid`). It refuses if the key lacks Write permission for the environment, if the data belongs to the other account, or if this cannot be verified. |
| Idempotency | always | Each prepared action has its own `x-request-id`, reused on retries, and it can be executed only once. |
| Tool annotations | always | Read and write tools are separate and carry MCP annotations (`readOnlyHint`, `destructiveHint`, `title`), so clients can apply sensible permission prompts. |
| Secrets | — | Never in tool inputs, results, errors or the audit log. `ETORO_BASE_URL` can only point to an `https://*.etoro.com` host. |

Also strongly recommended on the eToro side: create a **Read** key unless you need to trade, restrict it by **IP**, and set an **expiry**. Keys are separate for Demo and Real, so a demo key can never touch real money.

See [SECURITY.md](SECURITY.md) for the threat model and how to report a vulnerability.

## History and daily limits

Everything Claude prepares, and what you do with it (execute, reject, let it expire), is recorded in a local SQLite file (`ETORO_HISTORY_DB`, private to your user, never containing keys or approval links). It has three jobs:

- **Look back.** Ask Claude *"what did I do this week?"* (`etoro_get_action_history`), or ask it to **open the history** (`etoro_open_history`): a page in your browser with a search box (words, tickers, order or position ids), filters for date, environment, action and status, the detail of each action with eToro's answer and its timeline, today's use of the daily limits, and a CSV download. The page listens on `127.0.0.1` only, uses a secret address that expires after an hour, is read-only and has no JavaScript. Nothing in it can change or delete the history.
- **Hold the daily limits.** `ETORO_MAX_DAILY_USD` and `ETORO_MAX_DAILY_WRITES` are checked and reserved in one database transaction when you press Execute, so two server processes cannot both spend the same allowance, and a restart does not reset it.
- **Survive restarts.** An action that was waiting when the server stopped is shown as expired, and one that was executing is shown as failed with a note to check eToro.

The file is a plain SQLite database; you can open it with any SQLite tool, and deleting it only erases the record (and resets the day's counters).

## Install

### Claude Desktop (MCPB bundle)

1. Get `etoro-mcp-server-<version>.mcpb` from the GitHub release, or build it: `npm ci && npm run mcpb:pack`.
2. Open it with Claude Desktop (double-click, or drag it into **Settings → Extensions**).
3. Fill in the form: API key, user key, and leave **Use the REAL environment** off to start on demo. Keys are stored in your OS keychain.
4. Leave **Enable write tools** off until you have tried the read tools.

Claude Desktop shows a red warning before installing: the extension runs with your user's access to your computer, and any developer information shown "has not been verified by Anthropic". That is the case for any extension from outside Anthropic's own directory, and this bundle is also not signed with a code-signing certificate; see [Verifying a release](#verifying-a-release) for how to check where it came from. Some organisations restrict which extensions may be installed; if yours does, ask your administrator or build from source.

### Claude Code

```bash
git clone https://github.com/slemos/etoro-mcp-server.git && cd etoro-mcp-server
npm ci && npm run build
```

Export your keys in your shell profile or secret manager (do not paste them into chat or commit them), then register the server. A project-scoped `.mcp.json` can reference them without containing them:

```json
{
  "mcpServers": {
    "etoro": {
      "command": "node",
      "args": ["/absolute/path/to/etoro-mcp-server/dist/index.js"],
      "env": {
        "ETORO_API_KEY": "${ETORO_API_KEY}",
        "ETORO_USER_KEY": "${ETORO_USER_KEY}",
        "ETORO_ENV": "demo"
      }
    }
  }
}
```

Or from the CLI: `claude mcp add etoro -- node /absolute/path/to/etoro-mcp-server/dist/index.js` (the server reads `ETORO_API_KEY` / `ETORO_USER_KEY` from the environment Claude Code runs in). Check with `claude mcp list`.

### Any other MCP client

It is a standard **stdio** server: run `node dist/index.js` with the environment variables below. With `npx` once published: `npx -y etoro-mcp-server`.

## Getting eToro API keys

1. Use a verified eToro account.
2. In eToro go to **Settings → Trading → API Key Management → Create New Key**.
3. Choose the environment (**Demo** first), the permission (**Read**, or **Write** only if you want to trade) and, ideally, an IP allowlist and an expiry. SMS verification is required.
4. You get an API key (`x-api-key`) and a user key (`x-user-key`). Treat both like passwords.

Reference: [Authentication](https://api-portal.etoro.com/core/getting-started/authentication).

## Securing your setup

The server needs two secrets (the API key and the user key). **How you hand them over matters as much as what the server does with them.** Anyone who gets the pair can read your account, and with a Write key can trade on it.

**On the eToro side (always):** create a **Read** key unless you really need to trade; keep **Demo** and **Real** keys separate (eToro's documentation says a key serves one environment, but a key can carry scopes for both — `etoro_check_connection` shows them, and strict key scope — on by default for real, off for demo, configurable with `ETORO_STRICT_KEY_SCOPE` — makes the server refuse such keys for trading); restrict the key by **IP address**; set an **expiry**; revoke it at once if it may have leaked. `etoro_check_connection` shows the key's scopes and warns when a read-only server holds a Write key.

**On your side:** keep the secrets out of config files and shell history. Pick the first option that your client allows:

| Option | Where the secret lives | Use it with |
|---|---|---|
| **MCPB bundle** (`sensitive` fields) | OS keychain, managed by the host | Claude Desktop |
| **`ETORO_API_KEY_CMD` / `ETORO_USER_KEY_CMD`** | OS keychain or password manager; the config only holds the *command* | Any client, manual setup |
| **`ETORO_API_KEY_FILE` / `ETORO_USER_KEY_FILE`** | A file readable only by you (the server refuses it otherwise) | Any client, manual setup |
| Plain `ETORO_API_KEY` / `ETORO_USER_KEY` | Your shell or a config file in clear text | Quick local tests only |

Avoid putting the keys directly in `claude_desktop_config.json`, in `claude mcp add -e ETORO_API_KEY=...` (stored in clear text in `~/.claude.json` and in your shell history), or in a committed `.mcp.json`. Set exactly one source per key; the server refuses ambiguous setups.

`*_CMD` takes a command line that prints the secret on one line. It is split into words (quotes are honored) and run **without a shell**: no pipes, no expansion. It runs with your privileges, so treat it like the `command` of the server itself. `*_FILE` accepts `~/...` paths; on macOS and Linux the file must not be accessible to group or others (`chmod 600`).

### Recipes

**macOS Keychain** (prompts for the value without echoing it):

```bash
security add-generic-password -U -s etoro-mcp-server -a api-key -w
security add-generic-password -U -s etoro-mcp-server -a user-key -w
```

```json
"env": {
  "ETORO_API_KEY_CMD": "security find-generic-password -s etoro-mcp-server -a api-key -w",
  "ETORO_USER_KEY_CMD": "security find-generic-password -s etoro-mcp-server -a user-key -w",
  "ETORO_ENV": "demo"
}
```

When macOS asks whether `security` may read the item, prefer **Allow** over **Always Allow**: with "Always Allow", any program you run that calls `security` can read it without asking.

**Linux (libsecret):** `secret-tool store --label="eToro API key" service etoro-mcp-server key api-key` (same for `user-key`), then `ETORO_API_KEY_CMD="secret-tool lookup service etoro-mcp-server key api-key"`.

**Password managers:** any CLI that prints the secret works, for example `op read "op://Private/eToro/api-key"` (1Password, with biometric unlock), `pass show etoro/api-key`, or `bw get password etoro-api-key` (Bitwarden, needs an unlocked session).

**A protected file:**

```bash
mkdir -p ~/.config/etoro-mcp && chmod 700 ~/.config/etoro-mcp
( umask 077; printf "API key: "; read -rs v; echo; printf '%s' "$v" > ~/.config/etoro-mcp/api-key )
( umask 077; printf "User key: "; read -rs v; echo; printf '%s' "$v" > ~/.config/etoro-mcp/user-key )
# then: ETORO_API_KEY_FILE=~/.config/etoro-mcp/api-key  ETORO_USER_KEY_FILE=~/.config/etoro-mcp/user-key
```

**Windows:** use the MCPB bundle (keys go to the host's credential store), or a password manager CLI through `*_CMD`.

### What this does not protect against

The server runs as you. Another program running as your user can read what you can read, including the keychain items you allow and files with your permissions. Prompt injection is a separate risk, covered by the write-tool safeguards above. Use IP restrictions and short expiries so that a leaked key is worth little.

### Verifying what you install

Building from source is small and auditable (`npm ci` uses the committed lockfile). Release bundles are built by GitHub Actions from the tagged commit, after tests and security checks pass.

### Verifying a release

Each release attaches the bundle, a `SHA256SUMS` file and an SBOM (`*.sbom.cdx.json`, CycloneDX). The bundle is **not code-signed with a certificate**; its provenance is attested instead, which proves it was produced by this repository's release workflow:

```bash
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify etoro-mcp-server-<version>.mcpb --repo slemos/etoro-mcp-server
```

If you would rather not trust a binary at all, build it yourself (`npm ci && npm run mcpb:pack`) and compare the result with the release.

## Configuration

All settings are environment variables (see [`.env.example`](.env.example)). The server does not read a `.env` file by itself: export the variables, or load a git-ignored `.env` with Node's flag, e.g. `node --env-file=.env dist/index.js` (Node 22.13+, which the history database needs).

| Variable | Default | Description |
|---|---|---|
| `ETORO_API_KEY` | — (required) | Public API key (`x-api-key`). Alternatives: `ETORO_API_KEY_FILE` or `ETORO_API_KEY_CMD` (see [Securing your setup](#securing-your-setup)). Set exactly one. |
| `ETORO_USER_KEY` | — (required) | User key (`x-user-key`). Alternatives: `ETORO_USER_KEY_FILE` or `ETORO_USER_KEY_CMD`. Set exactly one. |
| `ETORO_ENV` | `demo` | `demo` or `real`. Must match the environment of the key pair. |
| `ETORO_USE_REAL` | unset | `true` or `false`: the same choice as `ETORO_ENV`, as a switch (the MCPB bundle's form uses it, since extension forms have toggles but no drop-down). If both are set they must agree; otherwise the server refuses to start. |
| `ETORO_ENABLE_WRITE` | `false` | Register the write tools. Needs a key with **Write** permission. |
| `ETORO_ALLOW_REAL_WRITE` | `false` | Second switch required for write tools when `ETORO_ENV=real`. |
| `ETORO_ALLOW_TRANSFERS` | `false` | Register the internal-transfer tool (real only, needs both switches above). |
| `ETORO_STRICT_KEY_SCOPE` | `true` on real, `false` on demo | Refuse trading previews when the key can also write in the *other* environment. On real it means the key must be real-only; on demo it would mean the key must not be able to trade real money. Set it explicitly to override either default. |
| `ETORO_OPEN_BROWSER` | `true` | Open the approval page in your default browser when an action is prepared. If it cannot be opened, the address is in the server's log (stderr). |
| `ETORO_SHOW_APPROVAL_URL` | `false` | Put the approval page's address in the tool result. That lets Claude see it, and with browser tools open it: meant for scripts (`npm run demo:order` uses it) and for machines without a browser. |
| `ETORO_MAX_ORDER_USD` | `100` | Max exposure (amount × leverage) or transfer amount per action. |
| `ETORO_MAX_SESSION_USD` | `500` | Max total exposure executed until the server restarts. |
| `ETORO_MAX_WRITES_PER_MINUTE` | `5` | Local brake on executed writes (eToro also rate-limits). |
| `ETORO_MAX_DAILY_USD` | `1000` | Max total exposure (and transfers) executed per calendar day, per environment. Counted in the history database: it survives restarts and is shared by every server process. A request that clearly fails at eToro (a 4xx answer) gives its share back; a timeout or network error does not, because the order may exist. |
| `ETORO_MAX_DAILY_WRITES` | `25` | Max executed writes (orders, closes, stop-loss changes, watchlist changes...) per calendar day, per environment. |
| `ETORO_TIMEZONE` | `UTC` | IANA time zone (for example `America/Santiago`) that decides where a day starts for the daily limits and how the history page shows times. |
| `ETORO_HISTORY_DB` | your data folder | Path of the SQLite file with the action history and the daily ledger (macOS `~/Library/Application Support/etoro-mcp-server/history.sqlite`, Linux `~/.local/share/etoro-mcp-server/`, Windows `%APPDATA%\etoro-mcp-server\`). `off` keeps it in memory only: the daily limits then restart with the server. A server that can write refuses to start if the file cannot be opened. |
| `ETORO_CONFIRM_TTL_SECONDS` | `600` | How long a prepared action waits for you on the approval page. |
| `ETORO_MAX_RESPONSE_CHARS` | `120000` | Output size cap per tool result. Above it, arrays are shortened to their first N items (the result stays valid JSON and lists each array's real length). |
| `ETORO_DEBUG` | `false` | Log each HTTP call to eToro (method, path, query names, status, duration) to stderr. Never logs keys, headers or bodies. |
| `ETORO_AUDIT_LOG` | unset | Append JSON-lines audit events to this file (also logged to stderr). |
| `ETORO_BASE_URL` | `https://public-api.etoro.com` | Must be `https` on an `etoro.com` host. |

## Tools

24 **read** tools (always available) and 12 **write** tools that only *prepare* (+1 gated transfer tool). Full parameters and the eToro routes they use are in [docs/TOOLS.md](docs/TOOLS.md).

| Tool | Kind | Purpose |
|---|---|---|
| `etoro_check_connection` | read | Verify the keys authenticate, show their scopes (demo/real, read/write), and prove which account (demo or real) the data comes from |
| `etoro_get_portfolio` | read | Aggregated portfolio snapshot |
| `etoro_get_portfolio_breakdown` | read | Open positions (ids, units), pending orders, credit |
| `etoro_get_pnl` | read | Unrealized PnL and portfolio details |
| `etoro_get_balances` | read | Balances across your eToro accounts |
| `etoro_get_trade_history` | read | Closed trades since a date |
| `etoro_get_order` | read | Status of one order |
| `etoro_get_instruments` | read | Resolve exact tickers / ids to instruments |
| `etoro_search_instruments` | read | Find instruments by name or partial text ("apple", "S&P 500") |
| `etoro_get_candles` | read | Historical price candles (1m to 1w) for a window, with a summary: first open, last close, high, low, % change, volume |
| `etoro_search_investors` | read | Find investors by performance and risk filters (period, risk score, copiers, instrument held...), compact public stats per investor |
| `etoro_get_investor` | read | One investor's public data by username: profile, statistics over a period, copiers, gain history and live portfolio. Free text they wrote is flagged as untrusted |
| `etoro_get_balance_history` | read | Day-by-day total balance between two dates, with a summary (start, end, change, lowest, highest) |
| `etoro_get_cash_transactions` | read | Movements of one of your cash accounts, newest first, paginated |
| `etoro_simulate_position` | read | What-if on past prices: a long or short with leverage, stop loss and take profit, followed over a window of candles (entry, exit and why, result in USD and %, worst and best moments). Places nothing |
| `etoro_backtest` | read | Backtest `buy_and_hold` or `dca` (an amount every N days) over a window, with the same total invested at the start for comparison. Places nothing |
| `etoro_get_rates` | read | Bid/ask for instruments |
| `etoro_check_eligibility` | read | Settlement types, leverage, limits per instrument |
| `etoro_get_trading_costs` | read | What-if cost breakdown for an order |
| `etoro_list_price_alerts` | read | Your active price alerts, with which way the price has to move to reach each target and how far |
| `etoro_list_watchlists` | read | Your watchlists |
| `etoro_get_action_status` | read | Where a prepared action stands (waiting, executed with eToro's answer, rejected, expired, failed); also finds actions from earlier sessions |
| `etoro_get_action_history` | read | Search the local history (text, ids, dates, environment, action, status) and see today's use of the daily limits |
| `etoro_open_history` | read | Open a read-only page in your browser to search, filter and export the history |
| `etoro_prepare_open_position` | write (preview) | Validate + preview an order, open its approval page; returns `actionId` |
| `etoro_prepare_close_position` | write (preview) | Preview closing all/part of a position: instrument, direction, current price, rough result, what stays open |
| `etoro_prepare_modify_position` | write (preview) | Preview changing the stop loss / take profit of an open position (new rates, trailing, or removing them) |
| `etoro_prepare_cancel_order` | write (preview) | Preview cancelling a pending order |
| `etoro_prepare_cancel_close_order` | write (preview) | Preview cancelling a pending close order (the position stays open) |
| `etoro_prepare_transfer` | write (preview, gated) | Preview an internal transfer (real + opt-in only) |
| `etoro_prepare_create_price_alert` / `..._update_price_alert` / `..._delete_price_alert` | write (preview) | Propose creating, changing the target of, or deleting a price alert (no order, no money); you execute them on the page |
| `etoro_prepare_create_watchlist` / `..._add_watchlist_items` / `..._remove_watchlist_items` / `..._delete_watchlist` | write (preview) | Propose watchlist changes (no money involved); you execute them on the page |

### Example: a guarded order

```
You:    Prepare a purchase of 50 USD of AAPL as a CFD on my demo account.
Claude: [etoro_prepare_open_position] → preview: BUY AAPL (id 1001) | $50.00 | 1x | cfd | mkt | DEMO,
        eligibility, estimated costs, actionId 6b1c…  (nothing sent; your browser opens the approval page)
You:    (review the page, press Execute)                → the server sends the order to eToro
Claude: [etoro_get_action_status]     → executed, eToro's orderId
Claude: [etoro_get_order]             → status of the order
```

eToro answers an order with "accepted for processing", not "filled": follow the order with `etoro_get_order`.

## Trying an order on the demo environment

`npm run demo:order` runs the whole flow through the real server on eToro's **demo** (virtual money) environment: connection and environment check, instrument lookup, eligibility, preview with cost estimate, **your confirmation in the terminal**, execution, and following the order until it has a position.

```bash
npm run demo:order -- --symbol AAPL --amount 50                 # buy $50 on demo, keep the position
npm run demo:order -- --symbol AAPL --amount 50 --close         # ... and close it afterwards (asks again)
npm run demo:order -- --symbol AAPL --amount 20 --settlement cfd
npm run demo:order -- --close-position 123456789                # close an open demo position by id
npm run demo:order -- --symbol AAPL --amount 50 -y 2>&1 | tee demo-order.log   # no questions, output to a log
```

The script forces `ETORO_ENV=demo` whatever your environment says, stops unless the connection check proves the key reaches your demo account, and asks in the terminal before sending anything; your "yes" (or `-y`) is what it uses to press Execute on the approval page for you. `-y` (or `--yes`) answers yes to the questions — the order and, with `--close`, the close — so you can pipe the output to a log (use `2>&1` to include the server's audit lines). Without a terminal and without `-y` it refuses to start instead of hanging. Use a key with demo **Write** permission. After a fill it prints the new position's `settlement` (`cfd` or `real`) with its `settlementTypeID` and `isSettled`. `--settlement real` on an account that is only offered CFDs is refused at the preview.

## Known limitations

- **Very large responses are shortened.** A big portfolio (many positions or copy-trading mirrors) can exceed the output cap; the server then keeps the first N items of each array and says how many there really were. Prefer narrower tools or raise `ETORO_MAX_RESPONSE_CHARS`.
- **Responses are passed through as eToro sends them.** The shapes come from eToro's reference pages and from a live demo account (see "Where it stands" above); the actions listed there have been tried live and the rest only through tests. If a field is missing or renamed, please open an issue with the (redacted) response shape (`--verbose --mask` in the smoke script produces one that is safe to paste).
- **`etoro_get_instruments` is an exact lookup** (ticker or id); use `etoro_search_instruments` for names. ETF tickers on eToro carry an exchange suffix such as `EXMPL.L`.
- **Claude cannot execute, by design.** Claude's own rules keep it from executing financial transactions, so the server never asks it to: it prepares, you press Execute on the approval page. That needs a browser on the same computer (or `ETORO_SHOW_APPROVAL_URL=true` to read the address from the log or result); without a screen, nothing can be executed.
- **Prepared actions live in memory.** They are forgotten when the server restarts (for example when Claude Desktop restarts it), and expire after `ETORO_CONFIRM_TTL_SECONDS`.
- Prompt injection is a real risk for any tool-using agent: do not let Claude read untrusted content (web pages, emails, documents) in the same session in which it can prepare real orders, and read the approval page carefully before pressing Execute. If Claude has browser tools, keep `ETORO_SHOW_APPROVAL_URL` off so it never sees the page's address.
- No streaming/WebSocket data, no copy-trading actions, no OAuth (API key pair only).
- Eligibility to use the API and the instruments available depend on your account and jurisdiction. In particular, depending on jurisdiction some accounts can only open **CFDs**, not real shares: `settlementType: "real"` is then rejected by eToro (seen on a demo account that was offered only CFDs). `etoro_prepare_open_position` reads the eligibility answer first and refuses a settlement type the account is not offered, before anything can be confirmed.
- **Two instruments for some stocks.** eToro lists a regular-trading-hours instrument (symbol ending in `.RTH`) next to the 24/5 one for some stocks. The preview shows the exact symbol and instrument id, and warns on `.RTH`; pass `instrumentId` when in doubt.

## Development

```bash
npm ci
npm run typecheck
npm test                 # unit + end-to-end tests with a mocked eToro API (no network, no keys)
npm run build            # tsc → dist/
node scripts/smoke.mjs   # launch the built server over stdio and list tools (dummy keys)
npm run security:check   # runs the built server with the network cut off: permission switches, hostile inputs, secret redaction
npm run mcpb:pack        # esbuild bundle → server/index.js, then etoro-mcp-server.mcpb
npm run pack:dev         # throwaway etoro-mcp-server-dev.mcpb, versioned <version>-dev.<n>, to try changes in Claude Desktop
```

With your own keys in a git-ignored `.env`, `npm run smoke:live` (or `node scripts/smoke.mjs --live` with the variables exported) runs `etoro_check_connection` and a few read tools and prints only the *shape* of the responses (never values), which is a safe first check. In a client, ask Claude to run `etoro_check_connection` to confirm the keys work and which mode the server is in.

Debugging options for the script (all run the real server over stdio):

| Option | Effect |
|---|---|
| `--report` | Only verify the connection and the environment (`npm run verify` with a `.env`): prints a one-line verdict and reads no account data. |
| `--verbose` | Print each tool's full output. It contains your real account data: keep it private. |
| `--verbose --mask` | Same, but every value is replaced by a placeholder (`<number>`, `<string, 12 chars>`), keeping field names, types and nesting. Safe to paste into an issue. |
| `--tool <name> --args '<json>'` | Call a single tool, e.g. `--tool etoro_get_trade_history --args '{"minDate":"2026-01-01"}'`. |
| `--debug` | Sets `ETORO_DEBUG=true` so the server logs each HTTP call. |
| `--entry <file>` | Launch another entry point, e.g. `server/index.js` (the bundle used by the `.mcpb`). |

```
src/
  config.ts      env parsing, switches, caps
  endpoints.ts   the complete route allowlist (read vs write)
  client.ts      HTTP client: auth headers, idempotency ids, 429 retry, redaction, policy checks
  approval/      proposals (limits, status), the local approval page (server, renderer, browser opener), the write permission
  audit.ts       JSON-lines audit trail
  tools/         read.ts, write.ts, common.ts
test/            vitest, including MCP client ↔ server tests over an in-memory transport
```

## Contributing

Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Security issues: see [SECURITY.md](SECURITY.md).

## License

[MIT](LICENSE)

TDQS

B3.3/5.0

Scored across 11 tools

Disambiguation3/5

etoro_get_portfolio, etoro_get_portfolio_breakdown, etoro_get_pnl, and etoro_get_balances all return overlapping portfolio/account data. Descriptions attempt to differentiate (e.g., aggregated vs. breakdown), but an agent could still misselect among the portfolio tools. Other tools are clearly distinct.

Naming Consistency5/5

All tools use the etoro_ prefix followed by a consistent snake_case verb_noun pattern (get_, check_, list_). No deviation in style or casing.

Tool Count4/5

11 tools is a reasonable count for a read-focused trading API surface. It is not excessive, but the absence of any execution tools makes the set feel slightly under-scoped relative to the platform's purpose.

Completeness2/5

Major gaps: no tools to place orders, cancel orders, or close positions, despite etoro_check_eligibility and etoro_get_trading_costs implying trading workflows. The description of etoro_get_portfolio_breakdown references etoro_prepare_close_position, which is not present, creating a dead end. Watchlists are read-only.

Maintenance

ActivityMaintained
ResponsivenessNo issues