feat(mcp): official CCXT MCP server - #29277
Conversation
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.
There was a problem hiding this comment.
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.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.
There was a problem hiding this comment.
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
allowedSymbolsbut notdeniedSymbols, 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
paramsthrough SubscriptionRegistry.buildWatchArgs, but buildWatchArgs currently just appends the params object without padding missing optional positional args. For watch* methods with optional args beforeparams(e.g. watchOHLCV(symbol, timeframe?, since?, limit?, params?)), calling watch_subscribe with onlyargs: [symbol]and a non-emptyparamswill shift the params object into thetimeframeslot 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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).
… 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.
…undle via GitHub Releases
…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).
…setup errors (hideDisabledTools opt-out)
…ID>_<CRED> env vars (ccxt --loadKeys parity)
There was a problem hiding this comment.
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.hideDisabledToolsis 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.
There was a problem hiding this comment.
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
.mcpbinstall 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
.mcpbinstall 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.
There was a problem hiding this comment.
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.
Merge digestLands the official
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
@@ -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 |
Summary
The first official CCXT MCP (Model Context Protocol) server — lives in
mcp/, published to npm asccxt-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 (ownpackage.json, tsc build, independently versioned). Two commits: the server, then distribution/CI (MCPB bundle, registry manifest, Claude Code plugin, workflows, skill).Highlights
"trading": trueis sandbox-only, live requires an explicit"live"+ a user-authored cap.infostripped by default.tools/listschema snapshot + real-stdio integration). Docs:mcp/README.md,mcp/DESIGN.md, andwiki/MCP.md(wired into the docs site).Release / publish — maintainer steps
npm — automated. Bump
mcp/package.json, runnpm run sync-versions(alignsmanifest.json+server.json), commit, then dispatch Actions → “MCP Release” (.github/workflows/mcp-release.yml): it builds, tests, and runsnpm publish --provenance(Node 24, maintainer-gated) exactly like the main release. One-time setup: configure an npm trusted publisher for theccxt-mcppackage → repoccxt/ccxt, workflowmcp-release.yml(OIDC, same model as the ccxt release). The very first publish may need a manualcd mcp && npm run publishPackage(or a temporaryNPM_TOKEN) until the package exists and the trusted publisher can be attached.MCP registry (after the npm publish):
server.jsonis ready (io.github.ccxt/ccxt-mcp). The registry is in preview and versions are immutable, so itsversionmust equal the published npm version (kept in sync bysync-versions.mjs).MCPB bundle (.mcpb) for Claude Desktop distribution:
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/ccxtthen/plugin install ccxt-mcp@ccxt.Not yet done (why this is a draft)
AUTH_FAILED); the rails, preview/confirm flow, and journal are verified, but a real testnet fill/cancel hasn't been run.watch_*streaming tools (designed inDESIGN.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 (seemcp/DESIGN.md). Diff ismcp/**plus root wiring (package.jsonscripts,.gitignore,README.mdsection,wiki/MCP.md+ converter,install-skills.sh, marketplace).