Skip to main content
Glama
aleksandrglibcenko-art

provetrade-mcp

README.md
# provetrade-mcp

An MCP server for the [ProveTrade](https://provetrade.com) trade-audit stack: it
gives a model typed access to the audit pipeline — check a CSV before spending
anything on it, run an audit and get structured metrics back, and see whether the
services are awake and which commit is live.

Six tools, two dependencies, stdio only, nothing written to disk.

---

## Install

```bash
git clone https://github.com/vuzl-dev/provetrade-mcp
cd provetrade-mcp
npm ci --ignore-scripts
npm run build
npm run selftest      # verifies configuration and probes both services
```

`--ignore-scripts` is not decoration: a lifecycle script from any transitive
package would run with your permissions, and nothing here needs one.

## Connect it

Ready to paste. Replace the path with wherever you cloned it.

```json
{
  "mcpServers": {
    "provetrade": {
      "command": "node",
      "args": ["/absolute/path/to/provetrade-mcp/dist/src/index.js"],
      "env": {
        "PROVETRADE_ALLOWED_ROOT": "/absolute/path/to/your/csv/folder",
        "PROVETRADE_AUDIT_MAX_RUNS": "5"
      }
    }
  }
}
```

Where that block goes:

| Client | File |
|---|---|
| Claude Code, one project | `.mcp.json` in the project root |
| Claude Code, everywhere | `~/.claude.json` — or just run `claude mcp add provetrade -- node /absolute/path/to/provetrade-mcp/dist/src/index.js` |
| Claude Desktop, macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Claude Desktop, Windows | `%APPDATA%\Claude\claude_desktop_config.json` |

Set `PROVETRADE_ALLOWED_ROOT` to the folder holding the CSVs you want audited.
Every path a tool accepts is confined to it, and the default — the process working
directory — is rarely what you want.

## The tools

| Tool | What it does | Read-only | Costs money |
|---|---|:---:|:---:|
| `provetrade_validate_csv` | Grades a trade CSV locally: which exchange adapter the engine would pick, row and closed-trade counts, and every issue that would break or silently degrade the audit. No network. | yes | no |
| `provetrade_health` | Probes both services' `/health`, distinguishing **sleeping** (spun-down free tier, recoverable) from **down**. | yes | no |
| `provetrade_deploy_status` | Compares the gateway's live build sha against local git HEAD, and how many commits behind the deploy is. | yes | no |
| `provetrade_warm_engine` | Wakes the Go analyzer and waits for it with a time budget. Honestly reports `still_sleeping` when it does not come up. | no — starts a service | no (burns free hosting hours) |
| `provetrade_metrics` | The gateway's request counters and latency percentiles from `/metrics-lite`. | yes | no |
| `provetrade_audit_csv` | **The flagship.** Uploads a CSV, reads the SSE stream, returns a typed analytical object: winrate, net P&L, expectancy, payoff ratio, Sharpe, Sortino, max drawdown, maker share, behavioural flags with the trade ids behind them, and breakdowns by symbol/hour/weekday/holding time. | no | **YES** — one LLM call per run |

Run `provetrade_validate_csv` before `provetrade_audit_csv`, always. The first is
free and local; the second is not.

Failures carry a stable code — `ENGINE_SLEEPING`, `GATEWAY_SLEEPING`,
`UPSTREAM_ERROR`, `TIMEOUT`, `BAD_INPUT`, `PATH_DENIED`, `FILE_TOO_LARGE`,
`NOT_CSV`, `TOKEN_MISSING`, `RATE_LIMITED`, `BUDGET_EXCEEDED`, `GIT_ERROR` — so a
model can branch on the code instead of pattern-matching prose.

## Environment

Every value is read from the environment only. **No tool accepts a URL, a host or a
token as a parameter** — see [SECURITY.md](SECURITY.md#ssrf) for why that matters.

| Variable | Default | What happens if you leave it unset |
|---|---|---|
| `PROVETRADE_ALLOWED_ROOT` | the working directory | Paths resolve against the working directory. Files elsewhere are refused with `PATH_DENIED`. |
| `METRICS_TOKEN` | *(empty)* | `provetrade_metrics` returns `TOKEN_MISSING`. Everything else works normally. |
| `PROVETRADE_AUDIT_MAX_RUNS` | `5` | At most 5 audits per server process; the 6th is refused with `BUDGET_EXCEEDED` before any request is sent. |
| `PROVETRADE_GATEWAY_URL` | the production gateway | Uses production. Must be https and on the host allowlist, or the process exits 2 at startup. |
| `PROVETRADE_ANALYZER_URL` | the production analyzer | Same. |
| `PROVETRADE_HEALTH_TIMEOUT_MS` | `12000` | 12 s per `/health` probe. |
| `PROVETRADE_AUDIT_IDLE_TIMEOUT_MS` | `150000` | The audit stream is abandoned after 150 s of **silence** — not of total duration. |
| `PROVETRADE_AUDIT_MAX_DURATION_MS` | `540000` | Hard ceiling on one audit, kept under the gateway's own 600 s stream limit. |
| `PROVETRADE_MAX_BODY_BYTES` | `8388608` | Cap on a streamed audit body. Exceeding it is an error, never a truncated parse. |
| `PROVETRADE_LIVE_TESTS` | *(off)* | The one live test in the suite is skipped. |

`INTERNAL_TOKEN` — the gateway↔analyzer shared secret — is **not used by this
server at all**, because it never calls the analyzer's `/analyze`. If it is set in
your environment, the server says so on stderr and suggests unsetting it for this
process.

## Why MCP, and not a wrapper around a CLI

This is the whole argument, so it gets a worked example rather than an assertion.

**As a shell wrapper**, the model gets a command line and a blob of text:

```
$ provetrade audit --file trades.csv --balance 5000 --tz 0
Reading trades.csv... ok
Winrate: 50.0%
Net P&L: -1053.16 USDT
Max drawdown: 1301.50 USDT (n/a%)
Sharpe: -0.62   Sortino: n/a
...
```

To use any of that, the model has to guess the flag names (`--tz`? `--utc`?
`--offset`?), then parse prose. And the parsing is where it goes wrong quietly:
`n/a%` becomes `0`, so "drawdown percentage could not be computed without a
starting balance" turns into "drawdown was 0%" — a confident, wrong number that
reads exactly like a real one. Nothing in the text says which fields are
computable, which are missing, or what the units are. Change one label upstream and
every consumer breaks silently.

**As an MCP tool**, both directions are typed. The input schema publishes the
parameter names, their types, their ranges and their defaults, so there is nothing
to guess — an unknown model id or an out-of-range UTC offset is refused at the
boundary with a message naming the field, before any request is sent. The output
schema declares that `sortino_ratio` is `number | null`, and the server returns
exactly that:

```json
{
  "metrics": {
    "winrate_percent": 50,
    "net_pnl_usdt": -1053.16,
    "max_drawdown_usdt": 1301.5,
    "max_drawdown_percent": null,
    "sharpe_ratio": -0.62,
    "sortino_ratio": null
  },
  "narrative_status": "not_requested",
  "runs_remaining": 4
}
```

`null` means "the engine could not compute this", and it is impossible to mistake
for zero. There is no text to parse, no unit to infer, and no field whose absence
is indistinguishable from a value.

The schema is also what lets the *server* say things the wrapper cannot express:
`readOnlyHint: false` on the audit tool marks it as consequential, the description
says in words that it costs money, and the run budget makes that enforceable. A
CLI wrapper hands the model a shell and hopes.

See [`examples/smoke-output.txt`](examples/smoke-output.txt) for a real recorded
run of all six tools, structured output included.

## Examples

`examples/` holds two generated CSVs that differ in **exactly one** respect: the
second appends ` USDT` to the `Amount`, `Fee` and `Realized Profit` columns.

That pair demonstrates the reason `provetrade_validate_csv` exists. The first file
audits cleanly. The second returns *"no valid trades parsed from CSV"* — because
`Realized Profit` is parsed as a bare number, so every row is skipped, while `Fee`
handles the same suffix correctly and `Amount` silently substitutes
`Price × Quantity`. The failure reads like a wrong export or a bad date range and
is neither. `validate_csv` names the actual column.

Both files are synthetic, generated from a fixed seed by
`scripts/make-fixtures.mjs`. No third party's trades are in this repository.

## Security

Read [SECURITY.md](SECURITY.md). The short version: two hosts on a source-level
allowlist, no shell, no port, no disk writes, one optional secret that never
appears in output, and every path confined to an allowed root with
Windows-correct comparison.

**Before you audit someone else's export:** the ProveTrade gateway forwards a
*skeleton* of each upload — the header plus a masked shape of the first row, no
trade values — to a private Telegram chat, to collect each exchange's file format.
Running a client's file through production therefore transmits the structure of
their data to a third party. Decide that before the upload.

## Limitations, honestly

- **Free-tier hosting.** Both services spin down after ~15 minutes idle. A first
  request after that waits for a cold start; the gateway absorbs up to 120 s of
  the analyzer's boot inside the request.
- **Server-side probes do not reliably wake the analyzer.** Measured on
  production: 72 s of `/health` probing at 4-second intervals produced no entry in
  the analyzer's own log, while a browser request started it and it was up in
  ~38 s. `provetrade_warm_engine` therefore reports `still_sleeping` honestly
  rather than pretending. The reliable fallback is opening
  <https://provetrade.com/app> in a browser — the page pings the analyzer from
  the client side for exactly this reason.
- **A cold start can present as a timeout, not just as a 429.** Seen while
  recording the smoke run: the analyzer's `/health` hung past an 8-second deadline
  and answered 4 seconds after a warm-up probe. `health` now reports that state as
  ambiguous and points at `warm_engine` instead of at the deploy, and the default
  deadline is 12 s.
- **`provetrade_metrics` has never been exercised against the live endpoint.** The
  token lives in the hosting dashboard and was not available while this was
  built. It is fully implemented and tested against a fake client — success,
  missing token, the 404-means-rejected-token case, a sleeping gateway, and the
  no-leak assertion — but the live path is **unverified**.
- **`estimated_closed_trades` is an estimate**, and only for Binance fills
  exports; it is `null` for every other format. The real count comes from position
  reconstruction, which lives in the Go engine, and reimplementing that here would
  create a second source of truth for trading math.
- **94 packages** in the production tree, all pulled in by the official SDK for
  transports this server does not use. Two direct dependencies; see
  [SECURITY.md](SECURITY.md#dependencies).
- **No HTTP transport.** Not an oversight — see below.

## If HTTP is ever needed

stdio was chosen because it opens no port, has no network surface, and ties the
process lifetime to the client. If a remote transport becomes necessary, all four
of these are required, not optional:

1. Bind to `127.0.0.1` only — never `0.0.0.0`.
2. Validate the `Origin` header against an allowlist. Without it a web page can
   drive a local server via DNS rebinding.
3. Require a bearer token, compared in constant time.
4. Keep the existing body caps, per-request timeouts and concurrency limits;
   they matter more once the endpoint is reachable by something other than a
   parent process.

## Development

```bash
npm test          # build, then node:test over the compiled output
npm run fixtures  # regenerate examples/ (deterministic — a diff means a real change)
npm run smoke     # drive all six tools over stdio; writes examples/smoke-output.txt
npm run selftest  # config check + health probe, non-zero exit on a real problem
```

`npm run smoke` makes **one real audit** and therefore one LLM call. Set
`PROVETRADE_SMOKE_SKIP_AUDIT=1` to rerun it for free.

Repository documents: [CONTRACT.md](CONTRACT.md) records the ProveTrade wire
contract this server depends on, read out of the sources rather than from
documentation. [DECISIONS.md](DECISIONS.md) records the forks taken and why.
[SECURITY.md](SECURITY.md) is the security review. [PROGRESS.md](PROGRESS.md) is
the build log.

## License

MIT. Author: Vuzl ([@vuzl.dev](https://vuzl.dev)).

TDQS

A4.5/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a distinct function: warm_engine wakes the analyzer, validate_csv pre-checks files, health reports service status, deploy_status compares versions, audit_csv runs the paid audit, and metrics reads operational counters. There is no overlap or ambiguity between any two tools.

Naming Consistency4/5

All tools share the consistent 'provetrade_' prefix and are descriptive, but they mix verb-based (warm_engine, validate_csv, audit_csv) and noun-based (health, deploy_status, metrics) patterns. While readable and predictable, the pattern is not strictly verb_noun.

Tool Count5/5

Six tools is well-scoped for the domain of trading CSV auditing, covering pre-validation, execution, health checks, waking, metrics, and deployment comparison. No tool feels redundant or missing.

Completeness5/5

The tool surface covers the full lifecycle: pre-audit validation, the paid audit itself, service health and wake-up, operational metrics, and deployment debugging. There are no obvious dead ends or missing operations for the stated purpose.

Maintenance

ActivitySlowing
ResponsivenessNo issues