@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 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
- **[All Client Configurations →](#client-configuration)**
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
- **Remote Server URL:** `https://crush-mcp-ats.dev.xexlab.com/mcp`
22
+ **[All Client Configurations →](#client-configuration)**
30
23
 
31
- Authentication is automatic (OAuth 2.1 Authorization Code + PKCE). On first use, a browser window opens for login; tokens are cached locally for reuse.
24
+ ## How It Works
32
25
 
33
- For hosts that support URL-only MCP (Remote mode), the full flow is:
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
- 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
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
- If a host cannot complete OAuth automatically, pre-authorize with:
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 https://crush-mcp-ats.dev.xexlab.com/mcp
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
- npx -y @crush-protocol/mcp-client login # 预授权 OAuth(浏览器登录)
877
- npx -y @crush-protocol/mcp-client setup --all # 写入所有客户端配置
878
- npx -y @crush-protocol/mcp-client tools:list # 列出可用工具
879
- npx -y @crush-protocol/mcp-client ping # 测试连通性
880
- npx -y @crush-protocol/mcp-client backtest:schema # 获取回测配置 schema
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
- const serverUrl = getServerUrl();
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crush-protocol/mcp-client",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Crush MCP npm client package (remote Streamable HTTP + optional ClickHouse direct)",
5
5
  "type": "module",
6
6
  "license": "MIT",