@crush-protocol/mcp-client 0.3.3 → 0.4.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.
- package/README.md +29 -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
|
@@ -19,31 +19,25 @@ To target a single host: `npx -y @crush-protocol/mcp-client setup --cursor`
|
|
|
19
19
|
|
|
20
20
|
**[All Client Configurations →](#client-configuration)**
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## How It Works
|
|
23
23
|
|
|
24
|
-
|
|
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 |
|
|
24
|
+
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.
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
1. Host connects to the MCP URL
|
|
36
|
-
2. Server returns `401` with `WWW-Authenticate` header
|
|
37
|
-
3. Host performs OAuth discovery and dynamic client registration
|
|
38
|
-
4. Completes Authorization Code + PKCE via browser
|
|
39
|
-
5. Persists tokens locally and reconnects
|
|
26
|
+
```
|
|
27
|
+
AI Tool ──stdio──▶ @crush-protocol/mcp-client ──HTTP+Bearer──▶ MCP Server
|
|
28
|
+
▲
|
|
29
|
+
reads ~/.crush-mcp/
|
|
30
|
+
(cached OAuth tokens)
|
|
31
|
+
```
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
**First-time setup (one-time):**
|
|
42
34
|
|
|
43
35
|
```sh
|
|
44
36
|
npx -y @crush-protocol/mcp-client login
|
|
45
37
|
```
|
|
46
38
|
|
|
39
|
+
This opens a browser for OAuth login. Tokens are cached in `~/.crush-mcp/` and shared across all AI tools. Token refresh is automatic.
|
|
40
|
+
|
|
47
41
|
## Available Tools
|
|
48
42
|
|
|
49
43
|
**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 +61,6 @@ Detailed tool guidance is in [INSTRUCTIONS.md](./INSTRUCTIONS.md).
|
|
|
67
61
|
|
|
68
62
|
[Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol) · Go to: `Settings` → `Cursor Settings` → `MCP` → `Add new global MCP server`
|
|
69
63
|
|
|
70
|
-
**Local:**
|
|
71
64
|
|
|
72
65
|
```json
|
|
73
66
|
{
|
|
@@ -80,17 +73,6 @@ Detailed tool guidance is in [INSTRUCTIONS.md](./INSTRUCTIONS.md).
|
|
|
80
73
|
}
|
|
81
74
|
```
|
|
82
75
|
|
|
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
76
|
|
|
95
77
|
</details>
|
|
96
78
|
|
|
@@ -99,17 +81,11 @@ Detailed tool guidance is in [INSTRUCTIONS.md](./INSTRUCTIONS.md).
|
|
|
99
81
|
|
|
100
82
|
[Claude Code MCP docs](https://docs.anthropic.com/en/docs/claude-code/mcp)
|
|
101
83
|
|
|
102
|
-
**Local:**
|
|
103
84
|
|
|
104
85
|
```sh
|
|
105
86
|
claude mcp add --scope user crush-protocol -- npx -y @crush-protocol/mcp-client
|
|
106
87
|
```
|
|
107
88
|
|
|
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
89
|
|
|
114
90
|
</details>
|
|
115
91
|
|
|
@@ -118,7 +94,6 @@ claude mcp add --scope user --transport http crush-protocol https://crush-mcp-at
|
|
|
118
94
|
|
|
119
95
|
[OpenCode MCP docs](https://opencode.ai/docs/mcp-servers) · Add to `~/.config/opencode/opencode.json`
|
|
120
96
|
|
|
121
|
-
**Local:**
|
|
122
97
|
|
|
123
98
|
```json
|
|
124
99
|
{
|
|
@@ -133,20 +108,6 @@ claude mcp add --scope user --transport http crush-protocol https://crush-mcp-at
|
|
|
133
108
|
}
|
|
134
109
|
```
|
|
135
110
|
|
|
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
111
|
|
|
151
112
|
</details>
|
|
152
113
|
|
|
@@ -166,12 +127,6 @@ args = ["-y", "@crush-protocol/mcp-client"]
|
|
|
166
127
|
startup_timeout_ms = 20000
|
|
167
128
|
```
|
|
168
129
|
|
|
169
|
-
**Remote:**
|
|
170
|
-
|
|
171
|
-
```toml
|
|
172
|
-
[mcp_servers.crush-protocol]
|
|
173
|
-
url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
174
|
-
```
|
|
175
130
|
|
|
176
131
|
</details>
|
|
177
132
|
|
|
@@ -180,19 +135,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
180
135
|
|
|
181
136
|
[Gemini CLI Configuration](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html) · Add to `~/.gemini/settings.json`
|
|
182
137
|
|
|
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
138
|
|
|
197
139
|
```json
|
|
198
140
|
{
|
|
@@ -212,20 +154,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
212
154
|
|
|
213
155
|
[VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) · Add to `.vscode/mcp.json`
|
|
214
156
|
|
|
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
157
|
|
|
230
158
|
```json
|
|
231
159
|
{
|
|
@@ -246,19 +174,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
246
174
|
|
|
247
175
|
[Windsurf MCP docs](https://docs.windsurf.com/windsurf/cascade/mcp)
|
|
248
176
|
|
|
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
177
|
|
|
263
178
|
```json
|
|
264
179
|
{
|
|
@@ -278,7 +193,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
278
193
|
|
|
279
194
|
[Claude Desktop MCP docs](https://modelcontextprotocol.io/quickstart/user) · Edit `claude_desktop_config.json`
|
|
280
195
|
|
|
281
|
-
**Local:**
|
|
282
196
|
|
|
283
197
|
```json
|
|
284
198
|
{
|
|
@@ -298,19 +212,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
298
212
|
|
|
299
213
|
[Kiro MCP docs](https://kiro.dev/docs/mcp/configuration/) · Navigate `Kiro` → `MCP Servers` → `+ Add`
|
|
300
214
|
|
|
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
215
|
|
|
315
216
|
```json
|
|
316
217
|
{
|
|
@@ -330,20 +231,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
330
231
|
|
|
331
232
|
[Roo Code MCP docs](https://docs.roocode.com/features/mcp/using-mcp-in-roo)
|
|
332
233
|
|
|
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
234
|
|
|
348
235
|
```json
|
|
349
236
|
{
|
|
@@ -363,20 +250,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
363
250
|
|
|
364
251
|
[Cline MCP Marketplace](https://cline.bot/mcp-marketplace)
|
|
365
252
|
|
|
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
253
|
|
|
381
254
|
```json
|
|
382
255
|
{
|
|
@@ -396,19 +269,6 @@ url = "https://crush-mcp-ats.dev.xexlab.com/mcp"
|
|
|
396
269
|
|
|
397
270
|
[Trae MCP docs](https://docs.trae.ai/ide/model-context-protocol?_lang=en)
|
|
398
271
|
|
|
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
272
|
|
|
413
273
|
```json
|
|
414
274
|
{
|
|
@@ -451,20 +311,6 @@ Manual config in VS Code settings:
|
|
|
451
311
|
|
|
452
312
|
[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
313
|
|
|
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
314
|
|
|
469
315
|
```json
|
|
470
316
|
{
|
|
@@ -485,20 +331,6 @@ Manual config in VS Code settings:
|
|
|
485
331
|
|
|
486
332
|
Add to `~/.copilot/mcp-config.json`:
|
|
487
333
|
|
|
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
334
|
|
|
503
335
|
```json
|
|
504
336
|
{
|
|
@@ -557,7 +389,7 @@ Add to `~/.copilot/mcp-config.json`:
|
|
|
557
389
|
[Amp MCP docs](https://ampcode.com/manual#mcp)
|
|
558
390
|
|
|
559
391
|
```sh
|
|
560
|
-
amp mcp add crush-protocol
|
|
392
|
+
amp mcp add crush-protocol -- npx -y @crush-protocol/mcp-client
|
|
561
393
|
```
|
|
562
394
|
|
|
563
395
|
</details>
|
|
@@ -586,19 +418,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
586
418
|
|
|
587
419
|
[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
420
|
|
|
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
421
|
|
|
603
422
|
```json
|
|
604
423
|
{
|
|
@@ -620,20 +439,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
620
439
|
|
|
621
440
|
**CLI:** `qwen mcp add crush-protocol npx -y @crush-protocol/mcp-client`
|
|
622
441
|
|
|
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
442
|
```json
|
|
638
443
|
{
|
|
639
444
|
"mcpServers": {
|
|
@@ -670,21 +475,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
670
475
|
|
|
671
476
|
[Visual Studio MCP docs](https://learn.microsoft.com/visualstudio/ide/mcp-servers?view=vs-2022)
|
|
672
477
|
|
|
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
478
|
|
|
689
479
|
```json
|
|
690
480
|
{
|
|
@@ -740,20 +530,6 @@ amp mcp add crush-protocol https://crush-mcp-ats.dev.xexlab.com/mcp
|
|
|
740
530
|
|
|
741
531
|
[Kilo Code docs](https://kilocode.ai)
|
|
742
532
|
|
|
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
533
|
|
|
758
534
|
```json
|
|
759
535
|
{
|
|
@@ -787,19 +563,6 @@ Go to Zencoder menu → Agent tools → Add custom MCP, paste:
|
|
|
787
563
|
|
|
788
564
|
[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
565
|
|
|
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
566
|
|
|
804
567
|
```json
|
|
805
568
|
{
|
|
@@ -873,14 +636,26 @@ Use `cmd` as the command wrapper:
|
|
|
873
636
|
## CLI Usage
|
|
874
637
|
|
|
875
638
|
```sh
|
|
876
|
-
|
|
877
|
-
npx -y @crush-protocol/mcp-client
|
|
878
|
-
npx -y @crush-protocol/mcp-client
|
|
879
|
-
|
|
880
|
-
|
|
639
|
+
# Auth
|
|
640
|
+
npx -y @crush-protocol/mcp-client login # OAuth login (browser)
|
|
641
|
+
npx -y @crush-protocol/mcp-client setup --all # Auto-write config for all supported hosts
|
|
642
|
+
|
|
643
|
+
# Tools
|
|
644
|
+
npx -y @crush-protocol/mcp-client tools:list # List available tools
|
|
645
|
+
npx -y @crush-protocol/mcp-client ping # Test connectivity
|
|
646
|
+
|
|
647
|
+
# Backtest
|
|
648
|
+
npx -y @crush-protocol/mcp-client backtest:schema # Backtest config schema
|
|
881
649
|
npx -y @crush-protocol/mcp-client backtest:list --limit 10
|
|
882
650
|
```
|
|
883
651
|
|
|
652
|
+
### Environment Variables
|
|
653
|
+
|
|
654
|
+
| Variable | Description | Default |
|
|
655
|
+
|----------|-------------|---------|
|
|
656
|
+
| `CRUSH_MCP_SERVER_URL` | Override the MCP server URL | `https://crush-mcp-ats.dev.xexlab.com/mcp` |
|
|
657
|
+
| `CRUSH_OAUTH_ACCESS_TOKEN` | Skip OAuth, use a pre-existing token | — |
|
|
658
|
+
|
|
884
659
|
## License
|
|
885
660
|
|
|
886
661
|
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
|
+
}
|