Skip to main content
Glama
KajcsaErno

tradingview

by KajcsaErno
README.md
# tradingview-mcp

An MCP server and CLI that read and control a **live TradingView Desktop chart** over the Chrome
DevTools Protocol. 82 tools — chart state, OHLCV and indicator values, Pine Script edit/compile,
drawings, alerts, bar replay, screenshots — reachable from an LLM client or from a shell.

[![CI](https://github.com/KajcsaErno/tradingview-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/KajcsaErno/tradingview-mcp/actions/workflows/ci.yml)

```
MCP client  <->  MCP server (stdio or HTTP)  <->  CDP :9222  <->  TradingView Desktop (Electron)
```

There is no REST API behind this. TradingView Desktop is an Electron app, so the whole surface is
JavaScript evaluated inside the running page against `window.TradingViewApi` and its internals —
which is what lets it read things no public API exposes (Pine `line.new()` / `label.new()` /
`table.new()` output, strategy equity curves, the data window, replay state), and also what makes it
fragile: see [Caveats](#caveats).

## Quick start

Requires Node.js >= 22.15 and TradingView Desktop installed.

```bash
npm install
npm run tv -- launch          # starts TradingView Desktop with CDP on :9222
npm run tv -- status          # confirm the link is live
npm run tv -- symbol BTCUSD   # drive the chart
npm run tv -- ohlcv --count 20 --summary
```

Then register the MCP server with your client:

```json
{
  "mcpServers": {
    "tradingview": {
      "command": "node",
      "args": ["/absolute/path/to/tradingview-mcp/src/tradingview/server.js"]
    }
  }
}
```

## Two surfaces, kept in sync by a test

Every MCP tool is also a shell command, and that is **enforced** rather than hoped for.
`tests/cli-parity.test.js` enumerates both surfaces from the real registration code — MCP tools via
a stub server that records `.tool()` calls, CLI commands via the router's own map — and diffs them
against `tests/_cli_parity_map.js`. Adding a tool without a CLI command (or the reverse) fails CI
unless the mismatch is declared with a written reason.

That map is also where the deliberate asymmetries live: the seven `tv stream *` commands are
CLI-only, because they are long-running JSONL poll loops that never return and so were never
sensible as MCP tools.

`tv --help` is grouped into labelled sections, and `tests/cli-groups.test.js` asserts those sections
are a *total, non-overlapping partition* of the registered commands — so a command added without a
section becomes a test failure instead of silently landing under "Other".

```bash
tv --help              # grouped command list
tv <command> --help    # that command's subcommands / options
tv help <term>         # search command names + descriptions
```

31 top-level commands, 90 leaf commands, 82 MCP tools.

## Tool groups

| Group | Tools | What |
|---|---:|---|
| `data` | 12 | OHLCV, indicator values, strategy results, trades, equity, quote, depth, Pine graphics |
| `pine` | 12 | Pine source get/set, compile, errors, console, save, static analysis, server-side check |
| `ui` | 12 | Raw UI automation — click, keyboard, type, hover, scroll, find element, evaluate |
| `chart` | 10 | Symbol, timeframe, chart type, indicators, visible range, scroll-to-date, symbol search |
| `replay` | 6 | Bar-replay practice: start, step, autoplay, trade, status, stop |
| `drawing` | 5 | Draw shapes, list, clear, remove one, read properties |
| `alerts` | 4 | Create, list, delete, activate |
| `health` | 4 | CDP health check, launch, API-path discovery, UI state |
| `pane` | 4 | Pane/layout management |
| `tab` | 4 | Chart-tab management |
| `morning` | 3 | Rules-driven session brief + save/get (see `rules.example.json`) |
| `indicators` | 2 | Set inputs, toggle visibility |
| `watchlist` | 2 | Read, add |
| `batch` | 1 | Run one action across many symbols and/or timeframes |
| `capture` | 1 | Screenshot the chart |

## Architecture

```
core/*.js     pure logic — builds a JS expression, evaluates it, returns { success, ... }
   |
   +-> tools/*.js   MCP wrappers (Zod schemas)     cli/*.js   shell commands
          |                                           |
          +-> mcp-app.js  assembles the server        +-> cli/index.js
                 |
                 +-> server.js (stdio)   http.js (:3333, bearer auth + optional CF Access)
```

**The `_deps` seam.** Every core function takes a trailing `_deps` parameter defaulting to the real
`evaluate` / `evaluateAsync` / `waitForChartReady`:

```js
export async function setSymbol(symbol, _deps = {evaluate, waitForChartReady}) {
    const expr = `window.TradingViewApi.setSymbol(${safeString(symbol)})`;
    await _deps.evaluate(expr);
    return _deps.waitForChartReady();
}
```

Tests inject a fake evaluator and assert on **the exact expression string** that would have been
sent. That is why the whole suite — 198 tests — runs offline with no TradingView, no chart and no
network, and why an injection regression is a red test rather than a live incident.

Two files carry most of the weight:

- **`connection.js`** — singleton CDP client with exponential-backoff reconnect, picking the Electron
  target whose URL matches `tradingview.com/chart`. Exports `KNOWN_PATHS`, the verified deep paths
  into the app internals.
- **`wait.js`** — `waitForChartReady()` polls the DOM (loader spinner, bar-count stability, symbol
  match) after any chart mutation. There is no `setTimeout` anywhere in the codebase.

## Injection safety

Every user value interpolated into an evaluated JS string goes through `safeString` (a
`JSON.stringify` round-trip producing a properly escaped literal); every numeric goes through
`requireFinite`. This is the entire CDP-injection defense, so it is tested two ways:

1. **Behavioural** — `tests/tradingview/sanitization.test.js` feeds injection payloads through the
   `_deps` seam and asserts on the generated expression.
2. **Source audit** — the same suite scans every `core/*.js` for raw interpolation of user input
   into an `evaluate()` string, so a new function that forgets `safeString` fails the test even if
   nobody wrote a case for it.

Filenames are additionally stripped of path separators (screenshot capture, batch output).

## HTTP transport

```bash
cp .env.example .env.http    # set MCP_AUTH_TOKEN_TV
npm run start:http           # 127.0.0.1:3333
```

Bearer auth is mandatory — the server refuses to start without a token of at least 16 characters.
`CF_ACCESS_TEAM_DOMAIN` + `CF_ACCESS_AUD` additionally require a valid Cloudflare Access JWT,
verified at the origin against the team JWKS with a zero-dependency RS256 check.

Beyond the full surface at `POST /mcp`, three group mounts expose a narrower tool set so a session
loads only what it needs: `POST /mcp/tv-chart` (read + light control), `/mcp/tv-pine` (Pine
development loop), `/mcp/tv-advanced` (drawing, alerts, replay, batch, raw UI). The grouping is a
clean partition of all 82 tools, enforced by a test.

## Testing

```bash
npm test                  # 198 tests, fully offline
npm run lint
npm run test:sanitization # the injection suite alone
npm run test:e2e          # REQUIRES a live TradingView Desktop on :9222
```

`node:test` only — no Jest, Mocha or Vitest. The package is ESM (`"type": "module"`).

## Caveats

- **Unofficial.** Not affiliated with, endorsed by, or supported by TradingView Inc. or Anthropic.
- **It drives your own installed desktop app** through its debugging port. Nothing is scraped from
  TradingView's servers and no account credentials pass through this code.
- **`KNOWN_PATHS` are undocumented internals** and can break on any TradingView update. Pin the
  desktop version if you need stability.
- `chart_manage_indicator` needs full indicator names — "Relative Strength Index", not "RSI".
- Entity IDs from `chart_get_state` are session-scoped; never cache them across sessions.
- `ui_evaluate` runs arbitrary JS in the page and **bypasses input sanitization** by design. It is a
  last-resort escape hatch, not a normal tool.
- Caps: OHLCV 500 bars, trades 20/request, Pine labels 50/study.
- Screenshots are written to `screenshots/` and the tool returns a **path**, not image bytes.

## License

MIT — see [LICENSE](LICENSE). Affiliation and trademark notices are in [NOTICE.md](NOTICE.md).