Skip to main content
Glama
candlestick-chart-mcp

candlestick-chart-mcp

by dzintars2
README.md
# candlestick-chart-mcp

An MCP server that renders OHLCV data as a candlestick chart PNG and returns it as
inline image content, so the model can actually look at the chart it asked for.

It is a **pure renderer**: no network access, no API keys, no market-data provider.
You pass the bars in; it draws them.

## Install

```bash
npm install
npm run build
```

Verify it end to end (spawns the server over stdio and calls the tool for real):

```bash
npm run check
```

Render sample charts to `out/` to eyeball the output:

```bash
npm run smoke
```

## Register with a client

Claude Code:

```bash
claude mcp add candlestick-chart -- node /absolute/path/to/candlestick-chart-mcp/dist/index.js
```

Or in a config file (`.mcp.json`, `claude_desktop_config.json`, …):

```json
{
  "mcpServers": {
    "candlestick-chart": {
      "command": "node",
      "args": ["/absolute/path/to/candlestick-chart-mcp/dist/index.js"]
    }
  }
}
```

## The tool

### `render_candlestick_chart`

Returns two content blocks: a short text summary (bar count, date range, OHLC,
percent change, plus any notes about inputs that were adjusted) and the PNG.

**Bars.** `candles` accepts either form per bar, and mixing them is fine:

```json
{ "time": "2026-01-05", "open": 100, "high": 103.2, "low": 99.4, "close": 102.8, "volume": 1200000 }
```

```json
[1767571200, 100, 103.2, 99.4, 102.8, 1200000]
```

The tuple form is `[time, open, high, low, close, volume?]` and costs far fewer
tokens — prefer it for anything longer than a handful of bars. `time` takes epoch
seconds, epoch milliseconds, or any string `Date.parse` understands. Bars are
sorted by time; a `high`/`low` that contradicts the body is clamped rather than
rejected.

**Everything else is optional:**

| Option | Default | Notes |
|---|---|---|
| `title`, `subtitle` | — | Subtitle defaults to date range · bar count · change |
| `chartType` | `candlestick` | or `heikin-ashi`, `ohlc-bars` |
| `width`, `height`, `scale` | `1400`, `800`, `1` | `scale` is the device-pixel ratio |
| `theme` | `dark` | or `light` |
| `colorScheme` | `diverging` | or `classic`, `mono` — see below |
| `bodyStyle` | per scheme | `filled` or `hollow-up` |
| `showVolume` | `true` | Needs a volume column; drawn in its own panel |
| `showGrid`, `showLegend`, `showLastPrice` | `true` | |
| `yScale` | `linear` | `log` for multi-year or high-growth ranges |
| `timeZone` | `UTC` | Any IANA zone; applies to every label |
| `sma`, `ema` | `[]` | Arrays of periods, e.g. `[20, 50]` |
| `bollinger` | — | `{ "period": 20, "stdDev": 2 }` |
| `vwap` | `false` | Session-anchored; needs volume on every bar |
| `priceLines` | `[]` | `{ price, label?, color?, style? }` — support, resistance, stops |
| `markers` | `[]` | `{ index? or time?, side: buy/sell/neutral, label? }` |

Example call:

```json
{
  "candles": [[1767571200, 100, 103.2, 99.4, 102.8, 1200000], "…"],
  "title": "BTCUSD · 1D",
  "sma": [20, 50],
  "bollinger": { "period": 20, "stdDev": 2 },
  "priceLines": [{ "price": 61941.5, "label": "resistance" }],
  "markers": [{ "time": "2026-02-14", "side": "buy", "label": "entry" }],
  "yScale": "log",
  "timeZone": "Europe/Riga"
}
```

Inputs that cannot be honoured do not fail the call — an SMA period longer than
the series, a missing volume column, a log scale with non-positive prices, an
unknown time zone are each dropped or downgraded and reported in the text block.

## Colour

Up/down is a polarity encoding, so the default `diverging` scheme uses the
**blue ↔ red** diverging pair. That pair clears the all-pairs colourblind gate in
both themes (CVD ΔE 21.6 light / 19.2 dark against a ≥ 8 target). The traditional
green/red pair **fails** it at ΔE 4.1 for deuteranopia, so `classic` ships with
hollow up-bodies: shape carries the direction and hue only reinforces it.
`mono` drops hue entirely — ink, hollow versus filled — for print and
forced-colours contexts.

With the up/down pair already on screen there is no room left for a palette of
overlay hues: every candidate measured below the normal-vision floor against one
of the poles. So all overlays share one validated accent per scheme, and their
identity comes from **dash pattern + legend + a direct label at the end of each
line**. Four overlays are distinguishable; a fifth reuses a dash pattern and the
response says so.

Volume bars inherit the bar's direction at half strength so the panel stays
recessive, and it is a separate panel — never a second y-axis on the price plot.

## Layout notes

- Bars sit on an **ordinal** x-axis (one slot per bar), so weekends and market
  holidays leave no gaps. Time labels appear where the label actually changes;
  intraday charts substitute the date wherever the day rolls over.
- Below ~3.5 px per bar, bodies are dropped and each bar becomes a single
  high-low hairline — a 1200-bar chart stays readable.
- Bodies cap at 26 px wide so a seven-bar chart still looks like candles.
- The PNG is capped near 4.5 MB; a request that would exceed it is re-rendered at
  `scale: 1` and, failing that, returns an error asking for smaller dimensions.

## Layout of the source

| File | Role |
|---|---|
| `src/index.ts` | MCP server, tool registration, marker/time-zone resolution |
| `src/schema.ts` | Zod input schema — the tool's contract |
| `src/render.ts` | All drawing: layout, axes, candles, overlays, annotations |
| `src/theme.ts` | Palettes and the validation results behind them |
| `src/indicators.ts` | SMA, EMA, Bollinger, VWAP, Heikin-Ashi |
| `src/candles.ts` | Input normalisation |
| `src/format.ts` | Tick stepping, number and date formatting |

Adding an indicator panel (RSI, MACD) means adding a panel band to the layout
block in `renderChart` and a series function in `indicators.ts`.

## License

MIT

TDQS

A4.4/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of confusion or overlapping purposes. The tool's purpose is clearly defined and distinct.

Naming Consistency5/5

The single tool name follows a clear verb_noun pattern (render_candlestick_chart), which is consistent and descriptive. There are no other tools to create inconsistencies.

Tool Count3/5

A single tool feels thin and borderline for a typical MCP server, even though the tool itself is comprehensive and covers the charting domain well. It sits at the lower boundary of the acceptable range.

Completeness5/5

The tool appears to fully cover the domain of candlestick chart rendering, supporting multiple chart types, overlays, markers, and themes. No obvious gaps in the server's stated purpose.