@bigbrain-work/mcp-connect 1.2.1 → 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,101 +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 { 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";
17
38
 
18
39
  function printHelp() {
19
40
  console.log(`${PACKAGE_NAME} ${PACKAGE_VERSION}
20
41
 
21
42
  用法:
22
- mcp-connect
23
- mcp-connect --agent <name>
24
- mcp-connect status
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 ...
55
+ mcp-connect proxy
25
56
 
26
57
  选项:
27
58
  -a, --agent <name> 配置指定客户端
28
59
  --dry-run 只显示将执行的操作
29
60
  --home <path> 指定用户目录(主要用于测试)
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
30
69
  -h, --help 显示帮助
31
70
  -v, --version 显示版本
32
71
 
33
- 认证:
34
- 安装器优先读取 ${API_KEY_ENV};未设置时会隐藏提示输入。
35
- 真实 API Key 不会写入生成的 MCP 配置。
72
+ 安全:
73
+ login 默认显示微信二维码,换取短期访问令牌和可轮换刷新令牌。
74
+ 令牌保存在系统凭据库,不会写入 Agent 配置,也不会由 status/tools 输出。
75
+ 兼容期可使用 --legacy-api-key;不提供会进入 shell 历史的密钥参数。
36
76
 
37
77
  智能配置:
38
- 不指定 --agent 时,自动检测并配置已安装的 ${SUPPORTED_AGENTS.join('')}。
39
- 其他 Agent 名称也可传入;未内置适配器时会输出通用 HTTP MCP 配置。`)
78
+ 未指定 --agent 时自动检测 ${SUPPORTED_AGENTS.join("")}。
79
+ 任意安全的 Agent 名称均可传入;未内置适配器时输出标准 stdio MCP 配置。`);
40
80
  }
41
81
 
42
82
  function printGenericInstructions(agent, { dryRun = false } = {}) {
43
- console.log(`暂未内置 ${agent} 的自动配置适配器,请在该 Agent 中添加远程 HTTP MCP:
83
+ const config = {
84
+ mcpServers: {
85
+ [SERVER_NAME]: stdioServerDefinition(),
86
+ },
87
+ };
88
+ console.log(`暂未内置 ${agent} 的自动配置适配器。请在该 Agent 中添加下面的标准 stdio MCP 配置:
44
89
 
45
- 名称:${SERVER_NAME}
46
- 地址:${MCP_URL}
47
- 认证头:Authorization: Bearer <你的石榴 AI API Key>
90
+ ${JSON.stringify(config, null, 2)}
48
91
 
49
- ${dryRun ? `演练模式未保存 API Key;正式运行时会安全保存到 ${API_KEY_ENV}。` : `API Key 已安全保存到 ${API_KEY_ENV}。`}请勿将真实 API Key 写入项目代码或提交到 Git。`)
92
+ 桥接器连接 ${MCP_URL},并从本机凭据存储读取授权信息。
93
+ ${dryRun ? "演练模式没有保存凭据。" : "Agent 配置中不包含真实凭据。"} 请勿将凭据写入项目代码或提交到 Git。`);
50
94
  }
51
95
 
52
- export async function runCli(argv = process.argv.slice(2)) {
53
- const options = parseCliArguments(argv)
54
- if (options.help) {
55
- printHelp()
56
- return
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}`);
57
101
  }
58
- if (options.version) {
59
- console.log(PACKAGE_VERSION)
60
- return
102
+ await persistApiKey(apiKey, { dryRun, home });
103
+ console.log(
104
+ dryRun
105
+ ? "登录演练通过:凭据格式有效,未连接服务、未写入本机。"
106
+ : "登录成功:凭据已验证并保存到本机;未写入 Agent 配置或项目文件。",
107
+ );
108
+ }
109
+
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
+ };
119
+ }
120
+
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;
61
158
  }
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
+ }
62
166
 
63
- const home = path.resolve(options.home || os.homedir())
64
- if (options.command === 'status') {
65
- const result = await printStatus({ home })
66
- if (!result.remote.ok) {
67
- process.exitCode = 1
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");
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;
68
221
  }
69
- return
222
+ if (error instanceof DeviceAuthError && error.code === "expired_token") {
223
+ await pendingLoginStore.clear();
224
+ }
225
+ throw error;
70
226
  }
71
227
 
72
- const apiKey = await resolveApiKey()
73
- await persistApiKey(apiKey, {
74
- dryRun: options.dryRun,
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
+ }
243
+
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;
259
+ }
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
+ );
337
+ }
338
+ }
339
+
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({
75
366
  home,
76
- })
77
- 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;
78
374
  if (requestedAgent && !SUPPORTED_AGENTS.includes(requestedAgent)) {
79
- printGenericInstructions(requestedAgent, { dryRun: options.dryRun })
80
- return
375
+ printGenericInstructions(requestedAgent, { dryRun });
376
+ return;
81
377
  }
82
378
 
83
379
  const agents = requestedAgent
84
380
  ? [requestedAgent]
85
- : await detectInstalledAgents({ home })
381
+ : await detectInstalledAgents({ home });
86
382
  if (agents.length === 0) {
87
- printGenericInstructions('当前客户端', { dryRun: options.dryRun })
88
- return
383
+ printGenericInstructions("当前客户端", { dryRun });
384
+ return;
385
+ }
386
+ for (const detectedAgent of agents) {
387
+ await configureAgent(detectedAgent, { dryRun, home });
89
388
  }
389
+ console.log(
390
+ dryRun
391
+ ? `检查完成:将为 ${agents.join("、")} 配置 ${MCP_URL},未写入文件。`
392
+ : `配置完成:${agents.join("、")} 已接入石榴 AI MCP。请重启客户端后运行 shiliu status。`,
393
+ );
394
+ }
90
395
 
91
- for (const agent of agents) {
92
- await configureAgent(agent, {
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;
402
+ }
403
+
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,
93
446
  dryRun: options.dryRun,
447
+ authUrl,
448
+ tokenStore,
449
+ pendingLoginStore,
450
+ });
451
+ }
452
+ if (options.command === "status") {
453
+ const result = await printStatus({
94
454
  home,
95
- })
455
+ url,
456
+ authUrl,
457
+ json: options.json,
458
+ });
459
+ if (!result.remote.ok) process.exitCode = 1;
460
+ return;
96
461
  }
97
-
98
- console.log(options.dryRun
99
- ? `检查完成:将为 ${agents.join('、')} 配置 ${MCP_URL},未写入任何文件。`
100
- : `配置完成:${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
+ });
101
480
  }