Skip to content

feat(mcp): official CCXT MCP server - #29277

Merged
carlosmiei merged 23 commits into
ccxt:masterfrom
pcriadoperez:feat/official-mcp-server
Aug 22, 2026
Merged

feat(mcp): official CCXT MCP server#29277
carlosmiei merged 23 commits into
ccxt:masterfrom
pcriadoperez:feat/official-mcp-server

Conversation

@pcriadoperez

Copy link
Copy Markdown
Contributor

Summary

The first official CCXT MCP (Model Context Protocol) server — lives in mcp/, published to npm as ccxt-mcp. It runs locally over stdio so an AI agent (Claude Desktop/Code, Cursor, VS Code, Windsurf, …) can drive the unified CCXT API across 100+ exchanges and prediction markets: public market data with no keys, private reads and opt-in trading once an account is configured. API keys stay on the user's machine and are never visible to the model — tools reference accounts by name only.

Mirrors the cli/ sub-package pattern (own package.json, tsc build, independently versioned). Two commits: the server, then distribution/CI (MCPB bundle, registry manifest, Claude Code plugin, workflows, skill).

Highlights

  • Tiered, opt-in tools: market → read → trading → funds → raw endpoints. Higher tiers are per-account config switches, never enableable from the conversation; disabled tiers aren't even registered.
  • Execution-point safety rails: per-order/daily notional caps, symbol allow/deny, market-limit + precision validation, confirmation (native elicitation or a single-use token), append-only audit journal. Sandbox/testnet via a per-account flag; "trading": true is sandbox-only, live requires an explicit "live" + a user-authored cap.
  • Prediction markets first-class; response sizes bounded to host context limits; info stripped by default.
  • 56 offline tests (unit + tools/list schema snapshot + real-stdio integration). Docs: mcp/README.md, mcp/DESIGN.md, and wiki/MCP.md (wired into the docs site).

Release / publish — maintainer steps

npm — automated. Bump mcp/package.json, run npm run sync-versions (aligns manifest.json + server.json), commit, then dispatch Actions → “MCP Release” (.github/workflows/mcp-release.yml): it builds, tests, and runs npm publish --provenance (Node 24, maintainer-gated) exactly like the main release. One-time setup: configure an npm trusted publisher for the ccxt-mcp package → repo ccxt/ccxt, workflow mcp-release.yml (OIDC, same model as the ccxt release). The very first publish may need a manual cd mcp && npm run publishPackage (or a temporary NPM_TOKEN) until the package exists and the trusted publisher can be attached.

MCP registry (after the npm publish):

brew install mcp-publisher            # or download the release binary
cd mcp
mcp-publisher login github            # must be a member of the ccxt GitHub org
mcp-publisher publish                 # validates package.json mcpName == server.json name

server.json is ready (io.github.ccxt/ccxt-mcp). The registry is in preview and versions are immutable, so its version must equal the published npm version (kept in sync by sync-versions.mjs).

MCPB bundle (.mcpb) for Claude Desktop distribution:

cd mcp
npm run mcpb                                  # -> ccxt-mcp.mcpb (validated + packed)
npx @anthropic-ai/mcpb sign ccxt-mcp.mcpb     # with the CCXT signing certificate

Verified locally: 14 MB bundle, manifest passes v0.4 schema validation, bundled server boots standalone.

Claude Code plugin — no publish step; once merged, /plugin marketplace add ccxt/ccxt then /plugin install ccxt-mcp@ccxt.

Not yet done (why this is a draft)

  • Live testnet order round-trip with real sandbox keys — every write-path test so far uses placeholder keys (→ AUTH_FAILED); the rails, preview/confirm flow, and journal are verified, but a real testnet fill/cancel hasn't been run.
  • v1.1 WebSocket watch_* streaming tools (designed in DESIGN.md, deferred).

Verification

npm run lint-equivalent tsc clean · 56/56 tests green · live public smoke on binance/kraken/polymarket/kalshi/hyperliquid · three independent first-time-agent DX passes + a multi-perspective adversarial review, all findings fixed (see mcp/DESIGN.md). Diff is mcp/** plus root wiring (package.json scripts, .gitignore, README.md section, wiki/MCP.md + converter, install-skills.sh, marketplace).

Add ccxt-mcp, a local stdio Model Context Protocol server exposing the
unified CCXT API (100+ exchanges + prediction markets) to AI agents in
Claude Desktop/Code, Cursor, VS Code, Windsurf and other MCP hosts.

- Tiered, opt-in tools: market data (no keys) -> private reads -> trading
  -> funds -> raw endpoints. Higher tiers are per-account config switches,
  never enableable from the conversation; disabled tiers are unregistered.
- Credentials are never tool parameters and never appear in results
  (redaction choke point); tools reference named accounts by alias only.
- Order rails enforced at the execution point: per-order/daily notional
  caps, symbol allow/deny, market-limit + precision validation,
  confirmation (native elicitation or a single-use confirm token), and an
  append-only audit journal. Sandbox/testnet via a per-account flag;
  trading:true is sandbox-only, live requires an explicit "live" + a cap.
- Prediction markets first-class (search_events + outcome handles).
- Response-size management sized to host context limits; info stripped.
- 56 tests (unit + tools/list schema snapshot + real-stdio integration).

Lives in mcp/ mirroring the cli/ sub-package (own package.json, tsc build,
manual publish). Adds root npm scripts (mcp.ts/mcp.js), a docs page
(wiki/MCP.md wired into the fumadocs converter + nav) and a README section.
… release

v1.1 distribution and CI for the ccxt-mcp server.

- MCPB desktop-extension bundle: mcpb/manifest.json (v0.4, API keys stored
  in the OS keychain via user_config sensitive fields; sandbox default true,
  trading checkbox = sandbox-only) + scripts/build-mcpb.mjs to stage, install
  runtime deps, validate and pack a .mcpb. Built bundle runs standalone.
- MCP registry: server.json (io.github.ccxt/ccxt-mcp, matches the package
  mcpName) for publishing to registry.modelcontextprotocol.io.
- Claude Code plugin: mcp/plugin/ (declares the ccxt server via npx and
  bundles the ccxt-mcp skill by symlink) + a repo-root marketplace so users
  can /plugin marketplace add ccxt/ccxt && /plugin install ccxt-mcp@ccxt.
- CI: .github/workflows/mcp.yml, path-filtered on mcp/**, runs build + the
  offline test matrix + a pack check (no transpiler pipeline).
- Automated npm release: .github/workflows/mcp-release.yml (manual dispatch,
  maintainer-gated, npm publish --provenance) mirroring the main release
  workflow; ccxt-mcp is versioned independently from ccxt.
- Usage skill: .claude/skills/ccxt-mcp/SKILL.md (install/config/tiers/safety),
  added to install-skills.sh.
- scripts/sync-versions.mjs keeps manifest/server.json versions in lockstep
  with package.json; .npmignore excludes mcpb/plugin/scripts/server.json.
The Claude Desktop .mcpb install form only exposed one exchange (MCPB
user_config fields are flat). Add a "Config file" file-picker field mapped
to CCXT_MCP_CONFIG so the desktop bundle can point at a config.json with an
accounts map — the server already supports unlimited named accounts. Guard
the loader so an empty/unsubstituted CCXT_MCP_CONFIG falls back to the
default path instead of erroring. Docs + tests updated.
@kroitor
kroitor marked this pull request as ready for review July 22, 2026 17:25
Copilot AI review requested due to automatic review settings July 22, 2026 17:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the first official CCXT MCP (Model Context Protocol) server as a self-contained mcp/ sub-package (ccxt-mcp) so MCP hosts can drive CCXT locally over stdio (keys remain on the user machine), with tiered capability gating, confirmations, notional caps, and an append-only audit journal.

Changes:

  • Introduces the mcp/ npm sub-package implementing the stdio MCP server, tiered tool registration, safety rails, redaction, caching, and journaling.
  • Adds an offline test matrix (unit + schema snapshot + real-stdio integration) plus CI workflows and release workflow for ccxt-mcp.
  • Wires user-facing docs and distribution surfaces (wiki page, README section, Claude Code plugin, marketplace entry, skill install).

Reviewed changes

Copilot reviewed 53 out of 56 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
wiki/MCP.md New end-user documentation page for installing/configuring ccxt-mcp and its safety model.
wiki/AI-Skills.md Adds a pointer from skills docs to the MCP server docs.
README.md Adds a top-level README section introducing the CCXT MCP server and install snippet.
package.json Adds root scripts to run the MCP server from TS (mcp.ts) or compiled JS (mcp.js).
mcp/tsconfig.json TypeScript build config for the mcp/ sub-package outputting to mcp/js/.
mcp/ts/types.ts Defines config/account types and safe account summaries for tool output.
mcp/ts/tools/write-common.ts Shared write-dispatch wrapper with journaling and optional clientOrderId injection.
mcp/ts/tools/read.ts Implements private read-tier tools (accounts/balances/orders/trades/positions).
mcp/ts/tools/index.ts Central tool registration with tier-based dynamic tool exposure + server instructions.
mcp/ts/tools/funds.ts Implements funds-tier tools (withdraw/transfer/deposit address) and implicit write tool.
mcp/ts/tools/common.ts Common tool helpers: argument parsing, envelope wrapper, and confirmation orchestration.
mcp/ts/shims/ccxt.d.ts Provides permissive type shims for loading ccxt and local monorepo ts/ccxt.
mcp/ts/server.ts Stdio server entrypoint with lifecycle handling and pools prewarm.
mcp/ts/redact.ts Secret registration + redaction logic for outputs/logs/journal.
mcp/ts/pools.ts Exchange instance pooling (public/auth/bare), credential loading, and hardening of verbose/logging.
mcp/ts/markets.ts Markets/currencies disk cache + in-flight load de-duplication for loadMarkets.
mcp/ts/logging.ts Reroutes all console output to stderr (protect stdout JSON-RPC channel) with redaction.
mcp/ts/journal.ts Append-only JSONL audit journal with fsync-before-dispatch for intents + daily-cap accumulator.
mcp/ts/introspect.ts Method signature introspection and implicit endpoint detection + optional doc manifest support.
mcp/ts/format.ts Envelope formatting, projection helpers, info stripping, and size-budget truncation.
mcp/ts/factory.ts Server factory wiring pools/safety/journal/tools with elicitation support.
mcp/ts/errors.ts CCXT error mapping into stable error envelopes + UNKNOWN_OUTCOME semantics for mutating timeouts.
mcp/ts/config.ts Config loading/validation (file + env), tier gating, and permissions checks.
mcp/ts/ccxt-loader.ts Dual-mode ccxt loading (installed package or monorepo TS sources).
mcp/test/unit/safety.test.ts Unit tests for safety rails (caps, allow/deny lists, confirmation tokens, locks).
mcp/test/unit/redact.test.ts Unit tests for secret redaction behavior.
mcp/test/unit/introspect.test.ts Unit tests for implicit method detection and signature parsing helpers.
mcp/test/unit/format.test.ts Unit tests for projections/truncation invariants and JSON validity.
mcp/test/unit/config.test.ts Unit tests for config parsing/validation and env behavior.
mcp/test/schema/tools.test.ts Tool-list/schema snapshot tests verifying tier exposure and credential-param absence.
mcp/test/integration/stdio.test.ts Real stdio integration test against compiled server output.
mcp/test/helpers/fake-ccxt.ts Fake CCXT module/exchange used to make tests deterministic and offline.
mcp/server.json MCP registry descriptor for io.github.ccxt/ccxt-mcp.
mcp/scripts/sync-versions.mjs Keeps server/manifest versions in sync with mcp/package.json.
mcp/scripts/build-mcpb.mjs Builds an .mcpb desktop bundle from compiled output and runtime deps.
mcp/scripts/build-doc-manifest.mjs Generates a method-doc manifest from monorepo JSDoc for describe_method.
mcp/README.md Sub-package README documenting usage, config, tiers, and development commands.
mcp/plugin/README.md Claude Code plugin README for installing the MCP server + skill.
mcp/plugin/.claude-plugin/plugin.json Claude Code plugin manifest wiring MCP server install + bundled skill directory.
mcp/package.json ccxt-mcp package metadata, scripts, dependencies, and bin entrypoint.
mcp/mcpb/manifest.json Desktop bundle manifest and user-config fields for MCPB distribution.
mcp/DESIGN.md Contributor-facing design/decision log and prior art analysis.
mcp/.npmignore Excludes sources/tests/scripts/bundling artifacts from npm publish output.
install-skills.sh Adds ccxt-mcp to the installable skill set.
build/wiki-to-fumadocs.ts Adds MCP page routing and navigation entry to docs-site conversion.
.gitignore Ignores mcp/ build output and MCPB staging artifacts.
.github/workflows/mcp.yml CI workflow to build/test/pack-check the mcp/ sub-package on PRs and pushes.
.github/workflows/mcp-release.yml Release workflow to publish ccxt-mcp to npm with provenance via OIDC.
.claude/skills/ccxt-mcp/SKILL.md Adds an AI skill describing how to install/configure/use the MCP server tools.
.claude-plugin/marketplace.json Adds marketplace entry for the ccxt-mcp Claude Code plugin distribution.
Files not reviewed (1)
  • mcp/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mcp/ts/markets.ts
Comment thread mcp/ts/markets.ts
Comment thread mcp/ts/tools/read.ts
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 19:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Live data over ccxt.pro, modelled as subscribe + poll since MCP has no
model-visible push. watch_subscribe starts a background watch* loop, watch_read
drains incremental updates since a cursor, watch_unsubscribe stops and releases
the socket, watch_list shows active streams.

- Ref-counted stream instance pool (separate from the LRU REST pool so watched
  sockets are never evicted); private streams reuse the per-account instance;
  newUpdates mode for incremental updates.
- Bounded ring buffer (200) with monotonic seq + projected/trimmed updates
  (order-book depth, ohlcv/trade/order batches, info stripped); read cap 50.
- Idle-TTL auto-stop (10 min, unref'd), 25-subscription cap, 5-error give-up
  with the error surfaced on the next read; loop stops promptly via a race
  against a stop signal; clean shutdown closes all streams.
- call_read_method now points watch* at the watch tools. Registered in the
  market tier (public streams keyless; private need an account).
- Verified live against Binance (concurrent watchOHLCV/watchTicker/watchTrades);
  63 tests incl. subscribe->read->cursor->unsubscribe lifecycle + socket release.
…n apply

The 'Apply suggestions from code review' commit improved the markets cache
key (namespace + marketType) but removed the enclosing
if (!forceRefresh && fs.existsSync(marketsPath)) guard while keeping its
closing brace, orphaning it and breaking loadMarketsWithCache. Restore the
guard so forceRefresh skips the cache and a missing cache file falls through
cleanly.
Copilot AI review requested due to automatic review settings July 28, 2026 12:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Comments suppressed due to low confidence (3)

mcp/ts/types.ts:107

  • accountSummary() returns allowedSymbols but not deniedSymbols, so callers can’t see the configured denylist via list_accounts/get_safety_status even after AccountSummary includes it.
    mcp/ts/subscriptions.ts:272
  • watch_subscribe passes params through SubscriptionRegistry.buildWatchArgs, but buildWatchArgs currently just appends the params object without padding missing optional positional args. For watch* methods with optional args before params (e.g. watchOHLCV(symbol, timeframe?, since?, limit?, params?)), calling watch_subscribe with only args: [symbol] and a non-empty params will shift the params object into the timeframe slot and break the call.
    mcp/ts/types.ts:65
  • AccountSummary omits deniedSymbols, even though AccountConfig supports it and safety enforcement uses allow/deny lists. That means list_accounts/get_safety_status can’t show the effective denylist to the user, which makes it harder to audit why certain symbols are being rejected.

This issue also appears on line 103 of the same file.

Fixes found by an adversarial review of the streaming code:

- BLOCKER: watch_read drained newest-first and advanced the cursor to the max
  seq, silently skipping older buffered updates on fast streams. Now drains
  oldest-first, returns nextCursor + moreBuffered so clients walk the buffer
  without skipping or replaying.
- Socket/slot leaks: a stream that dies on a fatal/too-many-errors loop now
  releases its socket immediately and sets a 30s reaper (was held until the
  10-min idle TTL); acquirePublicStream releases the ref if the markets load
  fails; buildWatchArgs failures release too.
- Concurrency: the 25-subscription cap is now reserved synchronously across
  the acquire await (was check-then-act); the error-backoff sleep races the
  stop signal so a stopped loop exits promptly (was up to 30s).
- Private streams (watchOrders/watchMyTrades/watchBalance/watchPositions) are
  rejected up front without an account (was a per-update auth error + the
  cleanest leak vector); marketType now routes via params.type on private
  streams; unWatch is guarded so it can't tear down a sibling's stream.
- DX: stream errors carry {code, retryable, hint}; private streams return
  account/environment; watch_read documents the nextCursor replay footgun;
  watch_subscribe points at describe_method for per-method args.

Verified live against Binance (oldest-first cursor walk is gap-free on a busy
trade stream; private-account guard); 66 tests incl. the blocker, fatal-error
socket release, and private-account guard.
Copilot AI review requested due to automatic review settings July 28, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Comments suppressed due to low confidence (3)

mcp/ts/types.ts:64

  • AccountSummary omits deniedSymbols, but Safety.checkSymbolAllowed can reject orders based on deniedSymbols. Since get_safety_status surfaces AccountSummary, the user currently can’t see (or debug) a denylist that is actively enforced.
    mcp/ts/types.ts:107
  • accountSummary() also drops deniedSymbols from the returned object. This makes get_safety_status incomplete relative to the safety model (symbol allow/deny lists).
    README.md:927
  • Spelling/grammar: "environment varibales eg" should be "environment variables e.g.".
You can easily provide API keys by setting them as environment varibales eg: `BINANCE_APIKEY="XXXX"` or adding them to the config file located at `$CACHE/config.json`

Address two review points:

- watch_read now matches the data semantics. ccxt.pro maintains a per-channel
  cache, so STATE streams (ticker/orderBook/ohlcv/balance/positions) return the
  CURRENT full snapshot in `latest` (what the agent actually wants — e.g. the
  live order book — not an oldest-first replay of deltas), with updatesSinceRead
  for freshness and no history buffer. EVENT streams (trades/myTrades/orders)
  keep the oldest-first cursor log in `events`. `streamKind` tells the agent
  which it is; watch_subscribe/read/list surface it.
- The 25-subscription cap was an arbitrary magic number. It is now
  settings.maxSubscriptions (default 100) — a runaway/injection backstop, not a
  socket ceiling (subs on one exchange share a socket).

Verified live against Binance: watchOrderBook returns the current 20-level book
snapshot (updatesSinceRead reports churn), watchTicker returns the current
price, watchTrades returns the event log + cursor. 66 tests.
Copilot AI review requested due to automatic review settings July 28, 2026 17:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Comments suppressed due to low confidence (3)

mcp/ts/tools/market.ts:556

  • buildArgs is typed to only accept primitive positional args, but call_read_method/watch tools need to support arrays/objects as arguments too (the coercion logic already safely passes non-null non-string values through). Widening this type avoids TypeScript forcing the tool schemas back to primitives.
    mcp/ts/tools/watch.ts:18
  • The watch_subscribe "args" schema only allows primitive values, but several ccxt.pro watch* methods accept arrays/objects as positional args (e.g., watchTickers([symbols]) or exchange-specific argument objects). With the current schema those valid calls are impossible.
    mcp/ts/tools/market.ts:448
  • call_read_method is intended as an escape hatch for the long tail of fetch*/load* methods, but its "args" schema only permits primitives. Many unified methods legitimately take arrays/objects as positional args, so callers cannot use them through this tool even though they are read-only.

This issue also appears on line 552 of the same file.

Comment thread mcp/ts/tools/trade.ts
Comment on lines +48 to +53
const byCost = args.amount === undefined;
if (byCost && args.side === 'sell' && !exchange.has?.['createMarketSellOrderWithCost']) {
// there is no safe emulation: passing cost in the amount slot would SELL
// that many BASE units (the buy-side flag convention is buy-only in ccxt)
return { 'ok': false, 'error': { 'code': 'NOT_SUPPORTED', 'message': exchange.id + ' has no createMarketSellOrderWithCost — sell-by-cost cannot be safely emulated', 'retryable': false, 'hint': 'compute the base amount from the current price (get_tickers) and pass "amount" instead' } };
}
Answering 'why is a max even needed?': streaming is read-only and single-user,
subs on one exchange share a socket, and the idle TTL + exchange-side stream
limits are the real backstops — a hard count cap only nannies a normal case.
settings.maxSubscriptions now defaults to 0 (unlimited); a positive value is an
optional runaway backstop. Verified live on Binance demo trading (balance,
create/confirm/cancel order round-trip, private watchOrders stream capturing
open+canceled events, notional cap enforced, zero credential leakage).
…atch_read

Streaming redesign so "state" streams behave like a live snapshot the agent
reads on demand, and to add a push-like wait:

- State streams now cover every snapshot method (watchTicker(s),
  watchOrderBook(ForSymbols), watchOHLCV(ForSymbols), watchBalance,
  watchPositions, watchBidsAsks, watchMarkPrice(s), watchFundingRate(s)). Under
  newUpdates=true a watch* returns only the changed subset each cycle; instead of
  storing that subset (which made a partial delta look like the whole set), each
  subscription keeps a merge accumulator and watch_read returns the coherent full
  snapshot in `latest`:
    - replace     — single whole object (ticker/book/balance)
    - mergeDict   — by symbol (tickers/bidsAsks/markPrices)
    - upsertArray — by symbol+side (positions; a closed position flattens to 0,
                    it does not vanish)
    - mergeBook   — by symbol (orderBookForSymbols)
    - window      — timestamp-keyed candle window (ohlcv), so closed candles are
                    retained, not overwritten
  Only true logs (trades/orders/myTrades/liquidations) remain event streams.
- watch_read gains waitForChange (+ timeoutMs): blocks until the next update
  lands, the stream stops, or the timeout elapses — a long-poll for slow event
  streams (order fills, position changes) instead of spin-polling. Response
  carries waited/timedOut.

Live-verified on Binance (multi-symbol watchTickers returns all subscribed
symbols after partial deltas, watchOHLCV window, waitForChange returns on the
next trade, watchOrderBook full book) + offline tests for merge and blocking.
Copilot AI review requested due to automatic review settings July 29, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 57 out of 60 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Comments suppressed due to low confidence (2)

mcp/ts/types.ts:108

  • accountSummary() should include deniedSymbols so get_safety_status accurately reports both allow- and deny-lists that are enforced by the safety layer.
    mcp/ts/types.ts:65
  • AccountSummary/accountSummary omit deniedSymbols even though the config supports it and Safety.checkSymbolAllowed enforces it. This means get_safety_status/list_accounts cannot surface an active denylist to the user, which is safety-relevant configuration.

This issue also appears on line 103 of the same file.

An independent agent drove the server as a first-time consumer and flagged:

- get_orderbook took "depth" while every sibling (get_ohlcv/get_trades) and
  ccxt's own fetchOrderBook take "limit", so get_orderbook {limit:3} silently
  returned 20 levels. Now accepts "limit" (primary) with "depth" as an alias.
- call_implicit_get's description said "endpoint" but the parameter is "method",
  causing a failed first call. Description now names the "method" argument
  explicitly and points at describe_method.
- watch_read didn't document that "latest"'s shape varies by method (single
  object vs symbol-keyed dict vs nested-by-timeframe for ohlcv), nor that each
  "events" element wraps a per-tick batch under "data". Both are now spelled out.
- Re-subscribing an identical stream opened a duplicate socket. watch_subscribe
  is now idempotent: an identical active (exchange, method, args, account) stream
  is reused (returns the same subscriptionId with reused:true).
@pcriadoperez
pcriadoperez marked this pull request as draft July 29, 2026 10:36
… from review

Adversarial correctness + performance review of the streaming subsystem.

Correctness:
- BLOCKER: multi-symbol snapshots (mergeDict) forced every item through
  TICKER_FIELDS, silently blanking markPrice/indexPrice (watchMarkPrices) and
  fundingRate (watchFundingRates) — their data isn't in TICKER_FIELDS — and the
  top-of-book sizes for watchBidsAsks. Now projected per method: ticker fields for
  tickers, +bidVolume/askVolume for bidsAsks, and the full unified structure for
  mark-price/funding-rate.
- Idempotent re-subscribe ignored the pool namespace, so a public re-subscribe
  differing only by marketType/prediction reused the wrong stream. findActive now
  includes streamKey (which encodes exchange|marketType|prediction).
- releaseTransport could unWatch a whole channel and disrupt a sibling private
  stream on the same account+method with different args; the sibling check now
  matches account+method only.
- A waitForChange parked when the stream is unsubscribed/idle-stopped returned
  SUBSCRIPTION_NOT_FOUND instead of a terminal read; it now reads the captured
  record (active:false + last state).
- isBenignStreamClose now recognizes WebSocket close-before-connect / teardown
  messages, so rapid subscribe/unsubscribe churn no longer logs at error level.

Performance (the review's key finding: CPU/memory scaled with tick rate, not
read rate):
- State snapshots are no longer rebuilt/re-projected/re-sorted on every socket
  tick. mergeDelta stores raw ccxt refs O(delta) on the tick path; the full
  snapshot is materialized lazily in buildSnapshot at read time. Turns a fast
  OHLCV/positions stream from O(state)/tick to O(delta)/tick.
- runLoop no longer races the persistent stopPromise every tick (which leaked a
  reaction/tick = O(ticks) closures on a long-lived stream). One reaction wakes
  the current per-iteration wait; the error backoff is interruptible the same way.
- OHLCV window Map is now bounded at merge time (was only slicing the output, so
  the retained Map grew unbounded for the session).
- notifyWaiters no longer allocates on the hot path when nobody is blocked;
  timed-out waitForChange waiters are removed immediately instead of leaking.

Deliberately not changed: no default subscription cap (owner's call); event-read
binary search (per-read micro-opt); positions closed-entry retention (matches
ccxt's own cache semantics, and per-tick cost is now gone).
…ed-with-no-symbol

An arbitrage-scout agent hit a footgun: passing a mis-named symbols:['BTC/USDT']
to watch_subscribe watchOrderBook was silently dropped and started a stream with
args:[] — a zombie that polled empty forever (active:true, updates:0, error:null
on okx/bybit/kraken, no signal anything was wrong).

- watch_subscribe now accepts "symbol" (single-symbol streams) and "symbols"
  (multi-symbol streams) as convenience aliases, folded into the positional args
  when args is omitted, so the natural shape works instead of being ignored.
- A symbol-required stream (watchTicker/watchOrderBook/watchTrades/watchOHLCV)
  subscribed with no symbol now fails fast with BAD_STREAM_REQUEST + a hint
  pointing at args:["BTC/USDT"], instead of creating a no-data stream.

Verified live: symbols:['BTC/USDT'] on okx/bybit watchOrderBook now produces data;
a missing symbol is rejected up front.
…blank query

Two DX defects in describe_method's search path:
- unifiedMethods listed every truthy `has` key, but `has` is a capability-flag
  map, not a method list — so non-method flags (publicAPI, privateAPI, sandbox,
  spot, margin, swap, future, option, ws, …) leaked into the "unified" results
  with description:null. Now filtered to keys that are actual functions on the
  instance. (Also cleans up describe_exchange's methods list.)
- an empty/whitespace query token-matched everything ([].every() === true) and
  dumped the first 25 names (starting with exactly those flags). A blank query is
  now rejected with BAD_REQUEST, like the no-argument case.

Verified live on binance: query:"ticker" -> 7 real methods, zero flags; empty
query -> BAD_REQUEST; exact method mode unchanged.
…dit findings)

A verified re-audit of the newly-added multi-exchange feature (no credential
leak found — the keys-never-in-context guarantee holds) surfaced:

- SECURITY (over-permissioning): a global CCXT_MCP_TRADING=live + MAX_ORDER_VALUE
  fell back onto every EXTRA env slot, silently arming live trading on an exchange
  the user only added to read. Live trading and its cap are now scoped PER SLOT
  (slot 1's suffix is '' so it stays the global — backwards compatible); safe
  toggles (sandbox/demo/prediction) still fall back to the global form switch.
- an env/.mcpb-form account shadowed by a same-named config-file account was
  dropped SILENTLY (form keys unused, no warning, secret not redacted). It now
  emits a config problem and registers the shadowed account's secrets with the
  redactor regardless.
- flag() now treats an empty string as unset (an unfilled optional form field no
  longer overrides the global toggle), and an unsubstituted "${...}" MCPB template
  in an exchange slot is treated as "slot unset" (no spurious unknown-exchange
  warning), mirroring the CCXT_MCP_CONFIG guard.
- CI: the versioned .mcpb release is now immutable — a re-run fails loudly instead
  of --clobber-overwriting an archived artifact (the moving "latest" still moves).
- watch_subscribe: watchOHLCVForSymbols is excluded from the symbol/symbols
  convenience fold (its [symbol,timeframe] pairs can't be expressed as a flat list).
@frosty00 frosty00 removed the ai label Aug 2, 2026
@pcriadoperez
pcriadoperez marked this pull request as ready for review August 2, 2026 17:57
Copilot AI review requested due to automatic review settings August 2, 2026 17:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 58 out of 61 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Suppressed comments (3)

mcp/README.md:123

  • The README says the Claude Desktop .mcpb install form configures “one exchange”, but the shipped mcpb manifest exposes three exchange slots (Exchange 1/2/3). This mismatch can confuse users who expect only one inline slot.
**Claude Desktop (`.mcpb` bundle):** the install form configures one exchange. To use **several** exchanges (or enable live trading/withdrawals), set the **Config file** field to a `config.json` with an `accounts` map like the one above — or just create it at the default path and leave the form fields blank.

.claude/skills/ccxt-mcp/SKILL.md:87

  • This skill says disabled tiers “do not appear in the tool list”, but the server registers all tiers by default (unless settings.hideDisabledTools is enabled). Update this to match actual default behavior so assistants/users don’t misdiagnose missing-vs-disabled tools.
Tiers can **only** be enabled by the user editing the config file — never from the conversation, and there is no tool that edits config. Disabled tiers do not appear in the tool list. `"trading": true` works on **sandbox/demo accounts only**; live trading requires typing `"trading": "live"` plus a `maxOrderValue`. Order-placing tools preview and require confirmation, validate against market limits and your caps, and journal every mutating call.

mcp/README.md:17

  • The README claims disabled capability tiers “do not even appear in the tool list”, but the server code registers all tiers by default (with execution gated at call time). This is inconsistent with the actual behavior and can mislead users debugging why they can see tools but get “configure an account / enable tier” errors.
- **Capability tiers are opt-in and file-owned.** Out of the box the server does public market data only. Configuring an account enables private reads. `trading`, `funds` (withdraw/transfer), and `implicitWrites` (raw endpoints) must each be enabled per account **in the config file** — there is no tool that edits config, so a conversation can never grant itself permissions. Unregistered tiers do not even appear in the tool list.

create_order derived byCost purely from `amount === undefined`, so a limit (or
any non-market) order sent with `cost` and no `amount` would silently take the
by-cost path and dispatch a MARKET-by-cost order — the wrong order type. Now a
non-market order missing `amount` is rejected with an actionable error, and a
by-cost order missing `cost` is rejected too.
Copilot AI review requested due to automatic review settings August 2, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 58 out of 61 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Suppressed comments (5)

mcp/README.md:17

  • This security-model bullet says disabled tiers "do not even appear in the tool list", but the server currently registers all tool tiers by default (unless settings.hideDisabledTools is enabled). This is a documentation/behavior mismatch and could cause users to assume unavailable tools are impossible to call rather than merely gated at execution time.
- **Capability tiers are opt-in and file-owned.** Out of the box the server does public market data only. Configuring an account enables private reads. `trading`, `funds` (withdraw/transfer), and `implicitWrites` (raw endpoints) must each be enabled per account **in the config file** — there is no tool that edits config, so a conversation can never grant itself permissions. Unregistered tiers do not even appear in the tool list.

.claude/skills/ccxt-mcp/SKILL.md:87

  • This section states "Disabled tiers do not appear in the tool list", but the implementation lists all tiers by default (and relies on handler-time gating), with hideDisabledTools as an optional setting. The skill should match the actual default behavior so users/admins configure it correctly.
Tiers can **only** be enabled by the user editing the config file — never from the conversation, and there is no tool that edits config. Disabled tiers do not appear in the tool list. `"trading": true` works on **sandbox/demo accounts only**; live trading requires typing `"trading": "live"` plus a `maxOrderValue`. Order-placing tools preview and require confirmation, validate against market limits and your caps, and journal every mutating call.

mcp/ts/tools/read.ts:48

  • The zero-balance check uses truthiness (!row.total/!row.free/!row.used). If an exchange returns numeric strings like "0" (truthy), those zero balances will be treated as non-zero and included even when nonzeroOnly is left at its default. Using numeric coercion avoids this misclassification.
    mcp/README.md:123
  • This says the .mcpb install form configures only one exchange, but the bundled manifest exposes 3 exchange slots (CCXT_MCP_EXCHANGE/_2/_3, etc.). Updating this avoids under-selling the bundle and matches the actual configuration surface.
**Claude Desktop (`.mcpb` bundle):** the install form configures one exchange. To use **several** exchanges (or enable live trading/withdrawals), set the **Config file** field to a `config.json` with an `accounts` map like the one above — or just create it at the default path and leave the form fields blank.

.claude/skills/ccxt-mcp/SKILL.md:75

  • This states the .mcpb install form is single-exchange, but the actual bundle manifest supports up to 3 inline exchange slots. The skill should match what users will see in the installer UI.
On **Claude Desktop** the `.mcpb` install form has fields for a single exchange plus a **Config file** picker — point that at a `config.json` (as above) to configure multiple exchanges, or create the file at the default path and leave the form blank.

From a real user's leveraged-trading + 25-exchange streaming session; each finding
verified against current code (TDD).

- P0 stop/trigger orders: create_order attaches meta.triggerOrder + a caveat note
  that conditional orders may not appear in get_orders even when live (many venues
  keep them on a separate query endpoint) — so an agent confirms before trusting a
  stop as protection. Warn-only: NO extra API call (ccxt already throws on a
  rejected order, so an accepted create is real). meta.clientOrderId now falls back
  to the returned order's id instead of null.
- P1 burst concurrency: a semaphore (bounded by UV_THREADPOOL_SIZE, default 4)
  gates cold loadMarkets, so a burst of watch_subscribe across many exchanges no
  longer starves DNS/threadpool (was 1/19, now 12/12 live). A local
  ENOTFOUND/EAI_AGAIN/ECONNRESET/ETIMEDOUT is reported as a local network/DNS
  failure, not "the exchange is under maintenance".
- P1 dead streams: an errored stream stays listable/readable for the retention
  window; a later read returns SUBSCRIPTION_FAILED with the real error instead of a
  misleading "idle-expired (10 min)" NOT_FOUND.
- P2 get_safety_status: no longer says "public market tier only" when accounts are
  configured via env vars (which contradicted tiers.trading:true).
- P3 watch_read: optional depth param (default 20, depth:1 = top of book) for cheap
  many-venue comparison.
- Docs: create_order description + SKILL.md state the confirm policy honestly (live
  = only live accounts; sandbox/demo execute without a preview; null cap = no cap).

99 offline tests; live-verified burst (12/12) + depth on Binance.
Copilot AI review requested due to automatic review settings August 3, 2026 00:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 59 out of 62 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • mcp/package-lock.json: Generated file
Suppressed comments (1)

mcp/ts/pools.ts:214

  • The authenticated exchange pool currently pulls missing required credentials from ambient <EXCHANGE>_<CREDENTIAL> environment variables unconditionally. This bypasses the documented opt-in (settings.loadEnvKeys / CCXT_MCP_LOAD_ENV_KEYS) and can accidentally arm an account with real keys that happen to be present in the user's environment, even when they intended config-only behavior.

@carlosmiei
carlosmiei merged commit 6ac59d3 into ccxt:master Aug 22, 2026
9 of 15 checks passed
@carlotestor

Copy link
Copy Markdown
Collaborator

Merge digest

Lands the official ccxt-mcp MCP server as a standalone mcp/ sub-package with tiered opt-in tools, execution safety rails, and its own publish/CI pipeline.

File Δ What
.claude/skills/ccxt-mcp/SKILL.md +121/−0 ccxt-mcp agent skill
.github/workflows/mcp-release.yml +89/−0 npm publish --provenance
.claude-plugin/marketplace.json +40/−0 plugin marketplace entry
.github/workflows/mcp.yml +36/−0 mcp build + test CI
mcp/.npmignore +8/−0 publish excludes
.gitignore +5/−0 mcp build artifacts
flowchart LR
  mcp_release_yml["mcp-release.yml"]
  README["README"]
  marketplace_json["marketplace.json"]
  manifest_json["manifest.json"]
  mcp_yml["mcp.yml"]
  SKILL["SKILL"]
  install_skills_sh["install-skills.sh"]
  _npmignore[".npmignore"]
  DESIGN["DESIGN"]
  package_json["package.json"]
  marketplace_json --> manifest_json
  mcp_release_yml --> package_json
  mcp_yml --> package_json
  README --> README
  install_skills_sh --> SKILL
  _npmignore --> DESIGN
  DESIGN --> README
  README --> DESIGN
  package_json --> README
Loading

mcp/ts/subscriptions.ts

@@ -0,0 +1,830 @@
+import crypto from 'crypto';
+import { redact } from './redact.js';
+import { project, stripInfo, TICKER_FIELDS, TRADE_FIELDS, ORDER_FIELDS, POSITION_FIELDS } from './format.js';
+import { accountEnvironment } from './types.js';
+import { log } from './logging.js';
+const BUFFER_MAX = 200;             // updates retained per EVENT subscription (ring buffer)
+const READ_MAX = 50;                // event updates returned per watch_read
+const OHLCV_WINDOW = 200;           // candles retained per (symbol, timeframe) in a snapshot
+const IDLE_TTL_MS = 10 * 60 * 1000; // stop a stream with no watch_read for 10 minutes
+const DEAD_TOMBSTONES_MAX = 100;    // cap on retained failure records for reaped streams
+const MAX_CONSECUTIVE_ERRORS = 5;   // give up a stream after this many back-to-back errors
+const WAIT_DEFAULT_MS = 25 * 1000;  // default block for waitForChange
+const WAIT_MAX_MS = 55 * 1000;      // clamp — stay under typical host tool-call timeouts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants