@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 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
- ## Connection Modes
22
+ ## How It Works
23
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 |
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
- **Remote Server URL:** `https://crush-mcp-ats.dev.xexlab.com/mcp`
30
-
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.
32
-
33
- For hosts that support URL-only MCP (Remote mode), the full flow is:
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
- If a host cannot complete OAuth automatically, pre-authorize with:
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 https://crush-mcp-ats.dev.xexlab.com/mcp
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
- 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
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
- 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.0",
4
4
  "description": "Crush MCP npm client package (remote Streamable HTTP + optional ClickHouse direct)",
5
5
  "type": "module",
6
6
  "license": "MIT",