@watb/mcp-server 0.1.6
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.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/bin/doctor.js +5 -0
- package/bin/init.js +5 -0
- package/bin/mcp-server.js +59 -0
- package/bin/watb-mcp.js +5 -0
- package/dist/doctor.js +8 -0
- package/dist/init.js +28 -0
- package/dist/server.js +38 -0
- package/package.json +56 -0
- package/templates/claude-desktop.json +11 -0
- package/templates/cursor.json +11 -0
- package/templates/vscode.json +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WATB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @watb/mcp-server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server for [WATB](https://watb.app) — exposes
|
|
4
|
+
backtest, strategy CRUD and worker dispatch tools to Claude Desktop, Cursor,
|
|
5
|
+
VS Code and any MCP-capable AI assistant.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Generate an API key at https://watb.app/strategies/ai
|
|
11
|
+
# 2. Configure the client (interactive — only asks for the API key,
|
|
12
|
+
# saves it to ~/.watb/config.json with mode 0600):
|
|
13
|
+
npx -y @watb/mcp-server init
|
|
14
|
+
|
|
15
|
+
# 3. Add to your MCP client (Claude Desktop example).
|
|
16
|
+
# No env block needed — the server reads the key from ~/.watb/config.json:
|
|
17
|
+
# ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"watb": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@watb/mcp-server"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Tools (Phase 1 — 19 tools)
|
|
29
|
+
|
|
30
|
+
All tools return concise text-formatted summaries optimised for AI
|
|
31
|
+
strategy-development workflows. Use `watb_get_raw_result` when you need
|
|
32
|
+
the underlying JSON payload of a backtest task.
|
|
33
|
+
|
|
34
|
+
| Category | Tool | Description |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Sanity | `watb_ping` | Returns user info, worker availability, server version. |
|
|
37
|
+
| Metadata | `watb_get_pairs` | Active trading pairs + index symbols (TOTAL, TOTAL2, …). |
|
|
38
|
+
| Metadata | `watb_get_metadata` | Indicators, exit tactics, risk guards and strategy guide. |
|
|
39
|
+
| Metadata | `watb_get_publish_date_range` | Standard 1-year publish-test backtest window. |
|
|
40
|
+
| Strategy CRUD | `watb_list_strategies` | List the caller's saved strategies. |
|
|
41
|
+
| Strategy CRUD | `watb_save_strategy` | Persist a strategy to the AI Strategies library. |
|
|
42
|
+
| Strategy CRUD | `watb_fetch_strategy_by_id` | Fetch an existing strategy by id. |
|
|
43
|
+
| Backtest | `watb_run_backtest` | Run a backtest, returns summary + diagnostics. |
|
|
44
|
+
| Backtest | `watb_sweep` | Parameter grid-search across many combinations. |
|
|
45
|
+
| Backtest | `watb_walk_forward` | Walk-forward validation for overfit detection. |
|
|
46
|
+
| Backtest | `watb_correlate` | Multi-strategy correlation (concurrent-position overlap). |
|
|
47
|
+
| Backtest | `watb_compare` | Compare multiple backtest task_ids with delta analysis. |
|
|
48
|
+
| Backtest | `watb_history` | List recent backtest history. |
|
|
49
|
+
| Backtest | `watb_get_raw_result` | Raw JSON access (section slicing) for debug & bug-hunts. |
|
|
50
|
+
| Indicator | `watb_indicator_preview` | Preview real indicator values & distribution stats. |
|
|
51
|
+
| Session | `watb_reserve` | Reserve the worker for exclusive session use. |
|
|
52
|
+
| Session | `watb_release` | Release the worker for general use. |
|
|
53
|
+
| Iteration log | `watb_record_iteration` | Record a backtest iteration to a local session file. |
|
|
54
|
+
| Iteration log | `watb_get_failed_iterations` | List iterations dropped from session context. |
|
|
55
|
+
|
|
56
|
+
## Config
|
|
57
|
+
|
|
58
|
+
`~/.watb/config.json` (created by `init`, mode `0600`):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"apiUrl": "https://mcp.watb.app",
|
|
63
|
+
"apiKey": "watb_mcp_..."
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Overriding from the MCP client (env block)
|
|
68
|
+
|
|
69
|
+
`WATB_API_URL` and `WATB_API_KEY` environment variables always win over
|
|
70
|
+
the config file. Useful for CI, Docker, multiple keys per host, or
|
|
71
|
+
keeping the key out of `~/.watb`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"watb": {
|
|
77
|
+
"command": "npx",
|
|
78
|
+
"args": ["-y", "@watb/mcp-server"],
|
|
79
|
+
"env": {
|
|
80
|
+
"WATB_API_KEY": "watb_mcp_..."
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Claude Code (CLI)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
claude mcp add watb -- npx -y @watb/mcp-server
|
|
91
|
+
# verify
|
|
92
|
+
claude mcp list
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If Claude Code reports "connected" but no tools appear, run
|
|
96
|
+
`npx @watb/mcp-server doctor` first to confirm the API key works,
|
|
97
|
+
then restart the Claude Code session so the tools list is re-fetched.
|
|
98
|
+
|
|
99
|
+
## Diagnostics
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx @watb/mcp-server doctor
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/bin/doctor.js
ADDED
package/bin/init.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dispatcher for the @watb/mcp-server package. Matches the package's
|
|
3
|
+
// unscoped name (`mcp-server`) so `npx @watb/mcp-server [subcommand]`
|
|
4
|
+
// resolves to this single entry point.
|
|
5
|
+
//
|
|
6
|
+
// Subcommands:
|
|
7
|
+
// (none) → start the MCP stdio server
|
|
8
|
+
// init → interactive client config wizard
|
|
9
|
+
// doctor → environment / connectivity diagnostics
|
|
10
|
+
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
import { dirname, resolve } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const sub = process.argv[2]
|
|
17
|
+
|
|
18
|
+
async function run(target) {
|
|
19
|
+
try {
|
|
20
|
+
await import(pathToFileURL(resolve(__dirname, target)).href)
|
|
21
|
+
} catch (e) {
|
|
22
|
+
process.stderr.write(`Failed to run watb mcp-server (${target}): ${e?.stack ?? e}\n`)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
switch (sub) {
|
|
28
|
+
case 'init':
|
|
29
|
+
// Strip the subcommand so init.js sees a clean argv
|
|
30
|
+
process.argv.splice(2, 1)
|
|
31
|
+
await run('./init.js')
|
|
32
|
+
break
|
|
33
|
+
case 'doctor':
|
|
34
|
+
process.argv.splice(2, 1)
|
|
35
|
+
await run('./doctor.js')
|
|
36
|
+
break
|
|
37
|
+
case '--help':
|
|
38
|
+
case '-h':
|
|
39
|
+
case 'help':
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
[
|
|
42
|
+
'Usage: npx @watb/mcp-server [command]',
|
|
43
|
+
'',
|
|
44
|
+
'Commands:',
|
|
45
|
+
' (default) Start the MCP stdio server (used by Claude / Cursor / VS Code)',
|
|
46
|
+
' init Interactive setup — writes ~/.watb/config.json',
|
|
47
|
+
' doctor Diagnose connectivity, API key and worker reachability',
|
|
48
|
+
' help Show this message',
|
|
49
|
+
'',
|
|
50
|
+
].join('\n'),
|
|
51
|
+
)
|
|
52
|
+
break
|
|
53
|
+
default:
|
|
54
|
+
if (sub && !sub.startsWith('-')) {
|
|
55
|
+
process.stderr.write(`Unknown command: ${sub}\nRun \`npx @watb/mcp-server help\` for usage.\n`)
|
|
56
|
+
process.exit(2)
|
|
57
|
+
}
|
|
58
|
+
await run('./watb-mcp.js')
|
|
59
|
+
}
|
package/bin/watb-mcp.js
ADDED
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{stdout as d}from"process";import{readFile as m,mkdir as C,writeFile as W}from"fs/promises";import{homedir as w}from"os";import{join as y,dirname as k}from"path";var n=class extends Error{constructor(e,r,i){super(r);this.code=e;this.details=i;this.name="WatbMcpError"}code;details},c={KeyMissing:"WATB_MCP_1001",KeyInvalid:"WATB_MCP_1002",Unauthorized:"WATB_MCP_1003",NoWorker:"WATB_MCP_2001",WorkerUnreachable:"WATB_MCP_2002",WorkerError:"WATB_MCP_2005",InvalidArgs:"WATB_MCP_3001",PollingTimeout:"WATB_MCP_4001",Internal:"WATB_MCP_5001"};function f(){return y(w(),".watb","config.json")}async function h(){let o=process.env.WATB_API_URL,t=process.env.WATB_API_KEY,e={};try{e=JSON.parse(await m(f(),"utf-8"))}catch{}let r=o??e.apiUrl??"https://mcp.watb.app",i=t??e.apiKey??"";if(!i)throw new n(c.KeyMissing,"WATB_API_KEY not set. Run `npx @watb/mcp-server init` to configure.");return{apiUrl:r,apiKey:i,log:e.log}}import{fetch as g,Agent as P}from"undici";var u=new P({keepAliveTimeout:3e4,bodyTimeout:0,headersTimeout:3e4}),l=class{constructor(t){this.cfg=t}cfg;headers(t){return{Authorization:`Bearer ${this.cfg.apiKey}`,"User-Agent":"@watb/mcp-server/0.1.1",...t}}async get(t){let e=await g(this.cfg.apiUrl+t,{method:"GET",headers:this.headers(),dispatcher:u});return this.handle(e)}async post(t,e){let r=await g(this.cfg.apiUrl+t,{method:"POST",headers:this.headers(e!==void 0?{"Content-Type":"application/json"}:{}),body:e!==void 0?JSON.stringify(e):void 0,dispatcher:u});return this.handle(r)}async dispatch(t,e,r){let i=r?.method??(e!==void 0?"POST":"GET"),a=`${this.cfg.apiUrl}/api/mcp/v1/workers/dispatch/${t.replace(/^\//,"")}`,p=await g(a,{method:i,headers:this.headers(e!==void 0?{"Content-Type":"application/json"}:{}),body:e!==void 0?JSON.stringify(e):void 0,dispatcher:u});return this.handle(p)}async handle(t){if(!t.ok){let r=await t.text();try{let a=JSON.parse(r).error;if(a&&typeof a=="object"){let p=a;throw new n(p.code??c.Internal,p.message??`HTTP ${t.status}`,p.details)}throw typeof a=="string"?new n(c.Internal,`HTTP ${t.status}: ${a}`):new n(c.Internal,`HTTP ${t.status}: ${r.slice(0,500)}`)}catch(i){throw i instanceof n?i:new n(c.Internal,r?`HTTP ${t.status}: ${r.slice(0,500)}`:`HTTP ${t.status}`)}}if((t.headers.get("content-type")??"").includes("application/json")){let r=await t.text();try{return JSON.parse(r)}catch{let i=r.replace(/\bNaN\b/g,"null").replace(/-Infinity\b/g,"-1e308").replace(/\bInfinity\b/g,"1e308");try{return JSON.parse(i)}catch{return r}}}return t.text()}};function s(o,t){d.write(`${o?"\u2713":"\u2717"} ${t}
|
|
2
|
+
`)}async function T(){d.write(`
|
|
3
|
+
watb-mcp doctor
|
|
4
|
+
---------------
|
|
5
|
+
`);let o=parseInt(process.versions.node.split(".")[0]??"0",10);s(o>=22,`Node ${process.versions.node} (need >=22)`);let t;try{t=await h(),s(!0,`Config: ${f()}`),s(!0,`API URL: ${t.apiUrl}`)}catch(e){s(!1,`Config: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}try{let r=await new l(t).get("/api/mcp/v1/me/context");s(!0,`API key valid (user: ${r.user.username}, server: ${r.serverVersion})`),r.worker.available?s(!0,`Worker: available (status=${r.worker.status}, health=${r.worker.healthStatus})`):s(!1,"Worker: NOT deployed \u2014 deploy one at https://watb.app/strategies/ai")}catch(e){e instanceof n?s(!1,`API: [${e.code}] ${e.message}`):s(!1,`API: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}d.write(`
|
|
6
|
+
Diagnosis: all systems healthy.
|
|
7
|
+
`)}T().catch(o=>{d.write(`doctor failed: ${o instanceof Error?o.message:String(o)}
|
|
8
|
+
`),process.exit(1)});export{T as main};
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import{createInterface as A}from"readline/promises";import{stdin as _,stdout as o}from"process";import{readFile as w,mkdir as h,writeFile as y}from"fs/promises";import{homedir as P}from"os";import{join as b,dirname as T}from"path";var a=class extends Error{constructor(n,r,e){super(r);this.code=n;this.details=e;this.name="WatbMcpError"}code;details},c={KeyMissing:"WATB_MCP_1001",KeyInvalid:"WATB_MCP_1002",Unauthorized:"WATB_MCP_1003",NoWorker:"WATB_MCP_2001",WorkerUnreachable:"WATB_MCP_2002",WorkerError:"WATB_MCP_2005",InvalidArgs:"WATB_MCP_3001",PollingTimeout:"WATB_MCP_4001",Internal:"WATB_MCP_5001"};function l(){return b(P(),".watb","config.json")}async function g(){let i=process.env.WATB_API_URL,t=process.env.WATB_API_KEY,n={};try{n=JSON.parse(await w(l(),"utf-8"))}catch{}let r=i??n.apiUrl??"https://mcp.watb.app",e=t??n.apiKey??"";if(!e)throw new a(c.KeyMissing,"WATB_API_KEY not set. Run `npx @watb/mcp-server init` to configure.");return{apiUrl:r,apiKey:e,log:n.log}}async function m(i){let t=l();await h(T(t),{recursive:!0}),await y(t,JSON.stringify(i,null,2)+`
|
|
2
|
+
`,{mode:384})}import{fetch as f,Agent as C}from"undici";var u=new C({keepAliveTimeout:3e4,bodyTimeout:0,headersTimeout:3e4}),d=class{constructor(t){this.cfg=t}cfg;headers(t){return{Authorization:`Bearer ${this.cfg.apiKey}`,"User-Agent":"@watb/mcp-server/0.1.1",...t}}async get(t){let n=await f(this.cfg.apiUrl+t,{method:"GET",headers:this.headers(),dispatcher:u});return this.handle(n)}async post(t,n){let r=await f(this.cfg.apiUrl+t,{method:"POST",headers:this.headers(n!==void 0?{"Content-Type":"application/json"}:{}),body:n!==void 0?JSON.stringify(n):void 0,dispatcher:u});return this.handle(r)}async dispatch(t,n,r){let e=r?.method??(n!==void 0?"POST":"GET"),s=`${this.cfg.apiUrl}/api/mcp/v1/workers/dispatch/${t.replace(/^\//,"")}`,p=await f(s,{method:e,headers:this.headers(n!==void 0?{"Content-Type":"application/json"}:{}),body:n!==void 0?JSON.stringify(n):void 0,dispatcher:u});return this.handle(p)}async handle(t){if(!t.ok){let r=await t.text();try{let s=JSON.parse(r).error;if(s&&typeof s=="object"){let p=s;throw new a(p.code??c.Internal,p.message??`HTTP ${t.status}`,p.details)}throw typeof s=="string"?new a(c.Internal,`HTTP ${t.status}: ${s}`):new a(c.Internal,`HTTP ${t.status}: ${r.slice(0,500)}`)}catch(e){throw e instanceof a?e:new a(c.Internal,r?`HTTP ${t.status}: ${r.slice(0,500)}`:`HTTP ${t.status}`)}}if((t.headers.get("content-type")??"").includes("application/json")){let r=await t.text();try{return JSON.parse(r)}catch{let e=r.replace(/\bNaN\b/g,"null").replace(/-Infinity\b/g,"-1e308").replace(/\bInfinity\b/g,"1e308");try{return JSON.parse(e)}catch{return r}}}return t.text()}};var v="https://mcp.watb.app";async function k(i,t,n){return(await i.question(t)).trim()||n||""}async function W(){let i=A({input:_,output:o});try{o.write(`
|
|
3
|
+
WATB MCP server \u2014 interactive setup
|
|
4
|
+
`),o.write(`-----------------------------------
|
|
5
|
+
`),o.write(`1. Open https://watb.app/strategies/ai and generate an API key.
|
|
6
|
+
`),o.write(`2. Paste it below.
|
|
7
|
+
|
|
8
|
+
`);let t=v;try{let e=await g();e.apiUrl&&(t=e.apiUrl)}catch{}let n=await k(i,"API key: ");n||(o.write(`
|
|
9
|
+
No API key entered \u2014 aborting.
|
|
10
|
+
`),process.exit(1));let r=new d({apiUrl:t,apiKey:n});try{let e=await r.get("/api/mcp/v1/me/context");o.write(`
|
|
11
|
+
\u2713 Authenticated as ${e.user.username} (worker available: ${e.worker.available})
|
|
12
|
+
`)}catch(e){e instanceof a?o.write(`
|
|
13
|
+
\u2717 Validation failed [${e.code}]: ${e.message}
|
|
14
|
+
`):o.write(`
|
|
15
|
+
\u2717 Validation failed: ${e instanceof Error?e.message:String(e)}
|
|
16
|
+
`),process.exit(1)}await m({apiUrl:t,apiKey:n}),o.write(`\u2713 Config saved to ${l()} (mode 0600)
|
|
17
|
+
|
|
18
|
+
`),x()}finally{i.close()}}function x(){o.write(`Add this to your MCP client config (no API key needed \u2014 it's read from ${l()}):
|
|
19
|
+
|
|
20
|
+
`);let i={mcpServers:{watb:{command:"npx",args:["-y","@watb/mcp-server"]}}};o.write(`# Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS)
|
|
21
|
+
`),o.write(JSON.stringify(i,null,2)+`
|
|
22
|
+
|
|
23
|
+
`),o.write(`# Cursor (~/.cursor/mcp.json)
|
|
24
|
+
`),o.write(JSON.stringify(i,null,2)+`
|
|
25
|
+
|
|
26
|
+
`),o.write(`Restart your MCP client and try the 'watb_ping' tool.
|
|
27
|
+
`)}W().catch(i=>{o.write(`init failed: ${i instanceof Error?i.message:String(i)}
|
|
28
|
+
`),process.exit(1)});export{W as main};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import{Server as Zt}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as te}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as ee,ListToolsRequestSchema as re}from"@modelcontextprotocol/sdk/types.js";import{readFileSync as se}from"fs";import{fileURLToPath as ne}from"url";import{dirname as ie,resolve as oe}from"path";import{readFile as Ot,mkdir as le,writeFile as de}from"fs/promises";import{homedir as Nt}from"os";import{join as Wt,dirname as _e}from"path";var f=class extends Error{constructor(e,s,o){super(s);this.code=e;this.details=o;this.name="WatbMcpError"}code;details},_={KeyMissing:"WATB_MCP_1001",KeyInvalid:"WATB_MCP_1002",Unauthorized:"WATB_MCP_1003",NoWorker:"WATB_MCP_2001",WorkerUnreachable:"WATB_MCP_2002",WorkerError:"WATB_MCP_2005",InvalidArgs:"WATB_MCP_3001",PollingTimeout:"WATB_MCP_4001",Internal:"WATB_MCP_5001"};function Lt(){return Wt(Nt(),".watb","config.json")}async function X(){let t=process.env.WATB_API_URL,r=process.env.WATB_API_KEY,e={};try{e=JSON.parse(await Ot(Lt(),"utf-8"))}catch{}let s=t??e.apiUrl??"https://mcp.watb.app",o=r??e.apiKey??"";if(!o)throw new f(_.KeyMissing,"WATB_API_KEY not set. Run `npx @watb/mcp-server init` to configure.");return{apiUrl:s,apiKey:o,log:e.log}}import{fetch as G,Agent as Mt}from"undici";var q=new Mt({keepAliveTimeout:3e4,bodyTimeout:0,headersTimeout:3e4}),W=class{constructor(r){this.cfg=r}cfg;headers(r){return{Authorization:`Bearer ${this.cfg.apiKey}`,"User-Agent":"@watb/mcp-server/0.1.1",...r}}async get(r){let e=await G(this.cfg.apiUrl+r,{method:"GET",headers:this.headers(),dispatcher:q});return this.handle(e)}async post(r,e){let s=await G(this.cfg.apiUrl+r,{method:"POST",headers:this.headers(e!==void 0?{"Content-Type":"application/json"}:{}),body:e!==void 0?JSON.stringify(e):void 0,dispatcher:q});return this.handle(s)}async dispatch(r,e,s){let o=s?.method??(e!==void 0?"POST":"GET"),i=`${this.cfg.apiUrl}/api/mcp/v1/workers/dispatch/${r.replace(/^\//,"")}`,n=await G(i,{method:o,headers:this.headers(e!==void 0?{"Content-Type":"application/json"}:{}),body:e!==void 0?JSON.stringify(e):void 0,dispatcher:q});return this.handle(n)}async handle(r){if(!r.ok){let s=await r.text();try{let i=JSON.parse(s).error;if(i&&typeof i=="object"){let n=i;throw new f(n.code??_.Internal,n.message??`HTTP ${r.status}`,n.details)}throw typeof i=="string"?new f(_.Internal,`HTTP ${r.status}: ${i}`):new f(_.Internal,`HTTP ${r.status}: ${s.slice(0,500)}`)}catch(o){throw o instanceof f?o:new f(_.Internal,s?`HTTP ${r.status}: ${s.slice(0,500)}`:`HTTP ${r.status}`)}}if((r.headers.get("content-type")??"").includes("application/json")){let s=await r.text();try{return JSON.parse(s)}catch{let o=s.replace(/\bNaN\b/g,"null").replace(/-Infinity\b/g,"-1e308").replace(/\bInfinity\b/g,"1e308");try{return JSON.parse(o)}catch{return s}}}return r.text()}};var z=[{name:"watb_ping",description:"Sanity check WATB connectivity. Returns the authenticated user, whether an MCP worker is available, and the server version.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"watb_get_pairs",description:"List active trading pairs and index symbols (TOTAL, TOTAL2, TOTAL3, OTHERS). Index symbols are usable as a condition `source` for cross-pair gates.",inputSchema:{type:"object",properties:{}}},{name:"watb_get_metadata",description:"Fetch available indicators, exit tactics and the strategy guide. Call once at session start.",inputSchema:{type:"object",properties:{}}},{name:"watb_get_publish_date_range",description:"Return the standard 1-year backtest window used for publish-tests. Computed from today: end_date = first day of previous month (exclusive), start_date = end_date - 1 year. Takes no args.",inputSchema:{type:"object",properties:{}}},{name:"watb_list_strategies",description:"List the caller's strategies. Optionally filter by folder or visibility.",inputSchema:{type:"object",properties:{folder_id:{type:"integer",description:"Filter by folder."},visibility:{type:"string",enum:["private","published","all"]},limit:{type:"integer",minimum:1,maximum:200,default:50},offset:{type:"integer",minimum:0,default:0}},additionalProperties:!1}},{name:"watb_save_strategy",description:"Save a strategy to your AI Strategies library. Persists the full Worker v4.x DSL; backend auto-extracts timeframe/direction/features for the UI.",inputSchema:{type:"object",required:["name","strategy_json"],properties:{name:{type:"string",minLength:1,maxLength:200},folder_id:{type:"integer"},strategy_json:{type:"object"},description:{type:"string",maxLength:1e3},tags:{type:"array",items:{type:"string"},maxItems:20}},additionalProperties:!1}},{name:"watb_fetch_strategy_by_id",description:"Fetch an existing strategy by id. Returns the strategy JSON wrapped in a fenced block. (v0.1: only 'ai' source supported; published/suggested sources land in Phase 2.)",inputSchema:{type:"object",required:["id"],properties:{id:{type:["number","string"],description:"The watb.api strategy id"},source:{type:"string",enum:["ai"],description:"Source endpoint. Default: 'ai'."}}}},{name:"watb_run_backtest",description:"Run a backtest. Submit the strategy JSON and the date range; returns a summary of the result. Pre-flight validation runs automatically inside the tool.",inputSchema:{type:"object",required:["strategy_json","from_timestamp","to_timestamp"],properties:{strategy_json:{type:"object",description:"Full strategy JSON object (v4.0 flat or v4.1 multi-regime)."},from_timestamp:{type:["number","string"],description:"Only 'YYYY-MM-DD' string or Unix ms integer."},to_timestamp:{type:["number","string"],description:"Only 'YYYY-MM-DD' string or Unix ms integer."},initial_balance:{type:"number",default:1e4},risk_per_trade:{type:"number",default:20,minimum:1e-4,description:"Risk per trade. Fractional (0.02) or absolute ($20)."},iteration:{type:"integer",default:1,minimum:1}},additionalProperties:!1}},{name:"watb_sweep",description:"Parameter grid-search optimisation. Tests many parameter combinations and ranks the best results.",inputSchema:{type:"object",required:["strategy_json","from_timestamp","to_timestamp","sweep"],properties:{strategy_json:{type:"object"},from_timestamp:{type:["number","string"]},to_timestamp:{type:["number","string"]},sweep:{type:"object",description:"Map of parameter paths \u2192 value arrays."},sweep_ranges:{type:"object",description:"OPTIONAL: {start,end,step} \u2192 auto-expanded."},sort_by:{type:"string",enum:["net_pnl","profit_factor","sharpe_ratio","win_rate","max_drawdown","total_trades"],default:"profit_factor"},max_combinations:{type:"number",default:200},iteration:{type:"integer"},initial_balance:{type:"number",default:1e4},risk_per_trade:{type:"number",default:20}}}},{name:"watb_walk_forward",description:"Walk-Forward Validation. Detects overfitting by training and testing on rolling folds.",inputSchema:{type:"object",required:["strategy_json","from_timestamp","to_timestamp"],properties:{strategy_json:{type:"object"},from_timestamp:{type:["number","string"]},to_timestamp:{type:["number","string"]},train_months:{type:"number",default:4},test_months:{type:"number",default:2},iteration:{type:"integer"},initial_balance:{type:"number",default:1e4},risk_per_trade:{type:"number",default:20}}}},{name:"watb_correlate",description:"Multi-strategy correlation analysis. Runs several strategies on the same window and reports concurrent-position overlap.",inputSchema:{type:"object",required:["strategies","from_timestamp","to_timestamp"],properties:{strategies:{type:"array",minItems:2,maxItems:5,items:{type:"object",required:["name","json"],properties:{name:{type:"string"},json:{type:"object"}}}},from_timestamp:{type:["number","string"]},to_timestamp:{type:["number","string"]},iteration:{type:"integer"}}}},{name:"watb_indicator_preview",description:"Preview real indicator values. Useful for picking thresholds; returns min/max/mean/median and the most recent values.",inputSchema:{type:"object",required:["symbol","indicator"],properties:{symbol:{type:"string"},indicator:{type:"string"},params:{type:"object"},timeframe:{type:"string",default:"15m"},limit:{type:"number",default:20}}}},{name:"watb_validate_strategy",description:"Validate a strategy JSON without running a backtest. Returns errors and warnings from schema + preflight semantic checks. Run before watb_run_backtest to catch typos and unsupported conditions early.",inputSchema:{type:"object",required:["strategy_json"],properties:{strategy_json:{type:"object",description:"Full strategy JSON object (v4.0 flat or v4.1 multi-regime)."}}}},{name:"watb_compare",description:"Compare multiple backtest results. Per-task metrics plus delta analysis against a baseline.",inputSchema:{type:"object",required:["task_ids"],properties:{task_ids:{type:"array",items:{type:"string"}}}}},{name:"watb_history",description:"List recent backtest history: task_id, strategy name, symbol and core metrics.",inputSchema:{type:"object",properties:{}}},{name:"watb_get_raw_result",description:"Return the RAW JSON result of a backtest task_id \u2014 summary fields plus section slicing. For debugging and bug-hunts. If task_id is empty, the most recent backtest is used.",inputSchema:{type:"object",properties:{task_id:{type:"string"},section:{type:"string",enum:["all","summary","metrics","long_metrics","short_metrics","per_regime_metrics","per_regime_pnl_distribution","pnl_distribution","diagnostics","condition_breakdown","warnings","errors","trades","strategy_snapshot"],default:"summary"},trade_limit:{type:"number",default:20},pretty:{type:"boolean",default:!0}}}},{name:"watb_reserve",description:"Reserve the worker \u2014 dedicates it to your session and pauses background job intake. Call BEFORE starting a sweep/backtest session. TTL: 3 minutes (extended by keepalive).",inputSchema:{type:"object",properties:{reason:{type:"string"}}}},{name:"watb_release",description:"Release the worker \u2014 resumes background job intake. Call when the session ends.",inputSchema:{type:"object",properties:{}}},{name:"watb_record_iteration",description:"Record an iteration (local session file). watb_run_backtest auto-records failed iterations; this tool is only for (a) adding a meaningful `changes` description and (b) marking a successful iteration as persistent.",inputSchema:{type:"object",required:["iteration","name","metrics","changes","passed"],properties:{iteration:{type:"number"},name:{type:"string"},metrics:{type:"object"},changes:{type:"string"},passed:{type:"boolean"},strategy_json:{type:"object"}}}},{name:"watb_get_failed_iterations",description:"Return iterations that were dropped from this session's context (local file).",inputSchema:{type:"object",properties:{}}}];var c=(t,r=0)=>typeof t=="number"&&Number.isFinite(t)?t:r,u=(t,r="")=>typeof t=="string"?t:r;function Z(t,r){if(t.error){let b=t.error,w=typeof b=="string"?b:JSON.stringify(b);return`\u274C BACKTEST FAILED (iter:${r})
|
|
2
|
+
Error: ${w}`}let e=t.result??t,s=e.metrics??{},o=e.long_metrics??null,i=e.short_metrics??null,n=e.diagnostics??{},m=e.strategy_snapshot??{},l=c(s.total_trades),a=u(m.name)||`v${r}`,d=u(t.task_id),p=d?` task_id: ${d}`:"";if(l===0){let b=n.bottleneck_condition,w=n.condition_breakdown??[],I=c(n.signals_generated),S=n.errors??[],T=n.warnings??[],k=[`\u274C ${a} | NO TRADES | iter:${r}`,` Signals:${I} | Candles:${c(n.total_candles)}`];if(w.length>0){k.push(" --- Condition Breakdown ---");for(let R of w){let B=c(R.true_pct).toFixed(1),N=Array.isArray(R.sample_values)?R.sample_values.map(J=>J.toFixed(3)).join(", "):"",U=c(R.true_count)===0?" \u26D4 NEVER TRUE":"";k.push(` [${u(R.id)}] ${u(R.indicator_key)} ${u(R.operator)} ${JSON.stringify(R.value)} \u2192 true:${B}%${U}`),N&&k.push(` samples: [${N}]`)}}return b&&I===0&&k.push(` Bottleneck: ${u(b)}`),S.length>0&&k.push(` Errors: ${S.slice(0,3).join(" | ")}`),T.length>0&&k.push(` Warn: ${T[0]}`),k.push(" \u2192 Loosen thresholds based on sample_values"),p&&k.push(p),k.filter(Boolean).join(`
|
|
3
|
+
`)}let y=c(s.net_pnl),h=c(s.total_pnl),x=c(s.total_fees),P=c(s.win_rate),F=c(s.profit_factor),A=c(s.max_drawdown_pct),Et=c(s.sharpe_ratio),K=Math.abs(h)>0?x/Math.abs(h)*100:0,$=[`${y>0&&K<30?"\u2705":y>0?"\u26A0\uFE0F":"\u274C"} ${a} | iter:${r}`,` Trades:${l} | WR:${P.toFixed(1)}% | Sharpe:${Et.toFixed(2)}`,` Gross:$${h.toFixed(0)} | Fees:$${x.toFixed(0)} | Net:$${y.toFixed(0)}`,` PF:${F.toFixed(2)} | DD:${A.toFixed(1)}% | Fee/Gross:${K.toFixed(1)}%`];o&&$.push(` LONG \u2192 Trades:${c(o.total_trades)} | WR:${c(o.win_rate).toFixed(1)}% | Net:$${c(o.net_pnl).toFixed(0)}`),i&&$.push(` SHORT \u2192 Trades:${c(i.total_trades)} | WR:${c(i.win_rate).toFixed(1)}% | Net:$${c(i.net_pnl).toFixed(0)}`);let D=e.per_regime_metrics??null;if(D&&Object.keys(D).length>0){$.push(" --- Per-Regime ---");for(let[b,w]of Object.entries(D)){let I=w&&typeof w=="object"&&"metrics"in w&&typeof w.metrics=="object",S=(I?w.metrics:w)??{},T=I?u(w.label,b):b,k=c(S.total_trades),R=c(S.net_pnl),B=c(S.win_rate),N=c(S.profit_factor),U=R>0?"\u2705":R<0?"\u274C":"\xB7",J=T!==b?`${b} "${T}"`:b;$.push(` ${U} [${J}] Trades:${k} | WR:${B.toFixed(1)}% | Net:$${R.toFixed(0)} | PF:${N.toFixed(2)}`)}}let O=e.pnl_distribution??null;if(O){let b=O.monthly??[],w=c(O.monthly_score),I=c(O.weekly_score);if(b.length>0){let S=b.map(T=>{let k=c(T.pnl);return`${u(T.label)}:${k>=0?"+":""}$${k.toFixed(0)}`}).join(" | ");$.push(` Monthly: ${S}`),$.push(` Scores: M:${w.toFixed(0)}% W:${I.toFixed(0)}%`)}}let E=n.warnings??[];if(Array.isArray(E)&&E.length>0){$.push(` --- Warnings (${E.length}) ---`);for(let b of E.slice(0,5))$.push(` \u26A0\uFE0F ${b}`);E.length>5&&$.push(` \u26A0\uFE0F ... +${E.length-5} more`)}return p&&$.push(p),$.join(`
|
|
4
|
+
`)}function Y(t){if(t.error){let n=t.error;return`\u274C SWEEP FAILED
|
|
5
|
+
Error: ${typeof n=="string"?n:JSON.stringify(n)}`}let r=t.result??t,e=["\u{1F50D} Sweep Result",`Combinations: ${c(r.total_combinations)} (successful: ${c(r.successful)}, failed: ${c(r.failed)})`,`Sort by: ${u(r.sort_by,"profit_factor")}`,`Parameters: ${(r.parameters??[]).join(", ")||"N/A"}`,""],s=r.results??[];s.slice(0,10).forEach((n,m)=>{let l=n.metrics??{};e.push(`#${m+1} ${JSON.stringify(n.combination)} \u2192 PF:${c(l.profit_factor).toFixed(2)} | Sharpe:${c(l.sharpe_ratio).toFixed(2)} | Trades:${c(l.total_trades)} | WR:${c(l.win_rate).toFixed(1)}% | Net:$${c(l.net_pnl).toFixed(0)} | DD:${c(l.max_drawdown).toFixed(1)}%`)}),s.length>10&&e.push(`... +${s.length-10} more`);let o=r.best;if(o?.combination&&(e.push(`
|
|
6
|
+
\u2B50 Best: ${JSON.stringify(o.combination)}`),o.metrics)){let n=o.metrics;e.push(` PF:${c(n.profit_factor).toFixed(2)} | Sharpe:${c(n.sharpe_ratio).toFixed(2)} | Trades:${c(n.total_trades)} | Net:$${c(n.net_pnl).toFixed(0)}`)}let i=u(t.task_id);return i&&e.push(`
|
|
7
|
+
task_id: ${i}`),e.join(`
|
|
8
|
+
`)}function H(t){if(t.error){let a=t.error;return`\u274C WALK-FORWARD FAILED
|
|
9
|
+
Error: ${typeof a=="string"?a:JSON.stringify(a)}`}let r=t.result??t,e=r.summary??{},s=r.folds??[],o=u(e.overfit_risk,"UNKNOWN"),i=o==="LOW"?"\u2705":o==="MEDIUM"?"\u26A0\uFE0F":o==="HIGH"?"\u{1F534}":"\u2753",n=[`\u{1F4CA} Walk-Forward Result (${s.length} folds)`,""];for(let a of s){let d=a.train??{},p=a.test??{},y=a.decay_pct,h=a.dates??{};if(d.error){n.push(` Fold ${u(a.fold)||a.fold}: Train ERROR: ${d.error}`);continue}if(p.error){n.push(` Fold ${u(a.fold)||a.fold}: Test ERROR: ${p.error}`);continue}let x=h.train_from?new Date(h.train_from).toISOString().slice(0,10):"?",P=h.test_to?new Date(h.test_to).toISOString().slice(0,10):"?",F=y!=null?`${y.toFixed(0)}%`:"N/A",A=y!=null&&y<-50?" \u{1F534}":y!=null&&y<-25?" \u26A0\uFE0F":"";n.push(` Fold ${a.fold} (${x}\u2192${P}): Train PF:${c(d.profit_factor).toFixed(2)} Sharpe:${c(d.sharpe_ratio).toFixed(2)} Net:$${c(d.net_pnl).toFixed(0)} | Test PF:${c(p.profit_factor).toFixed(2)} Sharpe:${c(p.sharpe_ratio).toFixed(2)} Net:$${c(p.net_pnl).toFixed(0)} | Decay: ${F}${A}`)}n.push(""),n.push("\u{1F4C8} Summary:"),n.push(` Avg train Sharpe: ${c(e.avg_train_sharpe).toFixed(2)}`),n.push(` Avg test Sharpe: ${c(e.avg_test_sharpe).toFixed(2)}`);let m=e.avg_decay_pct;n.push(` Avg decay: ${m!=null?`${m.toFixed(0)}%`:"N/A"}`),n.push(` Overfit risk: ${i} ${o}`),o==="HIGH"?n.push(`
|
|
10
|
+
\u26A1 Advice: simplify parameters, reduce condition count, or widen thresholds.`):o==="LOW"&&n.push(`
|
|
11
|
+
\u2705 Strategy generalises well out-of-sample. Safe to consider for live trading.`);let l=u(t.task_id);return l&&n.push(`
|
|
12
|
+
task_id: ${l}`),n.join(`
|
|
13
|
+
`)}function tt(t){if(t.error){let i=t.error;return`\u274C COMPARE FAILED
|
|
14
|
+
Error: ${typeof i=="string"?i:JSON.stringify(i)}`}let r=u(t.baseline_task_id),e=t.comparison??[],s=t.diff??[],o=[`\u{1F4CA} Backtest Comparison (baseline: ${r||"n/a"})`,""];o.push("--- Metrics ---");for(let i of e){let n=i.metrics??{},m=`${u(i.task_id)} (${u(i.strategy_name)} ${u(i.symbol)} ${u(i.timeframe)} ${u(i.direction)})`;o.push(` ${m}
|
|
15
|
+
Trades:${c(n.total_trades)} WR:${c(n.win_rate).toFixed(1)}% Net:$${c(n.net_pnl).toFixed(2)} PF:${c(n.profit_factor).toFixed(2)} Sharpe:${c(n.sharpe_ratio).toFixed(2)} DD:${c(n.max_drawdown).toFixed(2)}%`)}if(s.length>0){o.push(""),o.push("--- Diff vs baseline ---");for(let i of s){let n=i.metric_changes??{};o.push(` ${u(i.task_id)} vs ${u(i.vs_baseline)}:`);for(let[l,a]of Object.entries(n)){let d=a,p=d.pct_change,y=p==null?"":Math.abs(p)>1e10?" (\u221E%)":` (${p>=0?"+":""}${p.toFixed(1)}%)`,h=c(d.delta),x=h>0?"\u2191":h<0?"\u2193":"=";o.push(` ${x} ${l}: ${c(d.baseline).toFixed(2)} \u2192 ${c(d.current).toFixed(2)}${y}`)}let m=i.strategy_changes??[];m.length>0&&o.push(` strategy_changes: ${JSON.stringify(m)}`)}}return o.join(`
|
|
16
|
+
`)}function V(t){if(t.error){let a=t.error;return`\u274C CORRELATE FAILED
|
|
17
|
+
Error: ${typeof a=="string"?a:JSON.stringify(a)}`}let r=t.result??t,e=r.strategies??[],s=r.correlations??[],o=r.max_simultaneous_positions??{},i=c(r.combined_max_dd),n=[`\u{1F517} Correlation Analysis (${e.length} strategies)`,""];n.push("--- Strategies ---");for(let a of e){if(a.error){n.push(` \u274C ${u(a.name)}: ${u(a.error)}`);continue}let d=a.metrics??{};n.push(` \u2705 ${u(a.name)}: Trades:${c(d.total_trades)} WR:${c(d.win_rate).toFixed(1)}% Net:$${c(d.net_pnl).toFixed(0)} PF:${c(d.profit_factor).toFixed(2)} DD:${c(d.max_drawdown).toFixed(1)}%`)}if(s.length>0){n.push(""),n.push("--- Pairwise Overlap ---");for(let a of s){let d=c(a.overlap_pct),p=c(a.same_direction_pct),y=c(a.concurrent_trades),h=d<20?"\u2705":d<50?"\u26A0\uFE0F":"\u{1F534}";n.push(` ${h} ${u(a.pair)} \u2192 overlap:${d.toFixed(1)}% same-dir:${p.toFixed(1)}% concurrent:${y}`)}}n.push(""),n.push(`\u{1F4CA} Max simultaneous open positions: ${c(o.max)} (across ${c(o.total_strategies)} strategies)`),n.push(`\u{1F4C9} Combined max DD estimate: ${i.toFixed(1)}%`);let m=s.length?s.reduce((a,d)=>a+c(d.overlap_pct),0)/s.length:0;m<20?n.push(`
|
|
18
|
+
\u2705 Low correlation \u2014 strategies are largely independent, suitable for portfolio use.`):m>50&&n.push(`
|
|
19
|
+
\u{1F534} High correlation \u2014 strategies trigger in similar periods, weak diversification.`);let l=u(t.task_id);return l&&n.push(`
|
|
20
|
+
task_id: ${l}`),n.join(`
|
|
21
|
+
`)}function et(t){let r=t.user??{},e=t.worker,o=t.worker_available===!0&&e?`worker:\u2705 id=${u(e.id)} status=${u(e.status)} health=${u(e.health)}`:"worker:\u274C unavailable";return`\u{1F3D3} pong | user=${u(r.username)} (id=${u(r.id)}) | ${o} | server=${u(t.server_version)}`}function rt(t){return["\u{1F4C5} Publish-test backtest window:",` from_timestamp: "${u(t.start_date)}"`,` to_timestamp: "${u(t.end_date)}"`,` duration: ${c(t.days)} days (inclusive\u2192exclusive)`,"","Use directly in watb_run_backtest / watb_sweep / watb_walk_forward."].join(`
|
|
22
|
+
`)}function st(t){let r=t.id??t.strategyId??t.strategy_id,e=u(t.name??t.strategy?.name),s=t.folderId??t.folder_id;return`\u2705 Strategy saved | id=${r??"?"} | name="${e||"?"}"${s!=null?` | folder=${s}`:""}`}function nt(t){let r=t,e=Array.isArray(r)?r:r.items??r.results??r.strategies??[];if(e.length===0)return"No strategies saved yet.";let s=[`\u{1F4C2} Strategies (${e.length})`,""];e.forEach((i,n)=>{let m=i.id??i.strategyId??"?",l=u(i.name,"(unnamed)"),a=i.strategy??i.strategy_json??{},d=u(i.symbol??a.symbol),p=u(i.timeframe??a.timeframe),y=u(i.direction??a.direction),h=u(i.folderName??i.folder_name),x=u(i.visibility),P=u(i.createdAt??i.created_at),F=P?P.slice(0,10):"",A=[];(d||p||y)&&A.push([d,p,y].filter(Boolean).join(" ")),h&&A.push(`\u{1F4C1}${h}`),x&&A.push(x),F&&A.push(F),s.push(` ${n+1}. [${m}] ${l}${A.length?" \u2014 "+A.join(" | "):""}`)});let o=r.total??r.count;return typeof o=="number"&&o>e.length&&s.push(`
|
|
23
|
+
... +${o-e.length} more (use offset to paginate)`),s.join(`
|
|
24
|
+
`)}function it(t){let r=t.id??t.strategyId??"?",e=u(t.name,"(unnamed)"),s=t.strategy??t.strategy_json??t,o=u(s.symbol??t.symbol),i=u(s.timeframe??t.timeframe),n=u(s.direction??t.direction),m=u(t.folderName??t.folder_name),l=u(t.createdAt??t.created_at);return[`\u{1F4C4} Strategy [${r}] "${e}" \u2014 ${o} ${i} ${n}${m?` | \u{1F4C1}${m}`:""}${l?` | created ${l.slice(0,10)}`:""}`,"","```json",JSON.stringify(s,null,2),"```"].join(`
|
|
25
|
+
`)}function Q(t){let r=t.min!=null&&t.max!=null?`(${t.min}..${t.max})`:"",e=t.default!==void 0&&t.default!==null?`=${JSON.stringify(t.default)}`:"";return`${t.name}:${t.type}${r}${e}`}function ot(t,r){let e=t.indicators??[],s=t.tactics??{},o=t.risk_guards??[],i=[];if(e.length>0){let n={};for(let m of e){let l=m.category??"other";(n[l]??=[]).push(m)}i.push(`\u{1F9E9} INDICATORS (${e.length})`);for(let[m,l]of Object.entries(n)){i.push(` [${m}] (${l.length})`);for(let a of l){let d=(a.params??[]).map(Q).join(", "),p=a.outputs?.length?` outputs:[${a.outputs.join(",")}]`:"",y=a.default_output?` default:${a.default_output}`:"";i.push(` ${a.id} (${a.name})${p}${y}${d?` | ${d}`:""}`)}}}for(let[n,m]of[["entry","\u{1F3AF} ENTRY TACTICS"],["sl","\u{1F6D1} SL TACTICS"],["tp1","\u{1F4C8} TP1 TACTICS"],["tp2","\u{1F4C8} TP2 TACTICS"]]){let l=s[n]??[];if(l.length!==0){i.push(""),i.push(`${m} (${l.length})`);for(let a of l){let d=(a.params??[]).map(Q).join(", ");i.push(` ${a.id} (${a.name})${d?` | ${d}`:" | no params"}`)}}}if(o.length>0){i.push(""),i.push(`\u26A0\uFE0F RISK GUARDS (${o.length})`);for(let n of o){let m=u(n.type),l=n.default,a=n.min!=null&&n.max!=null?` (${n.min}..${n.max})`:"";i.push(` ${u(n.id)} (${u(n.name)}) ${m}${a}${l!==void 0?` =${JSON.stringify(l)}`:""}`)}}return i.push(""),i.push("\u{1F310} INDEX SYMBOLS: TOTAL, TOTAL2, TOTAL3, OTHERS \u2014 use as condition `source` for cross-pair gates."),r&&(i.push(""),i.push("\u{1F4D6} STRATEGY GUIDE"),i.push(typeof r=="string"?r:JSON.stringify(r,null,2))),i.join(`
|
|
26
|
+
`)}function g(t){return{content:[{type:"text",text:typeof t=="string"?t:JSON.stringify(t,null,2)}]}}function v(t){if(typeof t=="number")return t;if(!/^\d{4}-\d{2}-\d{2}$/.exec(t))throw new Error(`Invalid timestamp format: "${t}". Use Unix ms (number) or "YYYY-MM-DD" (string).`);return Date.UTC(parseInt(t.slice(0,4),10),parseInt(t.slice(5,7),10)-1,parseInt(t.slice(8,10),10))}async function at(t,r){let e=await r.get("/api/mcp/v1/me/context");return g(et({user:e.user,worker_available:e.worker.available,worker:e.worker.available?{id:e.worker.workerDeploymentId,status:e.worker.status,health:e.worker.healthStatus}:null,server_version:e.serverVersion}))}async function ct(t,r){let e=new URLSearchParams;t.folder_id!==void 0&&e.set("folderId",String(t.folder_id)),t.visibility&&e.set("visibility",t.visibility),t.limit!==void 0&&e.set("limit",String(t.limit)),t.offset!==void 0&&e.set("offset",String(t.offset));let s=e.toString()?`?${e.toString()}`:"",o=await r.get(`/api/mcp/v1/strategies${s}`);return g(nt(o))}var mt={debug:10,info:20,warn:30,error:40},pt=process.env.WATB_LOG_LEVEL??"info";function ut(t){pt=t}function L(t,r){if(mt[t]<mt[pt])return;let e=new Date().toISOString();process.stderr.write(`[${e}] [${t}] `+r.map(Dt).join(" ")+`
|
|
27
|
+
`)}function Dt(t){if(typeof t=="string")return t;if(t instanceof Error)return t.stack??t.message;try{return JSON.stringify(t)}catch{return String(t)}}var j={debug:(...t)=>L("debug",t),info:(...t)=>L("info",t),warn:(...t)=>L("warn",t),error:(...t)=>L("error",t)};var Bt=t=>new Promise(r=>setTimeout(r,t));async function C(t,r){let e=r.maxPolls??240,s=r.intervalMs??1500;for(let o=0;o<e;o++){await Bt(o===0?250:s),r.keepalivePath&&t.dispatch(r.keepalivePath,{},{method:"POST"}).catch(m=>{j.debug("keepalive failed (non-fatal):",m)});let i=await t.dispatch(r.statusPath,void 0,{method:"GET"}),n=r.classify(i);if(n==="done")return i;if(n==="failed"){let m=i?.error??"Worker reported failure";throw new f(_.WorkerError,m,i)}(o+1)%20===0&&j.debug(`${r.label??"poll"}: ${o+1}/${e}`)}throw new f(_.PollingTimeout,`Polling timed out after ${e} attempts (${e*s/1e3}s).`)}async function lt(t,r){if(!t?.strategy_json||typeof t.strategy_json!="object")throw new f(_.InvalidArgs,"strategy_json (object) is required");let e=await r.dispatch("dev/backtest",{strategy:t.strategy_json,from_timestamp:v(t.from_timestamp),to_timestamp:v(t.to_timestamp),initial_balance:t.initial_balance??1e4,risk_per_trade:t.risk_per_trade??20,iteration:t.iteration??1}),s=e.task_id??e.taskId;if(!s)throw new f(_.WorkerError,"Worker did not return task_id",e);let o=await C(r,{statusPath:`dev/backtest/${s}`,keepalivePath:"dev/keepalive",label:"backtest",classify:m=>{let l=m.status;return l==="completed"?"done":l==="failed"?"failed":"wait"}}),i=Ut(o),n=t.iteration??1;return g(Z({...i,task_id:s},n))}function Ut(t){if(!t||typeof t!="object")return t;let r=t.result;if(!r||typeof r!="object")return t;let e=r;if(!("strategy_snapshot"in e))return t;let{strategy_snapshot:s,...o}=e;return{...t,result:{...o,strategy_snapshot:"[omitted \u2014 fetch via watb_get_raw_result section=strategy_snapshot]"}}}async function dt(t,r){let e=await r.post("/api/mcp/v1/strategies",{name:t.name,folderId:t.folder_id,strategy:t.strategy_json,description:t.description,tags:t.tags});return g(st(e))}var Jt=[{symbol:"TOTAL",description:"Total Crypto Market Cap (all coins)"},{symbol:"TOTAL2",description:"Total Market Cap excl. BTC"},{symbol:"TOTAL3",description:"Total Market Cap excl. BTC & ETH"},{symbol:"OTHERS",description:"Total Market Cap excl. Top 10"}];async function gt(t,r){let e=await r.dispatch("dev/pairs",void 0,{method:"GET"}),s=Array.isArray(e)?e:e.symbols??e.pairs??[],o=s.map(n=>{if(typeof n=="string")return n;let m=n.symbol??n,l=n.first_candle??n.available_from,a=n.last_candle??n.available_to,d=n.total_candles;return`${m} (first: ${l??"?"}, last: ${a??"?"}, candles: ${d??"?"})`}),i=Jt.map(n=>`${n.symbol} [INDEX] \u2014 ${n.description} (use as condition source only, NOT as trading pair)`);return g([`Available trading pairs (${s.length}):`,...o,"","Index symbols (condition source only):",...i].join(`
|
|
28
|
+
`))}async function ft(t,r){let[e,s]=await Promise.all([r.dispatch("dev/ai-metadata",void 0,{method:"GET"}),r.dispatch("dev/guide",void 0,{method:"GET"})]);return g(ot(e,s))}async function _t(t,r){let e=await r.dispatch("dev/history",void 0,{method:"GET"}),s=Array.isArray(e)?e:e.results??[];if(s.length===0)return g("No backtest history yet.");let o=[`\u{1F4CB} Last ${s.length} backtests:`];return s.forEach((i,n)=>{let m=i.metrics,l=m?` | Trades:${m.total_trades} WR:${m.win_rate?.toFixed(1)}% Net:${m.net_pnl?.toFixed(2)} PF:${m.profit_factor?.toFixed(2)}`:` | ${i.status}`;o.push(`${n+1}. [${i.task_id}] ${i.strategy_name??"N/A"} \u2014 ${i.symbol} ${i.timeframe} ${i.direction}${l}`)}),g(o.join(`
|
|
29
|
+
`))}async function yt(t,r){if(!Array.isArray(t?.task_ids)||t.task_ids.length===0)throw new f(_.InvalidArgs,"task_ids (non-empty string array) is required");let e=t.task_ids.map(o=>encodeURIComponent(o)).join(","),s=await r.dispatch(`dev/compare?ids=${e}`,void 0,{method:"GET"});return g(tt(s))}async function bt(t,r){if(!t?.symbol||!t?.indicator)throw new f(_.InvalidArgs,"symbol and indicator are required");let e=await r.dispatch("dev/indicator-preview",{symbol:t.symbol,indicator:t.indicator,params:t.params??{},timeframe:t.timeframe??"15m",limit:t.limit??20}),s=e.stats,o=[`\u{1F4CA} ${e.indicator} \u2014 ${e.symbol} ${e.timeframe}`,`Total candles: ${e.total_candles}`,"","Stats:",` min: ${s?.min} | max: ${s?.max}`,` mean: ${s?.mean} | median: ${s?.median}`,` p25: ${s?.p25} | p75: ${s?.p75}`,` current: ${s?.current}`],i=e.values;return i?.length&&(o.push(`
|
|
30
|
+
Last ${Math.min(10,i.length)} values:`),i.slice(-10).forEach(n=>o.push(` ${n.time}: ${n.value}`))),g(o.join(`
|
|
31
|
+
`))}async function ht(t,r){try{return await r.dispatch("dev/reserve",{reserved_by:"mcp-stdio",reason:t?.reason??"MCP session"}),g("\u2705 Worker reserved. Dedicated to your session, background jobs paused. TTL: 3 minutes (extended by keepalive).")}catch(e){return{content:[{type:"text",text:`\u274C Reserve failed: ${e.message}`}],isError:!0}}}async function wt(t,r){try{return await r.dispatch("dev/release",{}),g("\u2705 Worker released. Background jobs resumed.")}catch(e){return{content:[{type:"text",text:`\u274C Release failed: ${e.message}`}],isError:!0}}}function Gt(t){if(!(t.step>0))throw new f(_.InvalidArgs,`Invalid sweep_ranges step ${t.step}`);if(t.end<t.start)throw new f(_.InvalidArgs,`Invalid sweep_ranges: end (${t.end}) < start (${t.start})`);let r=[],e=t.step*1e-6;for(let s=t.start;s<=t.end+e;s+=t.step)r.push(parseFloat(s.toFixed(8)));return r}async function kt(t,r){if(!t?.strategy_json)throw new f(_.InvalidArgs,"strategy_json (object) is required");let e={...t.sweep??{}};if(t.sweep_ranges)for(let[n,m]of Object.entries(t.sweep_ranges))e[n]=Gt(m);if(Object.keys(e).length===0)throw new f(_.InvalidArgs,"sweep is empty. Provide at least one parameter to scan or use sweep_ranges.");let s=await r.dispatch("dev/sweep",{strategy:t.strategy_json,from_timestamp:v(t.from_timestamp),to_timestamp:v(t.to_timestamp),sweep:e,sort_by:t.sort_by??"profit_factor",max_combinations:t.max_combinations??200,...t.initial_balance!=null&&{initial_balance:t.initial_balance},...t.risk_per_trade!=null&&{risk_per_trade:t.risk_per_trade}}),o=s.task_id??s.taskId;if(!o)return g(Y(s));let i=await C(r,{statusPath:`dev/backtest/${o}`,keepalivePath:"dev/keepalive",label:"sweep",maxPolls:360,classify:n=>{let m=n.status;return m==="completed"?"done":m==="failed"?"failed":"wait"}});return g(Y({...i,task_id:o}))}async function Rt(t,r){if(!t?.strategy_json)throw new f(_.InvalidArgs,"strategy_json (object) is required");let e=await r.dispatch("dev/walk-forward",{strategy:t.strategy_json,from_timestamp:v(t.from_timestamp),to_timestamp:v(t.to_timestamp),train_months:t.train_months??4,test_months:t.test_months??2,...t.initial_balance!=null&&{initial_balance:t.initial_balance},...t.risk_per_trade!=null&&{risk_per_trade:t.risk_per_trade}}),s=e.task_id??e.taskId;if(!s)return g(H(e));let o=await C(r,{statusPath:`dev/backtest/${s}`,keepalivePath:"dev/keepalive",label:"walk-forward",maxPolls:360,classify:i=>{let n=i.status;return n==="completed"?"done":n==="failed"?"failed":"wait"}});return g(H({...o,task_id:s}))}async function $t(t,r){if(!Array.isArray(t?.strategies)||t.strategies.length<2)throw new f(_.InvalidArgs,"strategies (>=2) is required");let e=await r.dispatch("dev/correlate",{strategies:t.strategies.map(i=>({name:i.name,json:i.json})),from_timestamp:t.from_timestamp,to_timestamp:t.to_timestamp}),s=e.task_id??e.taskId;if(!s)return g(V(e));let o=await C(r,{statusPath:`dev/backtest/${s}`,keepalivePath:"dev/keepalive",label:"correlate",maxPolls:360,classify:i=>{let n=i.status;return n==="completed"?"done":n==="failed"?"failed":"wait"}});return g(V({...o,task_id:s}))}var M=6e4,qt=20;async function At(t,r){let e=t?.task_id?.trim();if(!e){let p=await r.dispatch("dev/history",void 0,{method:"GET"}),y=Array.isArray(p)?p:p.results??[];if(y.length===0)return g("No backtest history and no task_id was provided.");e=y[0].task_id}let s=await r.dispatch(`dev/backtest/${e}`,void 0,{method:"GET"}),o=s.status;if(o!=="completed"){let p=JSON.stringify(s,null,2).slice(0,M);return g(`Task ${e} status=${o??"unknown"} (not completed yet).
|
|
32
|
+
${p}`)}let i=s.result??{},n=t.section??"summary",m=Math.max(0,Math.min(t.trade_limit??qt,200)),l=t.pretty===!1?0:2,a;switch(n){case"all":{let p=Array.isArray(i.trades)?i.trades:[];a={...i,trades:p.length>m?{_truncated:!0,total:p.length,shown:m,items:p.slice(0,m)}:p};break}case"summary":{let p=i.diagnostics,y=i.strategy_snapshot;a={task_id:e,symbol:i.symbol,timeframe:i.timeframe,direction:i.direction,regimes_enabled:y?.regimes_enabled,metrics:i.metrics,long_metrics:i.long_metrics,short_metrics:i.short_metrics,per_regime_metrics_keys:i.per_regime_metrics?Object.keys(i.per_regime_metrics):null,diagnostics_keys:p?Object.keys(p):null,warnings_count:Array.isArray(p?.warnings)?p.warnings.length:0,errors_count:Array.isArray(p?.errors)?p.errors.length:0,trade_count:Array.isArray(i.trades)?i.trades.length:0};break}case"trades":{let p=Array.isArray(i.trades)?i.trades:[];a={total:p.length,shown:Math.min(p.length,m),items:p.slice(0,m)};break}case"condition_breakdown":{let p=i.diagnostics??{};a={condition_breakdown:p.condition_breakdown,long_condition_breakdown:p.long_condition_breakdown,short_condition_breakdown:p.short_condition_breakdown,per_regime_condition_breakdown:p.per_regime_condition_breakdown};break}case"warnings":case"errors":{let p=i.diagnostics??{};a={[n]:p[n]??[]};break}default:a=i[n]??null}let d=JSON.stringify(a,null,l);return d.length>M&&(d=d.slice(0,M)+`
|
|
33
|
+
... [truncated; ${d.length-M} more bytes]`),g(`\u{1F4E6} task=${e} section=${n}
|
|
34
|
+
${d}`)}async function xt(){let t=new Date,r=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),1)),e=new Date(Date.UTC(r.getUTCFullYear()-1,r.getUTCMonth(),1)),s=m=>{let l=m.getUTCFullYear(),a=String(m.getUTCMonth()+1).padStart(2,"0"),d=String(m.getUTCDate()).padStart(2,"0");return`${l}-${a}-${d}`},o=s(e),i=s(r),n=Math.round((r.getTime()-e.getTime())/864e5);return g(rt({start_date:o,end_date:i,days:n}))}import{writeFileSync as Yt,readFileSync as Ht,existsSync as Vt,mkdirSync as Kt}from"fs";import{join as Xt,dirname as zt}from"path";import{tmpdir as Qt}from"os";function St(){return process.env.WATB_MCP_SESSION_ID?.trim()||`pid-${process.pid}`}function vt(t){return Xt(Qt(),`watb-failed-iters-${t}.json`)}function Tt(t){let r=vt(t);if(!Vt(r))return[];try{return JSON.parse(Ht(r,"utf8"))}catch{return[]}}async function jt(t){if(typeof t?.iteration!="number"||typeof t?.passed!="boolean")throw new f(_.InvalidArgs,"iteration (number) and passed (boolean) are required");if(!t.passed){let r=St(),e=vt(r);try{Kt(zt(e),{recursive:!0})}catch{}let s=Tt(r).filter(o=>o.iteration!==t.iteration);s.push(t),Yt(e,JSON.stringify(s,null,2))}return g(`ok \u2014 iteration ${t.iteration} recorded (passed=${t.passed})`)}async function Ct(){let t=Tt(St());if(t.length===0)return g("No failed iterations recorded.");let r=[`## Failed iterations (${t.length})
|
|
35
|
+
`];for(let e of t.sort((s,o)=>s.iteration-o.iteration)){r.push(`### Iter ${e.iteration} \u2014 ${e.name}`),r.push(`Changes: ${e.changes}`);let s=e.metrics??{};r.push(`Metrics: net_pnl=${(s.net_pnl??0).toFixed(2)}, trades=${s.total_trades??s.trades??0}, win_rate=${(s.win_rate??0).toFixed(1)}%, max_dd=${(s.max_drawdown_pct??0).toFixed(1)}%`),e.strategy_json&&(r.push("```json"),r.push(JSON.stringify(e.strategy_json,null,2)),r.push("```")),r.push("")}return g(r.join(`
|
|
36
|
+
`))}async function It(t,r){if(t?.id==null||t.id==="")throw new f(_.InvalidArgs,"id is required");let e=t.source??"ai";if(e!=="ai")throw new f(_.InvalidArgs,`source '${e}' is not yet supported via MCP (only 'ai' available in v0.1).`);let s=await r.get(`/api/mcp/v1/strategies/${encodeURIComponent(String(t.id))}`);return g(it(s))}async function Pt(t,r){if(!t?.strategy_json||typeof t.strategy_json!="object")throw new f(_.InvalidArgs,"strategy_json is required (object)");let e=await r.dispatch("dev/validate",{strategy:t.strategy_json}),s=[];return s.push(e.valid?"\u2705 Strategy is VALID":"\u274C Strategy is INVALID"),(e.symbol||e.direction||e.timeframe)&&s.push(`Header: ${e.symbol??"?"} ${e.timeframe??"?"} ${e.direction??"?"}`),typeof e.conditions_count=="number"&&s.push(`Conditions: total=${e.conditions_count}`+(typeof e.long_conditions_count=="number"?`, long=${e.long_conditions_count}`:"")+(typeof e.short_conditions_count=="number"?`, short=${e.short_conditions_count}`:"")),e.regimes_enabled&&s.push(`Regimes: enabled (count=${e.regimes_count??0})`),e.errors?.length&&(s.push("",`Errors (${e.errors.length}):`),e.errors.forEach(o=>s.push(` \u2022 ${o}`))),e.warnings?.length&&(s.push("",`Warnings (${e.warnings.length}):`),e.warnings.forEach(o=>s.push(` \u2022 ${o}`))),g(s.join(`
|
|
37
|
+
`))}async function Ft(t,r,e){let s=r??{};switch(t){case"watb_ping":return at(s,e);case"watb_get_pairs":return gt(s,e);case"watb_get_metadata":return ft(s,e);case"watb_get_publish_date_range":return xt();case"watb_list_strategies":return ct(s,e);case"watb_save_strategy":return dt(s,e);case"watb_fetch_strategy_by_id":return It(s,e);case"watb_run_backtest":return lt(s,e);case"watb_sweep":return kt(s,e);case"watb_walk_forward":return Rt(s,e);case"watb_correlate":return $t(s,e);case"watb_indicator_preview":return bt(s,e);case"watb_compare":return yt(s,e);case"watb_history":return _t(s,e);case"watb_get_raw_result":return At(s,e);case"watb_validate_strategy":return Pt(s,e);case"watb_reserve":return ht(s,e);case"watb_release":return wt(s,e);case"watb_record_iteration":return jt(s);case"watb_get_failed_iterations":return Ct();default:throw new f(_.InvalidArgs,`Unknown tool: ${t}`)}}function ae(){try{let t=ie(ne(import.meta.url));return JSON.parse(se(oe(t,"..","package.json"),"utf-8")).version??"0.0.0"}catch{return"0.0.0"}}async function ce(){let t=await X();t.log?.level&&ut(t.log.level);let r=ae();j.info(`watb-mcp ${r} starting (api=${t.apiUrl})`);let e=new W(t),s=new Zt({name:"watb",version:r},{capabilities:{tools:{}}});s.setRequestHandler(re,async()=>({tools:z})),s.setRequestHandler(ee,async i=>{let n=i.params.name,m=i.params.arguments??{};j.debug(`tool call: ${n}`);try{return await Ft(n,m,e)}catch(l){let a=l instanceof f?l:new f(_.Internal,l instanceof Error?l.message:String(l));return j.warn(`tool ${n} failed:`,a.code,a.message),{content:[{type:"text",text:JSON.stringify({error:{code:a.code,message:a.message,details:a.details}},null,2)}],isError:!0}}});let o=new te;await s.connect(o),j.info("watb-mcp connected via stdio")}ce().catch(t=>{process.stderr.write(`watb-mcp fatal: ${t instanceof Error?t.stack??t.message:String(t)}
|
|
38
|
+
`),process.exit(1)});export{ce as main};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@watb/mcp-server",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "WATB Model Context Protocol server — backtest, strategy CRUD and worker dispatch tools for AI assistants.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server": "bin/mcp-server.js",
|
|
9
|
+
"watb-mcp": "bin/watb-mcp.js",
|
|
10
|
+
"watb-mcp-init": "bin/init.js",
|
|
11
|
+
"watb-mcp-doctor": "bin/doctor.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/",
|
|
15
|
+
"bin/",
|
|
16
|
+
"templates/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "tsx src/server.ts",
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"lint": "tsc --noEmit",
|
|
25
|
+
"init": "node bin/init.js",
|
|
26
|
+
"doctor": "node bin/doctor.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
30
|
+
"undici": "^7.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.10.2",
|
|
34
|
+
"tsup": "^8.3.5",
|
|
35
|
+
"tsx": "^4.19.2",
|
|
36
|
+
"typescript": "^5.7.2",
|
|
37
|
+
"vitest": "^2.1.8"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.0.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"mcp",
|
|
44
|
+
"model-context-protocol",
|
|
45
|
+
"watb",
|
|
46
|
+
"trading",
|
|
47
|
+
"backtest",
|
|
48
|
+
"claude",
|
|
49
|
+
"cursor"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"homepage": "https://watb.app/mcp",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|