tradelab 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +185 -388
  3. package/dist/cjs/index.cjs +31 -9
  4. package/dist/cjs/live.cjs +409 -7
  5. package/docs/README.md +32 -66
  6. package/docs/api-reference.md +269 -144
  7. package/docs/backtest-engine.md +167 -321
  8. package/docs/data-reporting-cli.md +114 -156
  9. package/docs/examples.md +6 -6
  10. package/docs/live-trading.md +254 -134
  11. package/docs/mcp.md +244 -23
  12. package/docs/research.md +99 -45
  13. package/examples/mcpLiveTrading.js +77 -0
  14. package/package.json +11 -3
  15. package/src/engine/optimize.js +25 -1
  16. package/src/engine/portfolio.js +4 -1
  17. package/src/live/dashboard/server.js +67 -8
  18. package/src/live/engine/paperEngine.js +5 -0
  19. package/src/live/index.js +2 -0
  20. package/src/live/session.js +402 -0
  21. package/src/mcp/liveTools.js +179 -0
  22. package/src/mcp/schemas.js +119 -0
  23. package/src/mcp/server.js +5 -1
  24. package/src/mcp/tools.js +125 -2
  25. package/templates/dashboard.html +595 -108
  26. package/types/index.d.ts +25 -0
  27. package/types/live.d.ts +99 -0
  28. package/types/mcp.d.ts +17 -0
  29. package/docs/superpowers/plans/2026-00-overview.md +0 -101
  30. package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
  31. package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
  32. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
  33. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
  34. package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
  35. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
  36. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
  37. package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
  38. package/docs/superpowers/plans/HANDOFF.md +0 -88
@@ -1,758 +0,0 @@
1
- # `tradelab/mcp` Server Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Ship an MCP server that exposes tradelab's research loop (fetch data → choose/parameterize a strategy → backtest → read metrics → walk-forward) as tools any agent (Claude Desktop, Cursor, Claude Code) can call autonomously.
6
-
7
- **Architecture:** Agents cannot pass JS closures over MCP, so strategies are **named and parameterized** through a registry (`src/strategies/`). The MCP tools are pure async functions in `src/mcp/tools.js` (unit-testable without a transport); `src/mcp/server.js` wires them to `@modelcontextprotocol/sdk`'s `McpServer` over stdio. Tool outputs are LLM-sized summaries (metrics + trade counts), never full replay frames. A `bin/tradelab-mcp.js` launches the server.
8
-
9
- **Tech Stack:** Node ESM, `@modelcontextprotocol/sdk`, `zod` for tool input schemas, `node:test`. Builds on Plan 1 (clean metrics) and Plan 4 (`backtestAsync`, optional).
10
-
11
- ---
12
-
13
- ### Task 1: Named strategy registry
14
-
15
- **Files:**
16
-
17
- - Create: `src/strategies/index.js`
18
- - Create: `src/strategies/builtins.js`
19
- - Modify: `src/index.js`
20
- - Test: `test/strategies/registry.test.js`
21
-
22
- - [ ] **Step 1: Write the failing test**
23
-
24
- ```js
25
- // test/strategies/registry.test.js
26
- import test from "node:test";
27
- import assert from "node:assert/strict";
28
- import { listStrategies, getStrategy } from "../../src/strategies/index.js";
29
-
30
- test("listStrategies returns built-ins with name, description, params", () => {
31
- const all = listStrategies();
32
- const names = all.map((s) => s.name);
33
- assert.ok(names.includes("ema-cross"));
34
- assert.ok(names.includes("rsi-reversion"));
35
- assert.ok(names.includes("donchian-breakout"));
36
- assert.ok(names.includes("buy-hold"));
37
- const ema = all.find((s) => s.name === "ema-cross");
38
- assert.equal(typeof ema.description, "string");
39
- assert.equal(typeof ema.params.fast.default, "number");
40
- });
41
-
42
- test("getStrategy returns a signalFactory producing a working signal", () => {
43
- const factory = getStrategy("ema-cross");
44
- const signal = factory({ fast: 3, slow: 5, rr: 2 });
45
- assert.equal(typeof signal, "function");
46
- const candles = Array.from({ length: 20 }, (_, i) => ({
47
- time: i * 60000,
48
- high: 101 + i,
49
- low: 99 + i,
50
- close: 100 + i,
51
- }));
52
- const out = signal({ candles, index: 19, bar: candles[19], equity: 10_000 });
53
- assert.ok(out === null || typeof out === "object");
54
- });
55
-
56
- test("getStrategy throws on unknown name with the available list", () => {
57
- assert.throws(() => getStrategy("nope"), /Unknown strategy "nope"/);
58
- });
59
- ```
60
-
61
- - [ ] **Step 2: Run to verify it fails**
62
-
63
- Run: `node --test test/strategies/registry.test.js`
64
- Expected: FAIL — cannot find module.
65
-
66
- - [ ] **Step 3: Implement src/strategies/builtins.js**
67
-
68
- ```js
69
- // src/strategies/builtins.js
70
- import { ema } from "../utils/indicators.js";
71
- import { rsi } from "../ta/oscillators.js";
72
- import { donchian } from "../ta/channels.js";
73
-
74
- // Each entry: { description, params: { name: {type, default, description} }, factory }
75
- // factory(params) => signal(context) compatible with backtest().
76
-
77
- export const BUILTINS = {
78
- "ema-cross": {
79
- description: "Long when fast EMA crosses above slow EMA; stop at recent swing low.",
80
- params: {
81
- fast: { type: "number", default: 10, description: "fast EMA period" },
82
- slow: { type: "number", default: 30, description: "slow EMA period" },
83
- rr: { type: "number", default: 2, description: "reward:risk target" },
84
- lookback: { type: "number", default: 15, description: "swing-low lookback for stop" },
85
- },
86
- factory({ fast = 10, slow = 30, rr = 2, lookback = 15 } = {}) {
87
- return ({ candles, bar }) => {
88
- if (candles.length < slow + 2) return null;
89
- const closes = candles.map((c) => c.close);
90
- const f = ema(closes, fast);
91
- const s = ema(closes, slow);
92
- const last = closes.length - 1;
93
- if (f[last - 1] <= s[last - 1] && f[last] > s[last]) {
94
- const stop = Math.min(...candles.slice(-lookback).map((c) => c.low));
95
- if (stop >= bar.close) return null;
96
- return { side: "long", entry: bar.close, stop, rr };
97
- }
98
- return null;
99
- };
100
- },
101
- },
102
-
103
- "rsi-reversion": {
104
- description: "Long when RSI dips below `oversold`; stop a fixed pct below entry.",
105
- params: {
106
- period: { type: "number", default: 14, description: "RSI period" },
107
- oversold: { type: "number", default: 30, description: "RSI entry threshold" },
108
- stopPct: { type: "number", default: 2, description: "stop distance in percent" },
109
- rr: { type: "number", default: 1.5, description: "reward:risk target" },
110
- },
111
- factory({ period = 14, oversold = 30, stopPct = 2, rr = 1.5 } = {}) {
112
- return ({ candles, bar }) => {
113
- if (candles.length < period + 2) return null;
114
- const values = rsi(
115
- candles.map((c) => c.close),
116
- period
117
- );
118
- const r = values[values.length - 1];
119
- if (r === undefined || r > oversold) return null;
120
- return { side: "long", entry: bar.close, stop: bar.close * (1 - stopPct / 100), rr };
121
- };
122
- },
123
- },
124
-
125
- "donchian-breakout": {
126
- description: "Long on a close above the prior Donchian upper channel.",
127
- params: {
128
- period: { type: "number", default: 20, description: "channel lookback" },
129
- rr: { type: "number", default: 2, description: "reward:risk target" },
130
- },
131
- factory({ period = 20, rr = 2 } = {}) {
132
- return ({ candles, bar }) => {
133
- if (candles.length < period + 2) return null;
134
- const ch = donchian(candles, period);
135
- const i = candles.length - 1;
136
- const priorUpper = ch.upper[i - 1];
137
- const priorLower = ch.lower[i - 1];
138
- if (priorUpper === undefined) return null;
139
- if (bar.close > priorUpper) {
140
- return { side: "long", entry: bar.close, stop: priorLower, rr };
141
- }
142
- return null;
143
- };
144
- },
145
- },
146
-
147
- "buy-hold": {
148
- description: "Enter once at the first eligible bar and hold for `holdBars`.",
149
- params: {
150
- holdBars: { type: "number", default: 5, description: "bars to hold before exit" },
151
- stopPct: { type: "number", default: 10, description: "protective stop distance in percent" },
152
- },
153
- factory({ holdBars = 5, stopPct = 10 } = {}) {
154
- let entered = false;
155
- return ({ bar }) => {
156
- if (entered) return null;
157
- entered = true;
158
- return {
159
- side: "long",
160
- entry: bar.close,
161
- stop: bar.close * (1 - stopPct / 100),
162
- rr: 5,
163
- _maxBarsInTrade: holdBars,
164
- };
165
- };
166
- },
167
- },
168
- };
169
- ```
170
-
171
- - [ ] **Step 4: Implement src/strategies/index.js**
172
-
173
- ```js
174
- // src/strategies/index.js
175
- import { BUILTINS } from "./builtins.js";
176
-
177
- const registry = new Map(Object.entries(BUILTINS));
178
-
179
- /** Register a custom strategy at runtime. `def` is a BUILTINS-shaped object. */
180
- export function registerStrategy(name, def) {
181
- if (typeof def?.factory !== "function") {
182
- throw new Error(`registerStrategy("${name}") requires a factory function`);
183
- }
184
- registry.set(name, def);
185
- }
186
-
187
- /** List all strategies as { name, description, params }. */
188
- export function listStrategies() {
189
- return [...registry.entries()].map(([name, def]) => ({
190
- name,
191
- description: def.description,
192
- params: def.params,
193
- }));
194
- }
195
-
196
- /** Get a strategy's signalFactory(params) => signal. Throws on unknown name. */
197
- export function getStrategy(name) {
198
- const def = registry.get(name);
199
- if (!def) {
200
- const available = [...registry.keys()].join(", ");
201
- throw new Error(`Unknown strategy "${name}". Available: ${available}`);
202
- }
203
- return def.factory;
204
- }
205
- ```
206
-
207
- - [ ] **Step 5: Export from src/index.js**
208
-
209
- ```js
210
- export { listStrategies, getStrategy, registerStrategy } from "./strategies/index.js";
211
- ```
212
-
213
- - [ ] **Step 6: Run test + suite**
214
-
215
- Run: `node --test test/strategies/registry.test.js`
216
- Expected: PASS (3 tests).
217
-
218
- Run: `node --test`
219
- Expected: PASS.
220
-
221
- - [ ] **Step 7: Commit**
222
-
223
- ```bash
224
- git add src/strategies/index.js src/strategies/builtins.js src/index.js test/strategies/registry.test.js
225
- git commit -m "feat: add named strategy registry with built-ins
226
-
227
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
228
- ```
229
-
230
- ---
231
-
232
- ### Task 2: MCP tool functions (transport-free, testable)
233
-
234
- **Files:**
235
-
236
- - Create: `src/mcp/tools.js`
237
- - Test: `test/mcp/tools.test.js`
238
-
239
- - [ ] **Step 1: Write the failing test**
240
-
241
- ```js
242
- // test/mcp/tools.test.js
243
- import test from "node:test";
244
- import assert from "node:assert/strict";
245
- import { mcpTools } from "../../src/mcp/tools.js";
246
-
247
- function buildCandles(n = 60) {
248
- const start = Date.UTC(2025, 0, 2, 14, 30, 0);
249
- return Array.from({ length: n }, (_, i) => ({
250
- time: start + i * 86_400_000,
251
- open: 100 + i,
252
- high: 101 + i,
253
- low: 99 + i,
254
- close: 100 + i + (i % 3 === 0 ? -0.5 : 0.5),
255
- volume: 1000 + i,
256
- }));
257
- }
258
-
259
- test("list_strategies returns the registry", async () => {
260
- const out = await mcpTools.list_strategies.handler({});
261
- assert.ok(Array.isArray(out.strategies));
262
- assert.ok(out.strategies.some((s) => s.name === "ema-cross"));
263
- });
264
-
265
- test("run_backtest with inline candles returns an LLM-sized metrics summary", async () => {
266
- const candles = buildCandles();
267
- const out = await mcpTools.run_backtest.handler({
268
- candles,
269
- symbol: "TEST",
270
- interval: "1d",
271
- strategy: "ema-cross",
272
- params: { fast: 3, slow: 5, rr: 2 },
273
- });
274
- assert.equal(typeof out.metrics.trades, "number");
275
- assert.equal(typeof out.metrics.profitFactor, "number");
276
- assert.equal("sharpeAnnualized" in out.metrics, true);
277
- assert.equal("replay" in out, false); // never ship replay to an agent
278
- assert.ok(out.tradesPreview.length <= 10);
279
- });
280
-
281
- test("run_backtest rejects an unknown strategy", async () => {
282
- await assert.rejects(() =>
283
- mcpTools.run_backtest.handler({ candles: buildCandles(), strategy: "ghost" })
284
- );
285
- });
286
-
287
- test("walk_forward runs a parameter grid and returns window stability", async () => {
288
- const candles = buildCandles(200);
289
- const out = await mcpTools.walk_forward.handler({
290
- candles,
291
- interval: "1d",
292
- strategy: "ema-cross",
293
- trainBars: 60,
294
- testBars: 20,
295
- mode: "anchored",
296
- grid: { fast: [3, 5], slow: [8, 13] },
297
- });
298
- assert.ok(out.windows >= 1);
299
- assert.equal(typeof out.metrics.totalPnL, "number");
300
- assert.equal(typeof out.stability.uniqueWinnerCount, "number");
301
- });
302
- ```
303
-
304
- - [ ] **Step 2: Run to verify it fails**
305
-
306
- Run: `node --test test/mcp/tools.test.js`
307
- Expected: FAIL — cannot find module.
308
-
309
- - [ ] **Step 3: Implement src/mcp/tools.js**
310
-
311
- ```js
312
- // src/mcp/tools.js
313
- import { backtest } from "../engine/backtest.js";
314
- import { walkForwardOptimize } from "../engine/walkForward.js";
315
- import { getHistoricalCandles } from "../data/index.js";
316
- import { getStrategy, listStrategies } from "../strategies/index.js";
317
-
318
- // Strip heavy fields so tool output stays within an agent's context budget.
319
- function summarizeMetrics(metrics) {
320
- const {
321
- trades,
322
- winRate,
323
- profitFactor,
324
- expectancy,
325
- totalR,
326
- avgR,
327
- sharpe,
328
- sharpeAnnualized,
329
- sortinoAnnualized,
330
- maxDrawdown,
331
- calmar,
332
- returnPct,
333
- totalPnL,
334
- finalEquity,
335
- exposurePct,
336
- sideBreakdown,
337
- } = metrics;
338
- return {
339
- trades,
340
- winRate,
341
- profitFactor,
342
- expectancy,
343
- totalR,
344
- avgR,
345
- sharpe,
346
- sharpeAnnualized,
347
- sortinoAnnualized,
348
- maxDrawdown,
349
- calmar,
350
- returnPct,
351
- totalPnL,
352
- finalEquity,
353
- exposurePct,
354
- sideBreakdown,
355
- };
356
- }
357
-
358
- async function resolveCandles(args) {
359
- if (Array.isArray(args.candles) && args.candles.length) return args.candles;
360
- if (args.data) return getHistoricalCandles(args.data);
361
- throw new Error("Provide either `candles` (array) or `data` (getHistoricalCandles spec).");
362
- }
363
-
364
- // Cartesian product of a { key: value[] } grid into parameter set objects.
365
- function expandGrid(grid) {
366
- const keys = Object.keys(grid || {});
367
- if (!keys.length) return [{}];
368
- return keys.reduce(
369
- (acc, key) => acc.flatMap((base) => grid[key].map((v) => ({ ...base, [key]: v }))),
370
- [{}]
371
- );
372
- }
373
-
374
- export const mcpTools = {
375
- list_strategies: {
376
- description: "List built-in trading strategies with their tunable parameters.",
377
- handler: async () => ({ strategies: listStrategies() }),
378
- },
379
-
380
- fetch_candles: {
381
- description: "Download/caches OHLCV candles from Yahoo or CSV. Returns a compact summary.",
382
- handler: async (args) => {
383
- const candles = await getHistoricalCandles(args);
384
- return {
385
- count: candles.length,
386
- first: candles[0] ?? null,
387
- last: candles[candles.length - 1] ?? null,
388
- };
389
- },
390
- },
391
-
392
- run_backtest: {
393
- description:
394
- "Run a single backtest using a named strategy + params. Returns a metrics summary and a small trade preview (no replay).",
395
- handler: async (args) => {
396
- const candles = await resolveCandles(args);
397
- const factory = getStrategy(args.strategy);
398
- const signal = factory(args.params || {});
399
- const result = backtest({
400
- candles,
401
- symbol: args.symbol ?? "UNKNOWN",
402
- interval: args.interval,
403
- signal,
404
- collectReplay: false,
405
- ...(args.backtestOptions || {}),
406
- });
407
- return {
408
- symbol: result.symbol,
409
- interval: result.interval,
410
- metrics: summarizeMetrics(result.metrics),
411
- tradesPreview: result.positions.slice(0, 10).map((p) => ({
412
- side: p.side,
413
- entry: p.entryFill ?? p.entry,
414
- exit: p.exit.price,
415
- pnl: p.exit.pnl,
416
- reason: p.exit.reason,
417
- })),
418
- };
419
- },
420
- },
421
-
422
- walk_forward: {
423
- description:
424
- "Walk-forward optimize a named strategy over a parameter grid. Returns out-of-sample metrics and winner stability.",
425
- handler: async (args) => {
426
- const candles = await resolveCandles(args);
427
- const factory = getStrategy(args.strategy);
428
- const wf = walkForwardOptimize({
429
- candles,
430
- mode: args.mode ?? "rolling",
431
- trainBars: args.trainBars,
432
- testBars: args.testBars,
433
- stepBars: args.stepBars ?? args.testBars,
434
- scoreBy: args.scoreBy ?? "profitFactor",
435
- parameterSets: expandGrid(args.grid),
436
- signalFactory: (params) => factory(params),
437
- backtestOptions: {
438
- interval: args.interval,
439
- collectReplay: false,
440
- ...(args.backtestOptions || {}),
441
- },
442
- });
443
- return {
444
- windows: wf.windows.length,
445
- metrics: summarizeMetrics(wf.metrics),
446
- stability: wf.bestParamsSummary,
447
- windowSummaries: wf.windows.map((w) => ({
448
- bestParams: w.bestParams,
449
- oosTrades: w.oosTrades,
450
- profitable: w.profitable,
451
- stabilityScore: w.stabilityScore,
452
- })),
453
- };
454
- },
455
- },
456
- };
457
- ```
458
-
459
- - [ ] **Step 4: Run test + suite**
460
-
461
- Run: `node --test test/mcp/tools.test.js`
462
- Expected: PASS (4 tests).
463
-
464
- Run: `node --test`
465
- Expected: PASS.
466
-
467
- - [ ] **Step 5: Commit**
468
-
469
- ```bash
470
- git add src/mcp/tools.js test/mcp/tools.test.js
471
- git commit -m "feat: add transport-free MCP tool functions
472
-
473
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
474
- ```
475
-
476
- ---
477
-
478
- ### Task 3: Add MCP SDK dependencies + subpath
479
-
480
- **Files:**
481
-
482
- - Modify: `package.json`
483
-
484
- - [ ] **Step 1: Install the SDK and zod**
485
-
486
- Run:
487
-
488
- ```bash
489
- npm install @modelcontextprotocol/sdk zod
490
- ```
491
-
492
- Expected: both added to `dependencies` in `package.json`, lockfile updated.
493
-
494
- - [ ] **Step 2: Add the `./mcp` export and `tradelab-mcp` bin**
495
-
496
- In `package.json`, in the `exports` map add:
497
-
498
- ```json
499
- "./mcp": {
500
- "import": "./src/mcp/server.js"
501
- },
502
- ```
503
-
504
- In the `bin` map add:
505
-
506
- ```json
507
- "tradelab-mcp": "bin/tradelab-mcp.js"
508
- ```
509
-
510
- Add `"bin"` is already listed in `files`. Confirm `bin` stays in the `files` array.
511
-
512
- - [ ] **Step 3: Commit**
513
-
514
- ```bash
515
- git add package.json package-lock.json
516
- git commit -m "build: add MCP SDK + zod deps and tradelab-mcp bin
517
-
518
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
519
- ```
520
-
521
- ---
522
-
523
- ### Task 4: Wire tools into an McpServer over stdio
524
-
525
- **Files:**
526
-
527
- - Create: `src/mcp/schemas.js` (zod input shapes)
528
- - Create: `src/mcp/server.js`
529
- - Create: `bin/tradelab-mcp.js`
530
- - Test: `test/mcp/server.test.js`
531
-
532
- - [ ] **Step 1: Write the failing test**
533
-
534
- ```js
535
- // test/mcp/server.test.js
536
- import test from "node:test";
537
- import assert from "node:assert/strict";
538
- import { createServer } from "../../src/mcp/server.js";
539
-
540
- test("createServer registers all tradelab tools", () => {
541
- const server = createServer();
542
- // McpServer exposes registered tool names via its internal registry; we assert
543
- // the factory returns an object with a connect method (the SDK server handle).
544
- assert.equal(typeof server.connect, "function");
545
- });
546
- ```
547
-
548
- - [ ] **Step 2: Run to verify it fails**
549
-
550
- Run: `node --test test/mcp/server.test.js`
551
- Expected: FAIL — cannot find module.
552
-
553
- - [ ] **Step 3: Implement src/mcp/schemas.js**
554
-
555
- ```js
556
- // src/mcp/schemas.js
557
- import { z } from "zod";
558
-
559
- const candle = z.object({
560
- time: z.number(),
561
- open: z.number().optional(),
562
- high: z.number(),
563
- low: z.number(),
564
- close: z.number(),
565
- volume: z.number().optional(),
566
- });
567
-
568
- const dataSpec = z
569
- .object({
570
- source: z.enum(["yahoo", "csv", "auto"]).optional(),
571
- symbol: z.string().optional(),
572
- interval: z.string().optional(),
573
- period: z.string().optional(),
574
- csvPath: z.string().optional(),
575
- cache: z.boolean().optional(),
576
- })
577
- .passthrough();
578
-
579
- export const schemas = {
580
- list_strategies: {},
581
- fetch_candles: dataSpec.shape,
582
- run_backtest: {
583
- candles: z.array(candle).optional(),
584
- data: dataSpec.optional(),
585
- symbol: z.string().optional(),
586
- interval: z.string().optional(),
587
- strategy: z.string(),
588
- params: z.record(z.any()).optional(),
589
- backtestOptions: z.record(z.any()).optional(),
590
- },
591
- walk_forward: {
592
- candles: z.array(candle).optional(),
593
- data: dataSpec.optional(),
594
- interval: z.string().optional(),
595
- strategy: z.string(),
596
- trainBars: z.number(),
597
- testBars: z.number(),
598
- stepBars: z.number().optional(),
599
- mode: z.enum(["rolling", "anchored"]).optional(),
600
- scoreBy: z.string().optional(),
601
- grid: z.record(z.array(z.any())).optional(),
602
- backtestOptions: z.record(z.any()).optional(),
603
- },
604
- };
605
- ```
606
-
607
- - [ ] **Step 4: Implement src/mcp/server.js**
608
-
609
- ```js
610
- // src/mcp/server.js
611
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
612
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
613
- import { mcpTools } from "./tools.js";
614
- import { schemas } from "./schemas.js";
615
-
616
- /** Build (but do not start) an McpServer with all tradelab tools registered. */
617
- export function createServer() {
618
- const server = new McpServer({ name: "tradelab", version: "1.1.0" });
619
-
620
- for (const [name, def] of Object.entries(mcpTools)) {
621
- server.tool(name, def.description, schemas[name] ?? {}, async (args) => {
622
- try {
623
- const result = await def.handler(args ?? {});
624
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
625
- } catch (error) {
626
- const message = error instanceof Error ? error.message : String(error);
627
- return { isError: true, content: [{ type: "text", text: `Error: ${message}` }] };
628
- }
629
- });
630
- }
631
-
632
- return server;
633
- }
634
-
635
- /** Start the server on stdio. Called by bin/tradelab-mcp.js. */
636
- export async function startStdioServer() {
637
- const server = createServer();
638
- const transport = new StdioServerTransport();
639
- await server.connect(transport);
640
- return server;
641
- }
642
- ```
643
-
644
- - [ ] **Step 5: Implement bin/tradelab-mcp.js**
645
-
646
- ```js
647
- #!/usr/bin/env node
648
- // bin/tradelab-mcp.js
649
- import { startStdioServer } from "../src/mcp/server.js";
650
-
651
- startStdioServer().catch((error) => {
652
- console.error("tradelab-mcp failed to start:", error);
653
- process.exit(1);
654
- });
655
- ```
656
-
657
- - [ ] **Step 6: Make the bin executable**
658
-
659
- Run:
660
-
661
- ```bash
662
- chmod +x bin/tradelab-mcp.js
663
- ```
664
-
665
- - [ ] **Step 7: Run test + suite**
666
-
667
- Run: `node --test test/mcp/server.test.js`
668
- Expected: PASS.
669
-
670
- Run: `node --test`
671
- Expected: PASS.
672
-
673
- - [ ] **Step 8: Smoke-test the server starts and lists tools**
674
-
675
- Run (sends an MCP `initialize` + `tools/list` and exits):
676
-
677
- ```bash
678
- node bin/tradelab-mcp.js < /dev/null & sleep 1; kill %1 2>/dev/null; echo "started ok"
679
- ```
680
-
681
- Expected: process starts without throwing (it waits on stdio). For a real
682
- handshake test, configure it in Claude Desktop (Task 5) — automated stdio
683
- handshake testing is out of scope for unit tests.
684
-
685
- - [ ] **Step 9: Commit**
686
-
687
- ```bash
688
- git add src/mcp/schemas.js src/mcp/server.js bin/tradelab-mcp.js test/mcp/server.test.js
689
- git commit -m "feat: MCP stdio server wiring tradelab tools
690
-
691
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
692
- ```
693
-
694
- ---
695
-
696
- ### Task 5: Docs — agent setup guide
697
-
698
- **Files:**
699
-
700
- - Create: `docs/mcp.md`
701
- - Modify: `README.md`
702
-
703
- - [ ] **Step 1: Write docs/mcp.md**
704
-
705
- Document:
706
-
707
- - What the server exposes (`list_strategies`, `fetch_candles`, `run_backtest`, `walk_forward`).
708
- - The "agent research loop": list strategies → fetch candles → run_backtest → read metrics → walk_forward to validate.
709
- - Claude Desktop config snippet:
710
-
711
- ````markdown
712
- ```json
713
- {
714
- "mcpServers": {
715
- "tradelab": {
716
- "command": "npx",
717
- "args": ["-y", "tradelab", "tradelab-mcp"]
718
- }
719
- }
720
- }
721
- ```
722
- ````
723
-
724
- (After `npm install -g tradelab` you can instead use `"command": "tradelab-mcp"`.)
725
-
726
- - A note that strategies are name-addressable (agents can't pass code), and how to
727
- add custom strategies via `registerStrategy`.
728
-
729
- - [ ] **Step 2: Link from README**
730
-
731
- Add a documentation-table row:
732
-
733
- ```markdown
734
- | [MCP server](docs/mcp.md) | Run the research loop from any MCP-capable agent (Claude, Cursor) |
735
- ```
736
-
737
- And add a short "## AI agents / MCP" section near the top pointing to `docs/mcp.md`.
738
-
739
- - [ ] **Step 3: Build, lint, test, commit**
740
-
741
- ```bash
742
- npm run build && npm run lint && npm run format:check && npm test
743
- git add docs/mcp.md README.md
744
- git commit -m "docs: MCP server setup and agent research loop
745
-
746
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
747
- ```
748
-
749
- ---
750
-
751
- ## Self-review checklist
752
-
753
- - [ ] Agents never need to pass code — every tool is name + JSON params. ✔ (Task 1, 2)
754
- - [ ] Tool outputs are summaries; `replay` is explicitly excluded (asserted in test). ✔ (Task 2)
755
- - [ ] `mcpTools` handlers are pure async functions tested without a transport; `server.js` is a thin SDK adapter. ✔
756
- - [ ] Strategy names are consistent across registry, tools, tests, and docs (`ema-cross`, `rsi-reversion`, `donchian-breakout`, `buy-hold`). ✔
757
- - [ ] `run_backtest` surfaces `sharpeAnnualized` (depends on Plan 1 being merged; if Plan 1 is not yet done, the field is simply absent — the summary still works). ✔
758
- - [ ] New deps (`@modelcontextprotocol/sdk`, `zod`) are runtime `dependencies`, not `devDependencies`. ✔ (Task 3)