@bigbrain-work/mcp-connect 1.2.2 → 1.3.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/src/cli.js CHANGED
@@ -1,129 +1,480 @@
1
- import os from 'node:os'
2
- import path from 'node:path'
1
+ import os from "node:os";
2
+ import path from "node:path";
3
3
 
4
- import { parseCliArguments } from './arguments.js'
5
- import { configureAgent } from './configurators.js'
6
- import { detectInstalledAgents } from './detection.js'
4
+ import { resolveAuthorization } from "./authorization.js";
5
+ import { parseCliArguments } from "./arguments.js";
6
+ import { configureAgent, stdioServerDefinition } from "./configurators.js";
7
7
  import {
8
8
  API_KEY_ENV,
9
+ AUTH_URL,
9
10
  MCP_URL,
10
11
  PACKAGE_NAME,
11
12
  PACKAGE_VERSION,
12
13
  SERVER_NAME,
13
14
  SUPPORTED_AGENTS,
14
- } from './constants.js'
15
- import { persistApiKey, resolveApiKey } from './credentials.js'
16
- import { runProxy } from './proxy.js'
17
- import { printStatus } from './status.js'
15
+ } from "./constants.js";
16
+ import {
17
+ clearPersistedApiKey,
18
+ persistApiKey,
19
+ resolveApiKey,
20
+ } from "./credentials.js";
21
+ import { detectInstalledAgents } from "./detection.js";
22
+ import {
23
+ DeviceAuthClient,
24
+ DeviceAuthError,
25
+ renderQrCode,
26
+ waitForDeviceAuthorization,
27
+ } from "./device-auth-client.js";
28
+ import { verifyAndPersistLogin } from "./login-flow.js";
29
+ import { runProxy } from "./proxy.js";
30
+ import {
31
+ normalizeDeviceUserCode,
32
+ validateAuthUrl,
33
+ validateMcpUrl,
34
+ } from "./security.js";
35
+ import { printStatus, printTools, probeMcp } from "./status.js";
36
+ import { PendingLoginStore, TokenStore } from "./token-store.js";
37
+ import { printUpdateStatus } from "./updater.js";
18
38
 
19
39
  function printHelp() {
20
40
  console.log(`${PACKAGE_NAME} ${PACKAGE_VERSION}
21
41
 
22
42
  用法:
23
- mcp-connect
24
- mcp-connect --agent <name>
43
+ shiliu login
44
+ shiliu login --no-wait --json
45
+ shiliu login poll --session <id> [--wait] [--json]
46
+ shiliu install [--agent <name>]
47
+ shiliu status [--json]
48
+ shiliu tools [--json]
49
+ shiliu update
50
+ shiliu logout
51
+ shiliu mcp
52
+
53
+ 兼容命令:
54
+ mcp-connect ...
25
55
  mcp-connect proxy
26
- mcp-connect status
27
56
 
28
57
  选项:
29
58
  -a, --agent <name> 配置指定客户端
30
59
  --dry-run 只显示将执行的操作
31
60
  --home <path> 指定用户目录(主要用于测试)
32
- --url <url> proxy 使用的远程 MCP 地址
61
+ --url <url> 指定石榴 AI MCP 地址(诊断或本机测试)
62
+ --auth-url <url> 指定石榴 AI 授权地址(主要用于本机测试)
63
+ --allow-localhost 显式允许连接本机回环测试服务
64
+ --legacy-api-key 使用旧 API Key 兼容登录
65
+ --no-wait 创建登录会话后立即返回
66
+ --wait 等待指定登录会话完成
67
+ --session <id> 指定待继续的登录会话
68
+ --json 以 JSON 输出 status/tools
33
69
  -h, --help 显示帮助
34
70
  -v, --version 显示版本
35
71
 
36
- 认证:
37
- 安装器优先读取 ${API_KEY_ENV};未设置时会隐藏提示输入。
38
- 真实 API Key 不会写入生成的 MCP 配置。
72
+ 安全:
73
+ login 默认显示微信二维码,换取短期访问令牌和可轮换刷新令牌。
74
+ 令牌保存在系统凭据库,不会写入 Agent 配置,也不会由 status/tools 输出。
75
+ 兼容期可使用 --legacy-api-key;不提供会进入 shell 历史的密钥参数。
39
76
 
40
77
  智能配置:
41
- 不指定 --agent 时,自动检测并配置已安装的 ${SUPPORTED_AGENTS.join('')}。
42
- 其他 Agent 名称也可传入;未内置适配器时会输出通用 stdio MCP 配置。
43
- stdio 代理会从本机安全存储读取 API Key,再转发到远程 HTTP MCP。`)
44
- }
45
-
46
- export function stdioProxyDefinition(platform = process.platform) {
47
- if (platform === 'win32') {
48
- return {
49
- command: 'cmd',
50
- args: ['/d', '/s', '/c', 'npx', '-y', PACKAGE_NAME, 'proxy'],
51
- }
52
- }
53
- return {
54
- command: 'npx',
55
- args: ['-y', PACKAGE_NAME, 'proxy'],
56
- }
78
+ 未指定 --agent 时自动检测 ${SUPPORTED_AGENTS.join("")}。
79
+ 任意安全的 Agent 名称均可传入;未内置适配器时输出标准 stdio MCP 配置。`);
57
80
  }
58
81
 
59
82
  function printGenericInstructions(agent, { dryRun = false } = {}) {
60
- const proxy = stdioProxyDefinition()
61
83
  const config = {
62
84
  mcpServers: {
63
- [SERVER_NAME]: proxy,
85
+ [SERVER_NAME]: stdioServerDefinition(),
64
86
  },
65
- }
66
- console.log(`暂未内置 ${agent} 的自动配置适配器。请在该 Agent 中添加下面这份标准 stdio MCP 配置:
87
+ };
88
+ console.log(`暂未内置 ${agent} 的自动配置适配器。请在该 Agent 中添加下面的标准 stdio MCP 配置:
67
89
 
68
90
  ${JSON.stringify(config, null, 2)}
69
91
 
70
- 该命令会在本机启动通用桥接器,由桥接器连接:${MCP_URL}
71
- Agent 配置中不需要填写 Authorization 请求头,也不保存真实 API Key。
92
+ 桥接器连接 ${MCP_URL},并从本机凭据存储读取授权信息。
93
+ ${dryRun ? "演练模式没有保存凭据。" : "Agent 配置中不包含真实凭据。"} 请勿将凭据写入项目代码或提交到 Git。`);
94
+ }
95
+
96
+ async function legacyLogin({ home, dryRun, url }) {
97
+ const apiKey = await resolveApiKey();
98
+ if (!dryRun) {
99
+ const remote = await probeMcp(apiKey, { url });
100
+ if (!remote.ok) throw new Error(`凭据验证失败:${remote.detail}`);
101
+ }
102
+ await persistApiKey(apiKey, { dryRun, home });
103
+ console.log(
104
+ dryRun
105
+ ? "登录演练通过:凭据格式有效,未连接服务、未写入本机。"
106
+ : "登录成功:凭据已验证并保存到本机;未写入 Agent 配置或项目文件。",
107
+ );
108
+ }
72
109
 
73
- ${dryRun ? `演练模式未保存 API Key;正式运行时会安全保存到 ${API_KEY_ENV}。` : `API Key 已安全保存到 ${API_KEY_ENV}。`}请勿将真实 API Key 写入项目代码或提交到 Git。`)
110
+ function buildPendingLogin(start, authUrl, allowLocalhost) {
111
+ return {
112
+ sessionId: normalizeDeviceUserCode(start.user_code),
113
+ deviceCode: start.device_code,
114
+ expiresAt: Date.now() + Number(start.expires_in) * 1000,
115
+ interval: Number(start.interval) || 5,
116
+ authUrl,
117
+ allowLocalhost,
118
+ };
74
119
  }
75
120
 
76
- export async function runCli(argv = process.argv.slice(2)) {
77
- const options = parseCliArguments(argv)
78
- if (options.help) {
79
- printHelp()
80
- return
121
+ export function buildLoginInstructions(start, { allowLocalhost = false } = {}) {
122
+ const sessionId = normalizeDeviceUserCode(start.user_code);
123
+ const localhostOption = allowLocalhost ? " --allow-localhost" : "";
124
+ return {
125
+ status: "authorization_pending",
126
+ login_session_id: sessionId,
127
+ user_code: start.user_code,
128
+ verification_uri:
129
+ start.verification_uri_complete ||
130
+ start.qr_code_uri ||
131
+ start.verification_uri,
132
+ expires_in: Number(start.expires_in),
133
+ poll_command: `shiliu login poll --session ${sessionId} --wait --json${localhostOption}`,
134
+ next_action_hint:
135
+ "请让用户打开 verification_uri 完成微信授权;用户确认后运行 poll_command。",
136
+ };
137
+ }
138
+
139
+ function printJson(value) {
140
+ console.log(JSON.stringify(value, null, 2));
141
+ }
142
+
143
+ async function startPendingDeviceLogin({
144
+ authUrl,
145
+ allowLocalhost,
146
+ json,
147
+ pendingLoginStore,
148
+ }) {
149
+ const client = new DeviceAuthClient({ baseUrl: authUrl });
150
+ const start = await client.start();
151
+ await pendingLoginStore.save(
152
+ buildPendingLogin(start, authUrl, allowLocalhost),
153
+ );
154
+ const instructions = buildLoginInstructions(start, { allowLocalhost });
155
+ if (json) {
156
+ printJson(instructions);
157
+ return;
81
158
  }
82
- if (options.version) {
83
- console.log(PACKAGE_VERSION)
84
- return
159
+ console.log(`请使用微信扫描二维码完成登录(验证码 ${start.user_code}):`);
160
+ console.log(await renderQrCode(start.qr_code_uri));
161
+ console.log(`也可以打开:${instructions.verification_uri}`);
162
+ console.log(
163
+ `完成后运行:${instructions.poll_command.replace(" --json", "")}`,
164
+ );
165
+ }
166
+
167
+ async function pollPendingDeviceLogin({
168
+ session,
169
+ wait,
170
+ json,
171
+ url,
172
+ tokenStore,
173
+ pendingLoginStore,
174
+ }) {
175
+ if (!session) throw new Error("login poll 需要 --session <id>");
176
+ const pending = await pendingLoginStore.load();
177
+ if (!pending || pending.sessionId !== session) {
178
+ throw new Error("未找到对应的待处理登录会话,请重新运行 shiliu login");
85
179
  }
180
+ if (pending.expiresAt <= Date.now()) {
181
+ await pendingLoginStore.clear();
182
+ throw new Error("登录会话已过期,请重新运行 shiliu login");
183
+ }
184
+
185
+ const pendingAuthUrl = validateAuthUrl(pending.authUrl, {
186
+ allowLocalhost: pending.allowLocalhost === true,
187
+ });
188
+ const client = new DeviceAuthClient({ baseUrl: pendingAuthUrl });
189
+ let response;
190
+ try {
191
+ if (wait) {
192
+ response = await waitForDeviceAuthorization(
193
+ {
194
+ device_code: pending.deviceCode,
195
+ expires_in: Math.max(
196
+ 1,
197
+ Math.ceil((pending.expiresAt - Date.now()) / 1000),
198
+ ),
199
+ interval: pending.interval,
200
+ },
201
+ client,
202
+ { onPending: json ? undefined : () => process.stdout.write(".") },
203
+ );
204
+ if (!json) process.stdout.write("\n");
205
+ } else {
206
+ response = await client.exchange(pending.deviceCode);
207
+ }
208
+ } catch (error) {
209
+ if (
210
+ error instanceof DeviceAuthError &&
211
+ ["authorization_pending", "slow_down"].includes(error.code)
212
+ ) {
213
+ const result = {
214
+ status: "authorization_pending",
215
+ login_session_id: pending.sessionId,
216
+ poll_command: `shiliu login poll --session ${pending.sessionId} --wait --json`,
217
+ };
218
+ if (json) printJson(result);
219
+ else console.log("登录尚未完成,请授权后重新运行并加上 --wait。");
220
+ return;
221
+ }
222
+ if (error instanceof DeviceAuthError && error.code === "expired_token") {
223
+ await pendingLoginStore.clear();
224
+ }
225
+ throw error;
226
+ }
227
+
228
+ const remote = await verifyAndPersistLogin({
229
+ response,
230
+ client,
231
+ tokenStore,
232
+ url,
233
+ });
234
+ await pendingLoginStore.clear();
235
+ const result = {
236
+ status: "success",
237
+ message: "登录成功",
238
+ tool_count: remote.toolCount,
239
+ };
240
+ if (json) printJson(result);
241
+ else console.log(`登录成功:${remote.detail};令牌已保存到系统凭据库。`);
242
+ }
86
243
 
87
- const home = path.resolve(options.home || os.homedir())
88
- if (options.command === 'proxy') {
89
- await runProxy({ home, url: options.url || MCP_URL })
90
- return
244
+ async function deviceLogin({
245
+ dryRun,
246
+ authUrl,
247
+ url,
248
+ tokenStore,
249
+ pendingLoginStore,
250
+ noWait,
251
+ json,
252
+ allowLocalhost,
253
+ }) {
254
+ if (dryRun) {
255
+ console.log(
256
+ `登录演练:将从 ${authUrl} 获取设备码、显示微信二维码,并把令牌保存到系统凭据库;未发起网络请求。`,
257
+ );
258
+ return;
91
259
  }
92
- if (options.command === 'status') {
93
- const result = await printStatus({ home })
94
- if (!result.remote.ok) {
95
- process.exitCode = 1
260
+
261
+ if (noWait) {
262
+ return startPendingDeviceLogin({
263
+ authUrl,
264
+ allowLocalhost,
265
+ json,
266
+ pendingLoginStore,
267
+ });
268
+ }
269
+
270
+ const client = new DeviceAuthClient({ baseUrl: authUrl });
271
+ const start = await client.start();
272
+ console.log(`请使用微信扫描二维码完成登录(验证码 ${start.user_code}):`);
273
+ console.log(await renderQrCode(start.qr_code_uri));
274
+ console.log(
275
+ `若终端二维码无法识别,请打开:${start.verification_uri_complete}`,
276
+ );
277
+ const response = await waitForDeviceAuthorization(start, client, {
278
+ onPending: () => process.stdout.write("."),
279
+ });
280
+ process.stdout.write("\n");
281
+ const remote = await verifyAndPersistLogin({
282
+ response,
283
+ client,
284
+ tokenStore,
285
+ url,
286
+ });
287
+ console.log(`登录成功:${remote.detail};短期访问令牌已保存到系统凭据库。`);
288
+ }
289
+
290
+ async function login({
291
+ home,
292
+ dryRun,
293
+ url,
294
+ authUrl,
295
+ legacyApiKey,
296
+ tokenStore,
297
+ pendingLoginStore,
298
+ noWait = false,
299
+ json = false,
300
+ allowLocalhost = false,
301
+ }) {
302
+ if (legacyApiKey) return legacyLogin({ home, dryRun, url });
303
+ return deviceLogin({
304
+ dryRun,
305
+ authUrl,
306
+ url,
307
+ tokenStore,
308
+ pendingLoginStore,
309
+ noWait,
310
+ json,
311
+ allowLocalhost,
312
+ });
313
+ }
314
+
315
+ export async function logout({
316
+ home,
317
+ dryRun,
318
+ authUrl,
319
+ tokenStore,
320
+ pendingLoginStore,
321
+ clearLegacyApiKey = clearPersistedApiKey,
322
+ clientFactory = (baseUrl) => new DeviceAuthClient({ baseUrl }),
323
+ }) {
324
+ if (dryRun) {
325
+ console.log("演练模式:未清除本机凭据。");
326
+ return;
327
+ }
328
+
329
+ const current = await tokenStore.load();
330
+ if (current?.refreshToken) {
331
+ try {
332
+ await clientFactory(authUrl).revoke(current.refreshToken);
333
+ } catch (error) {
334
+ throw new Error(
335
+ `远端令牌撤销失败,本机凭据已保留,请稍后重试退出:${error.message}`,
336
+ );
96
337
  }
97
- return
98
338
  }
99
339
 
100
- const apiKey = await resolveApiKey()
101
- await persistApiKey(apiKey, {
102
- dryRun: options.dryRun,
340
+ await tokenStore.clear();
341
+ await pendingLoginStore.clear();
342
+ await clearLegacyApiKey({ home, dryRun: false });
343
+ console.log("已退出登录,并从系统凭据库清除令牌和旧版兼容凭据。");
344
+ }
345
+
346
+ async function ensureLogin(options) {
347
+ const authorization = await resolveAuthorization({
348
+ home: options.home,
349
+ authUrl: options.authUrl,
350
+ tokenStore: options.tokenStore,
351
+ });
352
+ if (authorization.token) return;
353
+ await login(options);
354
+ }
355
+
356
+ async function install({
357
+ agent,
358
+ dryRun,
359
+ home,
360
+ url,
361
+ authUrl,
362
+ legacyApiKey,
363
+ tokenStore,
364
+ }) {
365
+ await ensureLogin({
103
366
  home,
104
- })
105
- const requestedAgent = options.agent === 'auto' ? undefined : options.agent
367
+ dryRun,
368
+ url,
369
+ authUrl,
370
+ legacyApiKey,
371
+ tokenStore,
372
+ });
373
+ const requestedAgent = agent === "auto" ? undefined : agent;
106
374
  if (requestedAgent && !SUPPORTED_AGENTS.includes(requestedAgent)) {
107
- printGenericInstructions(requestedAgent, { dryRun: options.dryRun })
108
- return
375
+ printGenericInstructions(requestedAgent, { dryRun });
376
+ return;
109
377
  }
110
378
 
111
379
  const agents = requestedAgent
112
380
  ? [requestedAgent]
113
- : await detectInstalledAgents({ home })
381
+ : await detectInstalledAgents({ home });
114
382
  if (agents.length === 0) {
115
- printGenericInstructions('当前客户端', { dryRun: options.dryRun })
116
- return
383
+ printGenericInstructions("当前客户端", { dryRun });
384
+ return;
385
+ }
386
+ for (const detectedAgent of agents) {
387
+ await configureAgent(detectedAgent, { dryRun, home });
388
+ }
389
+ console.log(
390
+ dryRun
391
+ ? `检查完成:将为 ${agents.join("、")} 配置 ${MCP_URL},未写入文件。`
392
+ : `配置完成:${agents.join("、")} 已接入石榴 AI MCP。请重启客户端后运行 shiliu status。`,
393
+ );
394
+ }
395
+
396
+ export async function runCli(argv = process.argv.slice(2)) {
397
+ const options = parseCliArguments(argv);
398
+ if (options.help) return printHelp();
399
+ if (options.version) {
400
+ console.log(PACKAGE_VERSION);
401
+ return;
117
402
  }
118
403
 
119
- for (const agent of agents) {
120
- await configureAgent(agent, {
404
+ const home = path.resolve(options.home || os.homedir());
405
+ const url = validateMcpUrl(options.url || MCP_URL, {
406
+ allowLocalhost: options.allowLocalhost,
407
+ });
408
+ const authUrl = validateAuthUrl(options.authUrl || AUTH_URL, {
409
+ allowLocalhost: options.allowLocalhost,
410
+ });
411
+ const tokenStore = new TokenStore();
412
+ const pendingLoginStore = new PendingLoginStore();
413
+ if (options.command === "mcp" || options.command === "proxy") {
414
+ await runProxy({ home, url, authUrl });
415
+ return;
416
+ }
417
+ if (options.command === "login") {
418
+ if (options.subcommand === "poll") {
419
+ await pollPendingDeviceLogin({
420
+ session: options.session,
421
+ wait: options.wait,
422
+ json: options.json,
423
+ url,
424
+ tokenStore,
425
+ pendingLoginStore,
426
+ });
427
+ return;
428
+ }
429
+ await login({
430
+ home,
431
+ dryRun: options.dryRun,
432
+ url,
433
+ authUrl,
434
+ legacyApiKey: options.legacyApiKey,
435
+ allowLocalhost: options.allowLocalhost,
436
+ tokenStore,
437
+ pendingLoginStore,
438
+ noWait: options.noWait,
439
+ json: options.json,
440
+ });
441
+ return;
442
+ }
443
+ if (options.command === "logout") {
444
+ return logout({
445
+ home,
121
446
  dryRun: options.dryRun,
447
+ authUrl,
448
+ tokenStore,
449
+ pendingLoginStore,
450
+ });
451
+ }
452
+ if (options.command === "status") {
453
+ const result = await printStatus({
122
454
  home,
123
- })
455
+ url,
456
+ authUrl,
457
+ json: options.json,
458
+ });
459
+ if (!result.remote.ok) process.exitCode = 1;
460
+ return;
124
461
  }
125
-
126
- console.log(options.dryRun
127
- ? `检查完成:将为 ${agents.join('、')} 配置 ${MCP_URL},未写入任何文件。`
128
- : `配置完成:${agents.join('、')} 已接入石榴 AI MCP。请重启客户端后运行 ${PACKAGE_NAME} status 检查连接。`)
462
+ if (options.command === "tools") {
463
+ await printTools({ home, url, authUrl, json: options.json });
464
+ return;
465
+ }
466
+ if (options.command === "update") {
467
+ await printUpdateStatus();
468
+ return;
469
+ }
470
+ await install({
471
+ agent: options.agent,
472
+ dryRun: options.dryRun,
473
+ home,
474
+ url,
475
+ authUrl,
476
+ legacyApiKey: options.legacyApiKey,
477
+ allowLocalhost: options.allowLocalhost,
478
+ tokenStore,
479
+ });
129
480
  }