Skip to content

feat(labs): Hook B — labs-trading-data MCP service + openclaw wiring - #137

Merged
mashkovd merged 5 commits into
mainfrom
feat/labs-trading-data
May 6, 2026
Merged

mashkovd merged 5 commits into
mainfrom
feat/labs-trading-data

Conversation

@mashkovd

@mashkovd mashkovd commented May 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Phase 2 Hook B for labs-trading-intel. Closes the Bybit + Bitget public-API CVD coverage gap left by Phase 1.6 (PR #127, inline indicators) and Hook A (PR #131, Binance + OKX). New stateful MCP service exposes 6 tools: 3 WS-aggregated taker (Bybit/Bitget/aggregated) + 3 REST helpers (CryptoCompare News, Etherscan gas, Etherscan whale tx).

Two-commit layout per the Hook B plan (~/.claude/plans/golden-percolating-seal.md); a third commit bumps the image to 0.2.0 alongside the news-upstream swap (CryptoPanic free tier was discontinued April 2026 → CryptoCompare News v2).

  1. Area B — services/labs/trading-data/{values,catalog-info}.yaml deploys the new pod via the base-service chart. NetworkPolicy locks ingress to labs-openclaw only. replicaCount=1 is a hard invariant (in-process WS state).
  2. Area C — services/labs/openclaw/values.yaml adds the labs-trading-data-client ExternalSecret, the trading-data-mcp-proxy.js stdio bridge, and the trading-data MCP server registration in openclaw.json.
  3. Bump — services/labs/trading-data/values.yaml to image tag 0.2.0 + Vault path rename to cryptocompare.

Image already published from PR-source repo: ghcr.io/mctlhq/mctl-trading-data:0.2.0 (mctlhq/mctl-trading-data tag 0.2.0).

Coinglass tier stays HOBBYIST per operator decision; Coinglass paid tools (heatmap / whale Hyperliquid) are deliberately out of scope.

Pre-merge operator action required

ESO will fail to materialize the new Secrets until Vault is populated. Do NOT merge until the operator has written:

  • secret/teams/labs/trading-data/cryptocompare field api_key — CryptoCompare News free-tier key (cryptocompare.com/cryptopian/api-keys; ~100K calls/month)
  • secret/teams/labs/trading-data/etherscan field api_key — Etherscan free-tier key (etherscan.io/myapikey)
  • secret/teams/labs/trading-data/client-token field bearer_token — openssl rand -base64 48

Once the gitops PR merges, ArgoCD reconciles in ~3 min, ESO syncs Vault on its 1h refreshInterval (or force-sync), pod becomes Ready, openclaw Recreate restarts and picks up the new MCP server. Expect transient FailedCreate during openclaw Recreate per feedback_openclaw_recreate_quota_deadlock — self-heals within 2-3 min, do not abort.

Test plan

  • Operator pre-flight: Vault paths above provisioned + free-tier API keys obtained
  • mctl-trading-data 0.2.0 PR-validation + docker-publish workflows green (mctlhq/mctl-trading-data)
  • @codex review clean (or copilot clean — merge gate per feedback_pr_codex_review)
  • After merge + ~3 min: kubectl -n labs get pod -l app.kubernetes.io/instance=labs-trading-data Ready 1/1
  • kubectl -n labs exec deploy/labs-openclaw -- curl -sX POST -H "Authorization: Bearer $TRADING_DATA_TOKEN" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' http://labs-trading-data:8080/mcp returns 6 tools (bybit_taker_24h, bitget_taker_24h, aggregated_taker_24h, news_recent, etherscan_gas_snapshot, etherscan_whale_transactions)
  • Auth negative: same curl without Bearer → HTTP 401
  • NetworkPolicy: same curl from admins-openclaw pod → connection timeout
  • Cold-start window: bybit_taker_24h ETHUSDT returns meta.window_incomplete=true for first hour
  • Skill push (separate, mctl_save_openclaw_skill labs eth-trading-intel): /eth ETH Telegram smoke shows cvd_24h.per_exchange.{binance,okx,bybit,bitget} with N-leg agreement cascade and a populated news_score from CryptoCompare

mctl-deploy added 2 commits May 6, 2026 22:04
…ws/onchain

Adds the labs-trading-data service deployed via the base-service chart:

  - 6-tool MCP surface: bybit_taker_24h / bitget_taker_24h / aggregated_taker_24h
    plus cryptopanic_recent_news / etherscan_gas_snapshot /
    etherscan_whale_transactions
  - Stateful WS workers with rolling 24x1h taker buckets (replicaCount=1
    is a hard invariant — buckets live in process memory)
  - NetworkPolicy restricts ingress to labs-openclaw pods only
  - ExternalSecrets pull free-tier API keys + bearer token from
    secret/data/teams/labs/trading-data/{cryptopanic,etherscan,client-token}

Closes the public-API CVD coverage gap left by Phase 1.6 + Phase 2 Hook A
(Binance + OKX inline). Bybit + Bitget have no usable public REST taker
endpoints (see reference_multi_exchange_taker_endpoints.md), so a stateful
WS service is the only path to extend CVD beyond two venues without paying
for CoinGlass STANDARD tier.

Image: ghcr.io/mctlhq/mctl-trading-data:0.1.0 (mctlhq/mctl-trading-data 0.1.0)

This commit is Area B of the Hook B plan; Area C (openclaw consumer wiring)
follows as a separate commit on this branch.
Adds the labs-trading-data MCP service as a stdio-bridge entry in the
openclaw configuration so the agent gets six new tools alongside the
existing mctl + coinglass servers:

  - new ExternalSecret labs-trading-data-client pulling TRADING_DATA_TOKEN
    from secret/data/teams/labs/trading-data/client-token (server-side
    counterpart in services/labs/trading-data/values.yaml shares the same
    Vault path; the two stay in sync without manual rotation)
  - envFrom -> labs-trading-data-client makes TRADING_DATA_TOKEN available
    to /scripts/trading-data-mcp-proxy.js
  - new openclaw-scripts entry trading-data-mcp-proxy.js: ~50 LoC stdio
    -> HTTP+Bearer bridge. Static token, no OAuth refresh; SSE-aware
    response framing in case the FastAPI mount returns text/event-stream
  - mcp.servers.trading-data registers the bridge in openclaw.json with
    TRADING_DATA_URL pointing at the in-cluster service

Pod restart unavoidable (setup initContainer copies openclaw.json only at
start); `strategy: Recreate` + s3-sync mirror preserves OAuth + skill state.
Transient FailedCreate during Recreate is normal under tight quota -- see
feedback_openclaw_recreate_quota_deadlock memory.

This commit is Area C of the Hook B plan; pairs with the Area B service
manifests in the previous commit on this branch.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@copilot review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2181bfbfa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1488 to +1490
"env": {
"TRADING_DATA_URL": "http://labs-trading-data.labs.svc.cluster.local:8080/mcp"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass TRADING_DATA_TOKEN to trading-data MCP process

trading-data is configured with only TRADING_DATA_URL in mcp.servers.trading-data.env, but the proxy script requires both TRADING_DATA_URL and TRADING_DATA_TOKEN and immediately returns an error when the token is missing. In this same file, the setup comment explains that the bundle-mcp path uses mcp.servers.<name>.env verbatim for the spawned process, so pod-level envFrom secrets are not sufficient here; as committed, every call to this MCP server will fail with the proxy-not-configured error instead of reaching labs-trading-data.

Useful? React with 👍 / 👎.

…ptoCompare

CryptoPanic free Developer API plan was discontinued April 2026 (paid
Growth tier starts at USD 50/week). mctl-trading-data 0.2.0 swaps the
news upstream to CryptoCompare News v2 (Apikey header auth, ~100K
calls/month free) and renames the consumer-facing surface so a future
upstream swap stays a one-file change in mctl-trading-data:

  - tool: cryptopanic_recent_news -> news_recent
  - env:  CRYPTOPANIC_API_KEY     -> NEWS_API_KEY
  - Vault path: secret/teams/labs/trading-data/cryptopanic
                -> secret/teams/labs/trading-data/cryptocompare

Operator pre-flight (described in PR #137 body) is now: provision
.../cryptocompare with field api_key (free key from
cryptocompare.com/cryptopian/api-keys), not .../cryptopanic.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

Re-requesting review after CryptoPanic→CryptoCompare swap (commit df358e8, image bump 0.1.0→0.2.0).

@codex review

@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@copilot review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df358e857c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
return;
}
process.stdout.write(text.endsWith('\n') ? text : text + '\n');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return JSON-RPC errors for invalid upstream payloads

The new trading-data-mcp-proxy.js writes the upstream HTTP body straight to stdout for all non-SSE responses, without checking for empty or non-JSON content. If labs-trading-data (or an intermediate proxy) returns an empty body or plain-text/HTML error during a transient failure, the MCP client receives an invalid frame and the original request can stall or fail with protocol parse errors instead of a proper JSON-RPC error. This proxy should validate/parse the body and emit a structured {"jsonrpc":"2.0","id":...,"error":...} reply on invalid payloads, similar to the coinglass bridge behavior.

Useful? React with 👍 / 👎.

…odex P2)

The previous proxy wrote the raw upstream HTTP body to stdout for any
non-SSE response. If labs-trading-data (or an in-cluster proxy) returns
an empty body, plain-text error, or HTML gateway page during a transient
failure, the agent's MCP client receives an invalid frame and the
original tools/call request stalls or fails with a stdio framing
exception instead of a typed JSON-RPC error.

Bridge now:

  - validates the response Content-Type
  - for SSE: skips data: frames whose payload does not parse as JSON,
    and emits an rpcError if zero valid frames came back (status code
    + frame count exposed)
  - for non-SSE: rejects empty bodies and non-JSON bodies with a
    structured rpcError including status code + content-type + a
    short body preview for debugging
  - extracts the rpcError emit into a helper for consistent shape

No behaviour change on the happy path (valid JSON / valid SSE frames
still pass through). Same protocol on the wire; only the failure modes
gained a typed error.

Codex P2 on PR #137 (review of df358e8).
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

Addressed P2 in commit 871c4d8: trading-data proxy now validates upstream payloads — empty bodies, non-JSON bodies, and SSE responses with no parseable data: frames all emit a typed {jsonrpc, id, error} reply instead of breaking stdio framing. SSE handling also drops malformed individual frames. Same protocol on the wire on the happy path.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 871c4d80a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1101 to +1103
function rpcError(id, message) {
writeLine({ jsonrpc: '2.0', id: id ?? null, error: { code: -32603, message: String(message) } });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress JSON-RPC errors for notification requests

Guard rpcError for requests without an id; otherwise this bridge emits { "id": null, ... } responses for JSON-RPC notifications. This is problematic for standard MCP notification traffic (for example notifications/initialized) when the upstream returns an empty/202 body, because the empty body path will currently write an unsolicited error frame that does not correspond to any client request and can confuse strict MCP clients.

Useful? React with 👍 / 👎.

Comment on lines +1128 to +1134
for (const part of text.split(/\r?\n/)) {
const t = part.trim();
if (!t.startsWith('data:')) continue;
const payload = t.slice(5).trim();
if (!payload) continue;
try { JSON.parse(payload); } catch (_) { continue; }
process.stdout.write(payload + '\n');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse SSE events by block, not per data: line

The SSE handler assumes each individual data: line is a complete JSON document, but SSE allows one event payload to be split across multiple data: lines that must be concatenated before parsing. With a compliant multi-line event, each fragment fails JSON.parse, emitted stays zero, and the proxy incorrectly returns no parseable SSE payload instead of forwarding the valid message.

Useful? React with 👍 / 👎.

…ex P2 r2)

Two follow-ups on PR #137 from codex round-2 review of 871c4d8:

1. JSON-RPC notifications must not receive responses
   `rpcError` previously emitted `{ id: null, error: ... }` for any
   transport failure including those triggered by notification frames
   (a JSON-RPC request with no `id` member, e.g.
   `notifications/initialized`). Strict MCP clients tracking ids treat
   that as an orphan reply and warn or disconnect. `rpcError` now
   short-circuits when `'id' not in req`, and the empty-body path
   additionally treats notifications as already-handled (a 202 Accepted
   with no body is the canonical reply).

2. SSE allows multi-line `data:` blocks per event
   The previous handler `JSON.parse`'d each `data:` line individually,
   so any compliant event whose payload was split across multiple
   `data:` lines had every fragment fail to parse, `emitted` stayed
   zero, and the proxy returned `no parseable SSE payload` for valid
   traffic. New `parseSseEvents` splits the body on blank lines (event
   block separator) and joins all `data:` lines within a block with
   `\n` before parsing, per WHATWG SSE spec. The single-space-after-
   colon convention is honoured so `data: {x:1}` and `data:{x:1}` both
   parse to the same payload.

Manual unit harness (`/tmp/test-proxy.js`) covers 11 cases including
the regression: notifications, id=null, id=0, single-line, multi-line
concat, no-space, multiple events, comment/event lines ignored, empty
body, broken JSON dropped. All pass.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

Addressed both P2s in commit ab18b89:

  • isNotification helper ('id' not in req) suppresses rpcError for notifications; empty-body path also short-circuits on notifications (202 Accepted is canonical).
  • New parseSseEvents splits on blank-line event boundaries and joins all data: lines within a block with \n per WHATWG SSE spec.

Verified locally with an 11-case Node harness covering single-line, multi-line concat, no-space variant, multiple events, comment/event lines ignored, empty body, broken JSON dropped, and all four notification/request id shapes.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mashkovd
mashkovd merged commit e73d30c into main May 6, 2026
@mashkovd
mashkovd deleted the feat/labs-trading-data branch May 6, 2026 21:39
mashkovd pushed a commit that referenced this pull request May 6, 2026
Smoke right after PR #137 merged surfaced HTTP 404 on every tools/list
call. FastMCP defaults streamable_http_path="/mcp" inside its sub-app;
combined with `api.mount("/mcp", mcp_app)` in trading-data the public
path compounded into /mcp/mcp.

The intermediate fix (0.2.1, never deployed) set
streamable_http_path="/" so the path was /mcp/ — functional but at odds
with the rest of the MCP ecosystem (api.mctl.ai/mcp, every public
example) where the convention is bare /mcp without a trailing slash.

0.2.2 is the canonical fix: keep FastMCP's default streamable_http_path
"/mcp" and mount its sub-app at FastAPI "/" instead of "/mcp". Public
URL is now exactly /mcp. FastAPI explicit routes (/healthz, /readyz,
/openapi.json, /docs) match before the mount falls through, so there
is no route conflict.

  - bump labs-trading-data image to 0.2.2 (mctl-trading-data c7e8a37)
  - openclaw bridge URL stays at /mcp (no trailing slash)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant