@crush-protocol/mcp-client 0.3.3 → 0.4.1
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/README.md +31 -254
- package/dist/cli.js +31 -5
- package/dist/mcp/proxy.d.ts +1 -0
- package/dist/mcp/proxy.js +188 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,33 +17,29 @@ This writes MCP config for all **9** supported hosts: Cursor, Claude Code, Codex
|
|
|
17
17
|
|
|
18
18
|
To target a single host: `npx -y @crush-protocol/mcp-client setup --cursor`
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## Connection Modes
|
|
23
|
-
|
|
24
|
-
| Mode | Transport | Auth | Best For |
|
|
25
|
-
|------|-----------|------|----------|
|
|
26
|
-
| **Local** | stdio (npx) | OAuth — browser opens automatically | Most MCP clients |
|
|
27
|
-
| **Remote** | Streamable HTTP | OAuth — browser opens automatically | Clients with native HTTP support |
|
|
20
|
+
The default hosted MCP endpoint is `https://crush-mcp-ats.dev.xexlab.com/mcp`.
|
|
28
21
|
|
|
29
|
-
**
|
|
22
|
+
**[All Client Configurations →](#client-configuration)**
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
## How It Works
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
The CLI acts as a **stdio ↔ HTTP proxy**: it reads cached OAuth tokens from `~/.crush-mcp/`, connects to the Crush MCP server, and bridges all requests over stdio. No extra arguments needed.
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
```
|
|
29
|
+
AI Tool ──stdio──▶ @crush-protocol/mcp-client ──HTTP+Bearer──▶ MCP Server
|
|
30
|
+
▲
|
|
31
|
+
reads ~/.crush-mcp/
|
|
32
|
+
(cached OAuth tokens)
|
|
33
|
+
```
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
**First-time setup (one-time):**
|
|
42
36
|
|
|
43
37
|
```sh
|
|
44
38
|
npx -y @crush-protocol/mcp-client login
|
|
45
39
|
```
|
|
46
40
|
|
|
41
|
+
This opens a browser for OAuth login. Tokens are cached in `~/.crush-mcp/` and shared across all AI tools. Token refresh is automatic.
|
|
42
|
+
|
|
47
43
|
## Available Tools
|
|
48
44
|
|
|
49
45
|
**Market Data** — `list_tables` · `list_tokens` · `list_indicators` · `list_timeframes` · `get_data_range` · `check_query_size` · `fetch_ohlcv` · `fetch_indicator` · `fetch_news` · `get_connection_config` · `save_custom_indicator` · `list_custom_indicators` · `get_custom_indicator` · `delete_custom_indicator`
|
|
@@ -67,7 +63,6 @@ Detailed tool guidance is in [INSTRUCTIONS.md](./INSTRUCTIONS.md).
|
|
|
67
63
|
|
|
68
64
|
[Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol) · Go to: `Settings` → `Cursor Settings` → `MCP` → `Add new global MCP server`
|
|
69
65
|
|
|
70
|
-
**Local:**
|
|
71
66
|
|
|
72
67
|
```json
|
|
73
68
|
{
|
|
@@ -80,17 +75,6 @@ Detailed tool guidance is in [INSTRUCTIONS.md](./INSTRUCTIONS.md).
|
|
|
80
75
|
}
|
|
81
76
|
```
|
|
82
77
|
|
|
83
|
-
**Remote:**
|
|
84
|
-
|
|
85
|
-
```json
|
|
86
|
-
{
|
|
87
|
-
"mcpServers": {
|
|
88
|
-
"crush-protocol": {
|
|
89
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
78
|
|
|
95
79
|
</details>
|
|
96
80
|
|
|
@@ -99,17 +83,11 @@ Detailed tool guidance is in [INSTRUCTIONS.md](./INSTRUCTIONS.md).
|
|
|
99
83
|
|
|
100
84
|
[Claude Code MCP docs](https://docs.anthropic.com/en/docs/claude-code/mcp)
|
|
101
85
|
|
|
102
|
-
**Local:**
|
|
103
86
|
|
|
104
87
|
```sh
|
|
105
88
|
claude mcp add --scope user crush-protocol -- npx -y @crush-protocol/mcp-client
|
|
106
89
|
```
|
|
107
90
|
|
|
108
|
-
**Remote:**
|
|
109
|
-
|
|
110
|
-
```sh
|
|
111
|
-
claude mcp add --scope user --transport http crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
112
|
-
```
|
|
113
91
|
|
|
114
92
|
</details>
|
|
115
93
|
|
|
@@ -118,7 +96,6 @@ claude mcp add --scope user --transport http crush-protocol https://crush-mcp-at
|
|
|
118
96
|
|
|
119
97
|
[OpenCode MCP docs](https://opencode.ai/docs/mcp-servers) · Add to `~/.config/opencode/opencode.json`
|
|
120
98
|
|
|
121
|
-
**Local:**
|
|
122
99
|
|
|
123
100
|
```json
|
|
124
101
|
{
|
|
@@ -133,20 +110,6 @@ claude mcp add --scope user --transport http crush-protocol https://crush-mcp-at
|
|
|
133
110
|
}
|
|
134
111
|
```
|
|
135
112
|
|
|
136
|
-
**Remote:**
|
|
137
|
-
|
|
138
|
-
```json
|
|
139
|
-
{
|
|
140
|
-
"$schema": "https://opencode.ai/config.json",
|
|
141
|
-
"mcp": {
|
|
142
|
-
"crush-protocol": {
|
|
143
|
-
"type": "remote",
|
|
144
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp",
|
|
145
|
-
"enabled": true
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
113
|
|
|
151
114
|
</details>
|
|
152
115
|
|
|
@@ -166,12 +129,6 @@ args = ["-y", "@crush-protocol/mcp-client"]
|
|
|
166
129
|
startup_timeout_ms = 20000
|
|
167
130
|
```
|
|
168
131
|
|
|
169
|
-
**Remote:**
|
|
170
|
-
|
|
171
|
-
```toml
|
|
172
|
-
[mcp_servers.crush-protocol]
|
|
173
|
-
url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
174
|
-
```
|
|
175
132
|
|
|
176
133
|
</details>
|
|
177
134
|
|
|
@@ -180,19 +137,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
180
137
|
|
|
181
138
|
[Gemini CLI Configuration](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html) · Add to `~/.gemini/settings.json`
|
|
182
139
|
|
|
183
|
-
**Remote:**
|
|
184
|
-
|
|
185
|
-
```json
|
|
186
|
-
{
|
|
187
|
-
"mcpServers": {
|
|
188
|
-
"crush-protocol": {
|
|
189
|
-
"httpUrl": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Local:**
|
|
196
140
|
|
|
197
141
|
```json
|
|
198
142
|
{
|
|
@@ -212,20 +156,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
212
156
|
|
|
213
157
|
[VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) · Add to `.vscode/mcp.json`
|
|
214
158
|
|
|
215
|
-
**Remote:**
|
|
216
|
-
|
|
217
|
-
```json
|
|
218
|
-
{
|
|
219
|
-
"servers": {
|
|
220
|
-
"crush-protocol": {
|
|
221
|
-
"type": "http",
|
|
222
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**Local:**
|
|
229
159
|
|
|
230
160
|
```json
|
|
231
161
|
{
|
|
@@ -246,19 +176,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
246
176
|
|
|
247
177
|
[Windsurf MCP docs](https://docs.windsurf.com/windsurf/cascade/mcp)
|
|
248
178
|
|
|
249
|
-
**Remote:**
|
|
250
|
-
|
|
251
|
-
```json
|
|
252
|
-
{
|
|
253
|
-
"mcpServers": {
|
|
254
|
-
"crush-protocol": {
|
|
255
|
-
"serverUrl": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Local:**
|
|
262
179
|
|
|
263
180
|
```json
|
|
264
181
|
{
|
|
@@ -278,7 +195,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
278
195
|
|
|
279
196
|
[Claude Desktop MCP docs](https://modelcontextprotocol.io/quickstart/user) · Edit `claude_desktop_config.json`
|
|
280
197
|
|
|
281
|
-
**Local:**
|
|
282
198
|
|
|
283
199
|
```json
|
|
284
200
|
{
|
|
@@ -298,19 +214,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
298
214
|
|
|
299
215
|
[Kiro MCP docs](https://kiro.dev/docs/mcp/configuration/) · Navigate `Kiro` → `MCP Servers` → `+ Add`
|
|
300
216
|
|
|
301
|
-
**Remote:**
|
|
302
|
-
|
|
303
|
-
```json
|
|
304
|
-
{
|
|
305
|
-
"mcpServers": {
|
|
306
|
-
"crush-protocol": {
|
|
307
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
**Local:**
|
|
314
217
|
|
|
315
218
|
```json
|
|
316
219
|
{
|
|
@@ -330,20 +233,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
330
233
|
|
|
331
234
|
[Roo Code MCP docs](https://docs.roocode.com/features/mcp/using-mcp-in-roo)
|
|
332
235
|
|
|
333
|
-
**Remote:**
|
|
334
|
-
|
|
335
|
-
```json
|
|
336
|
-
{
|
|
337
|
-
"mcpServers": {
|
|
338
|
-
"crush-protocol": {
|
|
339
|
-
"type": "streamable-http",
|
|
340
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
**Local:**
|
|
347
236
|
|
|
348
237
|
```json
|
|
349
238
|
{
|
|
@@ -363,20 +252,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
363
252
|
|
|
364
253
|
[Cline MCP Marketplace](https://cline.bot/mcp-marketplace)
|
|
365
254
|
|
|
366
|
-
**Remote:**
|
|
367
|
-
|
|
368
|
-
```json
|
|
369
|
-
{
|
|
370
|
-
"mcpServers": {
|
|
371
|
-
"crush-protocol": {
|
|
372
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp",
|
|
373
|
-
"type": "streamableHttp"
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
**Local:**
|
|
380
255
|
|
|
381
256
|
```json
|
|
382
257
|
{
|
|
@@ -396,19 +271,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
396
271
|
|
|
397
272
|
[Trae MCP docs](https://docs.trae.ai/ide/model-context-protocol?_lang=en)
|
|
398
273
|
|
|
399
|
-
**Remote:**
|
|
400
|
-
|
|
401
|
-
```json
|
|
402
|
-
{
|
|
403
|
-
"mcpServers": {
|
|
404
|
-
"crush-protocol": {
|
|
405
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
**Local:**
|
|
412
274
|
|
|
413
275
|
```json
|
|
414
276
|
{
|
|
@@ -451,20 +313,6 @@ Manual config in VS Code settings:
|
|
|
451
313
|
|
|
452
314
|
[GitHub Copilot MCP docs](https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/agents/copilot-coding-agent/extending-copilot-coding-agent-with-mcp)
|
|
453
315
|
|
|
454
|
-
**Remote:**
|
|
455
|
-
|
|
456
|
-
```json
|
|
457
|
-
{
|
|
458
|
-
"mcpServers": {
|
|
459
|
-
"crush-protocol": {
|
|
460
|
-
"type": "http",
|
|
461
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
**Local:**
|
|
468
316
|
|
|
469
317
|
```json
|
|
470
318
|
{
|
|
@@ -485,20 +333,6 @@ Manual config in VS Code settings:
|
|
|
485
333
|
|
|
486
334
|
Add to `~/.copilot/mcp-config.json`:
|
|
487
335
|
|
|
488
|
-
**Remote:**
|
|
489
|
-
|
|
490
|
-
```json
|
|
491
|
-
{
|
|
492
|
-
"mcpServers": {
|
|
493
|
-
"crush-protocol": {
|
|
494
|
-
"type": "http",
|
|
495
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
**Local:**
|
|
502
336
|
|
|
503
337
|
```json
|
|
504
338
|
{
|
|
@@ -557,7 +391,7 @@ Add to `~/.copilot/mcp-config.json`:
|
|
|
557
391
|
[Amp MCP docs](https://ampcode.com/manual#mcp)
|
|
558
392
|
|
|
559
393
|
```sh
|
|
560
|
-
amp mcp add crush-protocol
|
|
394
|
+
amp mcp add crush-protocol -- npx -y @crush-protocol/mcp-client
|
|
561
395
|
```
|
|
562
396
|
|
|
563
397
|
</details>
|
|
@@ -586,19 +420,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
586
420
|
|
|
587
421
|
[JetBrains AI Assistant docs](https://www.jetbrains.com/help/ai-assistant/configure-an-mcp-server.html) · Go to `Settings` → `Tools` → `AI Assistant` → `Model Context Protocol (MCP)` → `+ Add`
|
|
588
422
|
|
|
589
|
-
**Remote:**
|
|
590
|
-
|
|
591
|
-
```json
|
|
592
|
-
{
|
|
593
|
-
"mcpServers": {
|
|
594
|
-
"crush-protocol": {
|
|
595
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
**Local:**
|
|
602
423
|
|
|
603
424
|
```json
|
|
604
425
|
{
|
|
@@ -620,20 +441,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
620
441
|
|
|
621
442
|
**CLI:** `qwen mcp add crush-protocol npx -y @crush-protocol/mcp-client`
|
|
622
443
|
|
|
623
|
-
**Remote** (add to `~/.qwen/settings.json`):
|
|
624
|
-
|
|
625
|
-
```json
|
|
626
|
-
{
|
|
627
|
-
"mcpServers": {
|
|
628
|
-
"crush-protocol": {
|
|
629
|
-
"httpUrl": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
**Local:**
|
|
636
|
-
|
|
637
444
|
```json
|
|
638
445
|
{
|
|
639
446
|
"mcpServers": {
|
|
@@ -670,21 +477,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
670
477
|
|
|
671
478
|
[Visual Studio MCP docs](https://learn.microsoft.com/visualstudio/ide/mcp-servers?view=vs-2022)
|
|
672
479
|
|
|
673
|
-
**Remote:**
|
|
674
|
-
|
|
675
|
-
```json
|
|
676
|
-
{
|
|
677
|
-
"inputs": [],
|
|
678
|
-
"servers": {
|
|
679
|
-
"crush-protocol": {
|
|
680
|
-
"type": "http",
|
|
681
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
**Local:**
|
|
688
480
|
|
|
689
481
|
```json
|
|
690
482
|
{
|
|
@@ -740,20 +532,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
740
532
|
|
|
741
533
|
[Kilo Code docs](https://kilocode.ai)
|
|
742
534
|
|
|
743
|
-
**Remote:**
|
|
744
|
-
|
|
745
|
-
```json
|
|
746
|
-
{
|
|
747
|
-
"mcpServers": {
|
|
748
|
-
"crush-protocol": {
|
|
749
|
-
"type": "streamable-http",
|
|
750
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
**Local:**
|
|
757
535
|
|
|
758
536
|
```json
|
|
759
537
|
{
|
|
@@ -787,19 +565,6 @@ Go to Zencoder menu → Agent tools → Add custom MCP, paste:
|
|
|
787
565
|
|
|
788
566
|
[Qodo Gen docs](https://docs.qodo.ai/qodo-documentation/qodo-gen/qodo-gen-chat/agentic-mode/agentic-tools-mcps) · Open Qodo Gen chat → Connect more tools → `+ Add new MCP`
|
|
789
567
|
|
|
790
|
-
**Remote:**
|
|
791
|
-
|
|
792
|
-
```json
|
|
793
|
-
{
|
|
794
|
-
"mcpServers": {
|
|
795
|
-
"crush-protocol": {
|
|
796
|
-
"url": "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
**Local:**
|
|
803
568
|
|
|
804
569
|
```json
|
|
805
570
|
{
|
|
@@ -873,14 +638,26 @@ Use `cmd` as the command wrapper:
|
|
|
873
638
|
## CLI Usage
|
|
874
639
|
|
|
875
640
|
```sh
|
|
876
|
-
|
|
877
|
-
npx -y @crush-protocol/mcp-client
|
|
878
|
-
npx -y @crush-protocol/mcp-client
|
|
879
|
-
|
|
880
|
-
|
|
641
|
+
# Auth
|
|
642
|
+
npx -y @crush-protocol/mcp-client login # OAuth login (browser)
|
|
643
|
+
npx -y @crush-protocol/mcp-client setup --all # Auto-write config for all supported hosts
|
|
644
|
+
|
|
645
|
+
# Tools
|
|
646
|
+
npx -y @crush-protocol/mcp-client tools:list # List available tools
|
|
647
|
+
npx -y @crush-protocol/mcp-client ping # Test connectivity
|
|
648
|
+
|
|
649
|
+
# Backtest
|
|
650
|
+
npx -y @crush-protocol/mcp-client backtest:schema # Backtest config schema
|
|
881
651
|
npx -y @crush-protocol/mcp-client backtest:list --limit 10
|
|
882
652
|
```
|
|
883
653
|
|
|
654
|
+
### Environment Variables
|
|
655
|
+
|
|
656
|
+
| Variable | Description | Default |
|
|
657
|
+
|----------|-------------|---------|
|
|
658
|
+
| `CRUSH_MCP_SERVER_URL` | Override the MCP server URL | `https://crush-mcp-ats.dev.xexlab.com/mcp` |
|
|
659
|
+
| `CRUSH_OAUTH_ACCESS_TOKEN` | Skip OAuth, use a pre-existing token | — |
|
|
660
|
+
|
|
884
661
|
## License
|
|
885
662
|
|
|
886
663
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,12 @@ import "dotenv/config";
|
|
|
3
3
|
import { BacktestClient } from "./backtest/backtestClient.js";
|
|
4
4
|
import { ClickHouseDirectClient } from "./clickhouse/directClient.js";
|
|
5
5
|
import { OAuthRemoteMcpClient } from "./mcp/oauthRemoteClient.js";
|
|
6
|
+
import { runProxy } from "./mcp/proxy.js";
|
|
6
7
|
import { RemoteMcpClient } from "./mcp/remoteClient.js";
|
|
7
8
|
import { ALL_TARGETS, installClientConfig } from "./setup/setupClients.js";
|
|
8
|
-
const MCP_SERVER_URL = "https://crush-mcp-ats.dev.xexlab.com/mcp";
|
|
9
|
+
const MCP_SERVER_URL = process.env.CRUSH_MCP_SERVER_URL || "https://crush-mcp-ats.dev.xexlab.com/mcp";
|
|
9
10
|
const printUsage = () => {
|
|
10
|
-
console.log(`\ncrush-mcp-client\n\nGeneral:\n login\n setup [--cursor] [--claude] [--codex] [--gemini] [--opencode] [--all] [--scope user|project]\n tools:list [--token TOKEN]\n tool:call --name TOOL_NAME [--args JSON] [--token TOKEN]\n ping [--token TOKEN]\n\nBacktest:\n backtest:schema [--token TOKEN]\n backtest:tokens [--platform PLATFORM] [--token TOKEN]\n backtest:validate --expression JSON [--data-source kline|factors] [--token TOKEN]\n backtest:create --config JSON [--backtest-id ID] [--token TOKEN]\n backtest:list [--status STATUS] [--limit N] [--offset N] [--token TOKEN]\n\nClickHouse:\n clickhouse:list-tables [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB]\n clickhouse:query --sql SQL [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB --ch-row-cap N]\n\nAuth:\n --token TOKEN uses a provided OAuth access token.\n Without --token, OAuth runs automatically in the browser when needed.\n\nEnv:\n CRUSH_OAUTH_ACCESS_TOKEN\n CH_HOST, CH_PORT, CH_USER, CH_PASSWORD, CH_DATABASE, CH_ROW_CAP\n`);
|
|
11
|
+
console.log(`\ncrush-mcp-client\n\nGeneral:\n login [SERVER_URL]\n proxy [SERVER_URL] — stdio proxy (login once, use everywhere)\n setup [--cursor] [--claude] [--codex] [--gemini] [--opencode] [--all] [--scope user|project]\n tools:list [--token TOKEN]\n tool:call --name TOOL_NAME [--args JSON] [--token TOKEN]\n ping [--token TOKEN]\n\nBacktest:\n backtest:schema [--token TOKEN]\n backtest:tokens [--platform PLATFORM] [--token TOKEN]\n backtest:validate --expression JSON [--data-source kline|factors] [--token TOKEN]\n backtest:create --config JSON [--backtest-id ID] [--token TOKEN]\n backtest:list [--status STATUS] [--limit N] [--offset N] [--token TOKEN]\n\nClickHouse:\n clickhouse:list-tables [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB]\n clickhouse:query --sql SQL [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB --ch-row-cap N]\n\nAuth:\n --token TOKEN uses a provided OAuth access token.\n Without --token, OAuth runs automatically in the browser when needed.\n\nProxy Mode (recommended for AI tools):\n 1. Login once: npx @crush-protocol/mcp-client login SERVER_URL\n 2. Configure: { "command": "npx", "args": ["-y", "@crush-protocol/mcp-client", "proxy", "SERVER_URL"] }\n\nEnv:\n CRUSH_MCP_SERVER_URL\n CRUSH_OAUTH_ACCESS_TOKEN\n CH_HOST, CH_PORT, CH_USER, CH_PASSWORD, CH_DATABASE, CH_ROW_CAP\n`);
|
|
11
12
|
};
|
|
12
13
|
const parseFlags = (args) => {
|
|
13
14
|
const flags = {};
|
|
@@ -32,7 +33,7 @@ const requireString = (value, message) => {
|
|
|
32
33
|
}
|
|
33
34
|
return value;
|
|
34
35
|
};
|
|
35
|
-
const getServerUrl = () => MCP_SERVER_URL;
|
|
36
|
+
const getServerUrl = (override) => override || MCP_SERVER_URL;
|
|
36
37
|
const getSetupTargets = (flags) => {
|
|
37
38
|
if (flags.all === true) {
|
|
38
39
|
return [...ALL_TARGETS];
|
|
@@ -51,7 +52,7 @@ const getSetupTargets = (flags) => {
|
|
|
51
52
|
* - 无 token → 自动拉起浏览器登录
|
|
52
53
|
*/
|
|
53
54
|
const createSmartClient = (flags) => {
|
|
54
|
-
const serverUrl = getServerUrl();
|
|
55
|
+
const serverUrl = getServerUrl(typeof flags.server === "string" ? flags.server : undefined);
|
|
55
56
|
// 显式传了 token → 用简单客户端
|
|
56
57
|
const explicitToken = typeof flags.token === "string" ? flags.token : process.env.CRUSH_OAUTH_ACCESS_TOKEN || "";
|
|
57
58
|
if (explicitToken) {
|
|
@@ -106,14 +107,39 @@ const createClickHouseClient = (flags) => {
|
|
|
106
107
|
};
|
|
107
108
|
const run = async () => {
|
|
108
109
|
const [, , command, ...rest] = process.argv;
|
|
110
|
+
// 自动 proxy 检测:当 stdin 不是 TTY(被 AI 工具通过 stdio 调用)时,
|
|
111
|
+
// 如果没有已知子命令,自动进入 proxy 模式
|
|
112
|
+
const isStdioPipe = !process.stdin.isTTY;
|
|
113
|
+
const knownCommands = new Set([
|
|
114
|
+
"proxy", "login", "setup", "tools:list", "tool:call", "ping",
|
|
115
|
+
"backtest:schema", "backtest:tokens", "backtest:validate", "backtest:create", "backtest:list",
|
|
116
|
+
"clickhouse:list-tables", "clickhouse:query",
|
|
117
|
+
"help", "--help", "-h",
|
|
118
|
+
]);
|
|
119
|
+
if (isStdioPipe && (!command || !knownCommands.has(command))) {
|
|
120
|
+
// 无命令或第一个参数是 URL → 自动进入 proxy 模式
|
|
121
|
+
const serverUrl = getServerUrl(command?.startsWith("http") ? command : undefined);
|
|
122
|
+
await runProxy(serverUrl);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
109
125
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
110
126
|
printUsage();
|
|
111
127
|
return;
|
|
112
128
|
}
|
|
113
129
|
const flags = parseFlags(rest);
|
|
114
130
|
switch (command) {
|
|
131
|
+
case "proxy": {
|
|
132
|
+
// proxy [SERVER_URL] — stdio ↔ HTTP 代理模式
|
|
133
|
+
const proxyUrl = rest.find((a) => !a.startsWith("--"));
|
|
134
|
+
const serverUrl = getServerUrl(proxyUrl);
|
|
135
|
+
await runProxy(serverUrl);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
115
138
|
case "login": {
|
|
116
|
-
|
|
139
|
+
// login [SERVER_URL] — 第一个非 flag 参数作为 server URL
|
|
140
|
+
const loginUrl = rest.find((a) => !a.startsWith("--"));
|
|
141
|
+
const serverUrl = getServerUrl(loginUrl);
|
|
142
|
+
console.log(`Connecting to ${serverUrl} ...`);
|
|
117
143
|
const client = new OAuthRemoteMcpClient({ serverUrl });
|
|
118
144
|
try {
|
|
119
145
|
await client.ensureAuthorized();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runProxy(serverUrl: string): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Proxy — stdio ↔ Streamable HTTP 桥接
|
|
3
|
+
*
|
|
4
|
+
* 原理:
|
|
5
|
+
* AI 工具(Cursor/Claude/Antigravity)通过 stdio 连接本 proxy,
|
|
6
|
+
* proxy 读取 ~/.crush-mcp/ 中缓存的 OAuth token,
|
|
7
|
+
* 将请求转发到远程 MCP Server(Streamable HTTP)。
|
|
8
|
+
*
|
|
9
|
+
* 使用:
|
|
10
|
+
* npx @crush-protocol/mcp-client proxy [SERVER_URL]
|
|
11
|
+
*
|
|
12
|
+
* 用户只需执行一次 `login` 获取 token,所有 AI 工具共享同一份凭证。
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
19
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
22
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { CLIENT_NAME, CLIENT_VERSION } from "./version.js";
|
|
24
|
+
const STORAGE_DIR = path.join(os.homedir(), ".crush-mcp");
|
|
25
|
+
const hashServerUrl = (serverUrl) => createHash("sha256").update(serverUrl).digest("hex").slice(0, 16);
|
|
26
|
+
const loadTokens = async (serverUrl) => {
|
|
27
|
+
// 尝试多个可能的 URL 变体(login 用 base URL,proxy 用 /mcp URL)
|
|
28
|
+
const candidates = [serverUrl];
|
|
29
|
+
if (serverUrl.endsWith("/mcp")) {
|
|
30
|
+
candidates.push(serverUrl.replace(/\/mcp$/, ""));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
candidates.push(`${serverUrl}/mcp`);
|
|
34
|
+
}
|
|
35
|
+
for (const url of candidates) {
|
|
36
|
+
const storageFile = path.join(STORAGE_DIR, `oauth-${hashServerUrl(url)}.json`);
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(storageFile, "utf8");
|
|
39
|
+
const state = JSON.parse(raw);
|
|
40
|
+
if (state.tokens?.access_token) {
|
|
41
|
+
return state.tokens;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// 继续尝试下一个
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
const saveTokens = async (serverUrl, tokens) => {
|
|
51
|
+
const storageFile = path.join(STORAGE_DIR, `oauth-${hashServerUrl(serverUrl)}.json`);
|
|
52
|
+
let state = {};
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readFile(storageFile, "utf8");
|
|
55
|
+
state = JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
state.tokens = tokens;
|
|
61
|
+
await mkdir(path.dirname(storageFile), { recursive: true });
|
|
62
|
+
await writeFile(storageFile, JSON.stringify(state, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* 尝试用 refresh_token 刷新 access_token
|
|
66
|
+
*/
|
|
67
|
+
const refreshAccessToken = async (serverUrl, tokens) => {
|
|
68
|
+
if (!tokens.refresh_token)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
// 先获取 token_endpoint
|
|
72
|
+
const metadataUrl = new URL("/.well-known/oauth-authorization-server", serverUrl);
|
|
73
|
+
const metaRes = await fetch(metadataUrl.toString());
|
|
74
|
+
if (!metaRes.ok)
|
|
75
|
+
return null;
|
|
76
|
+
const meta = (await metaRes.json());
|
|
77
|
+
// 获取 client_id
|
|
78
|
+
const storageFile = path.join(STORAGE_DIR, `oauth-${hashServerUrl(serverUrl)}.json`);
|
|
79
|
+
const raw = await readFile(storageFile, "utf8");
|
|
80
|
+
const state = JSON.parse(raw);
|
|
81
|
+
const clientInfo = state.clientInformation;
|
|
82
|
+
if (!clientInfo?.client_id)
|
|
83
|
+
return null;
|
|
84
|
+
const body = new URLSearchParams({
|
|
85
|
+
grant_type: "refresh_token",
|
|
86
|
+
refresh_token: tokens.refresh_token,
|
|
87
|
+
client_id: clientInfo.client_id,
|
|
88
|
+
});
|
|
89
|
+
const res = await fetch(meta.token_endpoint, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
92
|
+
body: body.toString(),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok)
|
|
95
|
+
return null;
|
|
96
|
+
const newTokens = (await res.json());
|
|
97
|
+
await saveTokens(serverUrl, newTokens);
|
|
98
|
+
return newTokens;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
export async function runProxy(serverUrl) {
|
|
105
|
+
// 1. 加载缓存的 token
|
|
106
|
+
let tokens = await loadTokens(serverUrl);
|
|
107
|
+
if (!tokens?.access_token) {
|
|
108
|
+
process.stderr.write(`[crush-mcp-proxy] No cached token found for ${serverUrl}\n` +
|
|
109
|
+
` Run: npx @crush-protocol/mcp-client login ${serverUrl}\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
// 2. 创建到远程 MCP Server 的 HTTP 客户端
|
|
113
|
+
const createTransport = (token) => new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
114
|
+
requestInit: {
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${token}`,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
const remoteClient = new Client({ name: `${CLIENT_NAME}-proxy`, version: CLIENT_VERSION }, { capabilities: {} });
|
|
121
|
+
// 尝试连接,如果 401 则刷新 token
|
|
122
|
+
let transport = createTransport(tokens.access_token);
|
|
123
|
+
try {
|
|
124
|
+
await remoteClient.connect(transport);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const msg = String(error);
|
|
128
|
+
if (msg.includes("401") || msg.includes("Unauthorized")) {
|
|
129
|
+
process.stderr.write("[crush-mcp-proxy] Token expired, refreshing...\n");
|
|
130
|
+
const refreshed = await refreshAccessToken(serverUrl, tokens);
|
|
131
|
+
if (!refreshed) {
|
|
132
|
+
process.stderr.write("[crush-mcp-proxy] Token refresh failed. Please re-login:\n" +
|
|
133
|
+
` npx @crush-protocol/mcp-client login ${serverUrl}\n`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
tokens = refreshed;
|
|
137
|
+
transport = createTransport(tokens.access_token);
|
|
138
|
+
await remoteClient.connect(transport);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 3. 获取远程 server 的能力
|
|
145
|
+
const serverInfo = remoteClient.getServerVersion();
|
|
146
|
+
const remoteTools = await remoteClient.listTools();
|
|
147
|
+
// 4. 创建本地 stdio server,代理所有请求
|
|
148
|
+
const localServer = new Server({
|
|
149
|
+
name: serverInfo?.name ?? "crush-mcp-proxy",
|
|
150
|
+
version: serverInfo?.version ?? "1.0.0",
|
|
151
|
+
}, {
|
|
152
|
+
capabilities: {
|
|
153
|
+
tools: { listChanged: false },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// 代理 tools/list
|
|
157
|
+
localServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
158
|
+
try {
|
|
159
|
+
const result = await remoteClient.listTools();
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { tools: remoteTools.tools };
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// 代理 tools/call
|
|
167
|
+
localServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
168
|
+
const { name, arguments: args } = request.params;
|
|
169
|
+
return remoteClient.callTool({ name, arguments: args ?? {} });
|
|
170
|
+
});
|
|
171
|
+
// 代理 ping
|
|
172
|
+
localServer.setRequestHandler(PingRequestSchema, async () => {
|
|
173
|
+
await remoteClient.ping();
|
|
174
|
+
return {};
|
|
175
|
+
});
|
|
176
|
+
// 5. 启动 stdio transport
|
|
177
|
+
const stdioTransport = new StdioServerTransport();
|
|
178
|
+
await localServer.connect(stdioTransport);
|
|
179
|
+
process.stderr.write(`[crush-mcp-proxy] Connected to ${serverUrl} | stdio proxy ready\n`);
|
|
180
|
+
// 优雅关闭
|
|
181
|
+
const cleanup = async () => {
|
|
182
|
+
await localServer.close();
|
|
183
|
+
await remoteClient.close();
|
|
184
|
+
process.exit(0);
|
|
185
|
+
};
|
|
186
|
+
process.on("SIGINT", cleanup);
|
|
187
|
+
process.on("SIGTERM", cleanup);
|
|
188
|
+
}
|